diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index b10c7349d..aa1efc828 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -17,6 +17,7 @@ import projectsCommand from './projects/command'; import checkoutCommand from './checkout/command'; import mergeCommand from './merge/command'; import workflowVersionCommand from './version/command'; +import fetchCommand from './fetch/command'; const y = yargs(hideBin(process.argv)); @@ -37,6 +38,7 @@ export const cmd = y .command(projectsCommand) .command(checkoutCommand) .command(mergeCommand) + .command(fetchCommand as any) .command(workflowVersionCommand) .command({ command: 'version', diff --git a/packages/cli/src/commands.ts b/packages/cli/src/commands.ts index 795d8e9e3..96f420eb0 100644 --- a/packages/cli/src/commands.ts +++ b/packages/cli/src/commands.ts @@ -13,6 +13,7 @@ import projects from './projects/handler'; import workflowVersion from './version/handler'; import checkout from './checkout/handler'; import merge from './merge/handler'; +import fetchHandler from './fetch/handler'; import { clean, install, pwd, list } from './repo/handler'; import createLogger, { CLI, Logger } from './util/logger'; @@ -34,6 +35,7 @@ export type CommandList = | 'execute' | 'metadata' | 'pull' + | 'fetch' | 'projects' | 'checkout' | 'merge' @@ -55,6 +57,7 @@ const handlers = { docs, metadata, pull, + fetch: fetchHandler, projects, checkout, merge, diff --git a/packages/cli/src/fetch/command.ts b/packages/cli/src/fetch/command.ts new file mode 100644 index 000000000..3113a7123 --- /dev/null +++ b/packages/cli/src/fetch/command.ts @@ -0,0 +1,57 @@ +import yargs from 'yargs'; +import { build, ensure, override } from '../util/command-builders'; +import { Opts } from '../options'; +import * as o from '../options'; + +export type FetchOptions = Required< + Pick< + Opts, + | 'beta' + | 'command' + | 'log' + | 'logJson' + | 'statePath' + | 'projectPath' + | 'configPath' + | 'projectId' + | 'confirm' + | 'snapshots' + > +>; + +const options = [ + o.apikey, + o.beta, + o.beta, + o.configPath, + o.endpoint, + o.env, + o.log, + override(o.path, { + description: 'path to output the project to', + }), + o.logJson, + o.projectPath, + o.snapshots, + o.statePath, + o.path, +]; + +const fetchCommand: yargs.CommandModule = { + command: 'fetch [projectId]', + describe: `Fetch a project's state and spec from a Lightning Instance to the local state file without expanding to the filesystem.`, + builder: (yargs: yargs.Argv) => + build(options, yargs) + .positional('projectId', { + describe: + 'The id of the project that should be fetched, should be a UUID', + demandOption: true, + }) + .example( + 'fetch 57862287-23e6-4650-8d79-e1dd88b24b1c', + 'Fetch an updated copy of a the above spec and state from a Lightning Instance' + ), + handler: ensure('fetch', options), +}; + +export default fetchCommand; diff --git a/packages/cli/src/fetch/handler.ts b/packages/cli/src/fetch/handler.ts new file mode 100644 index 000000000..3e2d8d7ec --- /dev/null +++ b/packages/cli/src/fetch/handler.ts @@ -0,0 +1,80 @@ +import { DeployConfig, getProject } from '@openfn/deploy'; +import Project from '@openfn/project'; +import fs from 'node:fs/promises'; +import path from 'path'; +import { Opts } from '../options'; +import type { Logger } from '../util/logger'; + +type Config = { + endpoint: string; + apiKey: string | null; + + requireConfirmation?: boolean; + dryRun?: boolean; +}; + +export type FetchOptions = Required< + Pick< + Opts, + | 'apiKey' + | 'beta' + | 'command' + | 'confirm' + | 'endpoint' + | 'env' + | 'log' + | 'logJson' + | 'path' + | 'projectId' + > +>; + +export default async function fetchHandler( + options: FetchOptions, + logger: Logger +) { + const { OPENFN_API_KEY, OPENFN_ENDPOINT } = process.env; + + const config: Partial = { + apiKey: options.apiKey, + endpoint: options.endpoint, + }; + + if (!options.apiKey && OPENFN_API_KEY) { + logger.info('Using OPENFN_API_KEY environment variable'); + config.apiKey = OPENFN_API_KEY; + } + + if (!options.endpoint && OPENFN_ENDPOINT) { + logger.info('Using OPENFN_ENDPOINT environment variable'); + config.endpoint = OPENFN_ENDPOINT; + } + + // download the state.json from lightning + const { data } = await getProject(config as DeployConfig, options.projectId); + const name = options.env || 'project'; + + const project = Project.from('state', data, { + endpoint: config.endpoint, + env: name, + fetched_at: new Date().toISOString(), + }); + + const outputRoot = path.resolve(options.path || '.'); + + const projectFileName = project.getIdentifier(); + + await fs.mkdir(`${outputRoot}/.projects`, { recursive: true }); + let stateOutputPath = `${outputRoot}/.projects/${projectFileName}`; + + const state = project?.serialize('state'); + if (project.repo?.formats.project === 'yaml') { + await fs.writeFile(`${stateOutputPath}.yaml`, state); + } else { + await fs.writeFile( + `${stateOutputPath}.json`, + JSON.stringify(state, null, 2) + ); + } + logger.success(`Fetched project file to ${stateOutputPath}`); +}