Skip to content

Commit

Permalink
add option to specify a URL in the --profile flag
Browse files Browse the repository at this point in the history
  • Loading branch information
Roy Razon committed Dec 4, 2023
1 parent 5931d7b commit 7ad87dd
Show file tree
Hide file tree
Showing 10 changed files with 204 additions and 124 deletions.
12 changes: 11 additions & 1 deletion packages/cli-common/src/commands/base-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ export type Args<T extends typeof Command> = Interfaces.InferredArgs<T['args']>

const argsFromRaw = (raw: ParsingToken[]) => raw.filter(arg => arg.type === 'arg').map(arg => arg.input).filter(Boolean)

const jsonFlags = {
json: Flags.boolean({
description: 'Format output as JSON',
helpGroup: 'GLOBAL',
}),
} as const

abstract class BaseCommand<T extends typeof Command=typeof Command> extends Command {
static baseFlags = {
'log-level': Flags.custom<LogLevel>({
Expand Down Expand Up @@ -86,7 +93,10 @@ abstract class BaseCommand<T extends typeof Command=typeof Command> extends Comm
await super.init()
const { args, flags, raw } = await this.parse({
flags: this.ctor.flags,
baseFlags: super.ctor.baseFlags,
baseFlags: {
...this.ctor.baseFlags,
...this.ctor.enableJsonFlag ? jsonFlags : {},
},
args: this.ctor.args,
strict: false,
})
Expand Down
10 changes: 2 additions & 8 deletions packages/cli/src/commands/profile/current.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,7 @@ export default class CurrentProfile extends ProfileCommand<typeof CurrentProfile
return ux.info('No profile is loaded, use init command to create or import a new profile')
}
const { alias, id, location } = currentProfile
if (this.flags.json) {
return { alias, id, location }
}
return ux.table([{ alias, id, location }], {
alias: { header: 'Alias' },
id: { header: 'ID' },
location: { header: 'Location' },
}, this.flags)
const result = { alias, id, location }
return this.flags.json ? result : ux.styledObject(result)
}
}
17 changes: 7 additions & 10 deletions packages/cli/src/commands/profile/import.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { Args, Flags } from '@oclif/core'
import { find, range, map } from 'iter-tools-es'
import { LocalProfilesConfig } from '@preevy/core'
import { fsTypeFromUrl } from '@preevy/core'
import { BaseCommand, text } from '@preevy/cli-common'
import { loadProfileConfig, onProfileChange } from '../../profile-command'

const DEFAULT_ALIAS_PREFIX = 'default'

const defaultAlias = async (profileConfig: LocalProfilesConfig) => {
const profiles = new Set((await profileConfig.list()).map(l => l.alias))
const defaultAlias = async (aliases: string[]) => {
const profiles = new Set(aliases)
return find(
(alias: string) => !profiles.has(alias),
map(suffix => (suffix ? `${DEFAULT_ALIAS_PREFIX}${suffix + 1}` : DEFAULT_ALIAS_PREFIX), range()),
Expand Down Expand Up @@ -38,14 +38,11 @@ export default class ImportProfile extends BaseCommand<typeof ImportProfile> {

async run(): Promise<void> {
const profileConfig = loadProfileConfig(this.config)
const alias = this.flags.name ?? await defaultAlias(profileConfig)

const { info } = await profileConfig.importExisting(alias, this.args.location)
onProfileChange(info, alias, this.args.location)
if (this.flags.use) {
await profileConfig.setCurrent(alias)
}
const aliases = Object.keys((await profileConfig.list()).profiles)
const alias = this.flags.name ?? await defaultAlias(aliases)

const { info } = await profileConfig.importExisting(alias, this.args.location, this.flags.use)
onProfileChange(info, fsTypeFromUrl(this.args.location))
text.success(`Profile ${text.code(info.id)} imported successfully as ${text.code(alias)} 👍`)
}
}
20 changes: 11 additions & 9 deletions packages/cli/src/commands/profile/ls.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ux } from '@oclif/core'
import { Flags, ux } from '@oclif/core'
import { tableFlags } from '@preevy/cli-common'
import ProfileCommand from '../../profile-command'

// eslint-disable-next-line no-use-before-define
Expand All @@ -7,21 +8,22 @@ export default class ListProfile extends ProfileCommand<typeof ListProfile> {

static enableJsonFlag = true

static flags = {
...tableFlags,
json: Flags.boolean({}),
}

async run(): Promise<unknown> {
const currentProfile = await this.profileConfig.current()
const profiles = await this.profileConfig.list()
const { profiles, current } = await this.profileConfig.list()

if (this.flags.json) {
return {
profiles: Object.fromEntries(profiles.map(({ alias, ...rest }) => [alias, rest])),
current: currentProfile?.alias,
}
return { profiles, current }
}

ux.table(profiles, {
ux.table(Object.values(profiles), {
alias: {
header: 'Alias',
get: ({ alias }) => `${alias}${alias === currentProfile?.alias ? ' *' : ''}`,
get: ({ alias }) => `${alias}${alias === current ? ' *' : ''}`,
},
id: {
header: 'Id',
Expand Down
14 changes: 11 additions & 3 deletions packages/cli/src/commands/profile/rm.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import { Args, ux } from '@oclif/core'
import { Args, Flags, ux } from '@oclif/core'
import { text } from '@preevy/cli-common'
import ProfileCommand from '../../profile-command'

// eslint-disable-next-line no-use-before-define
export default class RemoveProfile extends ProfileCommand<typeof RemoveProfile> {
static description = 'Remove a profile'

static flags = {
force: Flags.boolean({
description: 'Do not error if the profile is not found',
default: false,
}),
}

static args = {
name: Args.string({
description: 'name of the profile to remove',
Expand All @@ -19,8 +26,9 @@ export default class RemoveProfile extends ProfileCommand<typeof RemoveProfile>

async run(): Promise<unknown> {
const alias = this.args.name
await this.profileConfig.delete(alias)
ux.info(text.success(`Profile ${text.code(alias)} removed.`))
if (await this.profileConfig.delete(alias, { throwOnNotFound: !this.flags.force })) {
ux.info(text.success(`Profile ${text.code(alias)} removed.`))
}
return undefined
}
}
66 changes: 46 additions & 20 deletions packages/cli/src/profile-command.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import path from 'path'
import { Command, Flags, Interfaces } from '@oclif/core'
import { LocalProfilesConfig, Profile, Store, detectCiProvider, fsTypeFromUrl, localProfilesConfig, telemetryEmitter } from '@preevy/core'
import {
tryParseUrl, LocalProfilesConfig, Profile, Store, detectCiProvider, fsTypeFromUrl,
localProfilesConfig, telemetryEmitter, LocalProfilesConfigGetResult,
} from '@preevy/core'
import { BaseCommand, text } from '@preevy/cli-common'
import { fsFromUrl } from './fs'

export const onProfileChange = (profile: Profile, alias: string, location: string) => {
export const onProfileChange = (profile: Profile, profileStoreType: string) => {
const ciProvider = detectCiProvider()
if (ciProvider) {
telemetryEmitter().identify(`ci_${ciProvider.id ?? 'unknown'}_${profile.id}`, {
Expand All @@ -17,7 +20,7 @@ export const onProfileChange = (profile: Profile, alias: string, location: strin
profile_driver: profile.driver,
profile_id: profile.id,
name: profile.id,
profile_store_type: fsTypeFromUrl(location),
profile_store_type: profileStoreType,
}
)
}
Expand All @@ -31,6 +34,40 @@ export const loadProfileConfig = ({ dataDir }: { dataDir: string }): LocalProfil
export type Flags<T extends typeof Command> = Interfaces.InferredFlags<typeof ProfileCommand['baseFlags'] & T['flags']>
export type Args<T extends typeof Command> = Interfaces.InferredArgs<T['args']>

const findAvailableProfileAlias = (
{ existing, prefix }: { existing: Set<string>; prefix: string },
index = 0,
): string => {
const candidate = [prefix, index].filter(Boolean).join('-')
return existing.has(candidate) ? findAvailableProfileAlias({ existing, prefix }, index + 1) : candidate
}

const findProfile = async (
{ profileConfig, flags: { profile: profileFlag } }: {
profileConfig: LocalProfilesConfig
flags: { profile?: string }
},
): Promise<LocalProfilesConfigGetResult | undefined> => {
const profileUrl = tryParseUrl(profileFlag || '')
if (!profileUrl) {
return await profileConfig.get(profileFlag)
}

const { profiles } = await profileConfig.list()

const found = Object.values(profiles).find(p => p.location === profileFlag)
if (found) {
return await profileConfig.get(found.alias)
}

const newAlias = findAvailableProfileAlias({
existing: new Set(Object.keys(profiles)),
prefix: profileUrl.hostname,
})

return await profileConfig.importExisting(newAlias, profileUrl.toString())
}

abstract class ProfileCommand<T extends typeof Command> extends BaseCommand<T> {
static baseFlags = {
...BaseCommand.baseFlags,
Expand All @@ -45,25 +82,14 @@ abstract class ProfileCommand<T extends typeof Command> extends BaseCommand<T> {

public async init(): Promise<void> {
await super.init()
const { profileConfig } = this
let profileAlias = this.flags.profile
if (!profileAlias) {
const currentProfile = await profileConfig.current()
if (currentProfile) {
profileAlias = currentProfile.alias
}
}
if (!profileAlias) {
const { profileConfig, flags } = this
const profile = await findProfile({ profileConfig, flags })
if (!profile) {
return
}
const currentProfileConfig = await profileConfig.get(profileAlias)
if (!currentProfileConfig) {
return
}

this.#profile = currentProfileConfig.info
this.#store = currentProfileConfig.store
onProfileChange(currentProfileConfig.info, profileAlias, currentProfileConfig.location)
this.#profile = profile.info
this.#store = profile.store
onProfileChange(profile.info, fsTypeFromUrl(profile.location))
}

#profileConfig: LocalProfilesConfig | undefined
Expand Down
10 changes: 2 additions & 8 deletions packages/core/src/ci-providers/common.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
import { tryParseUrl } from '../url'

export const nanToUndefined = (value: number) => (Number.isNaN(value) ? undefined : value)

export const stringOrUndefinedToNumber = (
value: string | undefined
): number | undefined => (value === undefined ? undefined : nanToUndefined(Number(value)))

const tryParseUrl = (s: string) => {
try {
return new URL(s)
} catch (e) {
return undefined
}
}

export const extractPrNumberFromUrlPath = (s: string | undefined) => {
if (!s) {
return undefined
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export {
machineStatusNodeExporterCommand,
ensureMachine,
} from './driver'
export { profileStore, Profile, ProfileStore, link, Org } from './profile'
export { profileStore, Profile, ProfileStore, link, Org, LocalProfilesConfigGetResult } from './profile'
export { telemetryEmitter, registerEmitter, wireProcessExit, createTelemetryEmitter, machineId } from './telemetry'
export { fsTypeFromUrl, Store, VirtualFS, localFsFromUrl, localFs } from './store'
export { localComposeClient, ComposeModel, resolveComposeFiles, getExposedTcpServicePorts, fetchRemoteUserModel as remoteUserModel, NoComposeFilesError, addScriptInjectionsToServices as addScriptInjectionsToModel } from './compose'
Expand Down Expand Up @@ -57,6 +57,7 @@ export {
getTunnelNamesToServicePorts,
Connection as SshConnection,
} from './tunneling'
export { tryParseUrl } from './url'
export { TunnelOpts } from './ssh'
export { Spinner } from './spinner'
export { generateBasicAuthCredentials as getUserCredentials, jwtGenerator, jwkThumbprint, jwkThumbprintUri, parseKey } from './credentials'
Expand Down
Loading

0 comments on commit 7ad87dd

Please sign in to comment.