diff --git a/packages/cli/src/base-command.ts b/packages/cli/src/base-command.ts index 8c634e62..dc7cad13 100644 --- a/packages/cli/src/base-command.ts +++ b/packages/cli/src/base-command.ts @@ -1,10 +1,6 @@ import { Command, Flags, Interfaces, settings as oclifSettings } from '@oclif/core' import path from 'path' -import { ProfileConfig, profileConfig } from './lib/profile' -import { createStore } from './lib/store' -import { realFs } from './lib/store/fs' -import { s3fs } from './lib/store/s3' -import { tarSnapshotter } from './lib/store/tar' +import { LocalProfilesConfig, localProfilesConfig } from './lib/profile' import { commandLogger, Logger, LogLevel, logLevels } from './log' // eslint-disable-next-line no-use-before-define @@ -67,20 +63,12 @@ abstract class BaseCommand extends Comm this.stdErrLogger.info(message, ...args) } - #profileConfig: ProfileConfig | undefined - get profileConfig(): ProfileConfig { + #profileConfig: LocalProfilesConfig | undefined + get profileConfig(): LocalProfilesConfig { if (!this.#profileConfig) { const root = path.join(this.config.dataDir, 'v2') - this.debug('init profile config at:', root) - this.#profileConfig = profileConfig(root, async location => { - const { protocol, hostname } = new URL(location) - if (protocol === 'local:') { - return createStore(realFs(path.join(root, 'profiles', hostname)), tarSnapshotter()) - } if (protocol === 's3:') { - return createStore(await s3fs(location), tarSnapshotter()) - } - throw new Error(`Unsupported blob prefix: ${protocol}`) - }) + this.logger.debug('init profile config at:', root) + this.#profileConfig = localProfilesConfig(root) } return this.#profileConfig diff --git a/packages/cli/src/commands/init/index.ts b/packages/cli/src/commands/init/index.ts index 60bebcfb..a761fdb8 100644 --- a/packages/cli/src/commands/init/index.ts +++ b/packages/cli/src/commands/init/index.ts @@ -4,7 +4,7 @@ import inquirer from 'inquirer' import { pickBy } from 'lodash' import BaseCommand from '../../base-command' import { DriverFlagName, DriverName, machineDrivers } from '../../lib/machine' -import { suggestDefaultUrl } from '../../lib/store/s3' +import { suggestDefaultUrl } from '../../lib/store/fs/s3' export default class Init extends BaseCommand { static description = 'Initialize or import a new profile' diff --git a/packages/cli/src/commands/profile/import.ts b/packages/cli/src/commands/profile/import.ts index 67acf011..d7e1d473 100644 --- a/packages/cli/src/commands/profile/import.ts +++ b/packages/cli/src/commands/profile/import.ts @@ -1,6 +1,18 @@ import { Args, Flags, ux } from '@oclif/core' +import { find, range, map } from 'iter-tools-es' import BaseCommand from '../../base-command' import { onProfileChange } from '../../profile-command' +import { LocalProfilesConfig } from '../../lib/profile' + +const DEFAULT_ALIAS_PREFIX = 'default' + +const defaultAlias = async (profileConfig: LocalProfilesConfig) => { + const profiles = new Set((await profileConfig.list()).map(l => l.alias)) + return find( + (alias: string) => !profiles.has(alias), + map(suffix => (suffix ? `${DEFAULT_ALIAS_PREFIX}${suffix + 1}` : DEFAULT_ALIAS_PREFIX), range()), + ) as string +} // eslint-disable-next-line no-use-before-define export default class ImportProfile extends BaseCommand { @@ -10,7 +22,6 @@ export default class ImportProfile extends BaseCommand { name: Flags.string({ description: 'name of the profile', required: false, - default: 'default', }), } @@ -26,10 +37,10 @@ export default class ImportProfile extends BaseCommand { static enableJsonFlag = true async run(): Promise { - const alias = this.flags.name + const alias = this.flags.name ?? await defaultAlias(this.profileConfig) const { info } = await this.profileConfig.importExisting(alias, this.args.location) - onProfileChange(info) - ux.info(`Profile ${info.id} imported successfully`) + onProfileChange(info, alias, this.args.location) + ux.info(`Profile ${info.id} imported successfully as ${alias}`) } } diff --git a/packages/cli/src/lib/machine/driver/driver.ts b/packages/cli/src/lib/machine/driver/driver.ts index 477cfc0d..fbdb3433 100644 --- a/packages/cli/src/lib/machine/driver/driver.ts +++ b/packages/cli/src/lib/machine/driver/driver.ts @@ -1,4 +1,4 @@ -import { Profile } from '../../profile/types' +import { Profile } from '../../profile/profile' import { SSHKeyConfig } from '../../ssh/keypair' export type Machine = { diff --git a/packages/cli/src/lib/profile/config.ts b/packages/cli/src/lib/profile/config.ts new file mode 100644 index 00000000..d8b63b05 --- /dev/null +++ b/packages/cli/src/lib/profile/config.ts @@ -0,0 +1,129 @@ +import path from 'path' +import { localFs } from '../store/fs/local' +import { fsFromUrl, store, tarSnapshot } from '../store' +import { ProfileStore, profileStore } from './store' +import { Profile } from './profile' + +type ProfileListing = { + alias: string + id: string + location: string +} + +type ProfileList = { + current: string | undefined + profiles: Record> +} + +const profileListFileName = 'profileList.json' + +export const localProfilesConfig = (localDir: string) => { + const localStore = localFs(localDir) + const tarSnapshotFromUrl = async ( + url: string, + ) => store(async dir => tarSnapshot(await fsFromUrl(url, path.join(localDir, 'profiles')), dir)) + + async function readProfileList(): Promise { + const data = await localStore.read(profileListFileName) + if (!data) { + const initData = { current: undefined, profiles: {} } + await localStore.write(profileListFileName, JSON.stringify(initData)) + return initData + } + return JSON.parse(data.toString()) + } + + return { + async current() { + const { profiles, current: currentAlias } = await readProfileList() + const current = currentAlias && profiles[currentAlias] + if (!current) { + return undefined + } + return { + alias: currentAlias, + id: current.id, + location: current.location, + } + }, + async setCurrent(alias: string) { + const list = await readProfileList() + if (!list.profiles[alias]) { + throw new Error(`Profile ${alias} doesn't exists`) + } + list.current = alias + await localStore.write(profileListFileName, JSON.stringify(list)) + }, + async list(): Promise { + return Object.entries((await readProfileList()).profiles).map(([alias, profile]) => ({ alias, ...profile })) + }, + async get(alias: string) { + const { profiles } = await readProfileList() + const locationUrl = profiles[alias]?.location + if (!locationUrl) { + throw new Error(`Profile ${alias} not found`) + } + const tarSnapshotStore = await tarSnapshotFromUrl(locationUrl) + const profileInfo = await profileStore(tarSnapshotStore).info() + return { + info: profileInfo, + store: tarSnapshotStore, + } + }, + async delete(alias: string) { + const list = await readProfileList() + if (!list.profiles[alias]) { + throw new Error(`Profile ${alias} does not exist`) + } + delete list.profiles[alias] + if (list.current === alias) { + list.current = undefined + } + await localStore.write(profileListFileName, JSON.stringify(list)) + }, + async importExisting(alias: string, location: string) { + const list = await readProfileList() + if (list.profiles[alias]) { + throw new Error(`Profile ${alias} already exists`) + } + const tarSnapshotStore = await tarSnapshotFromUrl(location) + const info = await profileStore(tarSnapshotStore).info() + list.profiles[alias] = { + id: info.id, + location, + } + list.current = alias + await localStore.write(profileListFileName, JSON.stringify(list)) + return { + info, + store: tarSnapshotStore, + } + }, + async create(alias: string, location: string, profile: Omit, init: (store: ProfileStore) => Promise) { + const list = await readProfileList() + if (list.profiles[alias]) { + throw new Error(`Profile ${alias} already exists`) + } + const id = `${alias}-${Math.random().toString(36).substring(2, 9)}` + const tar = await tarSnapshotFromUrl(location) + const pStore = profileStore(tar) + await pStore.init({ id, ...profile }) + list.profiles[alias] = { + id, + location, + } + list.current = alias + await init(pStore) + await localStore.write(profileListFileName, JSON.stringify(list)) + return { + info: { + id, + ...profile, + }, + store: tar, + } + }, + } +} + +export type LocalProfilesConfig = ReturnType diff --git a/packages/cli/src/lib/profile/index.ts b/packages/cli/src/lib/profile/index.ts index 526a3fdf..80b49e44 100644 --- a/packages/cli/src/lib/profile/index.ts +++ b/packages/cli/src/lib/profile/index.ts @@ -1,3 +1,3 @@ export * from './store' -export * from './types' -export * from './profileConfig' +export * from './profile' +export * from './config' diff --git a/packages/cli/src/lib/profile/types.ts b/packages/cli/src/lib/profile/profile.ts similarity index 100% rename from packages/cli/src/lib/profile/types.ts rename to packages/cli/src/lib/profile/profile.ts diff --git a/packages/cli/src/lib/profile/profileConfig.ts b/packages/cli/src/lib/profile/profileConfig.ts deleted file mode 100644 index 8b35898c..00000000 --- a/packages/cli/src/lib/profile/profileConfig.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { realFs } from '../store/fs' -import { Store } from '../store' -import { ProfileStore, profileStore } from './store' -import { Profile } from './types' - -type ProfileListing = { - alias:string - id: string - location: string -} - -type ProfileList = { - current: string | undefined - profiles: Record> -} - -const profileListFileName = 'profileList.json' - -export const profileConfig = (localDir:string, profileStoreResolver: (location: string)=> Promise) => { - const localStore = realFs(localDir) - - async function getProfileList(): Promise { - const data = await localStore.read(profileListFileName) - if (!data) { - const initData = { current: undefined, profiles: {} } - await localStore.write(profileListFileName, JSON.stringify(initData)) - return initData - } - return JSON.parse(data.toString()) - } - - return { - async current() { - const profileData = await getProfileList() - if (!profileData.current || !profileData.profiles[profileData.current]) { - return undefined - } - return { - alias: profileData.current, - id: profileData.profiles[profileData.current].id, - location: profileData.profiles[profileData.current].location, - } - }, - async setCurrent(alias: string) { - const data = await getProfileList() - if (!data.profiles[alias]) { - throw new Error(`Profile ${alias} doesn't exists`) - } - data.current = alias - await localStore.write(profileListFileName, JSON.stringify(data)) - }, - async list(): Promise { - return Object.entries((await getProfileList()).profiles).map(([alias, profile]) => ({ alias, ...profile })) - }, - async get(alias: string) { - const data = await getProfileList() - const location = data.profiles[alias]?.location - if (!location) { - throw new Error(`Profile ${alias} not found`) - } - let store: Store - try { - store = await profileStoreResolver(location) - } catch (error) { - throw new Error(`Failed to resolve store for profile ${alias}, error: ${error}}`) - } - const profileInfo = await profileStore(store).info() - return { - info: profileInfo, - store, - } - }, - async delete(alias: string) { - const data = await getProfileList() - if (!data.profiles[alias]) { - throw new Error(`Profile ${alias} doesn't exists`) - } - delete data.profiles[alias] - await localStore.write(profileListFileName, JSON.stringify(data)) - }, - async importExisting(alias: string, location: string) { - const data = await getProfileList() - if (data.profiles[alias]) { - throw new Error(`Profile ${alias} already exists`) - } - const remoteStore = await profileStoreResolver(location) - const info = await profileStore(remoteStore).info() - data.profiles[alias] = { - id: info.id, - location, - } - data.current = alias - await localStore.write(profileListFileName, JSON.stringify(data)) - return { - info, - store: remoteStore, - } - }, - async create(alias: string, location: string, profile: Omit, init: (store: ProfileStore) => Promise) { - const data = await getProfileList() - if (data.profiles[alias]) { - throw new Error(`Profile ${alias} already exists`) - } - const id = `${alias}-${Math.random().toString(36).substring(2, 9)}` - const remoteStore = await profileStoreResolver(location) - const pStore = profileStore(remoteStore) - await pStore.init({ id, ...profile }) - data.profiles[alias] = { - id, - location, - } - data.current = alias - await init(pStore) - await localStore.write(profileListFileName, JSON.stringify(data)) - return { - info: { - id, - ...profile, - }, - store: remoteStore, - } - }, - } -} - -export type ProfileConfig = ReturnType diff --git a/packages/cli/src/lib/profile/store.ts b/packages/cli/src/lib/profile/store.ts index 35c093ef..148351a5 100644 --- a/packages/cli/src/lib/profile/store.ts +++ b/packages/cli/src/lib/profile/store.ts @@ -1,6 +1,6 @@ import path from 'path' import { parseKey } from '@preevy/common' -import { Profile } from './types' +import { Profile } from './profile' import { Store } from '../store' export const profileStore = (store: Store) => { diff --git a/packages/cli/src/lib/store/api.ts b/packages/cli/src/lib/store/api.ts deleted file mode 100644 index ce369ab6..00000000 --- a/packages/cli/src/lib/store/api.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { VirtualFS, Snapshot, Snapshotter } from './types' -import { snapshotTransactor } from './utils' - -export function createStore( - vfs: VirtualFS, - snapshotter: Snapshotter -) { - const transactor = snapshotTransactor(snapshotter) - - return { - ref(dir: string) { - async function read(file: string) { - const dirData = await vfs.read(dir) - if (!dirData) return undefined - return transactor.readFromSnapshot(dirData, async s => s.read(file)) - } - async function readOrThrow(file: string) { - const data = await read(file) - if (!data) { - throw new Error('missing data') - } - return data - } - return { - async list() { - const data = await vfs.read(dir) - if (!data) return [] - return transactor.readFromSnapshot(data, s => s.list()) - }, - read, - async readJSON(file:string) { - const data = await read(file) - if (!data) return undefined - return JSON.parse(data.toString()) as T - }, - async readJsonOrThrow(file: string) { - const data = await readOrThrow(file) - return JSON.parse(data.toString()) as T - }, - } - }, - async transaction(dir: string, op: (s: Pick) => Promise) { - const data = await vfs.read(dir) - return vfs.write(dir, await transactor.writeToSnapshot(data, op)) - }, - } -} - -export type Store = ReturnType diff --git a/packages/cli/src/lib/store/fs/base.ts b/packages/cli/src/lib/store/fs/base.ts new file mode 100644 index 00000000..b6c4bf06 --- /dev/null +++ b/packages/cli/src/lib/store/fs/base.ts @@ -0,0 +1,5 @@ +export type VirtualFS = { + read: (filename: string) => Promise + write: (filename: string, content: Buffer | string) => Promise + delete: (filename: string) => Promise +} diff --git a/packages/cli/src/lib/store/fs/index.ts b/packages/cli/src/lib/store/fs/index.ts new file mode 100644 index 00000000..a44378b5 --- /dev/null +++ b/packages/cli/src/lib/store/fs/index.ts @@ -0,0 +1,19 @@ +import { localFsFromUrl } from './local' +import { s3fs } from './s3' + +export { VirtualFS } from './base' +export { localFs } from './local' +export { jsonReader } from './json-reader' + +export const fsTypeFromUrl = (url: string): string => new URL(url).protocol.replace(':', '') + +export const fsFromUrl = async (url: string, localBaseDir: string) => { + const fsType = fsTypeFromUrl(url) + if (fsType === 'local') { + return localFsFromUrl(localBaseDir, url) + } + if (fsType === 's3') { + return s3fs(url) + } + throw new Error(`Unsupported URL type: ${fsType}`) +} diff --git a/packages/cli/src/lib/store/fs/json-reader.ts b/packages/cli/src/lib/store/fs/json-reader.ts new file mode 100644 index 00000000..d3d7c9de --- /dev/null +++ b/packages/cli/src/lib/store/fs/json-reader.ts @@ -0,0 +1,23 @@ +import { VirtualFS } from './base' + +export const jsonReader = (reader: Pick) => { + const readOrThrow = async (file: string): Promise => { + const data = await reader.read(file) + if (!data) { + throw new Error(`missing file: ${file}`) + } + return data + } + + return { + async readJSON(file:string) { + const data = await reader.read(file) + if (!data) return undefined + return JSON.parse(data.toString()) as T + }, + async readJsonOrThrow(file: string) { + const data = await readOrThrow(file) + return JSON.parse(data.toString()) as T + }, + } +} diff --git a/packages/cli/src/lib/store/fs.ts b/packages/cli/src/lib/store/fs/local.ts similarity index 79% rename from packages/cli/src/lib/store/fs.ts rename to packages/cli/src/lib/store/fs/local.ts index 9466d8a7..5abf3d8c 100644 --- a/packages/cli/src/lib/store/fs.ts +++ b/packages/cli/src/lib/store/fs/local.ts @@ -1,11 +1,11 @@ import fs from 'fs/promises' import path, { dirname } from 'path' import { rimraf } from 'rimraf' -import { VirtualFS } from './types' +import { VirtualFS } from './base' const isNotFoundError = (e: unknown) => (e as { code?: unknown })?.code === 'ENOENT' -export const realFs = (baseDir: string): VirtualFS => ({ +export const localFs = (baseDir: string): VirtualFS => ({ read: async (filename: string) => { const filepath = path.join(baseDir, filename) const f = () => fs.readFile(filepath) @@ -34,3 +34,8 @@ export const realFs = (baseDir: string): VirtualFS => ({ await rimraf(filename) }, }) + +export const localFsFromUrl = ( + baseDir: string, + url: string, +): VirtualFS => localFs(path.join(baseDir, new URL(url).hostname)) diff --git a/packages/cli/src/lib/store/s3.ts b/packages/cli/src/lib/store/fs/s3.ts similarity index 94% rename from packages/cli/src/lib/store/s3.ts rename to packages/cli/src/lib/store/fs/s3.ts index 7c4f428b..d5ed188c 100644 --- a/packages/cli/src/lib/store/s3.ts +++ b/packages/cli/src/lib/store/fs/s3.ts @@ -1,12 +1,12 @@ import { CreateBucketCommand, DeleteObjectCommand, GetObjectCommand, GetObjectCommandOutput, HeadBucketCommand, PutObjectCommand, S3Client, S3ServiceException } from '@aws-sdk/client-s3' import { STSClient, GetCallerIdentityCommand } from '@aws-sdk/client-sts' import path from 'path' -import { VirtualFS } from './types' +import { VirtualFS } from './base' export async function suggestDefaultUrl(profileAlias: string) { const sts = new STSClient({ region: 'us-east-1' }) const { Account: AccountId } = await sts.send(new GetCallerIdentityCommand({})) - return `s3://preview-${AccountId}-${profileAlias}?region=us-east-1` + return `s3://preevy-${AccountId}-${profileAlias}?region=us-east-1` } async function ensureBucketExists(s3: S3Client, bucket: string) { @@ -59,7 +59,7 @@ export function parseS3Url(s3Url: string) { } } -export const s3fs = async (s3Url: string):Promise => { +export const s3fs = async (s3Url: string): Promise => { const url = parseS3Url(s3Url) const { bucket, path: prefix } = url const s3 = new S3Client({ diff --git a/packages/cli/src/lib/store/index.ts b/packages/cli/src/lib/store/index.ts index eb442480..63ec5b6f 100644 --- a/packages/cli/src/lib/store/index.ts +++ b/packages/cli/src/lib/store/index.ts @@ -1,3 +1,27 @@ -export * from './api' -export * from './types' -export * from './utils' +import { jsonReader } from './fs' +import { Snapshot, snapshotStore } from './snapshot' + +export * from './tar' +export { fsFromUrl, VirtualFS, jsonReader } from './fs' + +export const store = ( + snapshotter: (dir: string) => Promise, +) => { + const s = (dir: string) => snapshotStore(() => snapshotter(dir)) + return ({ + ref: (dir: string) => { + const read = async (file: string) => (s(dir)).ref().read(file) + + return ({ + read, + ...jsonReader({ read }), + }) + }, + transaction: async ( + dir: string, + op: (s: Pick) => Promise, + ) => s(dir).transaction(op), + }) +} + +export type Store = ReturnType diff --git a/packages/cli/src/lib/store/snapshot.ts b/packages/cli/src/lib/store/snapshot.ts new file mode 100644 index 00000000..a8309c4a --- /dev/null +++ b/packages/cli/src/lib/store/snapshot.ts @@ -0,0 +1,47 @@ +import { VirtualFS } from './fs' + +type Closable = { + close: () => Promise +} + +const isClosable = ( + o: unknown +): o is Closable => typeof o === 'object' && o !== null && typeof (o as Closable).close === 'function' + +const ensureClose = async (o: T, f: (o: T) => PromiseLike | R) => { + try { + return await f(o) + } finally { + if (isClosable(o)) { + await o.close() + } + } +} + +export type Snapshot = VirtualFS & Partial & { + save: () => Promise +} + +export const snapshotStore = (snapshotter: () => Promise) => ({ + ref: (): Pick => ({ + read: async (file: string) => { + const snapshot = await snapshotter() + return ensureClose(snapshot, s => s.read(file)) + }, + }), + transaction: async (op: (s: Pick) => Promise) => { + const snapshot = await snapshotter() + return ensureClose(snapshot, async s => { + const result = await op(s) + await s.save() + return result + }) + }, +}) + +export type SnapshotStore = ReturnType + +export type FileBackedSnapshotter = ( + fs: Pick, + filename: string, +) => Promise diff --git a/packages/cli/src/lib/store/tar.ts b/packages/cli/src/lib/store/tar.ts index a50fc9a0..be66619c 100644 --- a/packages/cli/src/lib/store/tar.ts +++ b/packages/cli/src/lib/store/tar.ts @@ -1,83 +1,61 @@ -import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from 'fs/promises' import { tmpdir } from 'os' -import path, { dirname } from 'path' +import path from 'path' import { rimraf } from 'rimraf' import { Readable } from 'stream' import { pipeline } from 'stream/promises' +import { mkdtemp } from 'fs/promises' import tar from 'tar' -import { Snapshotter } from './types' +import { localFs } from './fs' +import { FileBackedSnapshotter, Snapshot } from './snapshot' -const isNotFoundError = (e: unknown) => - (e as { code?: unknown })?.code === 'ENOENT' +const readStream = (stream: Readable): Promise => new Promise((resolve, reject) => { + const buffer: Buffer[] = [] + stream.on('data', chunk => buffer.push(chunk)) + stream.on('end', () => resolve(Buffer.concat(buffer))) + stream.on('error', reject) +}) -async function readStream(stream: Readable): Promise { - return new Promise((resolve, reject) => { - const buffer:Buffer[] = [] - stream.on('data', chunk => buffer.push(chunk)) - stream.on('end', () => resolve(Buffer.concat(buffer))) - stream.on('error', err => reject(new Error(`error converting stream - ${err}`))) - }) -} +export const tarSnapshot: FileBackedSnapshotter = async (fs, filename): Promise => { + const transactionDir = await mkdtemp(path.join(tmpdir(), 'preevy-transactions-')) + const existingTar = await fs.read(filename) + + if (existingTar) { + await pipeline( + Readable.from(existingTar), + tar.x({ + cwd: transactionDir, + }) + ) + } + + let dirty = false + const setDirty = ( + f: (...args: Args) => Return, + ) => (...args: Args) => { dirty = true; return f(...args) } + + const save = async () => fs.write(filename, await readStream( + tar.c( + { + cwd: transactionDir, + prefix: '', + }, + ['.'] + ) + )) + + const local = localFs(transactionDir) -export function tarSnapshotter(): Snapshotter { return { - open: async (existingSnapshot: Buffer | undefined) => { - const transactionDir = await mkdtemp(path.join(tmpdir(), 'preview-transactions-')) - if (existingSnapshot) { - await pipeline( - Readable.from(existingSnapshot), - tar.x({ - cwd: transactionDir, - }) - ) - } - return { - snapshot: { - read: async (file: string) => { - try { - return await readFile(path.join(transactionDir, file)) - } catch (error) { - if (isNotFoundError(error)) { - return undefined - } - throw error - } - return undefined - }, - list: async () => { - const files = await readdir(transactionDir) - return files - }, - write: async (file: string, content: string | Buffer) => { - const target = path.join(transactionDir, file) - try { - await writeFile(target, content) - } catch (e) { - if (isNotFoundError(e)) { - await mkdir(dirname(target), { recursive: true }) - await writeFile(target, content) - return - } - throw e - } - }, - delete: async (fileName: string) => { - await rm(path.join(transactionDir, fileName)) - }, - }, - close: async () => { - await rimraf(transactionDir) - }, - save: async () => readStream( - tar.c( - { - cwd: transactionDir, - prefix: '', - }, - ['.'] - ) - ), + read: local.read, + write: setDirty(local.write), + delete: setDirty(local.delete), + save: async () => { + if (dirty) { + await save() } }, + close: async () => { + await rimraf(transactionDir) + }, } } diff --git a/packages/cli/src/lib/store/types.ts b/packages/cli/src/lib/store/types.ts deleted file mode 100644 index 23d484c4..00000000 --- a/packages/cli/src/lib/store/types.ts +++ /dev/null @@ -1,20 +0,0 @@ -export type VirtualFS = { - read: (filename: string) => Promise - write: (filename: string, content: Buffer | string) => Promise - delete: (filename: string) => Promise -} - -export type Snapshot = { - write(file: string, content: string | Buffer): Promise - list(): Promise - delete: (file: string) => Promise - read(file: string): Promise - }; - -export type Snapshotter = { - open(s: Buffer | undefined): Promise<{ - snapshot: Snapshot - close: () => Promise - save: () => Promise - }> - }; diff --git a/packages/cli/src/lib/store/utils.ts b/packages/cli/src/lib/store/utils.ts deleted file mode 100644 index ca36b37a..00000000 --- a/packages/cli/src/lib/store/utils.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Snapshot, Snapshotter } from './types' - -type SnapshotTransactor = { - readFromSnapshot( - s: Buffer, - op: (snapshot: Pick) => Promise - ): Promise - writeToSnapshot( - existingSnapshot: Buffer | undefined, - op: ( - snapshot: Pick - ) => Promise - ): Promise - }; - -export const snapshotTransactor = ( - snapshotter: Snapshotter -):SnapshotTransactor => ({ - async readFromSnapshot(data, op) { - const { snapshot, close } = await snapshotter.open(data) - try { - return await op(snapshot) - } finally { - await close() - } - }, - async writeToSnapshot(data: Buffer | undefined, op) { - const { snapshot, save, close } = await snapshotter.open(data) - try { - await op(snapshot) - return await save() - } finally { - await close() - } - }, -}) diff --git a/packages/cli/src/profile-command.ts b/packages/cli/src/profile-command.ts index a7fee7e1..6a86671d 100644 --- a/packages/cli/src/profile-command.ts +++ b/packages/cli/src/profile-command.ts @@ -4,13 +4,19 @@ import BaseCommand from './base-command' import { Profile } from './lib/profile' import { Store } from './lib/store' import { telemetryEmitter } from './lib/telemetry' +import { fsTypeFromUrl } from './lib/store/fs' // eslint-disable-next-line no-use-before-define export type Flags = Interfaces.InferredFlags export type Args = Interfaces.InferredArgs -export const onProfileChange = (profile: Profile) => { - telemetryEmitter().identify(profile.id, { profile_driver: profile.driver }) +export const onProfileChange = (profile: Profile, alias: string, location: string) => { + telemetryEmitter().identify(profile.id, { + profile_driver: profile.driver, + profile_id: profile.id, + name: profile.id, + profile_store_type: fsTypeFromUrl(location), + }) } abstract class ProfileCommand extends BaseCommand { @@ -23,19 +29,20 @@ abstract class ProfileCommand extends BaseCommand { public async init(): Promise { await super.init() - const pm = this.profileConfig - const currentProfile = await pm.current().then(x => x && pm.get(x.alias)) - if (currentProfile) { - this.#profile = currentProfile.info - this.#store = currentProfile.store - onProfileChange(currentProfile.info) + const { profileConfig } = this + const currentProfile = await profileConfig.current() + const currentProfileInfo = currentProfile && await profileConfig.get(currentProfile.alias) + if (currentProfileInfo) { + this.#profile = currentProfileInfo.info + this.#store = currentProfileInfo.store + onProfileChange(currentProfileInfo.info, currentProfile.alias, currentProfile.location) } } #store: Store | undefined get store(): Store { if (!this.#store) { - throw new Error("Store wasn't initialized") + throw new Error('Store was not initialized') } return this.#store }