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.
- package/README.md +15 -0
- package/c8ctl-plugin.js +123 -15
- 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
|
|
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 (
|
|
1366
|
-
console.log('This plugin
|
|
1367
|
-
console.log('
|
|
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
|
-
|
|
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',
|
|
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
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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.
|
|
53
|
-
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.
|
|
54
|
-
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.
|
|
55
|
-
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.
|
|
56
|
-
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.
|
|
57
|
-
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.
|
|
58
|
-
"@nanobpm/c8ctl-plugin-nano-win32-x64": "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
|
}
|