c8ctl-plugin-nano 1.7.1 → 1.8.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 (3) hide show
  1. package/README.md +15 -0
  2. package/c8ctl-plugin.js +123 -15
  3. package/package.json +8 -8
package/README.md CHANGED
@@ -169,6 +169,21 @@ and the history cap.
169
169
  > ⚠️ With `--in-memory`, restart recovers nothing, and Raft/replicated logs are
170
170
  > not persisted. Use it for stress/throughput testing, not durability testing.
171
171
 
172
+ ## Console profile (`--console` / `--profile`)
173
+
174
+ The server ships a browser console. Pick how much of it is exposed at runtime:
175
+
176
+ ```bash
177
+ c8ctl nano start # studio (default): full IDE + authoring API
178
+ c8ctl nano start --console observe # observability views only; authoring refused (403)
179
+ c8ctl nano start --console off # headless: no console router at all
180
+ ```
181
+
182
+ - Values: `studio` (default), `observe`, `off`. `--profile` is an alias for
183
+ `--console`, and an inherited `NANOBPMN_CONSOLE` env var is honored when neither
184
+ flag is passed. The plugin passes the choice through as `NANOBPMN_CONSOLE` on
185
+ every node.
186
+
172
187
  ## Configuration (`set` / `config`)
173
188
 
174
189
  Persistent settings are stored in `<state home>/config.json`:
package/c8ctl-plugin.js CHANGED
@@ -13,13 +13,15 @@
13
13
  * NANOBPMN_RF replication factor (1 = single-homed, no Raft)
14
14
  * NANOBPMN_RAFT set when RF > 1 to enable per-partition Raft
15
15
  * NANOBPMN_DATA_DIR this node's engine data directory
16
+ * NANOBPMN_CONSOLE runtime console profile (off | observe | studio)
17
+ * NANOBPMN_NODE_BIN Node path for the server's worker fallback runtime
16
18
  *
17
19
  * This plugin spawns N detached node processes wired to talk to each other on
18
20
  * localhost, tracks them in a state file, and stops them on request.
19
21
  *
20
22
  * Usage:
21
23
  * c8ctl nano start [<nodes>] [--port <basePort>] [--partitions <n>] [--rf <n>]
22
- * [--in-memory] [--history-max <n>]
24
+ * [--in-memory] [--history-max <n>] [--console <profile>]
23
25
  * c8ctl nano status
24
26
  * c8ctl nano stop [--purge]
25
27
  * c8ctl nano logs [<nodeId>] [--follow]
@@ -37,9 +39,10 @@ import {
37
39
  readdirSync,
38
40
  chmodSync,
39
41
  renameSync,
42
+ realpathSync,
40
43
  } from 'node:fs';
41
44
  import { homedir, platform as osPlatform } from 'node:os';
42
- import { join, isAbsolute, resolve as resolvePath, dirname } from 'node:path';
45
+ import { join, isAbsolute, resolve as resolvePath, dirname, sep } from 'node:path';
43
46
  import { createRequire } from 'node:module';
44
47
  import { fileURLToPath } from 'node:url';
45
48
  import { platformForHost } from './platforms.mjs';
