jarvis-launcher 1.2.0-alpha.6 → 1.2.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.
Files changed (125) hide show
  1. package/README.md +13 -11
  2. package/dist/cli.js +1 -152
  3. package/dist/commands/backup.js +1 -160
  4. package/dist/commands/delete.js +1 -0
  5. package/dist/commands/enable.js +1 -169
  6. package/dist/commands/init.js +2 -165
  7. package/dist/commands/login.js +1 -62
  8. package/dist/commands/registry-login.js +1 -46
  9. package/dist/commands/restore.js +1 -120
  10. package/dist/commands/start.js +4 -632
  11. package/dist/commands/stop.js +1 -56
  12. package/dist/commands/update.js +1 -83
  13. package/dist/homepage/home.js +7 -145
  14. package/dist/homepage/logo.js +12 -211
  15. package/dist/homepage/menu.js +1 -229
  16. package/dist/homepage/prompts.js +2 -521
  17. package/dist/homepage/seed-progress.js +1 -57
  18. package/dist/homepage/status-view.js +7 -192
  19. package/dist/lib/arch.js +1 -57
  20. package/dist/lib/azure-blob.js +1 -362
  21. package/dist/lib/backup.js +1 -298
  22. package/dist/lib/compose.js +3 -207
  23. package/dist/lib/config.js +2 -182
  24. package/dist/lib/credentials.js +2 -198
  25. package/dist/lib/docker.js +4 -337
  26. package/dist/lib/entra.js +1 -86
  27. package/dist/lib/exec-error.js +2 -58
  28. package/dist/lib/external-services.js +1 -201
  29. package/dist/lib/health.js +1 -166
  30. package/dist/lib/mongo.js +3 -134
  31. package/dist/lib/ports.js +1 -234
  32. package/dist/lib/profiles.js +1 -44
  33. package/dist/lib/progress.js +6 -162
  34. package/dist/lib/proxy-merge.js +3 -282
  35. package/dist/lib/registry.js +5 -192
  36. package/dist/lib/resources.js +9 -0
  37. package/dist/lib/restore.js +1 -582
  38. package/dist/lib/scenario.js +1 -158
  39. package/dist/lib/seed-plan.js +1 -37
  40. package/dist/lib/sql.js +1 -378
  41. package/dist/lib/state.js +2 -229
  42. package/dist/lib/status.js +1 -100
  43. package/dist/lib/terminal-style.js +1 -8
  44. package/dist/lib/version.js +1 -0
  45. package/package.json +3 -1
  46. package/dist/cli.d.ts +0 -3
  47. package/dist/cli.js.map +0 -1
  48. package/dist/commands/backup.d.ts +0 -64
  49. package/dist/commands/backup.js.map +0 -1
  50. package/dist/commands/enable.d.ts +0 -24
  51. package/dist/commands/enable.js.map +0 -1
  52. package/dist/commands/init.d.ts +0 -44
  53. package/dist/commands/init.js.map +0 -1
  54. package/dist/commands/login.d.ts +0 -42
  55. package/dist/commands/login.js.map +0 -1
  56. package/dist/commands/registry-login.d.ts +0 -21
  57. package/dist/commands/registry-login.js.map +0 -1
  58. package/dist/commands/restore.d.ts +0 -53
  59. package/dist/commands/restore.js.map +0 -1
  60. package/dist/commands/start.d.ts +0 -60
  61. package/dist/commands/start.js.map +0 -1
  62. package/dist/commands/stop.d.ts +0 -38
  63. package/dist/commands/stop.js.map +0 -1
  64. package/dist/commands/update.d.ts +0 -34
  65. package/dist/commands/update.js.map +0 -1
  66. package/dist/homepage/home.d.ts +0 -94
  67. package/dist/homepage/home.js.map +0 -1
  68. package/dist/homepage/logo.d.ts +0 -69
  69. package/dist/homepage/logo.js.map +0 -1
  70. package/dist/homepage/menu.d.ts +0 -123
  71. package/dist/homepage/menu.js.map +0 -1
  72. package/dist/homepage/prompts.d.ts +0 -241
  73. package/dist/homepage/prompts.js.map +0 -1
  74. package/dist/homepage/seed-progress.d.ts +0 -44
  75. package/dist/homepage/seed-progress.js.map +0 -1
  76. package/dist/homepage/status-view.d.ts +0 -15
  77. package/dist/homepage/status-view.js.map +0 -1
  78. package/dist/lib/arch.d.ts +0 -50
  79. package/dist/lib/arch.js.map +0 -1
  80. package/dist/lib/azure-blob.d.ts +0 -151
  81. package/dist/lib/azure-blob.js.map +0 -1
  82. package/dist/lib/backup.d.ts +0 -126
  83. package/dist/lib/backup.js.map +0 -1
  84. package/dist/lib/compose.d.ts +0 -70
  85. package/dist/lib/compose.js.map +0 -1
  86. package/dist/lib/config.d.ts +0 -93
  87. package/dist/lib/config.js.map +0 -1
  88. package/dist/lib/credentials.d.ts +0 -73
  89. package/dist/lib/credentials.js.map +0 -1
  90. package/dist/lib/docker.d.ts +0 -240
  91. package/dist/lib/docker.js.map +0 -1
  92. package/dist/lib/entra.d.ts +0 -69
  93. package/dist/lib/entra.js.map +0 -1
  94. package/dist/lib/exec-error.d.ts +0 -27
  95. package/dist/lib/exec-error.js.map +0 -1
  96. package/dist/lib/external-services.d.ts +0 -46
  97. package/dist/lib/external-services.js.map +0 -1
  98. package/dist/lib/health.d.ts +0 -102
  99. package/dist/lib/health.js.map +0 -1
  100. package/dist/lib/mongo.d.ts +0 -124
  101. package/dist/lib/mongo.js.map +0 -1
  102. package/dist/lib/ports.d.ts +0 -92
  103. package/dist/lib/ports.js.map +0 -1
  104. package/dist/lib/profiles.d.ts +0 -39
  105. package/dist/lib/profiles.js.map +0 -1
  106. package/dist/lib/progress.d.ts +0 -122
  107. package/dist/lib/progress.js.map +0 -1
  108. package/dist/lib/proxy-merge.d.ts +0 -15
  109. package/dist/lib/proxy-merge.js.map +0 -1
  110. package/dist/lib/registry.d.ts +0 -120
  111. package/dist/lib/registry.js.map +0 -1
  112. package/dist/lib/restore.d.ts +0 -313
  113. package/dist/lib/restore.js.map +0 -1
  114. package/dist/lib/scenario.d.ts +0 -91
  115. package/dist/lib/scenario.js.map +0 -1
  116. package/dist/lib/seed-plan.d.ts +0 -81
  117. package/dist/lib/seed-plan.js.map +0 -1
  118. package/dist/lib/sql.d.ts +0 -249
  119. package/dist/lib/sql.js.map +0 -1
  120. package/dist/lib/state.d.ts +0 -190
  121. package/dist/lib/state.js.map +0 -1
  122. package/dist/lib/status.d.ts +0 -98
  123. package/dist/lib/status.js.map +0 -1
  124. package/dist/lib/terminal-style.d.ts +0 -5
  125. package/dist/lib/terminal-style.js.map +0 -1
