@bizone-ai/cli 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -63,7 +63,7 @@ bizone <category> <command> [args] [--no-colors]
63
63
  | `start` | — | Start the platform |
64
64
  | `stop` | — | Stop the platform |
65
65
  | `configuration` | `config`,`c`| General CLI configuration |
66
- | `images` | `img` | Override container image URLs |
66
+ | `images` | `img` | Override image URLs / add custom services |
67
67
  | `environment` | `env` | Set environment variables per container |
68
68
  | `resources` | `res`, `r` | Resources imported into the resource-manager |
69
69
  | `database` | `db` | Manage the bundled MySQL database |
@@ -92,7 +92,8 @@ File shape:
92
92
  "config": { "key": "value" },
93
93
  "images": { "type": "image-url" },
94
94
  "env": { "type": { "NAME": "value" } },
95
- "resources": { "namespace": "path" }
95
+ "resources": { "namespace": "path" },
96
+ "custom": { "id": "path-to-service.json" }
96
97
  }
97
98
  ```
98
99
 
@@ -137,9 +138,10 @@ bizone config set cloud_tools_enable true
137
138
  ## `images` — override container images
138
139
 
139
140
  ```bash
140
- bizone images get # list image URLs (override or default)
141
+ bizone images get # list image URLs + custom services
141
142
  bizone images set {type} {url} # override an image
142
- bizone images remove {type} # reset to default
143
+ bizone images remove {type|id} # reset an image, or unregister a custom service
144
+ bizone images add {id} {path} # register an additional custom service
143
145
  ```
144
146
 
145
147
  Stored at `$.images.{type}`. Valid types:
@@ -152,6 +154,61 @@ Example:
152
154
  bizone images set state-store bizone-local/state-store:1.0.0
153
155
  ```
154
156
 
157
+ ### Additional custom services (`images add`)
158
+
159
+ Register extra services that aren't part of the built-in topology. Each is
160
+ identified by an `{id}` and points to a JSON file describing the service, using
161
+ the **same shape as the built-in service objects** (see
162
+ `src/containers/mysql.js`):
163
+
164
+ ```bash
165
+ bizone images add {id} {path-to-service.json} # register / update
166
+ bizone images remove {id} # unregister
167
+ ```
168
+
169
+ Stored at `$.custom.{id}`. The JSON file shape:
170
+
171
+ ```json
172
+ {
173
+ "type": "redis",
174
+ "image": "redis:7",
175
+ "containerName": "bizone-redis",
176
+ "containerPort": 6379,
177
+ "portKey": "redis_port",
178
+ "defaultPort": "6379",
179
+ "enableKey": null,
180
+ "readyPath": "/.system/status",
181
+ "readyCheck": "systemStatusOk",
182
+ "volume": { "name": "bizone-redis-data", "mount": "/data" },
183
+ "defaultEnv": { "SOME_VAR": "value" }
184
+ }
185
+ ```
186
+
187
+ - **`image`** and **`containerName`** are required; everything else is optional.
188
+ - `containerName` should start with `bizone-` (so `db destroy` recognizes it)
189
+ and must not collide with a built-in container.
190
+ - `type` defaults to the `{id}` if omitted; it is the key for image/env
191
+ overrides (`bizone env set {type} ...`).
192
+ - `readyCheck` (optional) must be one of `systemStatusOk`, `uiReadinessUp`,
193
+ `mysqlPing`. With `systemStatusOk`/`uiReadinessUp` the CLI polls
194
+ `http://localhost:{port}{readyPath}`; with `mysqlPing` it uses `mysqladmin
195
+ ping`. Omit both `readyPath` and `readyCheck` to skip the readiness wait.
196
+ - `enableKey` (optional) — a config key gating the service. Omit / `null` to
197
+ always run.
198
+
199
+ Custom services are validated when added (the CLI parses the file immediately).
200
+ They **start last**, after the built-in platform is ready, and are **stopped by
201
+ `bizone stop`** like any other container. Relative `{path}` values resolve
202
+ against the `.bizone` config directory.
203
+
204
+ Example:
205
+
206
+ ```bash
207
+ bizone images add redis ./redis.json
208
+ bizone start # built-ins first, then redis
209
+ bizone images get # redis shown under "Custom services"
210
+ ```
211
+
155
212
  ---
