c8ctl-plugin-nano 1.24.1 → 1.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -479,6 +479,7 @@ c8ctl nano supervisor
479
479
  c8ctl nano supervisor status # id, state, pid, restarts, uptime
480
480
  c8ctl nano supervisor add reviewer --max-parallel 2 # add + spawn a worker (forwards work flags)
481
481
  c8ctl nano supervisor add reviewer --name reviewer-2 # a SECOND reviewer, named so it stays distinct
482
+ c8ctl nano supervisor add reviewer --instances 3 # add 3 distinct auto-named reviewers in one call
482
483
  c8ctl nano supervisor restart reviewer # by worker id or profile name
483
484
  c8ctl nano supervisor remove coder # stop + drop a worker (also: `all`)
484
485
  c8ctl nano supervisor logs reviewer --follow # tail a worker's log (or the daemon's)
@@ -489,9 +490,13 @@ Each worker has a **name** — its supervisor id and the broker `workerName` it
489
490
  registers under. Pass `--name` on `supervisor add` (or `work`) to set it;
490
491
  omit it and one is auto-generated as `‹host›-‹profile›-‹random›`, so you can
491
492
  run **several instances of the same profile** and they stay distinct
492
- end-to-end (status, logs, and at the broker). `restart`/`remove` accept either
493
- a worker id **or** a profile name targeting a profile affects *every*
494
- instance of it.
493
+ end-to-end (status, logs, and at the broker). To scale a hire to several
494
+ instances in one call, pass `--instances N` on `supervisor add` it spawns N
495
+ distinct auto-named workers of the profile at once (default 1, capped per call).
496
+ Because each instance needs its own distinct name, `--instances N` (for N > 1)
497
+ cannot be combined with `--name`; omit `--name` to let them auto-name.
498
+ `restart`/`remove` accept either a worker id **or** a profile name — targeting a
499
+ profile affects *every* instance of it.
495
500
 