package/README.md CHANGED
@@ -116,7 +116,7 @@ Notes:
116
116
 
117
117
  ### Pre-seeding the initial state (`seed/state.json`)
118
118
 
119
- An integrator packaging a scenario can ship a **`seed/state.json`** file to give the seed an identity (a name and description) and pre-declare the environment's **initial** state, so the first `init`/`start` picks up the right project name, optional profiles, and image tag without anyone having to remember the flags. It is read **only when the scenario has no `.orchestrator/state.json` yet** (a brand-new environment); once the scenario has been started, its own `.orchestrator/state.json` drives everything and the seed is ignored.
119
+ An integrator packaging a scenario can ship a **`seed/state.json`** file to give the seed an identity (a name and description) and pre-declare the environment's **initial** state, so the first `init`/`start` picks up the right project name, optional profiles, and Jarvis version (image tag) without anyone having to remember the flags. It is read **only when the scenario has no `.orchestrator/state.json` yet** (a brand-new environment); once the scenario has been started, its own `.orchestrator/state.json` drives everything and the seed is ignored.
120
120
 
121
121
  Only the properties below are honored — anything else in the file (notably a `ports` block) is ignored, so you can copy a working environment's `.orchestrator/state.json` verbatim and only the portable bits are applied (fresh host ports are always re-allocated for the new machine):
122
122
 