156
213
 
157
214
  ## `environment` — per-container environment variables
@@ -198,6 +255,7 @@ namespace.
198
255
  bizone resources get # list configured sources
199
256
  bizone resources set {namespace} {path} # set a file or folder source
200
257
  bizone resources remove {namespace} # remove a namespace source
258
+ bizone resources import [global] # import now (see below)
201
259
  ```
202
260
 
203
261
  Stored at `$.resources.{namespace}`. `{path}` may be:
@@ -224,6 +282,20 @@ POST http://localhost:{resource_manager_port}/admin/{namespace}/status
224
282
  { "user": "bizone-cli", "message": "resource initialization", "items": [ ... ] }
225
283
  ```
226
284
 
285
+ ### `resources import` — import on demand
286
+
287
+ Run an import against the **already-running** platform (resource-manager must be up):
288
+
289
+ ```bash
290
+ bizone resources import # import the user-configured namespaces (above)
291
+ bizone resources import global # run ONLY the bizone-resources job (core resources)
292
+ ```
293
+
294
+ - Without `global`: imports the namespaces configured via `resources set`.
295
+ - With `global`: runs the one-shot `bizone-resources` docker job, which imports
296
+ the **core** resources into the `.global` namespace. This is the same job
297
+ `bizone start` runs (unconditionally here — used to (re)seed core resources).
298
+
227
299
  ---
228
300
 
229
301
  ## `start` — start the platform
@@ -246,6 +318,11 @@ returns control to the shell. Container ids are saved to
246
318
  3. Start `resource-manager`; wait until `GET /.system/status` returns `{ "status": "OK" }`.
247
319
  4. Run the `bizone-resources` initialization job (blocking).
248
320
  5. Import any configured `resources` namespaces (see above).
321
+
322
+ Steps 4–5 are **skipped entirely if the core resources were already
323
+ imported**, detected by querying
324
+ `GET /.global/resources?id=action/flow.start&expand=no`; a `200` with a
325
+ non-empty `items` array means the import already happened.
249
326
  6. Start `orchestrator`, `ui`, and any enabled optional services in parallel
250
327
  (state-store / cloud-tools run only when `*_enable=true`).
251
328
  - The orchestrator receives the AWS env vars when `forward_aws_env=true`.
@@ -253,6 +330,8 @@ returns control to the shell. Container ids are saved to
253
330
  - `ui` — `GET /actuator/health/readiness` returns `{"status":{"code":"UP"}}`
254
331
  - all others — `GET /.system/status` returns `{ "status": "OK" }`
255
332
  - each wait times out after `timeout_sec` seconds.
333
+ 8. Start any **custom services** registered with `bizone images add` (last),
334
+ then wait for the readiness each declares (see the `images add` section).
256
335
 
257
336
  Each container is started with:
258
337
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bizone-ai/cli",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Assistant CLI for running the Bizone orchestrator platform locally with Docker",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,7 +16,7 @@
16
16
  ],
17
17
  "scripts": {
18
18
  "start": "node bin/bizone.js",
19
- "test": "echo No tests"
19
+ "test": "node --test test/*.test.js"
20
20
  },
21
21
  "license": "UNLICENSED"
22
22
  }
package/src/cli.js CHANGED
@@ -66,7 +66,10 @@ function printHelp() {
66
66
  );
67
67
  log.plain(` ${c("bizone images set {type} {url}")} Override an image`);
68
68
  log.plain(
69
- ` ${c("bizone images remove {type}")} Reset image to default`,
69
+ ` ${c("bizone images remove {type|id}")} Reset image / drop custom service`,
70
+ );
71
+ log.plain(
72
+ ` ${c("bizone images add {id} {path}")} Register a custom service (json)`,
70
73
  );
71
74
  log.plain("");
72
75
  log.plain(b("Environment:") + color.dim(" (env)"));
