jarvis-launcher 0.1.0-alpha.43

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 (109) hide show
  1. package/README.md +451 -0
  2. package/dist/cli.d.ts +3 -0
  3. package/dist/cli.js +153 -0
  4. package/dist/cli.js.map +1 -0
  5. package/dist/commands/backup.d.ts +53 -0
  6. package/dist/commands/backup.js +113 -0
  7. package/dist/commands/backup.js.map +1 -0
  8. package/dist/commands/enable.d.ts +24 -0
  9. package/dist/commands/enable.js +169 -0
  10. package/dist/commands/enable.js.map +1 -0
  11. package/dist/commands/init.d.ts +40 -0
  12. package/dist/commands/init.js +131 -0
  13. package/dist/commands/init.js.map +1 -0
  14. package/dist/commands/login.d.ts +42 -0
  15. package/dist/commands/login.js +62 -0
  16. package/dist/commands/login.js.map +1 -0
  17. package/dist/commands/registry-login.d.ts +20 -0
  18. package/dist/commands/registry-login.js +48 -0
  19. package/dist/commands/registry-login.js.map +1 -0
  20. package/dist/commands/restore.d.ts +53 -0
  21. package/dist/commands/restore.js +120 -0
  22. package/dist/commands/restore.js.map +1 -0
  23. package/dist/commands/start.d.ts +42 -0
  24. package/dist/commands/start.js +508 -0
  25. package/dist/commands/start.js.map +1 -0
  26. package/dist/commands/stop.d.ts +38 -0
  27. package/dist/commands/stop.js +56 -0
  28. package/dist/commands/stop.js.map +1 -0
  29. package/dist/commands/update.d.ts +34 -0
  30. package/dist/commands/update.js +83 -0
  31. package/dist/commands/update.js.map +1 -0
  32. package/dist/homepage/home.d.ts +94 -0
  33. package/dist/homepage/home.js +145 -0
  34. package/dist/homepage/home.js.map +1 -0
  35. package/dist/homepage/logo.d.ts +68 -0
  36. package/dist/homepage/logo.js +208 -0
  37. package/dist/homepage/logo.js.map +1 -0
  38. package/dist/homepage/menu.d.ts +123 -0
  39. package/dist/homepage/menu.js +222 -0
  40. package/dist/homepage/menu.js.map +1 -0
  41. package/dist/homepage/prompts.d.ts +196 -0
  42. package/dist/homepage/prompts.js +332 -0
  43. package/dist/homepage/prompts.js.map +1 -0
  44. package/dist/homepage/status-view.d.ts +15 -0
  45. package/dist/homepage/status-view.js +191 -0
  46. package/dist/homepage/status-view.js.map +1 -0
  47. package/dist/lib/arch.d.ts +50 -0
  48. package/dist/lib/arch.js +57 -0
  49. package/dist/lib/arch.js.map +1 -0
  50. package/dist/lib/backup.d.ts +101 -0
  51. package/dist/lib/backup.js +219 -0
  52. package/dist/lib/backup.js.map +1 -0
  53. package/dist/lib/compose.d.ts +70 -0
  54. package/dist/lib/compose.js +207 -0
  55. package/dist/lib/compose.js.map +1 -0
  56. package/dist/lib/config.d.ts +70 -0
  57. package/dist/lib/config.js +141 -0
  58. package/dist/lib/config.js.map +1 -0
  59. package/dist/lib/docker.d.ts +206 -0
  60. package/dist/lib/docker.js +272 -0
  61. package/dist/lib/docker.js.map +1 -0
  62. package/dist/lib/exec-error.d.ts +27 -0
  63. package/dist/lib/exec-error.js +58 -0
  64. package/dist/lib/exec-error.js.map +1 -0
  65. package/dist/lib/external-services.d.ts +46 -0
  66. package/dist/lib/external-services.js +201 -0
  67. package/dist/lib/external-services.js.map +1 -0
  68. package/dist/lib/health.d.ts +102 -0
  69. package/dist/lib/health.js +165 -0
  70. package/dist/lib/health.js.map +1 -0
  71. package/dist/lib/mongo.d.ts +124 -0
  72. package/dist/lib/mongo.js +134 -0
  73. package/dist/lib/mongo.js.map +1 -0
  74. package/dist/lib/ports.d.ts +92 -0
  75. package/dist/lib/ports.js +234 -0
  76. package/dist/lib/ports.js.map +1 -0
  77. package/dist/lib/profiles.d.ts +39 -0
  78. package/dist/lib/profiles.js +44 -0
  79. package/dist/lib/profiles.js.map +1 -0
  80. package/dist/lib/progress.d.ts +66 -0
  81. package/dist/lib/progress.js +79 -0
  82. package/dist/lib/progress.js.map +1 -0
  83. package/dist/lib/proxy-merge.d.ts +15 -0
  84. package/dist/lib/proxy-merge.js +282 -0
  85. package/dist/lib/proxy-merge.js.map +1 -0
  86. package/dist/lib/registry.d.ts +106 -0
  87. package/dist/lib/registry.js +179 -0
  88. package/dist/lib/registry.js.map +1 -0
  89. package/dist/lib/restore.d.ts +288 -0
  90. package/dist/lib/restore.js +534 -0
  91. package/dist/lib/restore.js.map +1 -0
  92. package/dist/lib/scenario.d.ts +83 -0
  93. package/dist/lib/scenario.js +150 -0
  94. package/dist/lib/scenario.js.map +1 -0
  95. package/dist/lib/seed-plan.d.ts +67 -0
  96. package/dist/lib/seed-plan.js +36 -0
  97. package/dist/lib/seed-plan.js.map +1 -0
  98. package/dist/lib/sql.d.ts +182 -0
  99. package/dist/lib/sql.js +286 -0
  100. package/dist/lib/sql.js.map +1 -0
  101. package/dist/lib/state.d.ts +148 -0
  102. package/dist/lib/state.js +178 -0
  103. package/dist/lib/state.js.map +1 -0
  104. package/dist/lib/status.d.ts +98 -0
  105. package/dist/lib/status.js +100 -0
  106. package/dist/lib/status.js.map +1 -0
  107. package/package.json +36 -0
  108. package/templates/docker-compose.test.ts +259 -0
  109. package/templates/docker-compose.yml +193 -0