496
501
  Each worker takes the **same flags as `nano work`** (`--max-parallel`,
497
502
  `--recovery-window`, `--idle-timeout`, `--job-timeout`, `--poll-timeout`,
package/c8ctl-plugin.js CHANGED
@@ -3701,6 +3701,58 @@ function extractNameFlag(parts) {
3701
3701
  return { name: name != null && name.trim() !== '' ? name.trim() : undefined, rest };
3702
3702
  }
3703
3703
 
3704
+ /**
3705
+ * Upper bound on how many workers a single `supervisor add --instances N` may
3706
+ * spawn. Not a fleet cap (add again to grow further) — a typo guard, so a
3707
+ * fat-fingered `--instances 100000` can't fork-bomb the host in one keystroke.
3708
+ */
3709
+ const MAX_ADD_INSTANCES = 64;
3710
+
3711
+ /**
3712
+ * Parse the `--instances N` count for `supervisor add`. Accepts undefined/blank
3713
+ * (defaults to 1), and a whole number in `[1, MAX_ADD_INSTANCES]`. Rejects
3714
+ * non-integers, zero/negatives, and anything over the cap with a clear message.
3715
+ * When a flag is repeated the last occurrence wins (arrays are tolerated).
3716
+ * Returns `{ count }` on success or `{ error }` on rejection. Pure.
3717
+ */
3718
+ function parseInstancesCount(raw) {
3719
+ const v = Array.isArray(raw) ? raw[raw.length - 1] : raw;
3720
+ if (v === undefined || v === null || (typeof v === 'string' && v.trim() === '')) {
3721
+ return { count: 1 };
3722
+ }
3723
+ const s = String(v).trim();
3724
+ if (!/^\d+$/.test(s)) return { error: `Invalid --instances "${v}": use a whole number between 1 and ${MAX_ADD_INSTANCES}.` };
3725
+ const n = Number.parseInt(s, 10);
3726
+ if (n < 1) return { error: `Invalid --instances "${v}": use a whole number between 1 and ${MAX_ADD_INSTANCES}.` };
3727
+ if (n > MAX_ADD_INSTANCES) return { error: `--instances ${n} exceeds the ${MAX_ADD_INSTANCES}-per-command cap; run "supervisor add" again to add more.` };
3728
+ return { count: n };
3729
+ }
3730
+
3731
+ /**
3732
+ * Split `--instances N` / `--instances=N` out of a raw token list, returning
3733
+ * `{ count, rest, error }` where `rest` is the remaining work flags (with the
3734
+ * flag removed so it is never forwarded to `nano work`). Used by the interactive
3735
+ * console's `add`, whose tokens aren't parsed by the CLI flag layer. Last
3736
+ * occurrence wins; a trailing `--instances` with no value is treated as absent
3737
+ * (count 1), mirroring `extractNameFlag`. Pure.
3738
+ */
3739
+ function extractInstancesFlag(parts) {
3740
+ const rest = [];
3741
+ let raw;
3742
+ const list = Array.isArray(parts) ? parts : [];
3743
+ for (let i = 0; i < list.length; i++) {
3744
+ const tok = String(list[i]);
3745
+ const eq = /^--instances=(.*)$/.exec(tok);
3746
+ if (eq) { raw = eq[1]; continue; }
3747
+ if (tok === '--instances') {
3748
+ if (i + 1 < list.length) { raw = String(list[i + 1]); i++; }
3749
+ continue;
3750
+ }
3751
+ rest.push(tok);
3752
+ }
3753
+ return { ...parseInstancesCount(raw), rest };
3754
+ }
3755
+
3704
3756
  /** Assign a unique, stable worker id from a profile name (pure). */
3705
3757
  function supervisorWorkerId(profile, taken) {
3706
3758
  const base = String(profile || '').trim() || 'worker';
@@ -4479,14 +4531,31 @@ async function supervisorAddCmd(req, flags) {
4479
4531
  const logger = getLogger();
4480
4532
  // The positional profile is what runs; `--name` names this worker instance
4481
4533
  // (forwarded to the child as `nano work … --name`, and used as its supervisor
4482
- // id). Omit `--name` to auto-generate ‹host›-‹profile›-‹random›.
4534
+ // id). Omit `--name` to auto-generate ‹host›-‹profile›-‹random›. `--instances N`
4535
+ // spawns N distinct auto-named workers of the profile in one call.
4483
4536
  const profile = req.positional[1];
4484
- if (!profile) { logger.error('Usage: c8ctl nano supervisor add <profile> [--name <worker>] [work flags]'); process.exit(1); }
4537
+ if (!profile) { logger.error('Usage: c8ctl nano supervisor add <profile> [--name <worker>] [--instances <n>] [work flags]'); process.exit(1); }
4538
+ const { count, error } = parseInstancesCount(flags?.instances);
4539
+ if (error) { logger.error(error); process.exit(1); }
4485
4540
  const name = flags?.name ? String(flags.name).trim() : undefined;
4541
+ // A single `--name` can't apply to several distinct workers (each needs its
4542
+ // own broker workerName / supervisor id), so reject the combination and steer
4543
+ // the operator to auto-naming.
4544
+ if (name && count > 1) {
4545
+ logger.error('--name cannot be combined with --instances > 1 (each instance needs a distinct name); omit --name to auto-name them.');
4546
+ process.exit(1);
4547
+ }
4486
4548
  await startSupervisorDaemon();
4487
- const res = await supervisorRequest({ op: 'add', profile, name, args: reconstructWorkArgs(flags) });
4488
- if (res.ok) logger.info(`Added worker "${res.worker.id}" (profile ${profile}); pid ${res.worker.pid ?? 'starting'}.`);
4489
- else { logger.error(`Could not add "${profile}": ${res.error}`); process.exit(1); }
4549
+ const workArgs = reconstructWorkArgs(flags);
4550
+ let added = 0;
4551
+ let failed = 0;
4552
+ for (let i = 0; i < count; i++) {
4553
+ const res = await supervisorRequest({ op: 'add', profile, name, args: workArgs });
4554
+ if (res.ok) { added++; logger.info(`Added worker "${res.worker.id}" (profile ${profile}); pid ${res.worker.pid ?? 'starting'}.`); }
4555
+ else { failed++; logger.error(`Could not add "${profile}": ${res.error}`); }
4556
+ }
4557
+ if (count > 1) logger.info(`Added ${added}/${count} instance(s) of "${profile}".`);
4558
+ if (failed > 0) process.exit(1);
4490
4559
  }
4491
4560
 
4492
4561
  async function supervisorRemoveCmd(req) {
@@ -4636,16 +4705,21 @@ async function attachSupervisorConsole(state) {
4636
4705
  switch (cmd) {
4637
4706
  case '': break;
4638
4707
  case 'help':
4639
- out('Commands: status | add <profile> [--name <worker>] [work flags] |');
4708
+ out('Commands: status | add <profile> [--name <worker>] [--instances <n>] [work flags] |');
4640
4709
  out(' remove <id|profile|all> | restart <id|profile|all> |');
4641
4710
  out(' logs [id] | detach | stop | help');
4642
4711
  break;
4643
4712
  case 'status': sock.write(encodeFrame({ op: 'status' })); break;
4644
4713
  case 'add': {
4645
4714
  const profile = parts.shift();
4646
- if (!profile) { out('usage: add <profile> [--name <worker>] [work flags]'); break; }
4715
+ if (!profile) { out('usage: add <profile> [--name <worker>] [--instances <n>] [work flags]'); break; }
4647
4716
  const { name, rest } = extractNameFlag(parts);
4648
- sock.write(encodeFrame({ op: 'add', profile, name, args: rest }));
4717
+ const { count, rest: workArgs, error } = extractInstancesFlag(rest);
4718
+ if (error) { out(error); break; }
4719
+ if (name && count > 1) { out('--name cannot be combined with --instances > 1; omit --name to auto-name them.'); break; }
4720
+ for (let i = 0; i < count; i++) {
4721
+ sock.write(encodeFrame({ op: 'add', profile, name, args: workArgs }));
4722
+ }
4649
4723
  break;
4650
4724
  }
4651
4725
  case 'remove': case 'rm': {
@@ -6107,6 +6181,8 @@ export {
6107
6181
  isValidWorkerName,
6108
6182
  randomNameSuffix,
6109
6183
  extractNameFlag,
6184
+ parseInstancesCount,
6185
+ extractInstancesFlag,
6110
6186
  redactWorkArgs,
6111
6187
  supervisorBackoffMs,
6112
6188
  encodeFrame,
@@ -6120,6 +6196,7 @@ export {
6120
6196
  runSupervisorDaemon,
6121
6197
  startSupervisorDaemon,
6122
6198
  supervisorRequest,
6199
+ supervisorAddCmd,
6123
6200
  runningSupervisor,
6124
6201
  readSupervisorState,
6125
6202
  clearSupervisorState,
@@ -6169,6 +6246,7 @@ export const metadata = {
6169
6246
  { command: 'c8ctl nano supervisor', description: 'Attach an interactive console to the supervisor (detach with Ctrl-D, leaving it running)' },
6170
6247
  { command: 'c8ctl nano supervisor status', description: 'List supervised workers (pid, state, serviced job / idle, restarts, uptime) without the console' },
6171
6248
  { command: 'c8ctl nano supervisor add decider --max-parallel 2', description: 'Add a supervised worker (forwarding work flags) to the running supervisor' },
6249
+ { command: 'c8ctl nano supervisor add reviewer --instances 3', description: 'Add 3 distinct auto-named instances of a profile in one call' },
6172
6250
  { command: 'c8ctl nano supervisor restart reviewer', description: 'Restart a supervised worker by id or profile' },
6173
6251
  { command: 'c8ctl nano supervisor stop', description: 'Stop the supervisor daemon and all its workers' },
6174
6252
  ],
@@ -6236,6 +6314,7 @@ export const commands = {
6236
6314
  'poll-timeout': { type: 'string', description: 'work: broker long-poll window in ms each activateJobs request is held open (fewer reconnects → fewer transient connect errors); default 30000, 0 = broker default, negative = return immediately' },
6237
6315
  'job-type': { type: 'string', multiple: true, description: 'work: extra job type to service alongside the rank×capability matrix (repeatable)' },
6238
6316
  worker: { type: 'string', multiple: true, description: 'supervisor start: profile to launch as a supervised worker (repeatable)' },
6317
+ instances: { type: 'string', description: `supervisor add: spawn N distinct auto-named instances of the profile in one call (default 1, max ${MAX_ADD_INSTANCES}; cannot combine with --name)` },
6239
6318
  attach: { type: 'boolean', description: 'supervisor start: attach the interactive console after starting the daemon' },
6240
6319
  },
6241
6320
  handler: async (args, flags) => {
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.0.13",
3
- "commit": "d58b3d6",
4
- "updated": "2026-08-09T23:09:25Z"
2
+ "version": "0.0.14",
3
+ "commit": "db1cbf4",
4
+ "updated": "2026-08-10T01:55:19Z"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c8ctl-plugin-nano",
3
- "version": "1.24.1",
3
+ "version": "1.25.0",
4
4
  "type": "module",
5
5
  "description": "c8ctl plugin to start, inspect, and stop a local Nano BPM (nanobpmn) cluster",
6
6
  "main": "c8ctl-plugin.js",
@@ -47,12 +47,12 @@
47
47
  "semantic-release": "^25.0.3"
48
48
  },
49
49
  "optionalDependencies": {
50
- "@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.24.1",
51
- "@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.24.1",
52
- "@nanobpm/c8ctl-plugin-nano-linux-x64": "1.24.1",
53
- "@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.24.1",
54
- "@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.24.1",
55
- "@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.24.1",
56
- "@nanobpm/c8ctl-plugin-nano-win32-x64": "1.24.1"
50
+ "@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.25.0",
51
+ "@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.25.0",
52
+ "@nanobpm/c8ctl-plugin-nano-linux-x64": "1.25.0",
53
+ "@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.25.0",
54
+ "@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.25.0",
55
+ "@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.25.0",
56
+ "@nanobpm/c8ctl-plugin-nano-win32-x64": "1.25.0"
57
57
  }
58
58
  }