@@ -84,6 +87,9 @@ function printHelp() {
84
87
  log.plain(
85
88
  ` ${c("bizone resources remove {namespace}")} Remove a resource source`,
86
89
  );
90
+ log.plain(
91
+ ` ${c("bizone resources import [global]")} Import user resources (or core with 'global')`,
92
+ );
87
93
  log.plain("");
88
94
  log.plain(b("Database:") + color.dim(" (db)"));
89
95
  log.plain(
@@ -1,7 +1,8 @@
1
- // `bizone images|img get|set|remove`
1
+ // `bizone images|img get|set|remove|add`
2
2
 
3
3
  import { loadConfig, saveConfig } from "../config.js";
4
4
  import { CONTAINERS, IMAGE_TYPES } from "../defaults.js";
5
+ import { readCustomService } from "../custom-services.js";
5
6
  import { color, log } from "../colors.js";
6
7
 
7
8
  function isValidType(type) {
@@ -29,6 +30,38 @@ export function images(args) {
29
30
  : color.dim(" (default)");
30
31
  log.plain(` ${color.cyan(t)} -> ${val}${tag}`);
31
32
  }
33
+ const custom = Object.entries(cfg.custom || {});
34
+ if (custom.length > 0) {
35
+ log.plain(color.bold("Custom services:"));
36
+ for (const [id, path] of custom) {
37
+ log.plain(` ${color.cyan(id)} -> ${path}${color.dim(" (custom)")}`);
38
+ }
39
+ }
40
+ return 0;
41
+ }
42
+ case "add": {
43
+ if (!type || rest.length === 0) {
44
+ log.err("Usage: bizone images add {id} {path-to-service.json}");
45
+ return 1;
46
+ }
47
+ const id = type; // first positional is the custom service id
48
+ if (isValidType(id)) {
49
+ log.err(`"${id}" is a built-in service type; choose another id.`);
50
+ return 1;
51
+ }
52
+ const path = rest.join(" ");
53
+ try {
54
+ const svc = readCustomService(id, path); // parse + validate now (fail fast)
55
+ cfg.custom[id] = path;
56
+ saveConfig(cfg);
57
+ log.ok(
58
+ `Added custom service ${color.cyan(id)} -> ${path} ` +
59
+ color.dim(`(${svc.image})`),
60
+ );
61
+ } catch (err) {
62
+ log.err(`Invalid custom service ${err.message}`);
63
+ return 1;
64
+ }
32
65
  return 0;
33
66
  }
34
67
  case "set": {
@@ -49,10 +82,17 @@ export function images(args) {
49
82
  }
50
83
  case "remove": {
51
84
  if (!type) {
52
- log.err("Usage: bizone images remove {type}");
85
+ log.err("Usage: bizone images remove {type|custom-id}");
53
86
  typesHint();
54
87
  return 1;
55
88
  }
89
+ // A custom service id takes precedence over a built-in image override.
90
+ if (Object.prototype.hasOwnProperty.call(cfg.custom || {}, type)) {
91
+ delete cfg.custom[type];
92
+ saveConfig(cfg);
93
+ log.ok(`Removed custom service ${color.cyan(type)}`);
94
+ return 0;
95
+ }
56
96
  if (Object.prototype.hasOwnProperty.call(cfg.images, type)) {
57
97
  delete cfg.images[type];
58
98
  saveConfig(cfg);
@@ -67,7 +107,7 @@ export function images(args) {
67
107
  return 0;
68
108
  }
69
109
  default:
70
- log.err("Usage: bizone images <get|set|remove> ...");
110
+ log.err("Usage: bizone images <get|set|remove|add> ...");
71
111
  typesHint();
72
112
  return 1;
73
113
  }
@@ -1,12 +1,22 @@
1
- // `bizone resources|res|r get|set|remove`
1
+ // `bizone resources|res|r get|set|remove|import`
2
2
 
3
3
  import { existsSync } from "node:fs";
4
4
 
5
- import { loadConfig, saveConfig } from "../config.js";
5
+ import { loadConfig, saveConfig, getConfigValue } from "../config.js";
6
+ import { CONTAINERS } from "../defaults.js";
6
7
  import { resolveResourcePath } from "../resources-loader.js";
8
+ import {
9
+ dockerAvailable,
10
+ ensureNetwork,
11
+ isContainerRunning,
12
+ } from "../docker.js";
13
+ import {
14
+ runCoreResourcesJob,
15
+ importUserResources,
16
+ } from "../resource-import.js";
7
17
  import { color, log } from "../colors.js";
8
18
 
9
- export function resources(args) {
19
+ export async function resources(args) {
10
20
  const [sub, namespace, ...rest] = args;
11
21
  const cfg = loadConfig();
12
22
 
@@ -55,8 +65,66 @@ export function resources(args) {
55
65
  }
56
66
  return 0;
57
67
  }
68
+ case "import":
69
+ // `resources import` -> import user-configured namespaces.
70
+ // `resources import global` -> run ONLY the core bizone-resources job.
71
+ return await runImport(cfg, namespace);
58
72
  default:
59
- log.err("Usage: bizone resources <get|set|remove> ...");
73
+ log.err("Usage: bizone resources <get|set|remove|import> ...");
60
74
  return 1;
61
75
  }
62
76
  }
77
+
78
+ async function runImport(cfg, target) {
79
+ if (target && target !== "global") {
80
+ log.err("Usage: bizone resources import [global]");
81
+ return 1;
82
+ }
83
+ const rm = CONTAINERS["resource-manager"];
84
+
85
+ try {
86
+ if (target === "global") {
87
+ if (!dockerAvailable()) {
88
+ log.err("Docker does not appear to be running or installed.");
89
+ return 1;
90
+ }
91
+ if (!isContainerRunning(rm.containerName)) {
92
+ log.err(
93
+ `${rm.containerName} is not running; start the platform first (bizone start).`,
94
+ );
95
+ return 1;
96
+ }
97
+ const network = getConfigValue(cfg, "network_name");
98
+ ensureNetwork(network);
99
+ log.step("Running core resources import job (bizone-resources)...");
100
+ await runCoreResourcesJob(cfg, {
101
+ network,
102
+ imageHooks: {
103
+ onBeforePull: (image) =>
104
+ log.step(`Pulling image ${image} (this may take a while)...`),
105
+ onBeforeLogin: (registry) =>
106
+ log.step(`Authenticating to AWS ECR for ${registry}...`),
107
+ },
108
+ });
109
+ log.ok("Core resources import complete");
110
+ return 0;
111
+ }
112
+
113
+ // User-configured resources.
114
+ const count = Object.keys(cfg.resources || {}).length;
115
+ if (count === 0) {
116
+ log.warn(
117
+ "No resource namespaces configured. Add one with `bizone resources set {namespace} {path}`.",
118
+ );
119
+ return 0;
120
+ }
121
+ log.step(`Importing ${count} resource namespace(s)...`);
122
+ await importUserResources(cfg);
123
+ log.plain("");
124
+ log.plain(color.green(color.bold(`✓ Imported ${count} namespace(s)`)));
125
+ return 0;
126
+ } catch (err) {
127
+ log.err(err.message);
128
+ return 1;
129
+ }
130
+ }
@@ -9,33 +9,29 @@ import {
9
9
  loadContainers,
10
10
  saveContainers,
11
11
  } from "../config.js";
12
- import { CONTAINERS, AWS_ENV_VARS, defaultEnvFor } from "../defaults.js";
12
+ import { CONTAINERS, AWS_ENV_VARS } from "../defaults.js";
13
13
  import {
14
14
  dockerAvailable,
15
15
  ensureNetwork,
16
16
  ensureImage,
17
17
  runDetached,
18
- runBlocking,
19
18
  removeIfExists,
20
19
  mysqlReady,
21
20
  isContainerRunning,
22
21
  } from "../docker.js";
23
- import { waitForReady, importNamespace } from "../http.js";
24
- import { loadItems } from "../resources-loader.js";
22
+ import { waitForReady } from "../http.js";
23
+ import { loadCustomServices } from "../custom-services.js";
24
+ import {
25
+ coreResourcesImported,
26
+ runCoreResourcesJob,
27
+ importUserResources,
28
+ } from "../resource-import.js";
25
29
  import { color, log } from "../colors.js";
26
30
 
27
- function imageFor(cfg, type) {
28
- return cfg.images[type] || CONTAINERS[type].image;
29
- }
30
-
31
31
  function hostPort(cfg, service) {
32
- return getConfigValue(cfg, service.portKey);
33
- }
34
-
35
- // Effective env for a container type: baked-in defaults, overridden by any
36
- // user-set values (`bizone env set`).
37
- function envFor(cfg, type) {
38
- return { ...defaultEnvFor(type), ...(cfg.env[type] || {}) };
32
+ const v = service.portKey ? getConfigValue(cfg, service.portKey) : undefined;
33
+ // Custom services may carry no known config key; fall back to their defaultPort.
34
+ return v != null ? v : service.defaultPort;
39
35
  }
40
36
 
41
37
  // A service runs if it has no enableKey (always-on) or its enableKey is true.
@@ -78,16 +74,19 @@ export async function start() {
78
74
  awsProfile,
79
75
  };
80
76
 
81
- // Helper to start one service detached and record its id.
77
+ // Helper to start one service detached and record its id. Works for both
78
+ // built-in services and custom ones (which carry their own image/defaultEnv).
82
79
  const startService = (service, extraEnv = {}) => {
83
80
  removeIfExists(service.containerName); // avoid name conflicts from stale runs
84
- ensureImage(imageFor(cfg, service.type), imageHooks); // pull + ECR login if needed
81
+ const image = cfg.images[service.type] || service.image;
82
+ ensureImage(image, imageHooks); // pull + ECR login if needed
83
+ const baseEnv = { ...(service.defaultEnv ?? {}), ...(cfg.env[service.type] || {}) };
85
84
  const id = runDetached({
86
85
  name: service.containerName,
87
86
  hostPort: hostPort(cfg, service),
88
87
  containerPort: service.containerPort,
89
- image: imageFor(cfg, service.type),
90
- env: { ...envFor(cfg, service.type), ...extraEnv },
88
+ image,
89
+ env: { ...baseEnv, ...extraEnv },
91
90
  volume: service.volume || null,
92
91
  network,
93
92
  });
@@ -140,28 +139,21 @@ export async function start() {
140
139
  await waitService(rm);
141
140
  log.ok("resource-manager ready");
142
141
 
143
- // 3. Resources init job (blocking).
144
- const resources = CONTAINERS.resources;
145
- log.step("Running resources initialization job (bizone-resources)...");
146
- removeIfExists(resources.containerName);
147
- ensureImage(imageFor(cfg, resources.type), imageHooks);
148
- await runBlocking({
149
- name: resources.containerName,
150
- image: imageFor(cfg, resources.type),
151
- env: envFor(cfg, resources.type),
152
- network,
153
- });
154
- log.ok("Resources initialization job complete");
155
-
156
- // 4. Import user-configured resources.
157
- const namespaces = Object.entries(cfg.resources || {});
158
- if (namespaces.length > 0) {
159
- const rmPort = hostPort(cfg, rm);
160
- log.step(`Importing ${namespaces.length} resource namespace(s)...`);
161
- for (const [namespace, path] of namespaces) {
162
- const items = loadItems(path);
163
- await importNamespace({ port: rmPort, namespace, items });
164
- log.ok(`Imported "${namespace}" (${items.length} item(s))`);
142
+ // 3 + 4. Resource import skipped entirely if the core resources were
143
+ // already imported (the resource-manager keeps them in its persistent DB).
144
+ if (await coreResourcesImported(cfg)) {
145
+ log.ok("Resources already imported (skipping import)");
146
+ } else {
147
+ // 3. Core resources init job (blocking).
148
+ log.step("Running resources initialization job (bizone-resources)...");
149
+ await runCoreResourcesJob(cfg, { network, imageHooks });
150
+ log.ok("Resources initialization job complete");
151
+
152
+ // 4. Import user-configured resources.
153
+ const namespaces = Object.keys(cfg.resources || {});
154
+ if (namespaces.length > 0) {
155
+ log.step(`Importing ${namespaces.length} resource namespace(s)...`);
156
+ await importUserResources(cfg);
165
157
  }
166
158
  }
167
159
 
@@ -197,6 +189,36 @@ export async function start() {
197
189
  ),
198
190
  );
199
191
 
192
+ // 7. Custom services (registered via `bizone images add`) — started last.
193
+ const customServices = loadCustomServices(cfg).filter((s) =>
194
+ isEnabled(cfg, s),
195
+ );
196
+ if (customServices.length > 0) {
197
+ log.step(`Starting ${customServices.length} custom service(s)...`);
198
+ for (const service of customServices) {
199
+ log.step(
200
+ `Starting ${service.containerName} (${service.id})` +
201
+ (hostPort(cfg, service) != null
202
+ ? ` (port ${hostPort(cfg, service)})`
203
+ : ""),
204
+ );
205
+ startService(service);
206
+ }
207
+ // Wait for readiness where the custom service declares how to check.
208
+ await Promise.all(
209
+ customServices.map(async (s) => {
210
+ if (s.readyCheck === "mysqlPing") {
211
+ await waitForMysql(s.containerName);
212
+ } else if (s.readyPath && s.readyCheck) {
213
+ await waitService(s);
214
+ } else {
215
+ return; // no readiness declared; assume started
216
+ }
217
+ log.ok(`${s.containerName} ready`);
218
+ }),
219
+ );
220
+ }
221
+
200
222
  log.plain("");
201
223
  log.plain(color.green(color.bold("✓ Platform is running")));
202
224
  const ui = CONTAINERS.ui;
package/src/config.js CHANGED
@@ -20,7 +20,7 @@ function ensureDir() {
20
20
  }
21
21
  }
22
22
 
23
- const EMPTY = { config: {}, images: {}, env: {}, resources: {} };
23
+ const EMPTY = { config: {}, images: {}, env: {}, resources: {}, custom: {} };
24
24
 
25
25
  export function loadConfig() {
26
26
  if (!existsSync(CONFIG_PATH)) {
@@ -34,6 +34,7 @@ export function loadConfig() {
34
34
  images: parsed.images || {},
35
35
  env: parsed.env || {},
36
36
  resources: parsed.resources || {},
37
+ custom: parsed.custom || {},
37
38
  };
38
39
  } catch (err) {
39
40
  throw new Error(`Failed to read config at ${CONFIG_PATH}: ${err.message}`);
@@ -0,0 +1,89 @@
1
+ // Additional, user-defined services registered via `bizone images add {id} {path}`.
2
+ //
3
+ // Each registration maps an id to a JSON file whose contents mirror the service
4
+ // object exported by src/containers/*.js (see src/containers/mysql.js):
5
+ //
6
+ // {
7
+ // "type": "...", // identity used for image/env overrides (defaults to the id)
8
+ // "image": "repo/img:tag", // required
9
+ // "containerName": "...", // required (should start with "bizone-")
10
+ // "containerPort": 8080, // optional (needed for a -p mapping)
11
+ // "portKey": "...", // optional config key for the host port
12
+ // "defaultPort": "8080", // optional host port fallback
13
+ // "enableKey": null, // optional config key gating the service
14
+ // "readyPath": "/.system/status", // optional HTTP readiness path
15
+ // "readyCheck": "systemStatusOk", // optional: systemStatusOk | uiReadinessUp | mysqlPing
16
+ // "volume": { "name": "...", "mount": "..." }, // optional
17
+ // "defaultEnv": { "NAME": "value" } // optional
18
+ // }
19
+ //
20
+ // Custom services start LAST (after the built-in platform is ready) and are
21
+ // stopped by `bizone stop` like any other recorded container.
22
+
23
+ import { readFileSync } from "node:fs";
24
+
25
+ import { CONTAINERS } from "./defaults.js";
26
+ import { resolveResourcePath } from "./resources-loader.js";
27
+
28
+ // Readiness checks a custom service may reference (must match http.js / docker.js).
29
+ export const KNOWN_READY_CHECKS = [
30
+ "systemStatusOk",
31
+ "uiReadinessUp",
32
+ "mysqlPing",
33
+ ];
34
+
35
+ const BUILTIN_CONTAINER_NAMES = new Set(
36
+ Object.values(CONTAINERS).map((s) => s.containerName),
37
+ );
38
+
39
+ // Parse + validate one custom service definition. Throws on a bad shape.
40
+ export function parseCustomService(id, raw) {
41
+ let obj;
42
+ try {
43
+ obj = JSON.parse(raw);
44
+ } catch (err) {
45
+ throw new Error(`"${id}": not valid JSON (${err.message})`);
46
+ }
47
+ if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
48
+ throw new Error(`"${id}": expected a JSON object`);
49
+ }
50
+ if (typeof obj.image !== "string" || !obj.image.trim()) {
51
+ throw new Error(`"${id}": missing required string field "image"`);
52
+ }
53
+ if (typeof obj.containerName !== "string" || !obj.containerName.trim()) {
54
+ throw new Error(`"${id}": missing required string field "containerName"`);
55
+ }
56
+ if (BUILTIN_CONTAINER_NAMES.has(obj.containerName)) {
57
+ throw new Error(
58
+ `"${id}": containerName "${obj.containerName}" collides with a built-in service`,
59
+ );
60
+ }
61
+ if (obj.readyCheck && !KNOWN_READY_CHECKS.includes(obj.readyCheck)) {
62
+ throw new Error(
63
+ `"${id}": unknown readyCheck "${obj.readyCheck}" (valid: ${KNOWN_READY_CHECKS.join(", ")})`,
64
+ );
65
+ }
66
+
67
+ // `type` identifies the service for image/env overrides; default to the id.
68
+ return { id, ...obj, type: obj.type || id };
69
+ }
70
+
71
+ // Read + parse the registration at `path` (resolved like resource paths).
72
+ export function readCustomService(id, path) {
73
+ const resolved = resolveResourcePath(path);
74
+ const raw = readFileSync(resolved, "utf8");
75
+ return parseCustomService(id, raw);
76
+ }
77
+
78
+ // Load every registered custom service from config (`$.custom`).
79
+ // Returns [{ id, ...service }]. Throws with a clear message on the first bad one.
80
+ export function loadCustomServices(cfg) {
81
+ const entries = Object.entries(cfg.custom || {});
82
+ return entries.map(([id, path]) => {
83
+ try {
84
+ return readCustomService(id, path);
85
+ } catch (err) {
86
+ throw new Error(`Custom service ${err.message}`);
87
+ }
88
+ });
89
+ }
package/src/docker.js CHANGED
@@ -20,20 +20,27 @@ const AUTH_ERR_RE =
20
20
  // Registries we have already logged into during this process.
21
21
  const loggedInRegistries = new Set();
22
22
 
23
- function parseEcr(image) {
23
+ export function parseEcr(image) {
24
24
  const m = String(image).match(ECR_RE);
25
25
  if (!m) return null;
26
26
  return { registry: m[1], region: m[2] };
27
27
  }
28
28
 
29
+ // Build the `aws ecr get-login-password` argument list, optionally pinned to a
30
+ // named profile. Exported for testing the aws_profile wiring.
31
+ export function ecrPasswordArgs(region, profile) {
32
+ const args = ["ecr", "get-login-password", "--region", region];
33
+ if (profile) args.push("--profile", profile);
34
+ return args;
35
+ }
36
+
29
37
  // Authenticate docker to the ECR registry backing `image`, using the AWS CLI.
30
38
  // No-op (returns false) for non-ECR images. Throws on failure.
31
39
  function ecrLogin(ecr, profile) {
32
40
  if (!ecr) return false;
33
41
  if (loggedInRegistries.has(ecr.registry)) return true;
34
42
 
35
- const awsArgs = ["ecr", "get-login-password", "--region", ecr.region];
36
- if (profile) awsArgs.push("--profile", profile);
43
+ const awsArgs = ecrPasswordArgs(ecr.region, profile);
37
44
 
38
45
  const pw = spawnSync("aws", awsArgs, { encoding: "utf8" });
39
46
  if (pw.status !== 0) {
@@ -142,6 +149,8 @@ export function runDetached({
142
149
  network,
143
150
  "--network-alias",
144
151
  name,
152
+ "--pull",
153
+ "always"
145
154
  ];
146
155
  if (hostPort != null && containerPort != null) {
147
156
  args.push("-p", `${hostPort}:${containerPort}`);
@@ -165,7 +174,16 @@ export function runDetached({
165
174
  // Run a container in the foreground (blocking), removing it on exit.
166
175
  export function runBlocking({ name, image, env = {}, network }) {
167
176
  return new Promise((resolve, reject) => {
168
- const args = ["run", "--rm", "--name", name, "--network", network];
177
+ const args = [
178
+ "run",
179
+ "--rm",
180
+ "--name",
181
+ name,
182
+ "--network",
183
+ network,
184
+ "--pull",
185
+ "always"
186
+ ];
169
187
  for (const [k, v] of Object.entries(env)) {
170
188
  if (v === undefined || v === null || v === "") continue;
171
189
  args.push("-e", `${k}=${v}`);
package/src/http.js CHANGED
@@ -51,6 +51,17 @@ export async function waitForReady({
51
51
  );
52
52
  }
53
53
 
54
+ // Check whether a resource exists in the `.global` namespace. Returns true when
55
+ // the endpoint responds 200 with a JSON `{ items: [...] }` of non-zero length.
56
+ // Used to detect whether the core resources init already ran.
57
+ export async function globalResourceExists({ port, id }) {
58
+ const url = `http://localhost:${port}/.global/resources?id=${encodeURIComponent(
59
+ id,
60
+ )}&expand=no`;
61
+ const body = await probe(url);
62
+ return Boolean(body && Array.isArray(body.items) && body.items.length > 0);
63
+ }
64
+
54
65
  // Import a set of items into a resource-manager namespace.
55
66
  export async function importNamespace({ port, namespace, items }) {
56
67
  const url = `http://localhost:${port}/admin/${namespace}/resources`;
@@ -0,0 +1,61 @@
1
+ // Shared resource-import operations, used by both `bizone start` and
2
+ // `bizone resources import`.
3
+ //
4
+ // - core resources : the one-shot `bizone-resources` docker job, which imports
5
+ // into the `.global` namespace.
6
+ // - user resources : the namespaces the user configured via `bizone resources set`.
7
+
8
+ import { CONTAINERS, defaultEnvFor } from "./defaults.js";
9
+ import { getConfigValue } from "./config.js";
10
+ import { ensureImage, removeIfExists, runBlocking } from "./docker.js";
11
+ import { importNamespace, globalResourceExists } from "./http.js";
12
+ import { loadItems } from "./resources-loader.js";
13
+ import { log } from "./colors.js";
14
+
15
+ // A core resource that exists once the init job has run. Its presence in the
16
+ // `.global` namespace is how we detect a prior import.
17
+ export const CORE_INIT_PROBE_ID = "action/flow.start";
18
+
19
+ function imageFor(cfg, type) {
20
+ return cfg.images[type] || CONTAINERS[type].image;
21
+ }
22
+
23
+ function envFor(cfg, type) {
24
+ return { ...defaultEnvFor(type), ...(cfg.env[type] || {}) };
25
+ }
26
+
27
+ export function rmPort(cfg) {
28
+ return getConfigValue(cfg, CONTAINERS["resource-manager"].portKey);
29
+ }
30
+
31
+ // True when the core resources were already imported into `.global`.
32
+ export async function coreResourcesImported(cfg) {
33
+ return globalResourceExists({ port: rmPort(cfg), id: CORE_INIT_PROBE_ID });
34
+ }
35
+
36
+ // Run the one-shot `bizone-resources` job (blocking). Imports core resources
37
+ // into `.global`. Requires the resource-manager to be running on `network`.
38
+ export async function runCoreResourcesJob(cfg, { network, imageHooks } = {}) {
39
+ const job = CONTAINERS.resources;
40
+ removeIfExists(job.containerName);
41
+ ensureImage(imageFor(cfg, job.type), imageHooks);
42
+ await runBlocking({
43
+ name: job.containerName,
44
+ image: imageFor(cfg, job.type),
45
+ env: envFor(cfg, job.type),
46
+ network,
47
+ });
48
+ }
49
+
50
+ // Import every user-configured resource namespace into the resource-manager.
51
+ // Returns the number of namespaces imported.
52
+ export async function importUserResources(cfg) {
53
+ const namespaces = Object.entries(cfg.resources || {});
54
+ const port = rmPort(cfg);
55
+ for (const [namespace, path] of namespaces) {
56
+ const items = loadItems(path);
57
+ await importNamespace({ port, namespace, items });
58
+ log.ok(`Imported "${namespace}" (${items.length} item(s))`);
59
+ }
60
+ return namespaces.length;
61
+ }