package/README.md ADDED
@@ -0,0 +1,451 @@
1
+ # jarvis-launcher
2
+
3
+ `jarvis-launcher` is a cross-platform command-line tool that prepares a **scenario folder** and runs the full Jarvis stack around the published all-in-one image. It is a thin wrapper over `docker` / `docker compose` (plus a short-lived `mongo` container used only for restores) — it never re-implements what those tools already do.
4
+
5
+ Your only inputs are a **folder** and a **command**. The launcher authors every compose / `.env` file for you, picks free host ports, provisions the base configuration out of the image, and (when a prepared dump is present) restores it before Jarvis starts serving.
6
+
7
+ ---
8
+
9
+ ## Prerequisites
10
+
11
+ You need exactly three things on the host:
12
+
13
+ 1. **Docker** with **Compose v2** (`docker compose`, the plugin — not the old `docker-compose`). Docker Desktop on macOS/Windows or Docker Engine on Linux both work.
14
+ 2. **Node.js ≥ 20 (LTS)** — required to install and run the CLI.
15
+ 3. An **ACR repository-scoped token** for the integrator image registry `integratori-ggeybpb3azacdpfw.azurecr.io`. The token comes as a pair:
16
+ - the **token name** is the Docker login **username**;
17
+ - the **token password** is the **password**.
18
+
19
+ That is the whole floor. In particular you do **not** need:
20
+
21
+ - **No** host-side MongoDB tools (`mongosh`, `mongorestore`, …) — every Mongo operation runs inside a container.
22
+ - **No** Azure CLI — authentication is a plain `docker login` against the registry (see [`registry-login`](#registry-login)).
23
+
24
+ > **Apple Silicon (arm64):** the optional **SQL Server** service (`enable sql` / `start --sql`) runs only as an **x64** image — Microsoft ships no arm64 build of SQL Server — so on an Apple Silicon Mac it runs under emulation. Enable **"Use Rosetta for x86/amd64 emulation"** in Docker Desktop → *Settings → General* for it to start reliably; without it the QEMU fallback is slow and SQL Server can crash. The always-on core stack (Jarvis / Mongo / ES) runs natively on arm64 and is unaffected.
25
+
26
+ > **Note:** MongoDB, Elasticsearch and SQL Server data live on Docker **named volumes** (`<project>_mongo_data` / `<project>_es_data` / `<project>_sql_data`), not host bind-mounts. A macOS/Windows Docker Desktop bind-mount (grpcfuse) makes WiredTiger index builds fail intermittently with `Operation not permitted` (EPERM), which crashes Automation and wedges the stack; named volumes live on the Docker VM's native filesystem and avoid this. They also sidestep the raw-Linux non-root-UID bind-mount permission issue entirely (Docker initializes volume ownership for the container). Because the volumes survive `stop` (which never passes `-v`), a restored SQL database persists across a `stop`/`start` cycle — the seed itself only re-runs on a first start, a stale-volume replace, or an explicit `--sql` enable, never on a routine restart.
27
+
28
+ ---
29
+
30
+ ## Install
31
+
32
+ From a published package:
33
+
34
+ ```bash
35
+ npm install -g jarvis-launcher
36
+ ```
37
+
38
+ Or run it directly without installing, via `npx`:
39
+
40
+ ```bash
41
+ npx jarvis-launcher <command>
42
+ ```
43
+
44
+ Or, running from a checkout of this repository:
45
+
46
+ ```bash
47
+ npm install
48
+ npm run build
49
+ ```
50
+
51
+ This produces the `jarvis-launcher` executable (its bin is `dist/cli.js`). The examples below assume it is on your `PATH`; when running from source you can substitute `npm run dev --` for `jarvis-launcher`.
52
+
53
+ ---
54
+
55
+ ## Authenticate once: `registry-login`
56
+
57
+ The all-in-one image lives in a private ACR, so Docker must be authenticated before the launcher can pull it. Run this **once per machine**:
58
+
59
+ ```bash
60
+ jarvis-launcher registry-login
61
+ ```
62
+
63
+ You are prompted (masked) for the **token name** (username) and **token password**. The credential is stored by **Docker's own credential store** (`~/.docker/config.json` / your OS keychain) — the launcher persists no secret itself — so you are asked **only once per machine**. Subsequent commands that pull the image reuse the stored credential silently; if the credential is ever missing or rejected, `start`/`update` re-prompt automatically and retry once.
64
+
65
+ You can supply the credential non-interactively (e.g. in CI):
66
+
67
+ ```bash
68
+ # via flags
69
+ jarvis-launcher registry-login --user <token-name> --password <token-password>
70
+
71
+ # or via environment variables
72
+ export JARVIS_REGISTRY_USER=<token-name>
73
+ export JARVIS_REGISTRY_PASSWORD=<token-password>
74
+ jarvis-launcher registry-login
75
+ ```
76
+
77
+ The credential-source precedence is **flags → environment variables → masked prompt**. `--registry <host>` overrides the default registry host if needed.
78
+
79
+ > **`registry-login` is Docker registry authentication.** It is **not** the same as the Jarvis dev-[`login`](#login) command, which opens a browser impersonation session into a running Jarvis. Keep the two distinct.
80
+
81
+ ---
82
+
83
+ ## The scenario folder
84
+
85
+ A **scenario folder** is the single root the launcher operates on. It is **both** the source of the three base config files **and** the host data root for Jarvis-owned files (document store, etc.) via the image's `/scenario` bind-mount. MongoDB and Elasticsearch data, by contrast, live on Docker **named volumes** (see the note above). Everything **survives teardown and recreate**: `stop`/`update` remove containers but never the host data or the named volumes (only `docker compose down -v` would wipe the volumes).
86
+
87
+ The launcher creates the host data subfolders **idempotently** and **never wipes** existing content. Layout:
88
+
89
+ ```
90
+ <scenario>/
91
+ jarvis-host-config.json # base config — provisioned by init/start (copied from the image)
92
+ jarvis.proxy.json # "
93
+ jarvis.automation.json # "
94
+ data/ # Jarvis-owned data (mounted as /scenario/data)
95
+ seed/ # restore artifact: an inner dump/ (mongo bson.gz) + document-store files
96
+ state.json # optional: pre-declared initial state — see "Pre-seeding the initial state"
97
+ dsdata/ # document-store tree (original/ artifacts/ avatars/ …) -> data/ds/*
98
+ # (legacy alternative: dstore/ -> data/ds/original)
99
+ automation/ # optional: Elsa automation assets -> data/automation
100
+ # (merged; a same-named sub-folder is replaced entirely)
101
+ sqlbackup/ # optional SQL Server .bak file(s)
102
+ externalservice_*/ # optional: your own companion service(s) — see "External services" below
103
+ .orchestrator/ # launcher-owned: state.json (persisted ports + project name) + generated .env
104
+
105
+ # Mongo, Elasticsearch & SQL Server data are NOT here — they live on Docker named volumes:
106
+ # <project>_mongo_data -> /data/db
107
+ # <project>_es_data -> /usr/share/elasticsearch/data
108
+ # <project>_sql_data -> /var/opt/mssql (only when the `sql` profile is active)
109
+ ```
110
+
111
+ Notes:
112
+
113
+ - The three `jarvis-*.json` configs are **lifted out of the image** (`/jarvis/reference/`); there is no bundled offline fallback. If the image is unreachable, provisioning fails fast with a clear error.
114
+ - A **missing** config triggers auto-provisioning. A config that is **present but invalid** is **never overwritten** — the launcher fails fast so you can fix or remove it.
115
+ - There is **no `automationrepo/`** folder — git-backed automation repos are not wired; automation uses the in-container default.
116
+ - `.orchestrator/` is launcher-owned, regenerable state. Deleting it only means new host ports get chosen on the next `start`.
117
+
118
+ ### Pre-seeding the initial state (`seed/state.json`)
119
+
120
+ An integrator packaging a scenario can ship a **`seed/state.json`** file to 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.
121
+
122
+ Only three properties 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):
123
+
124
+ ```json
125
+ {
126
+ "projectName": "cmms",
127
+ "enabledProfiles": ["sql"],
128
+ "tag": "1.2.3"
129
+ }
130
+ ```
131
+
132
+ | Property | Meaning | Default when omitted |
133
+ |---|---|---|
134
+ | `projectName` | the `docker compose -p` project name | derived from the folder name |
135
+ | `enabledProfiles` | optional profiles to turn on (`sql`, `mail`) | none |
136
+ | `tag` | base image tag to pin the scenario to | `latest` |
137
+
138
+ Explicit CLI flags always win over the seed: `--tag` overrides `tag`, and `--sql`/`--mail` are **added to** the seed's `enabledProfiles`.
139
+
140
+ In the interactive homepage, choosing **Start** on a brand-new scenario that ships a `seed/state.json` skips the initial questions (folder / tag / SQL / mail / force) and starts straight away — the seed already pre-declares that initial state, so there is nothing to ask.
141
+
142
+ ---
143
+
144
+ ## Quick start (brand-new scenario)
145
+
146
+ ```bash
147
+ mkdir my-scenario && cd my-scenario
148
+ jarvis-launcher registry-login # once per machine, if not already done
149
+ jarvis-launcher start
150
+ ```
151
+
152
+ `start` from an empty folder does everything for you:
153
+
154
+ 1. scaffolds the data subfolders and **auto-provisions** the three base configs from the image;
155
+ 2. **allocates a free, clustered block of host ports** and persists them (so the URL stays stable across restarts);
156
+ 3. brings up `mongo` + `es`, waits until they are healthy;
157
+ 4. **auto-restores** the prepared dump **if** one is present under `seed/` **and** the database is empty (an empty `seed/` or no seed at all is fine — Jarvis simply comes up with an empty database; a populated DB is left untouched — restore only on `--force`);
158
+ 5. starts `jarvis`, waits for the proxy to answer, then prints the access URLs and the dev-login links.
159
+
160
+ After a restore, Elasticsearch repopulates in the background; the launcher prints a banner and best-effort indexing progress so you know search may be briefly incomplete.
161
+
162
+ ---
163
+
164
+ ## Commands
165
+
166
+ `<folder>` is an **optional** positional on every command — it defaults to the **current working directory**, so `cd`-ing into the scenario folder and omitting it is the common case.
167
+
168
+ | Command | Synopsis | What it does |
169
+ |---|---|---|
170
+ | [`init`](#init) | `init [folder]` | Provision the three base config files into an empty/config-less folder. |
171
+ | [`registry-login`](#registry-login) | `registry-login [--registry <host>] [--user <name>] [--password <pwd>]` | Authenticate Docker against the image registry (once per machine). |
172
+ | [`start`](#start) | `start [folder] [--sql] [--mail] [--force]` | Bring up the full stack (auto-init + auto-restore) and report the URLs. |
173
+ | [`stop`](#stop) | `stop [folder]` | Stop the stack (remove containers) while keeping all on-disk data. |
174
+ | [`restore`](#restore) | `restore [folder] [--force]` | Restore the prepared artifact into the running stack (gated by `--force`). |
175
+ | [`update`](#update) | `update [folder] --tag <T>` | Pull a new image tag and recreate the stack on it, preserving data. |
176
+ | [`enable`](#enable) | `enable [folder] <sql\|mail>` | Attach an optional service (SQL Server or mail catcher) to the stack. |
177
+ | [`login`](#login) | `login [folder] [--admin\|--user]` | Open the browser at the Jarvis dev-login (impersonation) URL. |
178
+
179
+ ### `init`
180
+
181
+ ```bash
182
+ jarvis-launcher init [folder]
183
+ ```
184
+
185
+ Resolves the folder (default: CWD), scaffolds the data subfolders, and copies any **absent** base config out of the image's `/jarvis/reference/`. It is only-if-absent: when all three configs already exist it is a no-op and the image is not even pulled. A present-but-invalid config is never overwritten — it fails fast instead.
186
+
187
+ You rarely need `init` on its own — `start` performs the same provisioning automatically — but it is useful to lay down (and review) the configs before the first `start`.
188
+
189
+ ### `registry-login`
190
+
191
+ ```bash
192
+ jarvis-launcher registry-login [--registry <host>] [--user <token-name>] [--password <token-password>]
193
+ ```
194
+
195
+ Forces a fresh `docker login` against the integrator registry so you can pre-seed or refresh the stored credential. See [Authenticate once](#authenticate-once-registry-login) above for the credential sources and the once-per-machine behavior.
196
+
197
+ ### `start`
198
+
199
+ ```bash
200
+ jarvis-launcher start [folder] [--sql] [--mail] [--force]
201
+ ```
202
+
203
+ Brings up the full, logged-in environment in one command. See [Quick start](#quick-start-brand-new-scenario) for the brand-new-folder flow.
204
+
205
+ | Flag | Effect |
206
+ |---|---|
207
+ | `--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. |
208
+ | `--mail` | Also start the optional **mail catcher** (catch-all SMTP + web UI). |
209
+ | `--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. |
210
+
211
+ 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.
212
+
213
+ ### `stop`
214
+
215
+ ```bash
216
+ jarvis-launcher stop [folder]
217
+ ```
218
+
219
+ Removes the scenario's containers (including any optional `sql`/`mail` ones) while **keeping all on-disk data** — it never passes `-v`/`--volumes`. The next `start` sees the data and performs no restore. If the scenario was never started, it reports there is nothing to stop and exits non-zero.
220
+
221
+ ### `restore`
222
+
223
+ ```bash
224
+ jarvis-launcher restore [folder] [--force]
225
+ ```
226
+
227
+ Explicitly reseeds Mongo / document-store / ES from the prepared artifact under `<folder>/seed`. It brings `mongo`+`es` up healthy first, then applies the gate:
228
+
229
+ - **`--force`** → restore regardless of DB contents (drops non-system DBs first, then replaces the data).
230
+ - **no `--force`, DB already populated** → **refuses** and changes nothing.
231
+ - **no `--force`, DB empty** → restores.
232
+
233
+ The artifact must live at `<folder>/seed/` and contain an inner `dump/` (the `mongorestore --gzip` payload). The document-store files are picked up from either `seed/dsdata/` — a full `data/ds` tree (`original/`, `artifacts/`, `avatars/`, …), copied subfolder-by-subfolder into `data/ds/*` — or, for backward compatibility, the legacy `seed/dstore/` (moved into `data/ds/original`). A missing/incomplete `dump/` aborts cleanly with a clear message; if neither document-store folder is present the restore proceeds but warns that stored blobs will be missing their files. If the artifact carries a `seed/automation/` folder, its contents are moved into `data/automation` (the container's `/scenario/data/automation`, Elsa's `AssetsLocation`): a top-level merge that overwrites same-named files, replaces a same-named sub-folder entirely, and leaves unrelated existing assets in place.
234
+
235
+ ### `update`
236
+
237
+ ```bash
238
+ jarvis-launcher update [folder] --tag <tag>
239
+ ```
240
+
241
+ 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:
242
+
243
+ 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);
244
+ 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;
245
+ 3. if the pull **fails** (typo, offline, unpublished tag, rejected credentials), it reports the error and **leaves the running stack untouched**.
246
+
247
+ `--tag` is required (e.g. `--tag latest` or `--tag 1.2.3`).
248
+
249
+ ### `enable`
250
+
251
+ ```bash
252
+ jarvis-launcher enable [folder] <sql|mail>
253
+ ```
254
+
255
+ Attaches one optional service to the scenario and remembers it for future starts:
256
+
257
+ - **`sql`** — starts the SQL Server service. When `<folder>/seed/sqlbackup` contains a `.bak`, it is restored into the database; with no `.bak`, the container is started and reachable but not seeded.
258
+ - **`mail`** — starts a catch-all mail catcher (Mailpit); all outgoing mail is captured and viewable in its web UI. No seed step is needed.
259
+
260
+ `enable` attaches **only** the requested service — it does not recreate the always-on `jarvis`/`mongo`/`es` containers. If the stack is not running yet, the profile is persisted and activates on the next `start`. (Enabling a service is equivalent to passing `--sql`/`--mail` to `start`.)
261
+
262
+ ### `login`
263
+
264
+ ```bash
265
+ jarvis-launcher login [folder] [--admin|--user]
266
+ ```
267
+
268
+ Opens your browser at the Jarvis **dev-login (impersonation)** URL through the scenario's persisted proxy port, so you can jump straight into a session:
269
+
270
+ - `--admin` (default) — log in as the admin account;
271
+ - `--user` — log in as the standard user account.
272
+
273
+ This works because the image is built as a non-production / dev environment with forced-login enabled. It is a Jarvis application convenience and is **distinct from [`registry-login`](#registry-login)** (Docker registry authentication). The stack must already be started (otherwise there is no persisted port and the command fails with a clear message).
274
+
275
+ ---
276
+
277
+ ## Optional services
278
+
279
+ Both optional services are off by default and can be turned on either at start time (`start --sql` / `start --mail`) or afterwards (`enable sql` / `enable mail`):
280
+
281
+ - **SQL Server (`sql`)** — for scenarios that need the relational side. Drop a prepared `.bak` into `<folder>/seed/sqlbackup` and it is restored automatically the first time SQL is brought up, when the service is enabled with `--sql`, or on a brand-new folder (only a seed, no `.orchestrator/state.json`) — in that last case the restore **replaces** any leftover data from a reused `<project>_sql_data` volume, without needing `--force`, mirroring the Mongo auto-restore. A routine restart never re-seeds, so the persisted database is left untouched. Without a `.bak` the server is simply available and empty.
282
+ - **Mail catcher (`mail`)** — a catch-all SMTP server with a web inbox (Mailpit). All mail Jarvis would send is intercepted there, so you can inspect notifications without a real mail server.
283
+
284
+ ---
285
+
286
+ ## External services
287
+
288
+ Any folder under `<folder>/seed/` whose name starts with `externalservice_`
289
+ is picked up automatically and brought up as an extra container alongside
290
+ the Jarvis stack — no compose file to hand-edit. This is for your own
291
+ companion services (a CMMS API, a mock third-party integration, …) that
292
+ Jarvis or its automation should be able to reach on the compose network.
293
+ It lives under `seed/`, grouped with the other integrator-supplied scenario
294
+ resources (`dsdata/`, `automation/`, `sqlbackup/`) — a top-level folder is
295
+ **not** picked up.
296
+
297
+ ```
298
+ <scenario>/
299
+ seed/
300
+ externalservice_cmms.api/
301
+ Dockerfile # your own — built as-is, context = this folder
302
+ jarvis-service.json # tells the launcher how to wire the service in
303
+ ... # the service's own source
304
+ ```
305
+
306
+ `jarvis-service.json`:
307
+
308
+ ```json
309
+ {
310
+ "name": "cmms-api",
311
+ "dockerfile": "Dockerfile",
312
+ "ports": ["8080:80"],
313
+ "environment": {
314
+ "ASPNETCORE_ENVIRONMENT": "Development"
315
+ }
316
+ }
317
+ ```
318
+
319
+ `Dockerfile` — entirely yours, built as-is (the launcher only sets `build:
320
+ { context: <folder>, dockerfile: <this file> }`, nothing else). A typical
321
+ .NET example:
322
+
323
+ ```dockerfile
324
+ # Build stage
325
+ FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
326
+ WORKDIR /externalservice_cmms.api/src
327
+
328
+ COPY . .
329
+ RUN dotnet publish -c Release -r linux-x64 --self-contained false -o /externalservice_cmms.api/app/publish
330
+
331
+ # Runtime stage
332
+ FROM mcr.microsoft.com/dotnet/aspnet:10.0
333
+ WORKDIR /externalservice_cmms.api/app
334
+
335
+ COPY --from=build /externalservice_cmms.api/app/publish .
336
+
337
+ ENTRYPOINT ["dotnet", "Cmms.Api.dll"]
338
+ ```
339
+
340
+ All fields are optional:
341
+
342
+ - **`name`** — the compose service name. Defaults to the folder name with
343
+ the `externalservice_` prefix stripped and sanitized (e.g.
344
+ `externalservice_cmms.api` → `cmms.api`).
345
+ - **`dockerfile`** — path relative to the folder. Defaults to `Dockerfile`.
346
+ - **`ports`** — `"host:container"` strings, published exactly as given
347
+ (host ports are **not** auto-allocated the way `sql`/`mail` are — pick a
348
+ port that's free on your machine). Omit for a service that's only reached
349
+ from inside the compose network, by service name.
350
+ - **`environment`** — a flat map of environment variables passed to the
351
+ container as-is.
352
+
353
+ ### Exposing an external service through the Jarvis proxy
354
+
355
+ An external service is reachable from `jarvis` over the compose network at
356
+ `http://<service-name>:<container-port>/` (the compose service `name` from
357
+ `jarvis-service.json`, and the port your app listens on **inside** the
358
+ container — never the host-published one, and never `localhost`, which
359
+ inside the `jarvis` container means the `jarvis` container itself).
360
+
361
+ To route Jarvis's reverse proxy at that address, drop a `jarvis.proxy.json`
362
+ **fragment** next to `Dockerfile`/`jarvis-service.json` in the same
363
+ `externalservice_*` folder:
364
+
365
+ ```
366
+ <scenario>/
367
+ seed/
368
+ externalservice_cmms.api/
369
+ Dockerfile
370
+ jarvis-service.json
371
+ jarvis.proxy.json # NEW — proxy wiring fragment (optional)
372
+ ```
373
+
374
+ ```json
375
+ {
376
+ "ReverseProxy": {
377
+ "Clusters": {
378
+ "jarvis-cmms": {
379
+ "Destinations": {
380
+ "jarvis-cmms/destination1": {
381
+ "Address": "http://cmms-api:5200/"
382
+ }
383
+ }
384
+ }
385
+ }
386
+ },
387
+ "ReverseProxyDynamicRoutes": {
388
+ "Routes": {
389
+ "cmms": {
390
+ "ClusterId": "jarvis-cmms",
391
+ "AuthorizationPolicy": "JarvisProxy",
392
+ "Match": { "Path": "/jarvis-cmms/{**catchall}" },
393
+ "Transforms": [{ "PathPattern": "/{**catchall}" }]
394
+ }
395
+ }
396
+ }
397
+ }
398
+ ```
399
+
400
+ The launcher merges this into the scenario's live `<folder>/jarvis.proxy.json`
401
+ on every `start`/`update` (`lib/proxy-merge.ts`). Only three dict paths are
402
+ ever touched: `ReverseProxy.Routes`, `ReverseProxy.Clusters`,
403
+ `ReverseProxyDynamicRoutes.Routes` — everything else in that file (the base
404
+ routes/clusters, anything you added by hand) is left alone. Rules:
405
+
406
+ - an entry is only **added** the first time it appears — it's never
407
+ overwritten if something with the same id already exists there with
408
+ different content (you get a warning instead, so a name clash never
409
+ silently clobbers your own edits);
410
+ - removing (or renaming) the `externalservice_*` folder removes the entries
411
+ it added on the next `start`/`update` — but only if you haven't hand-edited
412
+ them since (otherwise they're left in place with a warning);
413
+ - `jarvis.proxy.json` at the scenario root stays the file you provisioned
414
+ once and can still edit directly — this only ever *adds to* it, and only
415
+ through the fragment above.
416
+
417
+ The launcher scans for the compose overlay and (re)generates
418
+ `.orchestrator/external-services.yml` on every `start`/`stop`/`update` — like
419
+ `.orchestrator/state.json` and the `.env`, this file is launcher-owned and
420
+ regenerable; never hand-edit it. A folder that's missing its `Dockerfile`,
421
+ missing/malformed `jarvis-service.json`, or whose resolved name collides with
422
+ another service (including the core `jarvis`/`mongo`/`es`/`sql`/`mail`
423
+ names) is **skipped with a warning** — it never blocks the rest of the stack
424
+ from starting. Removing or renaming the folder and running `start`/`stop`
425
+ again tears its container down.
426
+
427
+ Images are built the first time the service comes up; a later code change to
428
+ the service is **not** picked up automatically on the next `start` — rebuild
429
+ explicitly (e.g. `docker compose -p <project> build <service>`) or remove the
430
+ built image first.
431
+
432
+ ---
433
+
434
+ ## Build and test (developing the launcher)
435
+
436
+ ```bash
437
+ npm install # install dependencies
438
+ npm run build # tsc → dist/ (this is the "does it compile" gate)
439
+ npm test # run the vitest unit suite
440
+ ```
441
+
442
+ Docker/Mongo/ES-driven behavior (real `compose up/down`, copy-from-image, live restore, real `docker login`) is covered by integration/manual checks that require Docker and the published image; the unit suite covers the pure logic.
443
+
444
+ ---
445
+
446
+ ## Troubleshooting
447
+
448
+ - **`unauthorized` / `401` when pulling** — your Docker credential for the registry is missing or expired. Run `jarvis-launcher registry-login` again (or supply `--user`/`--password`).
449
+ - **"Refusing to restore: data already exists"** — the database is already populated. Re-run `restore` with `--force` to drop and reload, or leave it as-is.
450
+ - **Port already in use** — the launcher re-probes persisted ports on every `start` and re-allocates a fresh block if one is taken, reporting the new proxy URL. There is a tiny window between picking a port and `compose up`; if a true conflict is hit, you get a clear bind-conflict error rather than a hang.
451
+ - **"Nothing to stop / no started stack"** — that scenario was never started (no `.orchestrator/state.json`). Run `start` first.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export declare function shouldRunHomepage(argv: string[], stdinIsTTY: boolean, stdoutIsTTY: boolean): boolean;
3
+ export declare function isEntrypoint(modulePath: string, argv1: string | undefined, canonicalize?: (path: string) => string): boolean;
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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}