jarvis-launcher 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +153 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/backup.d.ts +53 -0
- package/dist/commands/backup.js +113 -0
- package/dist/commands/backup.js.map +1 -0
- package/dist/commands/enable.d.ts +24 -0
- package/dist/commands/enable.js +169 -0
- package/dist/commands/enable.js.map +1 -0
- package/dist/commands/init.d.ts +40 -0
- package/dist/commands/init.js +131 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/login.d.ts +42 -0
- package/dist/commands/login.js +62 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/registry-login.d.ts +20 -0
- package/dist/commands/registry-login.js +48 -0
- package/dist/commands/registry-login.js.map +1 -0
- package/dist/commands/restore.d.ts +53 -0
- package/dist/commands/restore.js +120 -0
- package/dist/commands/restore.js.map +1 -0
- package/dist/commands/start.d.ts +42 -0
- package/dist/commands/start.js +508 -0
- package/dist/commands/start.js.map +1 -0
- package/dist/commands/stop.d.ts +38 -0
- package/dist/commands/stop.js +56 -0
- package/dist/commands/stop.js.map +1 -0
- package/dist/commands/update.d.ts +34 -0
- package/dist/commands/update.js +83 -0
- package/dist/commands/update.js.map +1 -0
- package/dist/homepage/home.d.ts +94 -0
- package/dist/homepage/home.js +145 -0
- package/dist/homepage/home.js.map +1 -0
- package/dist/homepage/logo.d.ts +68 -0
- package/dist/homepage/logo.js +208 -0
- package/dist/homepage/logo.js.map +1 -0
- package/dist/homepage/menu.d.ts +123 -0
- package/dist/homepage/menu.js +222 -0
- package/dist/homepage/menu.js.map +1 -0
- package/dist/homepage/prompts.d.ts +196 -0
- package/dist/homepage/prompts.js +332 -0
- package/dist/homepage/prompts.js.map +1 -0
- package/dist/homepage/status-view.d.ts +15 -0
- package/dist/homepage/status-view.js +191 -0
- package/dist/homepage/status-view.js.map +1 -0
- package/dist/lib/arch.d.ts +50 -0
- package/dist/lib/arch.js +57 -0
- package/dist/lib/arch.js.map +1 -0
- package/dist/lib/backup.d.ts +101 -0
- package/dist/lib/backup.js +219 -0
- package/dist/lib/backup.js.map +1 -0
- package/dist/lib/compose.d.ts +70 -0
- package/dist/lib/compose.js +207 -0
- package/dist/lib/compose.js.map +1 -0
- package/dist/lib/config.d.ts +70 -0
- package/dist/lib/config.js +141 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/docker.d.ts +206 -0
- package/dist/lib/docker.js +272 -0
- package/dist/lib/docker.js.map +1 -0
- package/dist/lib/exec-error.d.ts +27 -0
- package/dist/lib/exec-error.js +58 -0
- package/dist/lib/exec-error.js.map +1 -0
- package/dist/lib/external-services.d.ts +46 -0
- package/dist/lib/external-services.js +201 -0
- package/dist/lib/external-services.js.map +1 -0
- package/dist/lib/health.d.ts +102 -0
- package/dist/lib/health.js +165 -0
- package/dist/lib/health.js.map +1 -0
- package/dist/lib/mongo.d.ts +124 -0
- package/dist/lib/mongo.js +134 -0
- package/dist/lib/mongo.js.map +1 -0
- package/dist/lib/ports.d.ts +92 -0
- package/dist/lib/ports.js +234 -0
- package/dist/lib/ports.js.map +1 -0
- package/dist/lib/profiles.d.ts +39 -0
- package/dist/lib/profiles.js +44 -0
- package/dist/lib/profiles.js.map +1 -0
- package/dist/lib/progress.d.ts +66 -0
- package/dist/lib/progress.js +79 -0
- package/dist/lib/progress.js.map +1 -0
- package/dist/lib/proxy-merge.d.ts +15 -0
- package/dist/lib/proxy-merge.js +282 -0
- package/dist/lib/proxy-merge.js.map +1 -0
- package/dist/lib/registry.d.ts +106 -0
- package/dist/lib/registry.js +179 -0
- package/dist/lib/registry.js.map +1 -0
- package/dist/lib/restore.d.ts +288 -0
- package/dist/lib/restore.js +534 -0
- package/dist/lib/restore.js.map +1 -0
- package/dist/lib/scenario.d.ts +83 -0
- package/dist/lib/scenario.js +150 -0
- package/dist/lib/scenario.js.map +1 -0
- package/dist/lib/seed-plan.d.ts +67 -0
- package/dist/lib/seed-plan.js +36 -0
- package/dist/lib/seed-plan.js.map +1 -0
- package/dist/lib/sql.d.ts +182 -0
- package/dist/lib/sql.js +286 -0
- package/dist/lib/sql.js.map +1 -0
- package/dist/lib/state.d.ts +148 -0
- package/dist/lib/state.js +178 -0
- package/dist/lib/state.js.map +1 -0
- package/dist/lib/status.d.ts +98 -0
- package/dist/lib/status.js +100 -0
- package/dist/lib/status.js.map +1 -0
- package/package.json +1 -1
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { realpathSync } from 'node:fs';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { Command, CommanderError } from 'commander';
|
|
6
|
+
import { registerInit } from './commands/init.js';
|
|
7
|
+
import { registerRegistryLoginCommand } from './commands/registry-login.js';
|
|
8
|
+
import { registerStartCommand } from './commands/start.js';
|
|
9
|
+
import { registerStopCommand } from './commands/stop.js';
|
|
10
|
+
import { registerRestoreCommand } from './commands/restore.js';
|
|
11
|
+
import { registerBackupCommand } from './commands/backup.js';
|
|
12
|
+
import { loginCommand } from './commands/login.js';
|
|
13
|
+
import { update } from './commands/update.js';
|
|
14
|
+
import { enable } from './commands/enable.js';
|
|
15
|
+
import { OPTIONAL_PROFILES, isOptionalProfile } from './lib/profiles.js';
|
|
16
|
+
import { runHomepage } from './homepage/home.js';
|
|
17
|
+
// Read version from package.json at runtime. Using createRequire keeps this
|
|
18
|
+
// compatible with NodeNext ESM without relying on a JSON import assertion
|
|
19
|
+
// (which would also pull package.json into the emitted dist/ tree via rootDir).
|
|
20
|
+
const require = createRequire(import.meta.url);
|
|
21
|
+
const pkg = require('../package.json');
|
|
22
|
+
const program = new Command();
|
|
23
|
+
program
|
|
24
|
+
.name('jarvis-launcher')
|
|
25
|
+
.description('Prepare a scenario folder and run the full Jarvis all-in-one stack via docker compose.')
|
|
26
|
+
.version(pkg.version);
|
|
27
|
+
// --- subcommand registration (T-2-23) ---------------------------------------
|
|
28
|
+
//
|
|
29
|
+
// The command modules were built in parallel and expose intentionally
|
|
30
|
+
// different shapes. `cli.ts` is the single place that knows about all of them and
|
|
31
|
+
// reconciles those shapes onto one program (commands never import each other):
|
|
32
|
+
//
|
|
33
|
+
// * `register…Command(program)` builders register themselves and return nothing
|
|
34
|
+
// useful to us — call them for their side effect: init, registry-login, start,
|
|
35
|
+
// stop, restore, backup.
|
|
36
|
+
// * `loginCommand()` returns a ready-made `Command` instance → `addCommand`.
|
|
37
|
+
// * `update` / `enable` export only an action function (no builder), so we
|
|
38
|
+
// declare the `program.command(...)` surface here and call the action.
|
|
39
|
+
registerInit(program);
|
|
40
|
+
registerRegistryLoginCommand(program);
|
|
41
|
+
registerStartCommand(program);
|
|
42
|
+
registerStopCommand(program);
|
|
43
|
+
registerRestoreCommand(program);
|
|
44
|
+
registerBackupCommand(program);
|
|
45
|
+
program.addCommand(loginCommand());
|
|
46
|
+
// `update [folder] --tag <tag>` — the action fn lives in commands/update.ts. The
|
|
47
|
+
// tag is required (the update is meaningless without a target image tag).
|
|
48
|
+
program
|
|
49
|
+
.command('update')
|
|
50
|
+
.description('Pull a new image tag and recreate the stack on it, preserving all data.')
|
|
51
|
+
.argument('[folder]', 'scenario folder (defaults to the current directory)')
|
|
52
|
+
.requiredOption('--tag <tag>', 'the image tag to update to (e.g. latest, 1.2.3)')
|
|
53
|
+
.action(async (folder, opts) => {
|
|
54
|
+
await update({ folder, tag: opts.tag });
|
|
55
|
+
});
|
|
56
|
+
// `enable [folder] <service>` — `service` is `sql` | `mail`. The action fn lives
|
|
57
|
+
// in commands/enable.ts; we validate the positional here so an unknown service
|
|
58
|
+
// yields a clear message + non-zero exit (mapped by the dispatcher below).
|
|
59
|
+
program
|
|
60
|
+
.command('enable')
|
|
61
|
+
.description('Attach an optional service (sql | mail) to a scenario stack.')
|
|
62
|
+
.argument('[folder]', 'scenario folder (defaults to the current directory)')
|
|
63
|
+
.argument('<service>', 'optional service to enable: sql or mail')
|
|
64
|
+
.action(async (folder, service) => {
|
|
65
|
+
if (!isOptionalProfile(service)) {
|
|
66
|
+
throw new Error(`Unknown service "${service}". Expected one of: ${OPTIONAL_PROFILES.join(', ')}.`);
|
|
67
|
+
}
|
|
68
|
+
await enable({ folder, service });
|
|
69
|
+
});
|
|
70
|
+
// `help [command]` — print general usage, or the usage of a single command. The
|
|
71
|
+
// argument is optional; with no argument it behaves like the top-level help.
|
|
72
|
+
program
|
|
73
|
+
.command('help [command]')
|
|
74
|
+
.description('Display help for the CLI or for a specific command.')
|
|
75
|
+
.action((commandName) => {
|
|
76
|
+
if (!commandName) {
|
|
77
|
+
program.outputHelp();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const sub = program.commands.find((c) => c.name() === commandName);
|
|
81
|
+
if (!sub) {
|
|
82
|
+
throw new Error(`Unknown command "${commandName}". Run "jarvis-launcher help" to list commands.`);
|
|
83
|
+
}
|
|
84
|
+
sub.outputHelp();
|
|
85
|
+
});
|
|
86
|
+
// --- no-args TTY dispatch (FR-HOME-001/002/003, SC-HOME-001/007) -------------
|
|
87
|
+
//
|
|
88
|
+
// "No subcommand" = only the node binary + this script path (`argv.length <= 2`).
|
|
89
|
+
// The interactive homepage may only launch when BOTH stdin and stdout are real
|
|
90
|
+
// TTYs (R-HOME5): a stdout-piped-but-stdin-a-TTY invocation must NOT hang waiting
|
|
91
|
+
// on a keypress, so requiring both keeps the non-interactive path on the
|
|
92
|
+
// help+exit branch below. Pure and exported so the three branches are unit-tested
|
|
93
|
+
// with no TTY (see cli.test.ts).
|
|
94
|
+
export function shouldRunHomepage(argv, stdinIsTTY, stdoutIsTTY) {
|
|
95
|
+
return argv.length <= 2 && Boolean(stdinIsTTY && stdoutIsTTY);
|
|
96
|
+
}
|
|
97
|
+
export function isEntrypoint(modulePath, argv1, canonicalize = realpathSync.native) {
|
|
98
|
+
if (!argv1) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
return canonicalize(modulePath) === canonicalize(argv1);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return modulePath === argv1;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// --- error → message + non-zero exit (FR-ORC-034/035/046) -------------------
|
|
109
|
+
//
|
|
110
|
+
// Every command's action is async; commander surfaces a rejected action promise
|
|
111
|
+
// out of `parseAsync`. We catch here and print only the error's `.message` —
|
|
112
|
+
// which the libs build to carry surfaced execa stderr verbatim — never a raw
|
|
113
|
+
// stack trace or an execa object dump. A `CommanderError` (e.g. a missing
|
|
114
|
+
// required option, or `--version`/`--help` exit) is commander's own controlled
|
|
115
|
+
// flow: it has already printed its message and chosen an exit code, so we honor
|
|
116
|
+
// that exit code instead of re-printing.
|
|
117
|
+
async function main() {
|
|
118
|
+
// No subcommand + a real interactive terminal ⇒ the always-on homepage
|
|
119
|
+
// (FR-HOME-001). Done BEFORE parseAsync so commander never sees the empty argv.
|
|
120
|
+
if (shouldRunHomepage(process.argv, Boolean(process.stdin.isTTY), Boolean(process.stdout.isTTY))) {
|
|
121
|
+
await runHomepage();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
// No subcommand but NOT interactive (piped / CI / non-TTY) ⇒ print help to
|
|
125
|
+
// stderr and exit non-zero, EXPLICITLY (FR-HOME-002, SC-HOME-007). Handled
|
|
126
|
+
// here rather than relying on commander's default no-args behavior so the
|
|
127
|
+
// help+non-zero guarantee can't regress with a commander version bump.
|
|
128
|
+
if (process.argv.length <= 2) {
|
|
129
|
+
program.outputHelp({ error: true });
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
// An explicit command ⇒ the existing dispatch path, completely unchanged
|
|
133
|
+
// (FR-HOME-003, FR-HOME-027), including the CommanderError → exit-code mapping.
|
|
134
|
+
try {
|
|
135
|
+
await program.parseAsync(process.argv);
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
if (err instanceof CommanderError) {
|
|
139
|
+
process.exit(err.exitCode);
|
|
140
|
+
}
|
|
141
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
142
|
+
console.error(`Error: ${message}`);
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// Run only when executed as the program entry point (not when imported by a
|
|
147
|
+
// test). Comparing this module's file path to the invoked script (argv[1]) keeps
|
|
148
|
+
// `import { shouldRunHomepage }` from cli.test.ts side-effect-free, so importing
|
|
149
|
+
// the module never spawns the homepage or touches process.argv/exit.
|
|
150
|
+
if (isEntrypoint(fileURLToPath(import.meta.url), process.argv[1])) {
|
|
151
|
+
void main();
|
|
152
|
+
}
|
|
153
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
/**
|
|
3
|
+
* commands/backup.ts — snapshot a scenario's current state into its seed folder.
|
|
4
|
+
*
|
|
5
|
+
* `backup [folder]` is the inverse of `restore`: it persists the running
|
|
6
|
+
* scenario's Mongo data (as `seed/dump`), document-store files (`seed/dsdata`)
|
|
7
|
+
* and automation assets (`seed/automation`) — making `seed/` the exact,
|
|
8
|
+
* re-restorable artifact `restore` consumes. The previous snapshot is replaced
|
|
9
|
+
* automatically; safety comes from staging, not a gate: the engine
|
|
10
|
+
* (`lib/backup.ts`) stages everything first and swaps only after the dump
|
|
11
|
+
* verifiably holds payload, so a failure leaves `seed/` untouched.
|
|
12
|
+
*
|
|
13
|
+
* Ordering mirrors `runRestore` step-for-step: `mongo` is brought up and awaited
|
|
14
|
+
* healthy BEFORE the dump (the ephemeral mongodump container needs a reachable
|
|
15
|
+
* `mongo` on the compose project network) — `es`/`jarvis` are not needed and not
|
|
16
|
+
* started. One extra guard the restore doesn't have: the empty-check runs in
|
|
17
|
+
* REVERSE — an **empty** database means there is nothing to back up, so the
|
|
18
|
+
* command refuses rather than letting an empty dump clobber a good seed.
|
|
19
|
+
*
|
|
20
|
+
* Mongo creds are read ONCE here (`compose.readMongoCreds`, R-ORC2) and passed
|
|
21
|
+
* to both the empty-check and the backup — nothing hard-codes a second copy.
|
|
22
|
+
*/
|
|
23
|
+
/** Options for {@link runBackup}. */
|
|
24
|
+
export interface RunBackupOptions {
|
|
25
|
+
/** Scenario folder (default: CWD, resolved in `lib/scenario`). */
|
|
26
|
+
folder?: string;
|
|
27
|
+
}
|
|
28
|
+
/** Summary of seed artifacts written by {@link runBackup}. */
|
|
29
|
+
export interface RunBackupResult {
|
|
30
|
+
/** Absolute path to the Mongo dump artifact. */
|
|
31
|
+
dumpPath: string;
|
|
32
|
+
/** Absolute path to the document-store artifact when it was refreshed. */
|
|
33
|
+
dsdataPath: string | null;
|
|
34
|
+
/** Absolute path to the automation artifact when it was refreshed. */
|
|
35
|
+
automationPath: string | null;
|
|
36
|
+
/** Files snapshotted into `seed/dsdata` (0 when `data/ds` was absent). */
|
|
37
|
+
dsFiles: number;
|
|
38
|
+
/** Top-level entries snapshotted into `seed/automation` (0 when absent). */
|
|
39
|
+
automationEntries: number;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Run the seed-snapshot backup for a scenario folder.
|
|
43
|
+
*
|
|
44
|
+
* @throws when the scenario was never started (no persisted state) or its
|
|
45
|
+
* database is empty (the caller maps this to a clear message + non-zero exit in
|
|
46
|
+
* `cli.ts`).
|
|
47
|
+
*/
|
|
48
|
+
export declare function runBackup(options?: RunBackupOptions): Promise<RunBackupResult>;
|
|
49
|
+
/**
|
|
50
|
+
* Register the `backup` subcommand on the commander program (wired by `cli.ts`).
|
|
51
|
+
* `folder` is an optional positional defaulting to CWD.
|
|
52
|
+
*/
|
|
53
|
+
export declare function registerBackupCommand(program: Command): void;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { composeUp, composeStop, listComposeServices } from '../lib/docker.js';
|
|
4
|
+
import { fetchListDatabases, isJarvisDataPresent } from '../lib/mongo.js';
|
|
5
|
+
import { generateEnv, readMongoCreds, templatePath } from '../lib/compose.js';
|
|
6
|
+
import { backup } from '../lib/backup.js';
|
|
7
|
+
import { readState, defaultProjectName, envFilePath } from '../lib/state.js';
|
|
8
|
+
import { resolveScenario } from '../lib/scenario.js';
|
|
9
|
+
import { progress, createProgressBar } from '../lib/progress.js';
|
|
10
|
+
/**
|
|
11
|
+
* Run the seed-snapshot backup for a scenario folder.
|
|
12
|
+
*
|
|
13
|
+
* @throws when the scenario was never started (no persisted state) or its
|
|
14
|
+
* database is empty (the caller maps this to a clear message + non-zero exit in
|
|
15
|
+
* `cli.ts`).
|
|
16
|
+
*/
|
|
17
|
+
export async function runBackup(options = {}) {
|
|
18
|
+
const base = resolveScenario(options.folder);
|
|
19
|
+
// Single source of Mongo creds (R-ORC2): read once, pass everywhere.
|
|
20
|
+
const creds = readMongoCreds(base);
|
|
21
|
+
// A backup needs a started stack: the compose project name is persisted by
|
|
22
|
+
// `start`. With no state (never started) there is nothing to back up — fail
|
|
23
|
+
// fast with a clear, actionable message BEFORE any composeUp, mirroring
|
|
24
|
+
// `restore`/`stop`.
|
|
25
|
+
const state = await readState(base);
|
|
26
|
+
if (state === null) {
|
|
27
|
+
throw new Error(`No started stack found for "${base}" (no .orchestrator/state.json). ` +
|
|
28
|
+
'Run `jarvis-launcher start` first, then `backup`.');
|
|
29
|
+
}
|
|
30
|
+
const project = state.projectName ?? defaultProjectName(base);
|
|
31
|
+
const network = `${project}_default`;
|
|
32
|
+
const profiles = state.enabledProfiles ?? [];
|
|
33
|
+
// Regenerate the orchestrator-owned `.env` before any compose call (the
|
|
34
|
+
// template substitutes from it) — verbatim the restore pattern.
|
|
35
|
+
const template = templatePath();
|
|
36
|
+
const envFile = envFilePath(base);
|
|
37
|
+
const env = generateEnv({ base, state, tag: 'latest', profiles });
|
|
38
|
+
writeFileSync(envFile, env);
|
|
39
|
+
// Bring mongo up healthy BEFORE the empty-check/dump. `mongo` is a PUBLIC
|
|
40
|
+
// image (`mongo:6.0.15`) and the backup never starts `jarvis` at all, so the
|
|
41
|
+
// bring-up must NOT be auth-gated (same rationale as restore's).
|
|
42
|
+
progress('Starting MongoDB — waiting until it is healthy…');
|
|
43
|
+
await composeUp({
|
|
44
|
+
project,
|
|
45
|
+
template,
|
|
46
|
+
envFile,
|
|
47
|
+
services: ['mongo'],
|
|
48
|
+
wait: true,
|
|
49
|
+
authGate: false,
|
|
50
|
+
});
|
|
51
|
+
// The reverse of restore's gate: an EMPTY database means there is nothing to
|
|
52
|
+
// back up — refuse, so an empty dump can never clobber a good seed snapshot.
|
|
53
|
+
progress('Checking that the database has data to back up…');
|
|
54
|
+
const listDatabases = await fetchListDatabases({ creds, network });
|
|
55
|
+
if (!isJarvisDataPresent(listDatabases)) {
|
|
56
|
+
throw new Error('Nothing to back up: the scenario database is empty. ' +
|
|
57
|
+
'The existing seed folder was left untouched.');
|
|
58
|
+
}
|
|
59
|
+
// Render the engine's phase updates as one in-place progress bar (percentage +
|
|
60
|
+
// running status) instead of the raw mongodump chatter; warnings are collected
|
|
61
|
+
// and flushed AFTER the bar closes so they never corrupt the in-place render.
|
|
62
|
+
const bar = createProgressBar({ label: 'Backing up' });
|
|
63
|
+
const warnings = [];
|
|
64
|
+
const result = await backup({
|
|
65
|
+
base,
|
|
66
|
+
creds,
|
|
67
|
+
network,
|
|
68
|
+
onProgress: (update) => bar.update(update.percent, update.message),
|
|
69
|
+
onWarn: (message) => warnings.push(message),
|
|
70
|
+
});
|
|
71
|
+
bar.done();
|
|
72
|
+
for (const warning of warnings)
|
|
73
|
+
progress(`⚠️ ${warning}`);
|
|
74
|
+
const parts = ['seed/dump (mongo)'];
|
|
75
|
+
if (result.dsFiles > 0)
|
|
76
|
+
parts.push(`seed/dsdata (${result.dsFiles} document-store files)`);
|
|
77
|
+
if (result.automationEntries > 0)
|
|
78
|
+
parts.push('seed/automation');
|
|
79
|
+
progress(`Seed snapshot updated: ${parts.join(', ')}.`);
|
|
80
|
+
const backupResult = {
|
|
81
|
+
dumpPath: join(base, 'seed', 'dump'),
|
|
82
|
+
dsdataPath: result.dsFiles > 0 ? join(base, 'seed', 'dsdata') : null,
|
|
83
|
+
automationPath: result.automationEntries > 0 ? join(base, 'seed', 'automation') : null,
|
|
84
|
+
dsFiles: result.dsFiles,
|
|
85
|
+
automationEntries: result.automationEntries,
|
|
86
|
+
};
|
|
87
|
+
// The backup brought `mongo` up on its own to run the dump. Now that it's done,
|
|
88
|
+
// leave the environment as we found it: if the stack is NOT in a running state
|
|
89
|
+
// — the `jarvis` proxy is down, which is exactly how `lib/status` defines
|
|
90
|
+
// "running" — then that `mongo` was started solely for this backup, so stop the
|
|
91
|
+
// mongo container it spun up. When the stack IS running, `mongo` belongs to it
|
|
92
|
+
// and must stay up. `listComposeServices` is a read-only `docker ps` probe.
|
|
93
|
+
const running = await listComposeServices(project);
|
|
94
|
+
if (!running.includes('jarvis')) {
|
|
95
|
+
progress('Stopping MongoDB — it was started only for this backup…');
|
|
96
|
+
await composeStop({ project, template, envFile, services: ['mongo'] });
|
|
97
|
+
}
|
|
98
|
+
return backupResult;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Register the `backup` subcommand on the commander program (wired by `cli.ts`).
|
|
102
|
+
* `folder` is an optional positional defaulting to CWD.
|
|
103
|
+
*/
|
|
104
|
+
export function registerBackupCommand(program) {
|
|
105
|
+
program
|
|
106
|
+
.command('backup [folder]')
|
|
107
|
+
.description("Snapshot the running scenario's Mongo data, document-store files and automation assets " +
|
|
108
|
+
'into the seed folder (replaces the previous snapshot).')
|
|
109
|
+
.action(async (folder) => {
|
|
110
|
+
await runBackup({ folder });
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=backup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backup.js","sourceRoot":"","sources":["../../src/commands/backup.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type OptionalProfile } from '../lib/profiles.js';
|
|
2
|
+
import { type SeedSqlOptions } from '../lib/sql.js';
|
|
3
|
+
/** Inputs for {@link enable}. */
|
|
4
|
+
export interface EnableOptions {
|
|
5
|
+
/** Scenario folder (default: CWD). */
|
|
6
|
+
folder?: string;
|
|
7
|
+
/** Optional service to attach. */
|
|
8
|
+
service: OptionalProfile;
|
|
9
|
+
/**
|
|
10
|
+
* Bring the service up now (default true). When the stack is not yet running
|
|
11
|
+
* (no persisted state) pass `false` to only persist the profile for the next
|
|
12
|
+
* `start`.
|
|
13
|
+
*/
|
|
14
|
+
start?: boolean;
|
|
15
|
+
/** Injectable SQL-seed flow (default: `lib/sql`'s {@link seedSql}); tests pin it. */
|
|
16
|
+
seedSqlImpl?: (options: SeedSqlOptions) => Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Enable an optional service on a scenario stack — see the module doc for the
|
|
20
|
+
* ordered behavior. Persists the profile, regenerates `.env`, and (when running)
|
|
21
|
+
* attaches the single service without recreating the core stack; for `sql`,
|
|
22
|
+
* seeds from `<base>/seed/sqlbackup` when a `.bak` is present.
|
|
23
|
+
*/
|
|
24
|
+
export declare function enable(options: EnableOptions): Promise<void>;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { readState, writeState, defaultProjectName, envFilePath } from '../lib/state.js';
|
|
4
|
+
import { generateEnv, templatePath, DEFAULT_SQL_SA_PASSWORD } from '../lib/compose.js';
|
|
5
|
+
import { composeUp } from '../lib/docker.js';
|
|
6
|
+
import { allocateAttachedPorts } from '../lib/ports.js';
|
|
7
|
+
import { OPTIONAL_PROFILE_PORTS } from '../lib/profiles.js';
|
|
8
|
+
import { findBackupFile, seedSql } from '../lib/sql.js';
|
|
9
|
+
import { progress, createProgressBar } from '../lib/progress.js';
|
|
10
|
+
/**
|
|
11
|
+
* commands/enable.ts — attach an optional service (`sql` | `mail`) to a scenario
|
|
12
|
+
* (T-2-19).
|
|
13
|
+
*
|
|
14
|
+
* `enable [folder] <sql|mail>`:
|
|
15
|
+
* 1. resolve the folder (default CWD) → project name from persisted state;
|
|
16
|
+
* 2. add the requested profile to `state.enabledProfiles` and persist it
|
|
17
|
+
* (so a not-yet-running stack activates it on the next `start`);
|
|
18
|
+
* 3. regenerate `<base>/.orchestrator/.env` with the updated `COMPOSE_PROFILES`;
|
|
19
|
+
* 4. `docker compose up -d <service>` scoped to **only** the optional service —
|
|
20
|
+
* which attaches it to the existing project **without recreating** the core
|
|
21
|
+
* `jarvis`/`mongo`/`es` containers (FR-ORC-042);
|
|
22
|
+
* 5. for `sql`: when `<base>/seed/sqlbackup` holds a `.bak`, run the SQL-seed flow
|
|
23
|
+
* (FR-ORC-029); with no `.bak` the container is still started and reachable
|
|
24
|
+
* but not seeded (FR-ORC-028).
|
|
25
|
+
*
|
|
26
|
+
* `mail` (FR-ORC-030) needs no seed step — its env wiring already lives in
|
|
27
|
+
* `compose.generateEnv` (T-2-11); enabling the profile is enough to activate the
|
|
28
|
+
* mail container + those vars.
|
|
29
|
+
*
|
|
30
|
+
* The SQL-seed *logic* (`buildRestoreSql`, the `.bak`-presence guard, the live
|
|
31
|
+
* `seedSql` flow) lives in `lib/sql.ts` — a pure, unit-tested seam plus a
|
|
32
|
+
* DEFERRED live integration half — so BOTH `enable sql` and `start --sql` reuse
|
|
33
|
+
* it without importing one another.
|
|
34
|
+
*
|
|
35
|
+
* This command imports only `lib/*` — never another command (the command files
|
|
36
|
+
* stay mutually independent so the phase builds in parallel).
|
|
37
|
+
*/
|
|
38
|
+
/**
|
|
39
|
+
* Ensure `ports` carries a host port for every port the enabled `service`
|
|
40
|
+
* publishes ({@link OPTIONAL_PROFILE_PORTS}), allocating only the ones not already
|
|
41
|
+
* persisted (idempotent — a re-`enable` keeps the existing port and never rebinds).
|
|
42
|
+
* New ports slot into the scenario's contiguous base+offset block when free
|
|
43
|
+
* (base = the persisted proxy port), else an OS-assigned free port. Returns `ports`
|
|
44
|
+
* unchanged when all are already present. This is what backs the persisted
|
|
45
|
+
* `state.ports.sql`/`mailUi` so the container binds a known host port AND the
|
|
46
|
+
* homepage status header can surface the service's port + connect URL (without it
|
|
47
|
+
* the `.env` port renders empty, the container binds a random host port, and the
|
|
48
|
+
* header drops the row).
|
|
49
|
+
*/
|
|
50
|
+
async function ensurePortsFor(ports, service) {
|
|
51
|
+
const needed = OPTIONAL_PROFILE_PORTS[service].filter(({ stateKey }) => ports[stateKey] === undefined);
|
|
52
|
+
if (needed.length === 0) {
|
|
53
|
+
return ports;
|
|
54
|
+
}
|
|
55
|
+
const allocated = await allocateAttachedPorts(ports.proxy, needed.map(({ portKey }) => portKey));
|
|
56
|
+
const next = { ...ports };
|
|
57
|
+
for (const { portKey, stateKey } of needed) {
|
|
58
|
+
next[stateKey] = allocated[portKey];
|
|
59
|
+
}
|
|
60
|
+
return next;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Enable an optional service on a scenario stack — see the module doc for the
|
|
64
|
+
* ordered behavior. Persists the profile, regenerates `.env`, and (when running)
|
|
65
|
+
* attaches the single service without recreating the core stack; for `sql`,
|
|
66
|
+
* seeds from `<base>/seed/sqlbackup` when a `.bak` is present.
|
|
67
|
+
*/
|
|
68
|
+
export async function enable(options) {
|
|
69
|
+
const { service, start = true } = options;
|
|
70
|
+
const seedSqlImpl = options.seedSqlImpl ?? seedSql;
|
|
71
|
+
const base = resolve(options.folder ?? process.cwd());
|
|
72
|
+
// 1. Read state (regenerable; absent ⇒ stack never started). Derive the
|
|
73
|
+
// project name from state or the folder default.
|
|
74
|
+
const existing = await readState(base);
|
|
75
|
+
const projectName = existing?.projectName ?? defaultProjectName(base);
|
|
76
|
+
// The stack is "running" only when it has been started (persisted state with a
|
|
77
|
+
// real port block exists). With no state we must never synthesize placeholder
|
|
78
|
+
// `0` ports nor `composeUp` (that would publish an invalid `0:80`); we only
|
|
79
|
+
// record the profile so the next `start` activates it — regardless of the
|
|
80
|
+
// `start` flag the caller passed (FR-ORC-042, T-2-19).
|
|
81
|
+
const stackRunning = existing !== null;
|
|
82
|
+
const shouldStart = start && stackRunning;
|
|
83
|
+
// 2. Add the profile to the persisted set (idempotent) and persist.
|
|
84
|
+
const previousProfiles = existing?.enabledProfiles ?? [];
|
|
85
|
+
const enabledProfiles = previousProfiles.includes(service)
|
|
86
|
+
? previousProfiles
|
|
87
|
+
: [...previousProfiles, service];
|
|
88
|
+
// Carry the persisted ports when the stack is running, reserving a stable host
|
|
89
|
+
// port for the newly-enabled service (so the container binds a known port and the
|
|
90
|
+
// homepage status header can surface it — FR-ORC-042). When the stack is NOT
|
|
91
|
+
// running, persist an EMPTY port map: the next `start` re-probes, finds no real
|
|
92
|
+
// always-on ports, and allocates the whole block. Never synthesize placeholder
|
|
93
|
+
// `0` ports — a `0:80` publish is invalid and a persisted `0` would be reused.
|
|
94
|
+
let ports = existing?.ports ?? {};
|
|
95
|
+
if (shouldStart) {
|
|
96
|
+
ports = await ensurePortsFor(ports, service);
|
|
97
|
+
}
|
|
98
|
+
// Spread `existing` first so fields `enable` does not touch — notably the pinned
|
|
99
|
+
// image `tag` (and any future additive state) — survive; then override only the
|
|
100
|
+
// three this command owns. Dropping them would silently unpin the tag, so the
|
|
101
|
+
// next tag-less `start` falls back to `latest`. `{...null}` is `{}`, so this is
|
|
102
|
+
// also correct when the stack was never started.
|
|
103
|
+
const nextState = {
|
|
104
|
+
...existing,
|
|
105
|
+
projectName,
|
|
106
|
+
ports,
|
|
107
|
+
enabledProfiles,
|
|
108
|
+
};
|
|
109
|
+
await writeState(base, nextState);
|
|
110
|
+
// When the stack is not running yet, persist-only: the profile activates on the
|
|
111
|
+
// next `start`. Nothing is brought up now.
|
|
112
|
+
if (!shouldStart) {
|
|
113
|
+
progress(`Enabled the "${service}" service — it will start on the next \`start\`.`);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// 3. Regenerate the `.env` next to the persisted state with the updated
|
|
117
|
+
// profiles so `COMPOSE_PROFILES` activates the optional container. Ensure
|
|
118
|
+
// `.orchestrator/` exists (a not-yet-started folder may lack it).
|
|
119
|
+
const envFile = envFilePath(base);
|
|
120
|
+
await mkdir(dirname(envFile), { recursive: true });
|
|
121
|
+
const env = generateEnv({
|
|
122
|
+
base,
|
|
123
|
+
state: nextState,
|
|
124
|
+
tag: 'latest',
|
|
125
|
+
profiles: enabledProfiles,
|
|
126
|
+
});
|
|
127
|
+
await writeFile(envFile, env, 'utf8');
|
|
128
|
+
const template = templatePath();
|
|
129
|
+
// 4. Attach ONLY the optional service — scoped `up -d <service>` does NOT
|
|
130
|
+
// recreate the always-on jarvis/mongo/es (FR-ORC-042).
|
|
131
|
+
//
|
|
132
|
+
// Both optional services this command can enable are PUBLIC images (`sql` →
|
|
133
|
+
// mcr.microsoft.com/mssql/server, `mail` → axllent/mailpit) and never pull the
|
|
134
|
+
// authenticated `jarvis-dev` image, so skip the registry auth gate. Gating here
|
|
135
|
+
// would probe the ACR against the default canary — the possibly-unpublished
|
|
136
|
+
// `jarvis-dev:latest[-arm]` — and abort with a spurious "could not reach the
|
|
137
|
+
// image registry" (`no such manifest`) even though nothing this up touches the
|
|
138
|
+
// ACR (see ComposeUpOptions.authGate).
|
|
139
|
+
progress(`Starting the "${service}" container — waiting until it is healthy…`);
|
|
140
|
+
await composeUp({
|
|
141
|
+
project: projectName,
|
|
142
|
+
template,
|
|
143
|
+
envFile,
|
|
144
|
+
services: [service],
|
|
145
|
+
wait: true,
|
|
146
|
+
authGate: false,
|
|
147
|
+
});
|
|
148
|
+
// 5. SQL seeding (FR-ORC-029): only when a `.bak` is present. No `.bak` ⇒ the
|
|
149
|
+
// container is up + reachable but not seeded (FR-ORC-028).
|
|
150
|
+
if (service === 'sql') {
|
|
151
|
+
const bakFile = await findBackupFile(join(base, 'seed', 'sqlbackup'));
|
|
152
|
+
if (bakFile) {
|
|
153
|
+
progress(`Seeding SQL Server from "${basename(bakFile)}"…`);
|
|
154
|
+
// Render the seed's phases as a single in-place progress bar (percentage +
|
|
155
|
+
// status), mirroring the Mongo restore, rather than the raw `sqlcmd` chatter.
|
|
156
|
+
const bar = createProgressBar({ label: 'Restoring SQL' });
|
|
157
|
+
await seedSqlImpl({
|
|
158
|
+
base,
|
|
159
|
+
bakFile,
|
|
160
|
+
project: projectName,
|
|
161
|
+
saPassword: DEFAULT_SQL_SA_PASSWORD,
|
|
162
|
+
onProgress: (update) => bar.update(update.percent, update.message),
|
|
163
|
+
});
|
|
164
|
+
bar.done();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
progress(`The "${service}" service is ready.`);
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=enable.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"enable.js","sourceRoot":"","sources":["../../src/commands/enable.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
/**
|
|
3
|
+
* commands/init.ts — provision the three base config files into an empty or
|
|
4
|
+
* config-less scenario folder (FR-ORC-038/039/040).
|
|
5
|
+
*
|
|
6
|
+
* A thin command over `lib/scenario` + `lib/config`:
|
|
7
|
+
*
|
|
8
|
+
* 1. `scenario.prepareScenario(folder)` resolves the folder (default = CWD),
|
|
9
|
+
* scaffolds the data subfolders idempotently, and runs the fail-fast
|
|
10
|
+
* write-probe (on the base + the UID-sensitive `mongo/`/`es/` subpaths).
|
|
11
|
+
* It throws on a non-writable folder and reports the config state.
|
|
12
|
+
* 2. A present-but-invalid config (unparseable, or a partially-present set) is
|
|
13
|
+
* **never overwritten** (FR-ORC-040): we fail fast with a clear, actionable
|
|
14
|
+
* message before touching anything.
|
|
15
|
+
* 3. `config.provisionConfigs({ base, image })` copies only the **absent**
|
|
16
|
+
* configs out of the image's `/jarvis/reference/` — when all three are
|
|
17
|
+
* already present it is a no-op (the image is not even pulled). The image
|
|
18
|
+
* ref is **arch-correct** (`arch.imageTag('latest')`) so ARM hosts lift the
|
|
19
|
+
* `-arm` variant (R-ORC11); auth is gated inside `lib/docker`.
|
|
20
|
+
* 4. We log which files were newly provisioned (the lib returns their names).
|
|
21
|
+
*
|
|
22
|
+
* `start` (T-2-22) calls the **same libs** directly when config is absent — it
|
|
23
|
+
* does NOT import this command (commands never import other commands).
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Run the `init` action against an optional scenario folder argument.
|
|
27
|
+
*
|
|
28
|
+
* @param folder - Scenario folder; when omitted, `lib/scenario` defaults to the
|
|
29
|
+
* current working directory.
|
|
30
|
+
* @param tag - Optional base image tag to provision from; defaults to `latest`
|
|
31
|
+
* inside `resolveImageTag` (which still prompts for an alternative if the chosen
|
|
32
|
+
* tag turns out to be unpublished). `arch.imageTag` applies the `-arm` suffix.
|
|
33
|
+
*/
|
|
34
|
+
export declare function runInit(folder?: string, tag?: string): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Register the `init` subcommand on the commander program (called by cli.ts).
|
|
37
|
+
* `folder` is an optional positional that defaults to CWD (resolved in
|
|
38
|
+
* `lib/scenario`).
|
|
39
|
+
*/
|
|
40
|
+
export declare function registerInit(program: Command): void;
|