(
spec: DeploymentSpec,
resourceSecrets: EngineSecrets,
cfg: DeployConfig,
req: DeployRequirements,
componentEnv: Record<string, string> = {},
)
| 16 | import type { ComponentSecrets } from "./spec"; |
| 17 | |
| 18 | type DeploymentSpec = DeploymentSchemas["DeploymentSpec"]; |
| 19 | |
| 20 | // Resolve the operator's output dir, expanding a leading `~`. A shell expands that |
| 21 | // before the CLI sees it, but the interactive prompt and a --values file bypass the |
| 22 | // shell — there `~/bundle` would silently become a dir literally named "~" under the |
| 23 | // cwd. Only a leading segment expands, so `./~` still addresses a real "~" directory. |
| 24 | export function resolveOutputDir(dir: string): string { |
| 25 | const expanded = dir === "~" || dir.startsWith("~/") ? path.join(os.homedir(), dir.slice(1)) : dir; |
| 26 | return path.resolve(process.cwd(), expanded); |
| 27 | } |
| 28 | |
| 29 | export async function writeOutput( |
| 30 | spec: DeploymentSpec, |
| 31 | componentSecrets: Record<string, ComponentSecrets>, |
| 32 | cfg: DeployConfig, |
| 33 | req: DeployRequirements, |
| 34 | componentEnv: Record<string, string> = {}, |
| 35 | ): Promise<string[]> { |
| 36 | const dir = resolveOutputDir(cfg.outputDir); |
| 37 | |
| 38 | if (existsSync(dir)) { |
| 39 | const contents = await fs.readdir(dir); |
| 40 | if (contents.length > 0) { |
| 41 | if (!cfg.force) { |
| 42 | process.stderr.write(`output dir not empty: ${dir} (use --force to overwrite)\n`); |
| 43 | process.exit(1); |
| 44 | } |
| 45 | // force=true: wipe + recreate so stale files (an old engine.tar, a stray |
| 46 | // .env from a prior run, ...) don't end up in the new bundle. |
| 47 | await fs.rm(dir, { recursive: true, force: true }); |
| 48 | await fs.mkdir(dir, { recursive: true }); |
| 49 | } |
| 50 | } else { |
| 51 | await fs.mkdir(dir, { recursive: true }); |
| 52 | } |
| 53 | |
| 54 | const written: string[] = []; |
| 55 | // secret=true -> mode 0o600. The secret-bearing files: engine.env carries the |
| 56 | // provider keys + web-search key, a custom component's <name>.env may carry its |
| 57 | // own, and <name>-secrets.json is the resource-credential doc. The |
| 58 | // <name>-config.json files and deployment-spec.json are secret-free by |
| 59 | // construction — safe to share. |
| 60 | const emit = async (name: string, content: string, secret = false): Promise<void> => { |
| 61 | const out = path.join(dir, name); |
| 62 | const opts = secret ? { encoding: "utf-8" as const, mode: 0o600 } : { encoding: "utf-8" as const }; |
| 63 | await fs.writeFile(out, content, opts); |
| 64 | written.push(out); |
| 65 | }; |
| 66 | const json = (v: unknown): string => JSON.stringify(v, null, 2) + "\n"; |
| 67 | |
| 68 | // One <name>-config.json per component carrying a config blob (the engine's boot |
| 69 | // file today) — bind-mounted read-only by the renderer, secret-free. The full |
| 70 | // resolved spec is the deployment record. |
| 71 | for (const c of spec.components) { |
| 72 | if (c.config !== undefined) await emit(configFileName(c.name), json(c.config)); |
| 73 | } |
| 74 | // One secret doc per component that needs one, keyed by resource id — the |
| 75 | // resolver already split them so each component holds only its own credentials |
no test coverage detected