UNPKG

forest-cli

Version:

The Lumberjacks' toolbelt is the Forest CLI which makes easy to manage your back office application directly from the terminal.

156 lines 14.7 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const core_1 = require("@oclif/core"); const fs_1 = require("fs"); const path_1 = __importDefault(require("path")); const abstract_authenticated_command_1 = __importDefault(require("../../abstract-authenticated-command")); const errors_1 = require("../../services/layout/errors"); const layout_file_1 = require("../../services/layout/layout-file"); const layout_manager_1 = __importDefault(require("../../services/layout/layout-manager")); const read_1 = require("../../services/layout/read"); const resolve_command_scope_1 = require("../../services/layout/resolve-command-scope"); const workflow_sidecar_1 = require("../../services/layout/workflow-sidecar"); /** An unresolvable BPMN ref: the S3 version is gone (404), not an auth/network error. */ function isDeadBpmnRef(error) { return error instanceof errors_1.LayoutApiError && error.status === 404; } /** * Download one workflow's BPMN, returning '' when the ref is dead (a version * baked but never uploaded to this bucket). Genuine failures (auth, network, * server) propagate so an incomplete pull is never mistaken for a clean one. */ async function downloadWorkflowSidecar(manager, scope, workflow, renderingId) { try { return await manager.getWorkflowBpmn(scope, workflow.id, workflow.collectionId, workflow.bpmnAwsS3Identifier, renderingId); } catch (error) { if (isDeadBpmnRef(error)) return ''; throw error; } } /** Download each workflow's stored BPMN into memory (no disk write here). */ async function downloadWorkflowBpmns(manager, scope, workflows) { const downloads = []; const skipped = []; const withBpmn = workflows.filter(workflow => workflow.bpmnAwsS3Identifier); if (withBpmn.length === 0) return { downloads, skipped }; const renderingId = await manager.getRenderingId(scope); // eslint-disable-next-line no-restricted-syntax -- sequential: one signed download at a time for (const workflow of withBpmn) { // A workflow id becomes a filename — reject traversal before touching disk. if (!(0, workflow_sidecar_1.isSafeWorkflowId)(workflow.id)) { skipped.push(String(workflow.id)); } else { // eslint-disable-next-line no-await-in-loop -- intentional sequential downloads const bpmn = await downloadWorkflowSidecar(manager, scope, workflow, renderingId); if (bpmn) downloads.push({ bpmn, id: workflow.id }); else skipped.push(workflow.id); } } return { downloads, skipped }; } const DEFAULT_OUTPUT = 'forest-layout.json'; /** * `forest layout pull` — read the live rendering of an environment/team and * write it to a versionable forest-layout.json. First milestone of the * declarative layout-as-code flow (pull → diff → apply). */ class LayoutPullCommand extends abstract_authenticated_command_1.default { constructor(argv, config, plan) { super(argv, config, plan); const { assertPresent, env } = this.context; assertPresent({ env }); this.env = env; } async runAuthenticated() { const { flags } = await this.parse(LayoutPullCommand); const scope = await (0, resolve_command_scope_1.resolveCommandScope)({ baseEnv: this.env, flags: { env: flags.env, projectId: flags.projectId, team: flags.team }, }); const manager = new layout_manager_1.default(); const docs = await (0, read_1.fetchRemoteDocs)(manager, scope, read_1.PULL_DOMAINS); const content = (0, layout_file_1.serializeLayoutFile)(scope, docs, () => new Date()); const workflows = (docs.workflows ?? []); // Download every BPMN into memory BEFORE writing anything, so a mid-pull // network error leaves the previous layout + sidecars fully intact instead // of a fresh layout mixed with stale, unpruned sidecars. const sidecars = flags['with-workflows'] ? await downloadWorkflowBpmns(manager, scope, workflows) : null; // Sidecars first, layout last: the layout file is the source of truth, so it // only replaces the previous snapshot once the sidecars have converged. A // sidecar write failure leaves the old layout + old sidecars intact. const outputPath = path_1.default.resolve(process.cwd(), flags.out); if (sidecars) { this.writeWorkflowSidecars(path_1.default.join(path_1.default.dirname(outputPath), 'workflows'), sidecars, workflows); } (0, fs_1.writeFileSync)(outputPath, content); const { collections, workflows: workflowCount } = (0, read_1.summarize)(docs); this.logger.success(`Pulled the layout of ${this.chalk.bold(scope.environmentName)} / ${this.chalk.bold(scope.teamName)} into ${this.chalk.bold(flags.out)} (${collections} collections, ${workflowCount} workflows).`); } /** * Write the downloaded sidecars, then prune the STALE ones: only files named * like a managed sidecar whose workflow no longer exists in the environment. * Sidecars of still-existing workflows are always kept — including those whose * download was skipped (dead S3 ref): that file may be the last copy of the * BPMN, and a backup tool must never destroy the backup. Every deletion is * logged. The prune runs even when nothing was downloaded, so a layout whose * workflows all lost their BPMN still converges to a faithful mirror. */ writeWorkflowSidecars(dir, sidecars, workflows) { if (sidecars.downloads.length === 0 && !(0, fs_1.existsSync)(dir)) { this.warnSkipped(sidecars.skipped); return; } (0, fs_1.mkdirSync)(dir, { recursive: true }); sidecars.downloads.forEach(({ bpmn, id }) => (0, fs_1.writeFileSync)((0, workflow_sidecar_1.sidecarPath)(dir, id), bpmn)); const keep = new Set(workflows.map(workflow => String(workflow.id))); (0, workflow_sidecar_1.selectStaleSidecars)((0, fs_1.readdirSync)(dir), keep).forEach(entry => { (0, fs_1.unlinkSync)(path_1.default.join(dir, entry)); this.log(` ↺ pruned stale sidecar ${entry}`); }); this.log(` ↓ ${sidecars.downloads.length} workflow BPMN → ${path_1.default.relative(process.cwd(), dir)}/`); this.warnSkipped(sidecars.skipped); } warnSkipped(skipped) { if (skipped.length === 0) return; this.logger.warn(`Skipped ${skipped.length} workflow BPMN with an unresolvable ref (not uploaded to this env) — their existing ` + `sidecar files, if any, were kept: ${skipped.join(', ')}.`); } } exports.default = LayoutPullCommand; LayoutPullCommand.description = 'Pull the layout (rendering) of an environment into a forest-layout.json file.'; LayoutPullCommand.flags = { env: core_1.Flags.string({ char: 'e', description: 'Environment to pull from (name or id). Defaults to the environment of FOREST_ENV_SECRET ' + 'when set, otherwise the development environment.', }), team: core_1.Flags.string({ char: 't', description: 'Team whose layout to pull (name or id). Defaults to the "Operations" team.', }), projectId: core_1.Flags.integer({ char: 'p', description: 'Id of the project (resolved automatically when you only have one).', }), out: core_1.Flags.string({ char: 'o', description: `Output file path (default: ${DEFAULT_OUTPUT}).`, default: DEFAULT_OUTPUT, }), 'with-workflows': core_1.Flags.boolean({ description: "Also download each workflow's BPMN into sidecar files (workflows/<id>.bpmn) for round-trip.", }), }; //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicHVsbC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9jb21tYW5kcy9sYXlvdXQvcHVsbC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7OztBQUdBLHNDQUFvQztBQUNwQywyQkFBbUY7QUFDbkYsZ0RBQXdCO0FBRXhCLDBHQUFnRjtBQUNoRix5REFBOEQ7QUFDOUQsbUVBQXdFO0FBQ3hFLDBGQUFpRTtBQUNqRSxxREFBc0Y7QUFDdEYsdUZBQWtGO0FBQ2xGLDZFQUlnRDtBQUVoRCx5RkFBeUY7QUFDekYsU0FBUyxhQUFhLENBQUMsS0FBYztJQUNuQyxPQUFPLEtBQUssWUFBWSx1QkFBYyxJQUFJLEtBQUssQ0FBQyxNQUFNLEtBQUssR0FBRyxDQUFDO0FBQ2pFLENBQUM7QUFFRDs7OztHQUlHO0FBQ0gsS0FBSyxVQUFVLHVCQUF1QixDQUNwQyxPQUFzQixFQUN0QixLQUFrQixFQUNsQixRQUE0RSxFQUM1RSxXQUFtQjtJQUVuQixJQUFJO1FBQ0YsT0FBTyxNQUFNLE9BQU8sQ0FBQyxlQUFlLENBQ2xDLEtBQUssRUFDTCxRQUFRLENBQUMsRUFBRSxFQUNYLFFBQVEsQ0FBQyxZQUFZLEVBQ3JCLFFBQVEsQ0FBQyxtQkFBNkIsRUFDdEMsV0FBVyxDQUNaLENBQUM7S0FDSDtJQUFDLE9BQU8sS0FBSyxFQUFFO1FBQ2QsSUFBSSxhQUFhLENBQUMsS0FBSyxDQUFDO1lBQUUsT0FBTyxFQUFFLENBQUM7UUFFcEMsTUFBTSxLQUFLLENBQUM7S0FDYjtBQUNILENBQUM7QUFLRCw2RUFBNkU7QUFDN0UsS0FBSyxVQUFVLHFCQUFxQixDQUNsQyxPQUFzQixFQUN0QixLQUFrQixFQUNsQixTQUF3QjtJQUV4QixNQUFNLFNBQVMsR0FBd0MsRUFBRSxDQUFDO0lBQzFELE1BQU0sT0FBTyxHQUFhLEVBQUUsQ0FBQztJQUU3QixNQUFNLFFBQVEsR0FBRyxTQUFTLENBQUMsTUFBTSxDQUFDLFFBQVEsQ0FBQyxFQUFFLENBQUMsUUFBUSxDQUFDLG1CQUFtQixDQUFDLENBQUM7SUFDNUUsSUFBSSxRQUFRLENBQUMsTUFBTSxLQUFLLENBQUM7UUFBRSxPQUFPLEVBQUUsU0FBUyxFQUFFLE9BQU8sRUFBRSxDQUFDO0lBRXpELE1BQU0sV0FBVyxHQUFHLE1BQU0sT0FBTyxDQUFDLGNBQWMsQ0FBQyxLQUFLLENBQUMsQ0FBQztJQUN4RCw2RkFBNkY7SUFDN0YsS0FBSyxNQUFNLFFBQVEsSUFBSSxRQUFRLEVBQUU7UUFDL0IsNEVBQTRFO1FBQzVFLElBQUksQ0FBQyxJQUFBLG1DQUFnQixFQUFDLFFBQVEsQ0FBQyxFQUFFLENBQUMsRUFBRTtZQUNsQyxPQUFPLENBQUMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxRQUFRLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQztTQUNuQzthQUFNO1lBQ0wsZ0ZBQWdGO1lBQ2hGLE1BQU0sSUFBSSxHQUFHLE1BQU0sdUJBQXVCLENBQUMsT0FBTyxFQUFFLEtBQUssRUFBRSxRQUFRLEVBQUUsV0FBVyxDQUFDLENBQUM7WUFDbEYsSUFBSSxJQUFJO2dCQUFFLFNBQVMsQ0FBQyxJQUFJLENBQUMsRUFBRSxJQUFJLEVBQUUsRUFBRSxFQUFFLFFBQVEsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxDQUFDOztnQkFDL0MsT0FBTyxDQUFDLElBQUksQ0FBQyxRQUFRLENBQUMsRUFBRSxDQUFDLENBQUM7U0FDaEM7S0FDRjtJQUVELE9BQU8sRUFBRSxTQUFTLEVBQUUsT0FBTyxFQUFFLENBQUM7QUFDaEMsQ0FBQztBQUVELE1BQU0sY0FBYyxHQUFHLG9CQUFvQixDQUFDO0FBRTVDOzs7O0dBSUc7QUFDSCxNQUFxQixpQkFBa0IsU0FBUSx3Q0FBNEI7SUFnQ3pFLFlBQVksSUFBYyxFQUFFLE1BQWMsRUFBRSxJQUFLO1FBQy9DLEtBQUssQ0FBQyxJQUFJLEVBQUUsTUFBTSxFQUFFLElBQUksQ0FBQyxDQUFDO1FBQzFCLE1BQU0sRUFBRSxhQUFhLEVBQUUsR0FBRyxFQUFFLEdBQUcsSUFBSSxDQUFDLE9BQU8sQ0FBQztRQUM1QyxhQUFhLENBQUMsRUFBRSxHQUFHLEVBQUUsQ0FBQyxDQUFDO1FBQ3ZCLElBQUksQ0FBQyxHQUFHLEdBQUcsR0FBRyxDQUFDO0lBQ2pCLENBQUM7SUFFUyxLQUFLLENBQUMsZ0JBQWdCO1FBQzlCLE1BQU0sRUFBRSxLQUFLLEVBQUUsR0FBRyxNQUFNLElBQUksQ0FBQyxLQUFLLENBQUMsaUJBQWlCLENBQUMsQ0FBQztRQUV0RCxNQUFNLEtBQUssR0FBRyxNQUFNLElBQUEsMkNBQW1CLEVBQUM7WUFDdEMsT0FBTyxFQUFFLElBQUksQ0FBQyxHQUFHO1lBQ2pCLEtBQUssRUFBRSxFQUFFLEdBQUcsRUFBRSxLQUFLLENBQUMsR0FBRyxFQUFFLFNBQVMsRUFBRSxLQUFLLENBQUMsU0FBUyxFQUFFLElBQUksRUFBRSxLQUFLLENBQUMsSUFBSSxFQUFFO1NBQ3hFLENBQUMsQ0FBQztRQUVILE1BQU0sT0FBTyxHQUFHLElBQUksd0JBQWEsRUFBRSxDQUFDO1FBQ3BDLE1BQU0sSUFBSSxHQUFHLE1BQU0sSUFBQSxzQkFBZSxFQUFDLE9BQU8sRUFBRSxLQUFLLEVBQUUsbUJBQVksQ0FBQyxDQUFDO1FBQ2pFLE1BQU0sT0FBTyxHQUFHLElBQUEsaUNBQW1CLEVBQUMsS0FBSyxFQUFFLElBQUksRUFBRSxHQUFHLEVBQUUsQ0FBQyxJQUFJLElBQUksRUFBRSxDQUFDLENBQUM7UUFDbkUsTUFBTSxTQUFTLEdBQUcsQ0FBQyxJQUFJLENBQUMsU0FBUyxJQUFJLEVBQUUsQ0FBa0IsQ0FBQztRQUUxRCx5RUFBeUU7UUFDekUsMkVBQTJFO1FBQzNFLHlEQUF5RDtRQUN6RCxNQUFNLFFBQVEsR0FBRyxLQUFLLENBQUMsZ0JBQWdCLENBQUM7WUFDdEMsQ0FBQyxDQUFDLE1BQU0scUJBQXFCLENBQUMsT0FBTyxFQUFFLEtBQUssRUFBRSxTQUFTLENBQUM7WUFDeEQsQ0FBQyxDQUFDLElBQUksQ0FBQztRQUVULDZFQUE2RTtRQUM3RSwwRUFBMEU7UUFDMUUscUVBQXFFO1FBQ3JFLE1BQU0sVUFBVSxHQUFHLGNBQUksQ0FBQyxPQUFPLENBQUMsT0FBTyxDQUFDLEdBQUcsRUFBRSxFQUFFLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQztRQUMxRCxJQUFJLFFBQVEsRUFBRTtZQUNaLElBQUksQ0FBQyxxQkFBcUIsQ0FDeEIsY0FBSSxDQUFDLElBQUksQ0FBQyxjQUFJLENBQUMsT0FBTyxDQUFDLFVBQVUsQ0FBQyxFQUFFLFdBQVcsQ0FBQyxFQUNoRCxRQUFRLEVBQ1IsU0FBUyxDQUNWLENBQUM7U0FDSDtRQUNELElBQUEsa0JBQWEsRUFBQyxVQUFVLEVBQUUsT0FBTyxDQUFDLENBQUM7UUFFbkMsTUFBTSxFQUFFLFdBQVcsRUFBRSxTQUFTLEVBQUUsYUFBYSxFQUFFLEdBQUcsSUFBQSxnQkFBUyxFQUFDLElBQUksQ0FBQyxDQUFDO1FBQ2xFLElBQUksQ0FBQyxNQUFNLENBQUMsT0FBTyxDQUNqQix3QkFBd0IsSUFBSSxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLGVBQWUsQ0FBQyxNQUFNLElBQUksQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUNqRixLQUFLLENBQUMsUUFBUSxDQUNmLFNBQVMsSUFBSSxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQ3ZCLEtBQUssQ0FBQyxHQUFHLENBQ1YsS0FBSyxXQUFXLGlCQUFpQixhQUFhLGNBQWMsQ0FDOUQsQ0FBQztJQUNKLENBQUM7SUFFRDs7Ozs7Ozs7T0FRRztJQUNLLHFCQUFxQixDQUMzQixHQUFXLEVBQ1gsUUFBMEIsRUFDMUIsU0FBd0I7UUFFeEIsSUFBSSxRQUFRLENBQUMsU0FBUyxDQUFDLE1BQU0sS0FBSyxDQUFDLElBQUksQ0FBQyxJQUFBLGVBQVUsRUFBQyxHQUFHLENBQUMsRUFBRTtZQUN2RCxJQUFJLENBQUMsV0FBVyxDQUFDLFFBQVEsQ0FBQyxPQUFPLENBQUMsQ0FBQztZQUNuQyxPQUFPO1NBQ1I7UUFFRCxJQUFBLGNBQVMsRUFBQyxHQUFHLEVBQUUsRUFBRSxTQUFTLEVBQUUsSUFBSSxFQUFFLENBQUMsQ0FBQztRQUNwQyxRQUFRLENBQUMsU0FBUyxDQUFDLE9BQU8sQ0FBQyxDQUFDLEVBQUUsSUFBSSxFQUFFLEVBQUUsRUFBRSxFQUFFLEVBQUUsQ0FDMUMsSUFBQSxrQkFBYSxFQUFDLElBQUEsOEJBQVcsRUFBQyxHQUFHLEVBQUUsRUFBRSxDQUFXLEVBQUUsSUFBSSxDQUFDLENBQ3BELENBQUM7UUFFRixNQUFNLElBQUksR0FBRyxJQUFJLEdBQUcsQ0FBQyxTQUFTLENBQUMsR0FBRyxDQUFDLFFBQVEsQ0FBQyxFQUFFLENBQUMsTUFBTSxDQUFDLFFBQVEsQ0FBQyxFQUFFLENBQUMsQ0FBQyxDQUFDLENBQUM7UUFDckUsSUFBQSxzQ0FBbUIsRUFBQyxJQUFBLGdCQUFXLEVBQUMsR0FBRyxDQUFDLEVBQUUsSUFBSSxDQUFDLENBQUMsT0FBTyxDQUFDLEtBQUssQ0FBQyxFQUFFO1lBQzFELElBQUEsZUFBVSxFQUFDLGNBQUksQ0FBQyxJQUFJLENBQUMsR0FBRyxFQUFFLEtBQUssQ0FBQyxDQUFDLENBQUM7WUFDbEMsSUFBSSxDQUFDLEdBQUcsQ0FBQyw0QkFBNEIsS0FBSyxFQUFFLENBQUMsQ0FBQztRQUNoRCxDQUFDLENBQUMsQ0FBQztRQUVILElBQUksQ0FBQyxHQUFHLENBQ04sT0FBTyxRQUFRLENBQUMsU0FBUyxDQUFDLE1BQU0sb0JBQW9CLGNBQUksQ0FBQyxRQUFRLENBQUMsT0FBTyxDQUFDLEdBQUcsRUFBRSxFQUFFLEdBQUcsQ0FBQyxHQUFHLENBQ3pGLENBQUM7UUFDRixJQUFJLENBQUMsV0FBVyxDQUFDLFFBQVEsQ0FBQyxPQUFPLENBQUMsQ0FBQztJQUNyQyxDQUFDO0lBRU8sV0FBVyxDQUFDLE9BQWlCO1FBQ25DLElBQUksT0FBTyxDQUFDLE1BQU0sS0FBSyxDQUFDO1lBQUUsT0FBTztRQUVqQyxJQUFJLENBQUMsTUFBTSxDQUFDLElBQUksQ0FDZCxXQUFXLE9BQU8sQ0FBQyxNQUFNLHNGQUFzRjtZQUM3RyxxQ0FBcUMsT0FBTyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUM3RCxDQUFDO0lBQ0osQ0FBQzs7QUE3SEgsb0NBOEhDO0FBM0hpQiw2QkFBVyxHQUN6QiwrRUFBK0UsQ0FBQztBQUVsRSx1QkFBSyxHQUFHO0lBQ3RCLEdBQUcsRUFBRSxZQUFLLENBQUMsTUFBTSxDQUFDO1FBQ2hCLElBQUksRUFBRSxHQUFHO1FBQ1QsV0FBVyxFQUNULDBGQUEwRjtZQUMxRixrREFBa0Q7S0FDckQsQ0FBQztJQUNGLElBQUksRUFBRSxZQUFLLENBQUMsTUFBTSxDQUFDO1FBQ2pCLElBQUksRUFBRSxHQUFHO1FBQ1QsV0FBVyxFQUFFLDRFQUE0RTtLQUMxRixDQUFDO0lBQ0YsU0FBUyxFQUFFLFlBQUssQ0FBQyxPQUFPLENBQUM7UUFDdkIsSUFBSSxFQUFFLEdBQUc7UUFDVCxXQUFXLEVBQUUsb0VBQW9FO0tBQ2xGLENBQUM7SUFDRixHQUFHLEVBQUUsWUFBSyxDQUFDLE1BQU0sQ0FBQztRQUNoQixJQUFJLEVBQUUsR0FBRztRQUNULFdBQVcsRUFBRSw4QkFBOEIsY0FBYyxJQUFJO1FBQzdELE9BQU8sRUFBRSxjQUFjO0tBQ3hCLENBQUM7SUFDRixnQkFBZ0IsRUFBRSxZQUFLLENBQUMsT0FBTyxDQUFDO1FBQzlCLFdBQVcsRUFDVCw2RkFBNkY7S0FDaEcsQ0FBQztDQUNILENBQUMifQ==