@@ -333,6 +336,18 @@ function findBinary(flags) {
333
336
  */
334
337
  function launcherEnvMarkers(resolved) {
335
338
  const markers = { NANOBPMN_LAUNCHER: 'c8ctl-plugin-nano' };
339
+
340
+ // This launcher IS a Node runtime, so hand the server a known-good Node path
341
+ // for its worker fallback (Deno-preferred, Node >= 22.6). Avoid pinning an
342
+ // older Node runtime (the plugin supports Node >=18) so the server can still
343
+ // fall back to a newer Node on PATH when available.
344
+ const [nodeMajor, nodeMinor, nodePatch] = process.versions.node
345
+ .split('.')
346
+ .map((n) => Number.parseInt(n, 10));
347
+ const nodeOk =
348
+ nodeMajor > 22 ||
349
+ (nodeMajor === 22 && (nodeMinor > 6 || (nodeMinor === 6 && nodePatch >= 0)));
350
+ if (nodeOk) markers.NANOBPMN_NODE_BIN = process.execPath;
336
351
  const { version } = pluginPackage();
337
352
  // The plugin version is the update unit's "current" in the npm channel's
338
353
  // version space (same space as `npm view <plugin> version` -> latest), so the
@@ -380,6 +395,7 @@ function parseRequest(args, flags) {
380
395
  capture: Boolean(flags?.capture),
381
396
  inMemory: Boolean(flags?.['in-memory'] || flags?.['no-journal']),
382
397
  historyMax: intFlag('history-max'),
398
+ console: flags?.console ?? flags?.profile,
383
399
  workspace: Boolean(flags?.workspace),
384
400
  check: Boolean(flags?.check),
385
401
  binary: flags?.binary,
@@ -528,6 +544,26 @@ async function waitForHealthy(url, timeoutMs = READINESS_TIMEOUT_MS) { const st
528
544
  // start
529
545
  // ---------------------------------------------------------------------------
530
546
 
547
+ /** Runtime console profiles the server understands (nano-bpm ADR 0035 §C). */
548
+ const CONSOLE_PROFILES = ['off', 'observe', 'studio'];
549
+
550
+ /**
551
+ * Resolves the runtime console profile to pass through as NANOBPMN_CONSOLE.
552
+ * Precedence: --console/--profile flag > inherited NANOBPMN_CONSOLE env >
553
+ * 'studio' (the full IDE, our default). Unknown values are rejected so a typo
554
+ * fails fast here rather than silently degrading the console in the server.
555
+ */
556
+ function resolveConsoleProfile(reqConsole) {
557
+ const raw = reqConsole ?? process.env.NANOBPMN_CONSOLE ?? 'studio';
558
+ const profile = String(raw).trim().toLowerCase();
559
+ if (!CONSOLE_PROFILES.includes(profile)) {
560
+ throw new Error(
561
+ `invalid console profile "${raw}" (use one of: ${CONSOLE_PROFILES.join(', ')})`,
562
+ );
563
+ }
564
+ return profile;
565
+ }
566
+
531
567
  async function startCluster(req) {
532
568
  const logger = getLogger();
533
569
 
@@ -556,6 +592,7 @@ async function startCluster(req) {
556
592
  const capture = Boolean(req.capture);
557
593
  const inMemory = Boolean(req.inMemory);
558
594
  const historyMax = req.historyMax;
595
+ const consoleProfile = resolveConsoleProfile(req.console);
559
596
 
560
597
  if (partitions < nodeCount) {
561
598
  logger.warn(
@@ -629,7 +666,8 @@ async function startCluster(req) {
629
666
  `Starting Nano BPM cluster: ${nodeCount} node(s), ${partitions} partition(s), ` +
630
667
  `RF=${rf}${raft ? ', Raft on' : ''}${capture ? ', trace capture on' : ''}` +
631
668
  `${inMemory ? ', in-memory (no disk)' : ''}` +
632
- `${historyMax !== undefined ? `, history-max=${historyMax}` : ''}`,
669
+ `${historyMax !== undefined ? `, history-max=${historyMax}` : ''}` +
670
+ `${consoleProfile !== 'studio' ? `, console=${consoleProfile}` : ''}`,
633
671
  );
634
672
  logger.info(`Binary: ${binary}`);
635
673
  logger.info(`Workspace: ${workspaceDir} (models/, workers/)`);
@@ -666,6 +704,10 @@ async function startCluster(req) {
666
704
  // Shared, persistent authoring workspace (models + workers). Lives
667
705
  // outside the per-node data dir so "nano clean" never wipes it.
668
706
  NANOBPMN_WORKSPACE_DIR: workspaceDir,
707
+ // Runtime console profile (off | observe | studio). Default studio (full
708
+ // IDE); pass-through so --console/--profile or an inherited NANOBPMN_CONSOLE
709
+ // picks the observability-only or headless surface. See nano-bpm ADR 0035 §C.
710
+ NANOBPMN_CONSOLE: consoleProfile,
669
711
  };
670
712
  // Storage axis: an on-disk journal + read-model under the per-node data dir
671
713
  // (default), or a fully in-memory engine (in-memory journal + :memory: read
@@ -1308,6 +1350,51 @@ function isGlobalInstall() {
1308
1350
  return Boolean(root) && pluginDir.startsWith(root);
1309
1351
  }
1310
1352
 
1353
+ /**
1354
+ * How this plugin is installed, which decides how `nano update` self-updates:
1355
+ * - 'managed': under c8ctl's own plugin store (…/c8ctl/plugins/node_modules),
1356
+ * where `c8ctl load plugin` installed it. Self-update in place by
1357
+ * reinstalling into that same npm --prefix. This is the norm for the
1358
+ * integrated c8ctl plugin architecture, so it takes precedence over a
1359
+ * coincidental global install of the same name.
1360
+ * - 'global': under `npm root -g` (a plain `npm install -g`).
1361
+ * - 'local': a checkout / `npm link` — self-update isn't safe; tell the user.
1362
+ */
1363
+ function pluginInstallInfo() {
1364
+ const rt = globalThis.c8ctl;
1365
+ if (rt && typeof rt.getUserDataDir === 'function') {
1366
+ try {
1367
+ // Node resolves symlinks when computing this module's path, so realpath
1368
+ // both sides before comparing (e.g. macOS /var → /private/var, or a
1369
+ // C8CTL_DATA_DIR that isn't canonicalized) to avoid a false 'local'.
1370
+ const real = (p) => {
1371
+ try {
1372
+ return realpathSync(p);
1373
+ } catch {
1374
+ return p;
1375
+ }
1376
+ };
1377
+ const pluginsDir = join(rt.getUserDataDir(), 'plugins');
1378
+ const nm = real(join(pluginsDir, 'node_modules'));
1379
+ const self = real(pluginDir);
1380
+ if (self === nm || self.startsWith(nm + sep)) {
1381
+ return { mode: 'managed', prefix: real(pluginsDir) };
1382
+ }
1383
+ } catch {
1384
+ /* fall through to the global/local probes */
1385
+ }
1386
+ }
1387
+ if (isGlobalInstall()) return { mode: 'global' };
1388
+ return { mode: 'local' };
1389
+ }
1390
+
1391
+ /** The copy-pasteable command that matches how this plugin is installed. */
1392
+ function manualUpdateCommand(name, info) {
1393
+ if (info.mode === 'managed') return ` c8ctl load plugin ${name}@latest`;
1394
+ if (info.mode === 'local') return ' git pull # in your checkout, then reload the plugin';
1395
+ return ` npm install -g ${name}@latest`;
1396
+ }
1397
+
1311
1398
  function updatePlugin(req) {
1312
1399
  const { name, version: current } = pluginPackage();
1313
1400
 
@@ -1326,7 +1413,8 @@ function updatePlugin(req) {
1326
1413
  const nanoNote = nanoBin
1327
1414
  ? ` (nano server ${nanoVer ?? bundled?.version ?? 'present'})`
1328
1415
  : ' (nano server: not installed for this platform)';
1329
- const manual = ` npm install -g ${name}@latest`;
1416
+ const info = pluginInstallInfo();
1417
+ const manual = manualUpdateCommand(name, info);
1330
1418
 
1331
1419
  console.log(`Installed: ${name} v${current ?? '?'}${nanoNote}`);
1332
1420
 
@@ -1362,26 +1450,42 @@ function updatePlugin(req) {
1362
1450
  return;
1363
1451
  }
1364
1452
 
1365
- if (!isGlobalInstall()) {
1366
- console.log('This plugin is not a global npm install, so it cannot self-update in place.');
1367
- console.log('Pull the latest release with:');
1453
+ if (info.mode === 'local') {
1454
+ console.log('This plugin runs from a local checkout, so it cannot self-update in place.');
1455
+ console.log('Update it with:');
1368
1456
  console.log(manual);
1369
- console.log('(or, for a local checkout, `git pull` then reload the plugin).');
1370
1457
  return;
1371
1458
  }
1372
1459
 
1373
- console.log(`Pulling ${name}@${latest} via npm...`);
1460
+ const installArgs =
1461
+ info.mode === 'managed'
1462
+ ? ['install', `${name}@${latest}`, '--prefix', info.prefix]
1463
+ : ['install', '-g', `${name}@${latest}`];
1464
+ const where = info.mode === 'managed' ? 'the c8ctl plugin store' : "npm's global prefix";
1465
+ console.log(`Pulling ${name}@${latest} into ${where}...`);
1374
1466
  console.log('');
1375
- const res = spawnSync('npm', ['install', '-g', `${name}@${latest}`], { stdio: 'inherit' });
1467
+ const res = spawnSync('npm', installArgs, { stdio: 'inherit' });
1376
1468
  if (res.error) throw new Error(res.error.message);
1377
1469
  if (res.status !== 0) {
1470
+ let hint;
1471
+ if (info.mode === 'managed') {
1472
+ hint = `You can also run:\n${manual}`;
1473
+ } else if (osPlatform() === 'win32') {
1474
+ hint = `You may need to run this command in an elevated terminal (Administrator): ${manual.trim()}`;
1475
+ } else {
1476
+ hint = `You may need elevated permissions: sudo ${manual.trim()}`;
1477
+ }
1378
1478
  throw new Error(
1379
- `npm install -g ${name}@${latest} failed (exit ${res.status}). ` +
1380
- `You may need elevated permissions: sudo ${manual.trim()}`,
1479
+ `npm ${installArgs.join(' ')} failed (exit ${res.status}). ${hint}`,
1381
1480
  );
1382
1481
  }
1383
1482
  console.log('');
1384
- console.log(`Updated to v${latest}. Restart any running cluster to use the new binary:`);
1483
+ if (info.mode === 'managed') {
1484
+ console.log(`Updated to v${latest}. The new plugin and bundled nano server load on your next c8ctl command.`);
1485
+ } else {
1486
+ console.log(`Updated to v${latest}.`);
1487
+ }
1488
+ console.log('Restart any running cluster to use the new server binary:');
1385
1489
  console.log(' c8ctl nano restart');
1386
1490
  }
1387
1491
 
@@ -1449,12 +1553,13 @@ function spawnUpdateRefresh(name, cacheFile) {
1449
1553
  }
1450
1554
 
1451
1555
  function printUpdateNotice(name, current, latest) {
1556
+ const manual = manualUpdateCommand(name, pluginInstallInfo()).trim();
1452
1557
  const lines = [
1453
1558
  '',
1454
1559
  `╭─ Update available: ${name} v${current} → v${latest}`,
1455
1560
  '│ A newer nano release (plugin + bundled server) is published on npm.',
1456
1561
  '│ Install it: c8ctl nano update',
1457
- `│ Or manually: npm install -g ${name}@latest`,
1562
+ `│ Or manually: ${manual}`,
1458
1563
  '╰─ Then restart any running cluster: c8ctl nano restart',
1459
1564
  '',
1460
1565
  ];
@@ -2476,6 +2581,8 @@ export const commands = {
2476
2581
  'in-memory': { type: 'boolean', description: 'start: run with NO on-disk journal/read-model (in-memory engine; state lost on restart). Alias: --no-journal' },
2477
2582
  'no-journal': { type: 'boolean', description: 'start: alias for --in-memory' },
2478
2583
  'history-max': { type: 'string', description: 'start: cap retained terminal instances in the read model (NANOBPMN_HISTORY_MAX_INSTANCES; 0/unset = unbounded)' },
2584
+ console: { type: 'string', description: 'start: runtime console profile off|observe|studio (NANOBPMN_CONSOLE; default studio). Alias: --profile' },
2585
+ profile: { type: 'string', description: 'start: alias for --console (off|observe|studio; default studio)' },
2479
2586
  follow: { type: 'boolean', description: 'logs: stream output (tail -F)', short: 'f' },
2480
2587
  purge: { type: 'boolean', description: 'stop/restart: also delete per-node engine data' },
2481
2588
  force: { type: 'boolean', description: 'start: stop any existing cluster first' },
@@ -2607,7 +2714,7 @@ export const commands = {
2607
2714
 
2608
2715
  function printUsage() {
2609
2716
  console.log('Usage:');
2610
- console.log(' c8ctl nano start [<nodes>] [--port <basePort>] [--partitions <n>] [--rf <n>] [--raft] [--capture] [--in-memory] [--history-max <n>] [--binary <path>]');
2717
+ console.log(' c8ctl nano start [<nodes>] [--port <basePort>] [--partitions <n>] [--rf <n>] [--raft] [--capture] [--in-memory] [--history-max <n>] [--console <profile>] [--binary <path>]');
2611
2718
  console.log(' c8ctl nano status [--port <port>]');
2612
2719
  console.log(' c8ctl nano stop [--purge]');
2613
2720
  console.log(' c8ctl nano logs [<nodeId>] [--follow]');
@@ -2641,6 +2748,7 @@ function printUsage() {
2641
2748
  console.log(' --capture start: enable trace capture (recorded-input replay) on every node');
2642
2749
  console.log(' --in-memory start: run with NO on-disk journal/read-model (alias --no-journal; state lost on restart)');
2643
2750
  console.log(' --history-max <n> start: cap retained terminal instances in the read model (0/unset = unbounded)');
2751
+ console.log(' --console <profile> start: runtime console profile off|observe|studio (alias --profile; default studio)');
2644
2752
  console.log(' --binary <path> Path to the nanobpmn server binary (overrides "set bin")');
2645
2753
  console.log(' --purge stop: also delete per-node engine data');
2646
2754
  console.log(' --force start: stop any existing cluster first');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c8ctl-plugin-nano",
3
- "version": "1.7.1",
3
+ "version": "1.8.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",
@@ -49,12 +49,12 @@
49
49
  "semantic-release": "^25.0.3"
50
50
  },
51
51
  "optionalDependencies": {
52
- "@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.7.1",
53
- "@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.7.1",
54
- "@nanobpm/c8ctl-plugin-nano-linux-x64": "1.7.1",
55
- "@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.7.1",
56
- "@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.7.1",
57
- "@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.7.1",
58
- "@nanobpm/c8ctl-plugin-nano-win32-x64": "1.7.1"
52
+ "@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.8.0",
53
+ "@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.8.0",
54
+ "@nanobpm/c8ctl-plugin-nano-linux-x64": "1.8.0",
55
+ "@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.8.0",
56
+ "@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.8.0",
57
+ "@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.8.0",
58
+ "@nanobpm/c8ctl-plugin-nano-win32-x64": "1.8.0"
59
59
  }
60
60
  }