diff --git a/packages/cli/src/commands/profile/ls.ts b/packages/cli/src/commands/profile/ls.ts index ce349e29..7c878bf3 100644 --- a/packages/cli/src/commands/profile/ls.ts +++ b/packages/cli/src/commands/profile/ls.ts @@ -13,6 +13,8 @@ export default class ListProfile extends ProfileCommand { json: Flags.boolean({}), } + protected throwOnProfileNotFound = false + async run(): Promise { const { profiles, current } = await this.profileConfig.list() diff --git a/packages/cli/src/commands/profile/rm.ts b/packages/cli/src/commands/profile/rm.ts index 4bf42cfc..cd5dc01f 100644 --- a/packages/cli/src/commands/profile/rm.ts +++ b/packages/cli/src/commands/profile/rm.ts @@ -24,6 +24,8 @@ export default class RemoveProfile extends ProfileCommand static enableJsonFlag = true + protected throwOnProfileNotFound = false + async run(): Promise { const alias = this.args.name if (await this.profileConfig.delete(alias, { throwOnNotFound: !this.flags.force })) { diff --git a/packages/cli/src/profile-command.ts b/packages/cli/src/profile-command.ts index cca3bb0f..aecffec5 100644 --- a/packages/cli/src/profile-command.ts +++ b/packages/cli/src/profile-command.ts @@ -2,7 +2,7 @@ import path from 'path' import { Command, Flags, Interfaces } from '@oclif/core' import { tryParseUrl, LocalProfilesConfig, Profile, Store, detectCiProvider, fsTypeFromUrl, - localProfilesConfig, telemetryEmitter, LocalProfilesConfigGetResult, + localProfilesConfig, telemetryEmitter, LocalProfilesConfigGetResult, ProfileLoadError, } from '@preevy/core' import { BaseCommand, text } from '@preevy/cli-common' import { fsFromUrl } from './fs.js' @@ -86,10 +86,18 @@ abstract class ProfileCommand extends BaseCommand { protected flags!: Flags protected args!: Args + protected throwOnProfileNotFound = true + public async init(): Promise { await super.init() const { profileConfig, flags } = this - const profile = await findProfile({ profileConfig, flags }) + const profile = await findProfile({ profileConfig, flags }).catch(e => { + if (!(e instanceof ProfileLoadError) || this.throwOnProfileNotFound) { + throw e + } + this.logger.warn(`Profile load error: ${e.message}`) + return undefined + }) if (!profile) { return } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 430d67e4..71cac76e 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -24,6 +24,7 @@ export { profileStore, Profile, ProfileStore, ProfileStoreRef, ProfileStoreTransaction, ProfileEditor, ProfileEditorOp, link, Org, LocalProfilesConfigGetResult, + ProfileLoadError, } from './profile/index.js' export { telemetryEmitter, registerEmitter, wireProcessExit, createTelemetryEmitter, machineId } from './telemetry/index.js' export { fsTypeFromUrl, Store, VirtualFS, FsReader, localFsFromUrl, localFs } from './store/index.js' diff --git a/packages/core/src/profile/config.ts b/packages/core/src/profile/config.ts index cd85d7d1..cdd87682 100644 --- a/packages/core/src/profile/config.ts +++ b/packages/core/src/profile/config.ts @@ -58,6 +58,12 @@ const listPersistence = ({ localDir }: { localDir: string }) => { } } +export class ProfileLoadError extends Error { + constructor(message: string) { + super(message) + } +} + export const localProfilesConfig = ( localDir: string, fsFromUrl: (url: string, baseDir: string) => Promise, @@ -74,7 +80,7 @@ export const localProfilesConfig = ( async function get(alias: string | undefined, opts?: { throwOnNotFound: boolean }): Promise { const throwOrUndefined = () => { if (opts?.throwOnNotFound) { - throw new Error(`Profile ${alias} not found`) + throw new ProfileLoadError(`Profile ${alias} not found`) } return undefined } @@ -82,14 +88,23 @@ export const localProfilesConfig = ( const { profiles, current } = await listP.read() const aliasToGet = alias ?? current if (!aliasToGet) { - return throwOrUndefined() + if (opts?.throwOnNotFound) { + throw new ProfileLoadError('Profile not specified and no current profile is set') + } + return undefined } const locationUrl = profiles[aliasToGet]?.location if (!locationUrl) { + if (opts?.throwOnNotFound) { + throw new ProfileLoadError(`No profile with alias ${aliasToGet}`) + } return throwOrUndefined() } const tarSnapshotStore = await storeFromUrl(locationUrl) - const profileInfo = await profileStore(tarSnapshotStore).ref.info() + const profileInfo = await profileStore(tarSnapshotStore).ref.info({ throwOnNotFound: false }) + if (!profileInfo) { + throw new ProfileLoadError(`Could not load profile "${aliasToGet}" at ${locationUrl}. The profile may have been deleted`) + } return { alias: aliasToGet, location: locationUrl, @@ -200,7 +215,10 @@ export const localProfilesConfig = ( throw new Error(`Profile ${alias} already exists`) } const tarSnapshotStore = await storeFromUrl(fromLocation) - const info = await profileStore(tarSnapshotStore).ref.info() + const info = await profileStore(tarSnapshotStore).ref.info({ throwOnNotFound: false }) + if (!info) { + throw new Error(`Cannot import profile from ${fromLocation}. The profile may have been deleted`) + } const newProfile = { id: info.id, alias, diff --git a/packages/core/src/profile/store.ts b/packages/core/src/profile/store.ts index f4092102..63ee12bc 100644 --- a/packages/core/src/profile/store.ts +++ b/packages/core/src/profile/store.ts @@ -27,7 +27,14 @@ const readLines = (buffer: Buffer | undefined) => { const profileReader = (reader: FsReader) => { const { readJsonOrThrow, readJSON } = jsonReader(reader) - const info = async () => await readJsonOrThrow(filenames.info) + async function info(opts: { throwOnNotFound: false }): Promise + async function info(opts: { throwOnNotFound: true }): Promise + async function info(): Promise + async function info( + { throwOnNotFound = true }: { throwOnNotFound?: boolean } = { throwOnNotFound: true }, + ): Promise { + return await (throwOnNotFound ? readJsonOrThrow : readJSON)(filenames.info) + } return { info, driver: async () => (await info()).driver,