@@ -136,19 +136,19 @@ Only the properties below are honored — anything else in the file (notably a `
136
136
  | `description` | free-text summary of what the seed installs (identity metadata; not applied to the environment) | none |
137
137
  | `projectName` | the `docker compose -p` project name | derived from the folder name |
138
138
  | `enabledProfiles` | optional profiles to turn on (`sql`, `mail`) | none |
139
- | `tag` | base image tag to pin the scenario to | `latest` |
139
+ | `tag` | Jarvis version (base image tag) to pin the scenario to | `latest` |
140
140
 
141
141
  `name` and `description` describe the seed itself — they are read back via `readSeedState` (the groundwork for letting the launcher list and choose a seed to import) but do not change what `start` brings up.
142
142
 
143
143
  Explicit CLI flags always win over the seed: `--tag` overrides `tag`, and `--sql`/`--mail` are **added to** the seed's `enabledProfiles`.
144
144
 
145
- In the interactive homepage, choosing **Start** on a brand-new (uninitialized) folder first asks for the **scenario folder** (default: the current directory — one Enter keeps it, like Stop/Restore/Backup), then opens a submenu for that folder. A **local `seed/` takes precedence** over the remote catalog — when the folder already ships one (any payload counts, including a config-only seed), the remote catalog is never contacted (no client-secret prompt / network):
145
+ The interactive homepage always operates on the **current working directory** — it never asks for a folder (use the CLI's positional `[folder]` to target elsewhere). Choosing **Start** on a brand-new (uninitialized) directory opens a submenu for it, and every branch creates a **new environment**, so each one first asks for the **environment name** (pre-filled with the name that would be derived anyway — the seed-declared one when present, else the folder name — so one Enter keeps it). A **local `seed/` takes precedence** over the remote catalog — when the directory already ships one (any payload counts, including a config-only seed), the remote catalog is never contacted (no client-secret prompt / network):
146
146
 
147
- - **Start from local seed** *(shown only when a local `seed/` is present)* — install the shipped seed by starting the folder. A seed **declared** by `seed/state.json` dictates its own environment and starts as-is (the status header above already previews what it will install); an **undeclared** seed (data/config only) still asks for the image tag and the optional SQL/mail toggles — with SQL defaulting to **on** when the seed ships a SQL backup.
148
- - **Start from an existing scenario** *(shown only when there is no local seed)* — pick from the **remote seed catalog**: the `.zip` blobs in the `seedintegratori`/`seeds` container, each shown by its blob-metadata `name`/`description`. Picking one downloads and extracts it into `<folder>/seed` (staged first — a failed download never leaves a half-installed or wiped seed), then starts it. An empty or unreachable catalog (offline / rejected secret) shows a short notice and returns.
149
- - **Start from an empty environment** — bring up a blank stack. If the folder ships a `seed/` (even a config-only one), this first asks to confirm and then **permanently deletes** the `seed/` folder (so the stack comes up empty instead of re-applying the seed), then asks for the image tag and the optional SQL/mail toggles.
147
+ - **Start from local seed** *(shown only when a local `seed/` is present)* — install the shipped seed by starting the directory. A seed **declared** by `seed/state.json` dictates the rest of its environment and starts right after the name prompt (the status header above already previews what it will install); an **undeclared** seed (data/config only) still asks for the Jarvis version and the optional SQL/mail toggles — with SQL defaulting to **on** when the seed ships a SQL backup.
148
+ - **Start from an existing scenario** *(shown only when there is no local seed)* — pick from the **remote seed catalog**: the `.zip` blobs in the `seedintegratori`/`seeds` container, each shown by its blob-metadata `name`/`description`. Picking one downloads and extracts it into the directory's `seed/` (staged first — a failed download never leaves a half-installed or wiped seed), asks for the environment name (pre-filled with the installed seed's declared name when it pins one), then starts it. An empty or unreachable catalog (offline / rejected secret) shows a short notice and returns.
149
+ - **Start from an empty environment** — bring up a blank stack. If the directory ships a `seed/` (even a config-only one), this first asks to confirm and then **permanently deletes** the `seed/` folder (so the stack comes up empty instead of re-applying the seed), then asks for the environment name, the Jarvis version and the optional SQL/mail toggles.
150
150
 
151
- Choosing **Start** on an already-initialized folder (a stopped environment) skips the folder prompt and the submenu and resumes it directly.
151
+ Choosing **Start** in an already-initialized directory (a stopped environment) skips the submenu — and the name prompt, since the environment already has its name — and resumes it directly.
152
152
 
153
153
  ### Pre-seeding the base configs (`seed/config/`)
154
154
 
@@ -189,10 +189,10 @@ After a restore, Elasticsearch repopulates in the background; the launcher print
189
189
  |---|---|---|
190
190
  | [`init`](#init) | `init [folder]` | Provision the three base config files into an empty/config-less folder. |
191
191
  | [`registry-login`](#registry-login) | `registry-login [--registry <host>] [--client-secret <secret>]` | Authenticate Docker against the image registry as the Entra application (once per machine). |
192
- | [`start`](#start) | `start [folder] [--sql] [--mail] [--force]` | Bring up the full stack (auto-init + auto-restore) and report the URLs. |
192
+ | [`start`](#start) | `start [folder] [--sql] [--mail] [--force] [--tag <T>] [--name <N>]` | Bring up the full stack (auto-init + auto-restore) and report the URLs. |
193
193
  | [`stop`](#stop) | `stop [folder]` | Stop the stack (remove containers) while keeping all on-disk data. |
194
194
  | [`restore`](#restore) | `restore [folder] [--force]` | Restore the prepared artifact into the running stack (gated by `--force`). |
195
- | [`update`](#update) | `update [folder] --tag <T>` | Pull a new image tag and recreate the stack on it, preserving data. |
195
+ | [`update`](#update) | `update [folder] --tag <T>` | Update the stack to another Jarvis version (pull + recreate), preserving data. |
196
196
  | [`enable`](#enable) | `enable [folder] <sql\|mail>` | Attach an optional service (SQL Server or mail catcher) to the stack. |
197
197
  | [`login`](#login) | `login [folder] [--admin\|--user]` | Open the browser at the Jarvis dev-login (impersonation) URL. |
198
198
 
@@ -217,7 +217,7 @@ Forces a fresh `docker login` against the integrator registry (as the Entra appl
217
217
  ### `start`
218
218
 
219
219
  ```bash
220
- jarvis-launcher start [folder] [--sql] [--mail] [--force]
220
+ jarvis-launcher start [folder] [--sql] [--mail] [--force] [--tag <tag>] [--name <name>]
221
221
  ```
222
222
 
223
223
  Brings up the full, logged-in environment in one command. See [Quick start](#quick-start-brand-new-scenario) for the brand-new-folder flow.
@@ -227,6 +227,8 @@ Brings up the full, logged-in environment in one command. See [Quick start](#qui
227
227
  | `--sql` | Also start the optional **SQL Server** service. If a `.bak` is present under `<folder>/seed/sqlbackup`, it is restored after the SQL container is healthy. |
228
228
  | `--mail` | Also start the optional **mail catcher** (catch-all SMTP + web UI). |
229
229
  | `--force` | Run the restore **even if the database already has data** (drops the existing data first). Without it, a populated DB is left as-is. |
230
+ | `--tag <tag>` | The Jarvis version (image tag) to bring the stack up on (defaults to `latest`, or the version the environment is already pinned to). |
231
+ | `--name <name>` | The environment name (the compose project name) for a **brand-new** environment; defaults to the seed-declared name, else the folder name. Ignored on an existing environment — its persisted name always wins. |
230
232
 
231
233
  On success it prints the Web UI / API URL, both dev-login links, and the Mongo / ES (and optional SQL / mail) host ports. Enabled optional profiles are remembered, so a later plain `start` keeps them on.
232
234
 
@@ -260,7 +262,7 @@ The explicit `restore` never touches the three base config files at the scenario
260
262
  jarvis-launcher update [folder] --tag <tag>
261
263
  ```
262
264
 
263
- Moves the stack to a new image tag, **preserving all data and without restoring**. It is **pull-first** so you are never left with no runnable environment:
265
+ Moves the stack to another Jarvis version (image tag), **preserving all data and without restoring**. It is **pull-first** so you are never left with no runnable environment:
264
266
 
265
267
  1. resolves the architecture-correct image variant (a `-arm` suffix is added automatically on ARM hosts) and **pulls it first** (re-authenticating + retrying once if needed);
266
268
  2. **only if the pull succeeds**, recreates the stack on the new image (down → up, data kept via the `/scenario` host mount and the Mongo/ES named volumes) — **no restore**, the existing data is reused;
package/dist/cli.js CHANGED
@@ -1,153 +1,2 @@
1
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
2
+ var a=Object.defineProperty;var n=(r,e)=>a(r,"name",{value:e,configurable:!0});import{realpathSync as i}from"node:fs";import{createRequire as s}from"node:module";import{fileURLToPath as m}from"node:url";import{Command as p,CommanderError as c}from"commander";import{registerInit as l}from"./commands/init.js";import{registerRegistryLoginCommand as d}from"./commands/registry-login.js";import{registerStartCommand as f}from"./commands/start.js";import{registerStopCommand as u}from"./commands/stop.js";import{registerRestoreCommand as g}from"./commands/restore.js";import{registerBackupCommand as h}from"./commands/backup.js";import{registerDeleteCommand as v}from"./commands/delete.js";import{loginCommand as y}from"./commands/login.js";import{update as C}from"./commands/update.js";import{enable as w}from"./commands/enable.js";import{OPTIONAL_PROFILES as k,isOptionalProfile as E}from"./lib/profiles.js";import{runHomepage as x}from"./homepage/home.js";const R=s(import.meta.url),T=R("../package.json"),o=new p;o.name("jarvis-launcher").description("Prepare a scenario folder and run the full Jarvis all-in-one stack via docker compose.").version(T.version),l(o),d(o),f(o),u(o),g(o),h(o),v(o),o.addCommand(y()),o.command("update").description("Update the stack to another Jarvis version (pull + recreate), preserving all data.").argument("[folder]","scenario folder (defaults to the current directory)").requiredOption("--tag <tag>","the Jarvis version (image tag) to update to (e.g. latest, 1.2.3)").action(async(r,e)=>{await C({folder:r,tag:e.tag})}),o.command("enable").description("Attach an optional service (sql | mail) to a scenario stack.").argument("[folder]","scenario folder (defaults to the current directory)").argument("<service>","optional service to enable: sql or mail").action(async(r,e)=>{if(!E(e))throw new Error(`Unknown service "${e}". Expected one of: ${k.join(", ")}.`);await w({folder:r,service:e})}),o.command("help [command]").description("Display help for the CLI or for a specific command.").action(r=>{if(!r){o.outputHelp();return}const e=o.commands.find(t=>t.name()===r);if(!e)throw new Error(`Unknown command "${r}". Run "jarvis-launcher help" to list commands.`);e.outputHelp()});function b(r,e,t){return r.length<=2&&!!(e&&t)}n(b,"shouldRunHomepage");function q(r,e,t=i.native){if(!e)return!1;try{return t(r)===t(e)}catch{return r===e}}n(q,"isEntrypoint");async function H(){if(b(process.argv,!!process.stdin.isTTY,!!process.stdout.isTTY)){await x();return}process.argv.length<=2&&(o.outputHelp({error:!0}),process.exit(1));try{await o.parseAsync(process.argv)}catch(r){r instanceof c&&process.exit(r.exitCode);const e=r instanceof Error?r.message:String(r);console.error(`Error: ${e}`),process.exit(1)}}n(H,"main"),q(m(import.meta.url),process.argv[1])&&H();export{q as isEntrypoint,b as shouldRunHomepage};
@@ -1,160 +1 @@
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, DEFAULT_SQL_SA_PASSWORD } from '../lib/compose.js';
6
- import { backup } from '../lib/backup.js';
7
- import { findBackupFile, snapshotSql } from '../lib/sql.js';
8
- import { readState, defaultProjectName, envFilePath } from '../lib/state.js';
9
- import { resolveScenario } from '../lib/scenario.js';
10
- import { progress, createProgressBar } from '../lib/progress.js';
11
- /**
12
- * Run the seed-snapshot backup for a scenario folder.
13
- *
14
- * @throws when the scenario was never started (no persisted state) or its
15
- * database is empty (the caller maps this to a clear message + non-zero exit in
16
- * `cli.ts`).
17
- */
18
- export async function runBackup(options = {}) {
19
- const base = resolveScenario(options.folder);
20
- // Single source of Mongo creds (R-ORC2): read once, pass everywhere.
21
- const creds = readMongoCreds(base);
22
- // A backup needs a started stack: the compose project name is persisted by
23
- // `start`. With no state (never started) there is nothing to back up — fail
24
- // fast with a clear, actionable message BEFORE any composeUp, mirroring
25
- // `restore`/`stop`.
26
- const state = await readState(base);
27
- if (state === null) {
28
- throw new Error(`No started stack found for "${base}" (no .orchestrator/state.json). ` +
29
- 'Run `jarvis-launcher start` first, then `backup`.');
30
- }
31
- const project = state.projectName ?? defaultProjectName(base);
32
- const network = `${project}_default`;
33
- const profiles = state.enabledProfiles ?? [];
34
- // When the scenario runs SQL, the snapshot must also refresh its `.bak` — so the
35
- // `sql` service is brought up alongside `mongo` (both PUBLIC images) and stopped
36
- // again afterwards on the same terms. With SQL off there is nothing to snapshot.
37
- const sqlEnabled = profiles.includes('sql');
38
- const startupServices = sqlEnabled ? ['mongo', 'sql'] : ['mongo'];
39
- // Regenerate the orchestrator-owned `.env` before any compose call (the
40
- // template substitutes from it) — verbatim the restore pattern.
41
- const template = templatePath();
42
- const envFile = envFilePath(base);
43
- const env = generateEnv({ base, state, tag: 'latest', profiles });
44
- writeFileSync(envFile, env);
45
- // Bring mongo (and sql, when enabled) up healthy BEFORE the empty-check/dump.
46
- // Both are PUBLIC images (`mongo:6.0.15`, `mcr.microsoft.com/mssql/server`) and
47
- // the backup never starts `jarvis` at all, so the bring-up must NOT be
48
- // auth-gated (same rationale as restore's). `sql` has no compose healthcheck, so
49
- // `--wait` only confirms it is running — `snapshotSql` does its own SELECT-1
50
- // wait-healthy poll before the BACKUP.
51
- progress(sqlEnabled
52
- ? 'Starting MongoDB and SQL Server — waiting until they are up…'
53
- : 'Starting MongoDB — waiting until it is healthy…');
54
- await composeUp({
55
- project,
56
- template,
57
- envFile,
58
- services: startupServices,
59
- wait: true,
60
- authGate: false,
61
- });
62
- // The reverse of restore's gate: an EMPTY database means there is nothing to
63
- // back up — refuse, so an empty dump can never clobber a good seed snapshot.
64
- progress('Checking that the database has data to back up…');
65
- const listDatabases = await fetchListDatabases({ creds, network });
66
- if (!isJarvisDataPresent(listDatabases)) {
67
- throw new Error('Nothing to back up: the scenario database is empty. ' +
68
- 'The existing seed folder was left untouched.');
69
- }
70
- // Render the engine's phase updates as one in-place progress bar (percentage +
71
- // running status) instead of the raw mongodump chatter; warnings are collected
72
- // and flushed AFTER the bar closes so they never corrupt the in-place render.
73
- const bar = createProgressBar({ label: 'Backing up' });
74
- const warnings = [];
75
- const result = await backup({
76
- base,
77
- creds,
78
- network,
79
- onProgress: (update) => bar.update(update.percent, update.message),
80
- onWarn: (message) => warnings.push(message),
81
- });
82
- bar.done();
83
- for (const warning of warnings)
84
- progress(`⚠️ ${warning}`);
85
- // Snapshot SQL too, when the scenario runs it: refresh the seed's `.bak` from the
86
- // live database so the seed is a complete, re-restorable artifact (Mongo + SQL).
87
- // Nothing to do when SQL is off, or when the seed ships no `.bak` to refresh (SQL
88
- // was never seeded from this scenario — `findBackupFile` returns null; there is no
89
- // database name to snapshot against). Its own progress bar mirrors the Mongo one.
90
- let sqlBackupPath = null;
91
- if (sqlEnabled) {
92
- const bakFile = await findBackupFile(join(base, 'seed', 'sqlbackup'));
93
- if (bakFile) {
94
- progress(`Backing up SQL Server to "${bakFile}"…`);
95
- const sqlBar = createProgressBar({ label: 'Backing up SQL' });
96
- await snapshotSql({
97
- base,
98
- bakFile,
99
- project,
100
- saPassword: DEFAULT_SQL_SA_PASSWORD,
101
- onProgress: (update) => sqlBar.update(update.percent, update.message),
102
- });
103
- sqlBar.done();
104
- sqlBackupPath = join(base, 'seed', 'sqlbackup', bakFile);
105
- }
106
- else {
107
- progress('⚠️ SQL is enabled but seed/sqlbackup holds no .bak to refresh — skipping the SQL snapshot.');
108
- }
109
- }
110
- const parts = ['seed/dump (mongo)'];
111
- if (result.dsFiles > 0)
112
- parts.push(`seed/dsdata (${result.dsFiles} document-store files)`);
113
- if (result.automationEntries > 0)
114
- parts.push('seed/automation');
115
- if (result.keyEntries > 0)
116
- parts.push('seed/keys (encryption keys)');
117
- if (result.configFiles > 0)
118
- parts.push('seed/config (base configuration)');
119
- if (sqlBackupPath)
120
- parts.push('seed/sqlbackup (sql)');
121
- progress(`Seed snapshot updated: ${parts.join(', ')}.`);
122
- const backupResult = {
123
- dumpPath: join(base, 'seed', 'dump'),
124
- dsdataPath: result.dsFiles > 0 ? join(base, 'seed', 'dsdata') : null,
125
- automationPath: result.automationEntries > 0 ? join(base, 'seed', 'automation') : null,
126
- keysPath: result.keyEntries > 0 ? join(base, 'seed', 'keys') : null,
127
- configPath: result.configFiles > 0 ? join(base, 'seed', 'config') : null,
128
- sqlBackupPath,
129
- dsFiles: result.dsFiles,
130
- automationEntries: result.automationEntries,
131
- keyEntries: result.keyEntries,
132
- configFiles: result.configFiles,
133
- };
134
- // The backup brought `mongo` (and `sql`, when enabled) up on its own to run the
135
- // dump/snapshot. Now that it's done, leave the environment as we found it: if the
136
- // stack is NOT in a running state — the `jarvis` proxy is down, which is exactly
137
- // how `lib/status` defines "running" — then those containers were started solely
138
- // for this backup, so stop them. When the stack IS running, they belong to it and
139
- // must stay up. `listComposeServices` is a read-only `docker ps` probe.
140
- const running = await listComposeServices(project);
141
- if (!running.includes('jarvis')) {
142
- progress('Stopping the containers started only for this backup…');
143
- await composeStop({ project, template, envFile, services: startupServices });
144
- }
145
- return backupResult;
146
- }
147
- /**
148
- * Register the `backup` subcommand on the commander program (wired by `cli.ts`).
149
- * `folder` is an optional positional defaulting to CWD.
150
- */
151
- export function registerBackupCommand(program) {
152
- program
153
- .command('backup [folder]')
154
- .description("Snapshot the running scenario's Mongo data, document-store files, automation assets " +
155
- 'and base config files into the seed folder (replaces the previous snapshot).')
156
- .action(async (folder) => {
157
- await runBackup({ folder });
158
- });
159
- }
160
- //# sourceMappingURL=backup.js.map
1
+ var q=Object.defineProperty;var i=(r,e)=>q(r,"name",{value:e,configurable:!0});import{writeFileSync as B}from"node:fs";import{join as a}from"node:path";import{composeUp as j,composeStop as L,listComposeServices as D}from"../lib/docker.js";import{fetchListDatabases as Q,isJarvisDataPresent as $}from"../lib/mongo.js";import{generateEnv as C,readMongoCreds as M,templatePath as N,DEFAULT_SQL_SA_PASSWORD as _}from"../lib/compose.js";import{backup as x}from"../lib/backup.js";import{findBackupFile as A,snapshotSql as R}from"../lib/sql.js";import{readState as T,defaultProjectName as U,envFilePath as W}from"../lib/state.js";import{resolveScenario as G}from"../lib/scenario.js";import{progress as o,createProgressBar as P}from"../lib/progress.js";async function J(r={}){const e=G(r.folder),f=M(e),l=await T(e);if(l===null)throw new Error(`No started stack found for "${e}" (no .orchestrator/state.json). Run \`jarvis-launcher start\` first, then \`backup\`.`);const c=l.projectName??U(e),m=`${c}_default`,h=l.enabledProfiles??[],u=h.includes("sql"),g=u?["mongo","sql"]:["mongo"],k=N(),p=W(e),v=C({base:e,state:l,tag:"latest",profiles:h});B(p,v),o(u?"Starting MongoDB and SQL Server \u2014 waiting until they are up\u2026":"Starting MongoDB \u2014 waiting until it is healthy\u2026"),await j({project:c,template:k,envFile:p,services:g,wait:!0,authGate:!1}),o("Checking that the database has data to back up\u2026");const F=await Q({creds:f,network:m});if(!$(F))throw new Error("Nothing to back up: the scenario database is empty. The existing seed folder was left untouched.");const b=P({label:"Backing up"}),S=[],t=await x({base:e,creds:f,network:m,onProgress:i(s=>b.update(s.percent,s.message),"onProgress"),onWarn:i(s=>S.push(s),"onWarn")});b.done();for(const s of S)o(`\u26A0\uFE0F ${s}`);let d=null;if(u){const s=await A(a(e,"seed","sqlbackup"));if(s){o(`Backing up SQL Server to "${s}"\u2026`);const w=P({label:"Backing up SQL"});await R({base:e,bakFile:s,project:c,saPassword:_,onProgress:i(y=>w.update(y.percent,y.message),"onProgress")}),w.done(),d=a(e,"seed","sqlbackup",s)}else o("\u26A0\uFE0F SQL is enabled but seed/sqlbackup holds no .bak to refresh \u2014 skipping the SQL snapshot.")}const n=["seed/dump (mongo)"];t.dsFiles>0&&n.push(`seed/dsdata (${t.dsFiles} document-store files)`),t.automationEntries>0&&n.push("seed/automation"),t.keyEntries>0&&n.push("seed/keys (encryption keys)"),t.configFiles>0&&n.push("seed/config (base configuration)"),d&&n.push("seed/sqlbackup (sql)"),o(`Seed snapshot updated: ${n.join(", ")}.`);const E={dumpPath:a(e,"seed","dump"),dsdataPath:t.dsFiles>0?a(e,"seed","dsdata"):null,automationPath:t.automationEntries>0?a(e,"seed","automation"):null,keysPath:t.keyEntries>0?a(e,"seed","keys"):null,configPath:t.configFiles>0?a(e,"seed","config"):null,sqlBackupPath:d,dsFiles:t.dsFiles,automationEntries:t.automationEntries,keyEntries:t.keyEntries,configFiles:t.configFiles};return(await D(c)).includes("jarvis")||(o("Stopping the containers started only for this backup\u2026"),await L({project:c,template:k,envFile:p,services:g})),E}i(J,"runBackup");function ae(r){r.command("backup [folder]").description("Snapshot the running scenario's Mongo data, document-store files, automation assets and base config files into the seed folder (replaces the previous snapshot).").action(async e=>{await J({folder:e})})}i(ae,"registerBackupCommand");export{ae as registerBackupCommand,J as runBackup};
@@ -0,0 +1 @@
1
+ var a=Object.defineProperty;var n=(t,e)=>a(t,"name",{value:e,configurable:!0});import{confirm as s}from"@inquirer/prompts";import{destroyComposeProject as l}from"../lib/docker.js";import{readState as c,defaultProjectName as d}from"../lib/state.js";import{resolveScenario as m,configState as f,emptyScenarioFolder as u}from"../lib/scenario.js";import{progress as i}from"../lib/progress.js";function p(t,e){return`This will permanently DELETE the "${t}" environment: its docker containers, data volumes and network, plus ALL files in "${e}" (the folder will be emptied, including external-service folders and any backups stored there). Docker images are kept. This cannot be undone. Continue?`}n(p,"deleteConfirmationMessage");async function h(t,e){if(!process.stdin.isTTY)throw new Error(`Refusing to delete the environment "${t}" without confirmation. Re-run with --yes to delete non-interactively. Nothing was deleted.`);return s({message:p(t,e),default:!1})}n(h,"promptDeleteConfirmation");async function w(t){const e=m(t.folder),r=await c(e);if(r===null&&f(e)==="absent")throw new Error(`Nothing to delete: "${e}" is not a Jarvis environment (no config and no .orchestrator/state.json).`);const o=r?.projectName||d(e);if(!t.yes&&!await(t.confirmDeletion??h)(o,e))throw new Error("Aborted: nothing was deleted.");return i(`Deleting the "${o}" docker environment (containers, data volumes, network)\u2026`),await l(o),i(`Removing all files in "${e}"\u2026`),u(e),i("Deleted. Docker images are kept (they are shared between environments)."),{project:o,base:e}}n(w,"runDelete");function $(t){return t.command("delete").description("Permanently delete the environment: docker containers, data volumes and network, plus all files in the scenario folder. Docker images are kept.").argument("[folder]","scenario folder (defaults to the current directory)").option("--yes","skip the confirmation prompt (required for non-interactive use)").action(async(e,r)=>{await w({folder:e,yes:r.yes})})}n($,"registerDeleteCommand");export{p as deleteConfirmationMessage,$ as registerDeleteCommand,w as runDelete};
@@ -1,169 +1 @@
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
1
+ var v=Object.defineProperty;var n=(r,e)=>v(r,"name",{value:e,configurable:!0});import{mkdir as b,writeFile as F}from"node:fs/promises";import{basename as y,dirname as j,join as k,resolve as x}from"node:path";import{readState as L,writeState as R,defaultProjectName as q,envFilePath as A}from"../lib/state.js";import{generateEnv as O,templatePath as _,DEFAULT_SQL_SA_PASSWORD as E}from"../lib/compose.js";import{composeUp as I}from"../lib/docker.js";import{allocateAttachedPorts as N}from"../lib/ports.js";import{OPTIONAL_PROFILE_PORTS as T}from"../lib/profiles.js";import{findBackupFile as $,seedSql as Q}from"../lib/sql.js";import{progress as i,createProgressBar as B}from"../lib/progress.js";async function D(r,e){const o=T[e].filter(({stateKey:t})=>r[t]===void 0);if(o.length===0)return r;const c=await N(r.proxy,o.map(({portKey:t})=>t)),a={...r};for(const{portKey:t,stateKey:s}of o)a[s]=c[t];return a}n(D,"ensurePortsFor");async function Y(r){const{service:e,start:o=!0}=r,c=r.seedSqlImpl??Q,a=x(r.folder??process.cwd()),t=await L(a),s=t?.projectName??q(a),d=o&&t!==null,l=t?.enabledProfiles??[],u=l.includes(e)?l:[...l,e];let m=t?.ports??{};d&&(m=await D(m,e));const S={...t,projectName:s,ports:m,enabledProfiles:u};if(await R(a,S),!d){i(`Enabled the "${e}" service \u2014 it will start on the next \`start\`.`);return}const f=A(a);await b(j(f),{recursive:!0});const g=O({base:a,state:S,tag:"latest",profiles:u});await F(f,g,"utf8");const h=_();if(i(`Starting the "${e}" container \u2014 waiting until it is healthy\u2026`),await I({project:s,template:h,envFile:f,services:[e],wait:!0,authGate:!1}),e==="sql"){const p=await $(k(a,"seed","sqlbackup"));if(p){i(`Seeding SQL Server from "${y(p)}"\u2026`);const w=B({label:"Restoring SQL"});await c({base:a,bakFile:p,project:s,saPassword:E,onProgress:n(P=>w.update(P.percent,P.message),"onProgress")}),w.done()}}i(`The "${e}" service is ready.`)}n(Y,"enable");export{Y as enable};