c8ctl-plugin-nano 1.3.2 → 1.4.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 +61 -6
  2. package/c8ctl-plugin.js +167 -56
  3. package/package.json +6 -6
package/README.md CHANGED
@@ -30,6 +30,30 @@ c8ctl nano start|status|stop|restart|logs|clean|set|config
30
30
  on `localhost` (round-robin partition ownership), tracks them in a state file,
31
31
  and waits until every node is reachable.
32
32
 
33
+ ## Installation
34
+
35
+ This is a plugin for the [Camunda 8 CLI](https://www.npmjs.com/package/@camunda8/cli)
36
+ (`c8ctl`). Install the CLI, then load the plugin from npm:
37
+
38
+ ```bash
39
+ # 1. Install the Camunda 8 CLI (once); requires Node.js 18+
40
+ npm install -g @camunda8/cli
41
+
42
+ # 2. Load this plugin from the npm registry
43
+ c8ctl load plugin c8ctl-plugin-nano
44
+
45
+ # 3. Verify it's available
46
+ c8ctl nano --help
47
+ ```
48
+
49
+ The prebuilt Nano BPM server binary for your platform is pulled in automatically
50
+ as an npm `optionalDependency`, so there is nothing to compile. To pull a newer
51
+ release later, run `c8ctl nano update` (see
52
+ [Updating to a new release](#updating-to-a-new-release-update)).
53
+
54
+ > Loading from a local checkout instead? Use
55
+ > `c8ctl load plugin --from file:///path/to/c8ctl-nano`.
56
+
33
57
  ## Usage
34
58
 
35
59
  ```bash
@@ -113,6 +137,35 @@ c8ctl nano stop --purge # stop and remove engine data in one step
113
137
 
114
138
  `clean` refuses to run while any node is alive.
115
139
 
140
+ ### Stress / throughput runs: bounding disk and RAM
141
+
142
+ The engine journal (`journal.jsonl`) is **append-only** — there is currently no
143
+ compaction or rotation — and the read-model retains every terminal instance by
144
+ default. Under sustained high load (tens of thousands of PI/s) this fills the
145
+ disk quickly. Two `start` flags keep a long run bounded:
146
+
147
+ ```bash
148
+ # Pure throughput: no journal / read-model on disk at all (in-memory engine).
149
+ # State is lost on stop/restart, and instances live in RAM — so cap them.
150
+ c8ctl nano start --in-memory --history-max 50000
151
+
152
+ # Exercise the disk path but cap the read model's terminal-instance history.
153
+ # (The journal still grows append-only; watch free space.)
154
+ c8ctl nano start --history-max 50000
155
+ ```
156
+
157
+ - `--in-memory` (alias `--no-journal`) routes the engine to an in-memory journal
158
+ and a `:memory:` read store — nothing is written under `NANOBPMN_DATA_DIR`.
159
+ - `--history-max <n>` sets `NANOBPMN_HISTORY_MAX_INSTANCES`, continuously pruning
160
+ all but the most recent *n* terminal instances from the read model (`0`/unset =
161
+ unbounded). Works in both storage modes.
162
+
163
+ `c8ctl nano status` reports the active storage mode (`in-memory` vs `on-disk`)
164
+ and the history cap.
165
+
166
+ > ⚠️ With `--in-memory`, restart recovers nothing, and Raft/replicated logs are
167
+ > not persisted. Use it for stress/throughput testing, not durability testing.
168
+
116
169
  ## Configuration (`set` / `config`)
117
170
 
118
171
  Persistent settings are stored in `<state home>/config.json`:
@@ -370,13 +423,13 @@ ProcessOS is the optimization-plane server that analyses a running Nano BPM
370
423
  engine. The plugin can manage a single local ProcessOS instance with the same
371
424
  start/stop/status/logs lifecycle as `nano`.
372
425
 
373
- > **ProcessOS is a closed alpha.** The operational commands (`start`, `stop`,
426
+ > **ProcessOS is a closed beta.** The operational commands (`start`, `stop`,
374
427
  > `status`, `logs`, `restart`) stay locked with a *"not available yet"* notice
375
428
  > until you opt in. Only `set` and `config` work before then. Opt in either by
376
429
  > setting the download URL you were given by the Nano BPM team, or by pointing
377
430
  > the plugin at a binary you already have.
378
431
 
379
- ### Quick install (closed-alpha invitees)
432
+ ### Quick install (closed-beta invitees)
380
433
 
381
434
  If you were given a ProcessOS download URL, this one-liner installs the Camunda 8
382
435
  CLI (`@camunda8/cli`) and this plugin, then configures the download URL:
@@ -391,7 +444,7 @@ curl -fsSL https://gist.githubusercontent.com/jwulf/9015a7c660b274c568d80e85c391
391
444
 
392
445
 
393
446
  ```bash
394
- # Closed-alpha channel: persist the download URL, then start
447
+ # Closed-beta channel: persist the download URL, then start
395
448
  c8ctl processos set download-url <url you were given>
396
449
  c8ctl processos start # fetches processos-<os>-<arch> on first run
397
450
 
@@ -423,7 +476,7 @@ c8ctl processos stop
423
476
 
424
477
  ### Automatic update notice
425
478
 
426
- When you're on the closed-alpha channel (download URL configured), the
479
+ When you're on the closed-beta channel (download URL configured), the
427
480
  plugin checks for newer ProcessOS builds in the background and prints a short
428
481
  one-line notice (at most **once per day**) when the published version is newer
429
482
  than the one you're running. It compares your installed binary's version against
@@ -473,7 +526,7 @@ Settings persist under a `processos` key in the same `config.json` as `nano`.
473
526
 
474
527
  ```bash
475
528
  c8ctl processos set bin <path> # path to the downloaded ProcessOS binary
476
- c8ctl processos set download-url <url> # closed-alpha binary download URL (enables ProcessOS)
529
+ c8ctl processos set download-url <url> # closed-beta binary download URL (enables ProcessOS)
477
530
  c8ctl processos set port <n> # listen port (default 8090)
478
531
  c8ctl processos set nano-url <url> # target Nano BPM engine (default http://localhost:8080)
479
532
  c8ctl processos set data-dir <path> # PROCESSOS_DATA_DIR (default <stateHome>/processos-data)
@@ -489,7 +542,7 @@ configured download URL (`set download-url` / `$PROCESSOS_DOWNLOAD_URL`). Typed
489
542
  settings (`port`, `nano-url`, `data-dir`) always
490
543
  win over generic `env` passthrough values when launching.
491
544
 
492
- ## Installing
545
+ ## Installing from a local checkout (development)
493
546
 
494
547
  ```bash
495
548
  c8ctl load plugin --from file:///path/to/c8ctl-nano
@@ -501,6 +554,8 @@ Then verify it shows up:
501
554
  c8ctl help | grep nano
502
555
  ```
503
556
 
557
+ For the normal npm install, see [Installation](#installation) above.
558
+
504
559
  ## Distribution & releasing
505
560
 
506
561
  Releases are automated with **semantic-release** (`.github/workflows/release.yml`,
package/c8ctl-plugin.js CHANGED
@@ -19,6 +19,7 @@
19
19
  *
20
20
  * Usage:
21
21
  * c8ctl nano start [<nodes>] [--port <basePort>] [--partitions <n>] [--rf <n>]
22
+ * [--in-memory] [--history-max <n>]
22
23
  * c8ctl nano status
23
24
  * c8ctl nano stop [--purge]
24
25
  * c8ctl nano logs [<nodeId>] [--follow]
@@ -63,6 +64,21 @@ function readBundledBinaryInfo() {
63
64
  }
64
65
  }
65
66
 
67
+ /** Run `<binary> --version` and extract a semver-ish token. Null on failure. */
68
+ function binaryVersion(binary) {
69
+ if (!binary) return null;
70
+ try {
71
+ const res = spawnSync(binary, ['--version'], { encoding: 'utf8', timeout: 3000 });
72
+ if (res.status === 0) {
73
+ const m = String(res.stdout || res.stderr || '').match(/(\d+\.\d+\.\d+[^\s]*)/);
74
+ if (m) return m[1];
75
+ }
76
+ } catch {
77
+ /* ignore */
78
+ }
79
+ return null;
80
+ }
81
+
66
82
  /**
67
83
  * Locate the nanobpmn binary shipped by the matching platform package
68
84
  * (an optionalDependency such as @nanobpm/c8ctl-plugin-nano-darwin-arm64).
@@ -104,7 +120,7 @@ const UPDATE_CACHE_FILE = 'update-check.json';
104
120
  const UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1000;
105
121
  const UPDATE_NOTIFY_TTL_MS = 24 * 60 * 60 * 1000;
106
122
 
107
- // ProcessOS is a closed alpha distributed out-of-band: the binary lives in an
123
+ // ProcessOS is a closed beta distributed out-of-band: the binary lives in an
108
124
  // S3 bucket whose base URL is handed to enabled users via PROCESSOS_DOWNLOAD_URL.
109
125
  // `<base>/processos-<os>-<arch>[.exe]` is the per-platform binary and
110
126
  // `<base>/version.json` is the {version,commit,updated} metadata the CI writes
@@ -307,6 +323,8 @@ function parseRequest(args, flags) {
307
323
  purge: Boolean(flags?.purge),
308
324
  force: Boolean(flags?.force),
309
325
  capture: Boolean(flags?.capture),
326
+ inMemory: Boolean(flags?.['in-memory'] || flags?.['no-journal']),
327
+ historyMax: intFlag('history-max'),
310
328
  workspace: Boolean(flags?.workspace),
311
329
  check: Boolean(flags?.check),
312
330
  binary: flags?.binary,
@@ -481,6 +499,8 @@ async function startCluster(req) {
481
499
  // Raft is required for replication; auto-enable when RF > 1, allow override.
482
500
  const raft = req.raft === undefined ? rf > 1 : Boolean(req.raft);
483
501
  const capture = Boolean(req.capture);
502
+ const inMemory = Boolean(req.inMemory);
503
+ const historyMax = req.historyMax;
484
504
 
485
505
  if (partitions < nodeCount) {
486
506
  logger.warn(
@@ -491,6 +511,21 @@ async function startCluster(req) {
491
511
  if (req.rf && req.rf > nodeCount) {
492
512
  logger.warn(`--rf ${req.rf} clamped to node count (${nodeCount}).`);
493
513
  }
514
+ if (inMemory) {
515
+ logger.warn(
516
+ 'In-memory mode: no journal or read-model is written to disk. Engine state is ' +
517
+ 'lost on stop/restart, and every retained instance lives in RAM' +
518
+ (historyMax === undefined
519
+ ? ' — pair with --history-max <N> to bound RAM under sustained load.'
520
+ : '.'),
521
+ );
522
+ if (raft || rf > 1) {
523
+ logger.warn(
524
+ 'In-memory mode with Raft/replication: replicated logs are not persisted; ' +
525
+ 'a restarted node recovers nothing.',
526
+ );
527
+ }
528
+ }
494
529
 
495
530
  const binary = findBinary(req);
496
531
 
@@ -533,7 +568,9 @@ async function startCluster(req) {
533
568
 
534
569
  logger.info(
535
570
  `Starting Nano BPM cluster: ${nodeCount} node(s), ${partitions} partition(s), ` +
536
- `RF=${rf}${raft ? ', Raft on' : ''}${capture ? ', trace capture on' : ''}`,
571
+ `RF=${rf}${raft ? ', Raft on' : ''}${capture ? ', trace capture on' : ''}` +
572
+ `${inMemory ? ', in-memory (no disk)' : ''}` +
573
+ `${historyMax !== undefined ? `, history-max=${historyMax}` : ''}`,
537
574
  );
538
575
  logger.info(`Binary: ${binary}`);
539
576
  logger.info(`Workspace: ${workspaceDir} (models/, workers/)`);
@@ -543,7 +580,7 @@ async function startCluster(req) {
543
580
  const port = ports[id];
544
581
  const dataDir = join(getDataDir(), `node-${id}`);
545
582
  const logFile = join(getLogDir(), `node-${id}.log`);
546
- mkdirSync(dataDir, { recursive: true });
583
+ if (!inMemory) mkdirSync(dataDir, { recursive: true });
547
584
 
548
585
  const env = {
549
586
  ...process.env,
@@ -552,7 +589,6 @@ async function startCluster(req) {
552
589
  NANOBPMN_NODES: nodesEnv,
553
590
  NANOBPMN_PARTITIONS: String(partitions),
554
591
  NANOBPMN_RF: String(rf),
555
- NANOBPMN_DATA_DIR: dataDir,
556
592
  // Default to async durability (group-commit) for throughput; the user can
557
593
  // override per the spread of process.env above by exporting
558
594
  // NANOBPMN_DURABILITY (e.g. "sync") before running.
@@ -569,6 +605,22 @@ async function startCluster(req) {
569
605
  // outside the per-node data dir so "nano clean" never wipes it.
570
606
  NANOBPMN_WORKSPACE_DIR: workspaceDir,
571
607
  };
608
+ // Storage axis: an on-disk journal + read-model under the per-node data dir
609
+ // (default), or a fully in-memory engine (in-memory journal + :memory: read
610
+ // store) when --in-memory is set. In in-memory mode, scrub any inherited
611
+ // path vars so nothing leaks back to disk.
612
+ if (inMemory) {
613
+ delete env.NANOBPMN_DATA_DIR;
614
+ delete env.NANOBPMN_JOURNAL;
615
+ delete env.NANOBPMN_READ_DB;
616
+ } else {
617
+ env.NANOBPMN_DATA_DIR = dataDir;
618
+ }
619
+ // Bound retained terminal instances in the read model when requested. Works
620
+ // in both storage modes (caps disk growth on-disk; caps RAM in-memory).
621
+ if (historyMax !== undefined) {
622
+ env.NANOBPMN_HISTORY_MAX_INSTANCES = String(historyMax);
623
+ }
572
624
  if (raft) env.NANOBPMN_RAFT = '1';
573
625
  // Trace capture: a single flag enables the Tier 2 recorded-input (stimuli)
574
626
  // log AND auto-enables Tier 1 variable capture, so historical replay /
@@ -599,7 +651,7 @@ async function startCluster(req) {
599
651
  process.exit(1);
600
652
  }
601
653
 
602
- nodes.push({ id, port, pid: child.pid, url: peers[id], dataDir, logFile });
654
+ nodes.push({ id, port, pid: child.pid, url: peers[id], dataDir: inMemory ? null : dataDir, logFile });
603
655
  logger.info(` node ${id}: pid ${child.pid} → ${peers[id]} (log: ${logFile})`);
604
656
  }
605
657
 
@@ -612,6 +664,8 @@ async function startCluster(req) {
612
664
  rf,
613
665
  raft,
614
666
  capture,
667
+ inMemory,
668
+ historyMax: historyMax ?? null,
615
669
  basePort,
616
670
  nodes,
617
671
  };
@@ -648,7 +702,7 @@ async function printSummary(state) {
648
702
  console.log('');
649
703
  console.log(
650
704
  `Nano BPM cluster is up: ${state.nodes.length} node(s), ${state.partitions} partition(s), ` +
651
- `RF=${state.rf}${state.raft ? ', Raft on' : ''}`,
705
+ `RF=${state.rf}${state.raft ? ', Raft on' : ''}${state.inMemory ? ', in-memory (no disk)' : ''}`,
652
706
  );
653
707
  console.log('');
654
708
  for (const n of state.nodes) {
@@ -855,8 +909,16 @@ async function statusCluster(req) {
855
909
  `${state.raft ? ' raft: on' : ''}${state.capture ? ' trace capture: on' : ''}`,
856
910
  );
857
911
  console.log(` binary: ${state.binary}`);
912
+ console.log(` version: ${binaryVersion(state.binary) ?? 'unknown'}`);
858
913
  console.log(` workspace: ${state.workspaceDir || getWorkspaceDir()}`);
859
- console.log(` data: ${getDataDir()}`);
914
+ const historyNote =
915
+ state.historyMax != null ? `, history-max ${state.historyMax}` : '';
916
+ if (state.inMemory) {
917
+ console.log(` storage: in-memory (no journal/read-model on disk${historyNote})`);
918
+ } else {
919
+ console.log(` storage: on-disk${historyNote}`);
920
+ console.log(` data: ${getDataDir()}`);
921
+ }
860
922
  console.log('');
861
923
  console.log(' NODE PORT PID PROCESS HEALTH URL');
862
924
  for (const c of checks) {
@@ -1186,8 +1248,22 @@ function isGlobalInstall() {
1186
1248
 
1187
1249
  function updatePlugin(req) {
1188
1250
  const { name, version: current } = pluginPackage();
1251
+
1252
+ // The nano server binary ships with the plugin as its platform package
1253
+ // (an optionalDependency pinned to the plugin version), so a plugin update is
1254
+ // what delivers a new server. Surface the resolved binary's version, and flag
1255
+ // it when the platform package isn't installed for this host.
1256
+ let nanoBin = null;
1257
+ try {
1258
+ nanoBin = findBinary({});
1259
+ } catch {
1260
+ nanoBin = null;
1261
+ }
1189
1262
  const bundled = readBundledBinaryInfo();
1190
- const nanoNote = bundled ? ` (bundled nano ${bundled.version})` : '';
1263
+ const nanoVer = nanoBin ? binaryVersion(nanoBin) : null;
1264
+ const nanoNote = nanoBin
1265
+ ? ` (nano server ${nanoVer ?? bundled?.version ?? 'present'})`
1266
+ : ' (nano server: not installed for this platform)';
1191
1267
  const manual = ` npm install -g ${name}@latest`;
1192
1268
 
1193
1269
  console.log(`Installed: ${name} v${current ?? '?'}${nanoNote}`);
@@ -1205,6 +1281,13 @@ function updatePlugin(req) {
1205
1281
  console.log('');
1206
1282
 
1207
1283
  if (current && compareSemver(current, latest) >= 0) {
1284
+ if (!nanoBin) {
1285
+ // Plugin is current but npm never fetched the matching server binary.
1286
+ console.log('Plugin is current, but the nano server binary is not installed for this platform.');
1287
+ console.log('Provision it by reinstalling the plugin so npm fetches the platform package:');
1288
+ console.log(' c8ctl sync plugin');
1289
+ return;
1290
+ }
1208
1291
  console.log('Already on the latest release — nothing to do.');
1209
1292
  return;
1210
1293
  }
@@ -1401,7 +1484,7 @@ function getProcessosNanoUrl() {
1401
1484
  }
1402
1485
 
1403
1486
  /**
1404
- * The closed-alpha download URL: env var (PROCESSOS_DOWNLOAD_URL) wins, then the
1487
+ * The closed-beta download URL: env var (PROCESSOS_DOWNLOAD_URL) wins, then the
1405
1488
  * persisted `processos set download-url` config value. Null when neither is set.
1406
1489
  */
1407
1490
  function getProcessosDownloadUrl() {
@@ -1451,7 +1534,7 @@ function clearProcessosState() {
1451
1534
  * only when an *explicitly* configured source points at a missing file (so the
1452
1535
  * user gets an actionable error rather than a silent fallthrough).
1453
1536
  */
1454
- function findConfiguredProcessosBinary(req) {
1537
+ function findConfiguredProcessosBinary(req, { includeCached = true } = {}) {
1455
1538
  const cfg = readProcessosConfig();
1456
1539
  const sources = [
1457
1540
  { val: req?.binary && String(req.binary), from: '--binary' },
@@ -1467,9 +1550,7 @@ function findConfiguredProcessosBinary(req) {
1467
1550
  return abs;
1468
1551
  }
1469
1552
 
1470
- const cached = getProcessosCachedBinaryPath();
1471
- if (existsSync(cached)) return cached;
1472
-
1553
+ // A local source build wins over a downloaded copy for developers in the repo.
1473
1554
  let repo = null;
1474
1555
  try {
1475
1556
  repo = getRepoRoot();
@@ -1485,6 +1566,14 @@ function findConfiguredProcessosBinary(req) {
1485
1566
  if (existsSync(c)) return c;
1486
1567
  }
1487
1568
  }
1569
+
1570
+ // The auto-downloaded copy. The resolver skips it (includeCached:false) so it
1571
+ // can manage that copy with a version check and re-fetch newer published
1572
+ // builds; all other callers still see it as the installed binary.
1573
+ if (includeCached) {
1574
+ const cached = getProcessosCachedBinaryPath();
1575
+ if (existsSync(cached)) return cached;
1576
+ }
1488
1577
  return null;
1489
1578
  }
1490
1579
 
@@ -1598,33 +1687,55 @@ async function downloadProcessosBinary(url, dest) {
1598
1687
  * configured/local binary -> cached download -> fresh download -> error.
1599
1688
  */
1600
1689
  async function resolveProcessosBinary(req) {
1601
- const configured = findConfiguredProcessosBinary(req); // may throw on a missing configured path
1690
+ // An explicitly configured or local source build wins and is used as-is (no
1691
+ // auto-update). The auto-downloaded copy is handled below with a version
1692
+ // check so `start` can pull a newer published build.
1693
+ const configured = findConfiguredProcessosBinary(req, { includeCached: false });
1602
1694
  if (configured) return configured;
1603
1695
 
1604
1696
  const dlUrl = getProcessosDownloadUrl();
1697
+ const cached = getProcessosCachedBinaryPath();
1698
+
1605
1699
  if (dlUrl) {
1606
- const dest = getProcessosCachedBinaryPath();
1607
1700
  const meta = await fetchProcessosVersionMeta(dlUrl);
1608
- await downloadProcessosBinary(processosBinaryUrl(dlUrl), dest);
1609
- // Record what we fetched so the update notifier can compare later.
1610
- try {
1611
- mkdirSync(getProcessosBinDir(), { recursive: true });
1612
- writeFileSync(
1613
- getProcessosBinaryMetaPath(),
1614
- JSON.stringify({
1615
- version: meta?.version ?? null,
1616
- commit: meta?.commit ?? null,
1617
- updated: meta?.updated ?? null,
1618
- source: processosDownloadBase(dlUrl),
1619
- downloaded: new Date().toISOString(),
1620
- }),
1621
- );
1622
- } catch {
1623
- /* sidecar is best-effort */
1701
+ const have = readProcessosBinaryMeta();
1702
+ const remoteVer = meta?.version ?? null;
1703
+ const haveVer = have?.version ?? null;
1704
+ const haveCached = existsSync(cached);
1705
+
1706
+ // Download when there is no cached copy, or when the published version.json
1707
+ // reports a version different from the one recorded for the cached copy.
1708
+ // This also covers binaries cached before version tracking (no haveVer).
1709
+ const needDownload = !haveCached || (remoteVer && remoteVer !== haveVer);
1710
+ if (needDownload) {
1711
+ const logger = getLogger();
1712
+ if (haveCached && remoteVer) {
1713
+ logger.info(`Updating ProcessOS ${haveVer ?? '?'} -> ${remoteVer} ...`);
1714
+ }
1715
+ await downloadProcessosBinary(processosBinaryUrl(dlUrl), cached);
1716
+ // Record what we fetched so the update notifier/status can compare later.
1717
+ try {
1718
+ mkdirSync(getProcessosBinDir(), { recursive: true });
1719
+ writeFileSync(
1720
+ getProcessosBinaryMetaPath(),
1721
+ JSON.stringify({
1722
+ version: meta?.version ?? null,
1723
+ commit: meta?.commit ?? null,
1724
+ updated: meta?.updated ?? null,
1725
+ source: processosDownloadBase(dlUrl),
1726
+ downloaded: new Date().toISOString(),
1727
+ }),
1728
+ );
1729
+ } catch {
1730
+ /* sidecar is best-effort */
1731
+ }
1624
1732
  }
1625
- return dest;
1733
+ if (existsSync(cached)) return cached;
1626
1734
  }
1627
1735
 
1736
+ // A previously downloaded copy still runs even if the URL is now unset.
1737
+ if (existsSync(cached)) return cached;
1738
+
1628
1739
  throw new Error(
1629
1740
  `Could not find or download the ProcessOS binary.\n` +
1630
1741
  `Set the download URL you were given (PROCESSOS_DOWNLOAD_URL), point the plugin at a\n` +
@@ -1634,7 +1745,7 @@ async function resolveProcessosBinary(req) {
1634
1745
  }
1635
1746
 
1636
1747
  /**
1637
- * Whether ProcessOS is enabled for this user. It is a closed alpha, so the
1748
+ * Whether ProcessOS is enabled for this user. It is a closed beta, so the
1638
1749
  * operational commands stay locked until the user either has the binary on
1639
1750
  * their system (configured path / cached download / local build) or has been
1640
1751
  * given a PROCESSOS_DOWNLOAD_URL to fetch it from.
@@ -1645,16 +1756,16 @@ function processosEnabled(req) {
1645
1756
  if (findConfiguredProcessosBinary(req)) return true;
1646
1757
  } catch {
1647
1758
  // A configured-but-missing path still means the user opted in; let the real
1648
- // not-found error surface from the command rather than the closed-alpha gate.
1759
+ // not-found error surface from the command rather than the closed-beta gate.
1649
1760
  return true;
1650
1761
  }
1651
1762
  return false;
1652
1763
  }
1653
1764
 
1654
- function printProcessosClosedAlpha() {
1765
+ function printProcessosClosedBeta() {
1655
1766
  const logger = getLogger();
1656
1767
  logger.error(
1657
- 'ProcessOS is in closed alpha and is not available yet.\n' +
1768
+ 'ProcessOS is in closed beta and is not available yet.\n' +
1658
1769
  '\n' +
1659
1770
  'To enable it, set the download URL you were given by the Nano BPM team:\n' +
1660
1771
  ' c8ctl processos set download-url <url> # persists it for this machine\n' +
@@ -1707,17 +1818,7 @@ function getInstalledProcessosVersion(req) {
1707
1818
  } catch {
1708
1819
  binary = null;
1709
1820
  }
1710
- if (!binary) return null;
1711
- try {
1712
- const res = spawnSync(binary, ['--version'], { encoding: 'utf8', timeout: 3000 });
1713
- if (res.status === 0) {
1714
- const m = String(res.stdout || '').match(/(\d+\.\d+\.\d+[^\s]*)/);
1715
- if (m) return m[1];
1716
- }
1717
- } catch {
1718
- /* ignore */
1719
- }
1720
- return null;
1821
+ return binaryVersion(binary);
1721
1822
  }
1722
1823
 
1723
1824
  /**
@@ -1760,7 +1861,7 @@ function printProcessosUpdateNotice(current, latest) {
1760
1861
  * Best-effort, non-blocking ProcessOS update check. Triggers a background
1761
1862
  * version.json fetch when the cache is stale and prints a notice (at most once
1762
1863
  * per day) when the published version is newer than the installed one. Only
1763
- * meaningful when a download URL is configured (the closed-alpha channel).
1864
+ * meaningful when a download URL is configured (the closed-beta channel).
1764
1865
  */
1765
1866
  function maybeNotifyProcessosUpdate(req) {
1766
1867
  try {
@@ -1990,10 +2091,14 @@ async function statusProcessos() {
1990
2091
 
1991
2092
  const alive = isPidAlive(state.pid);
1992
2093
  const healthy = alive ? await probeProcessosHealthy(state.url) : false;
2094
+ // Prefer the actual running binary's reported version; fall back to the
2095
+ // recorded download metadata if the binary can't be probed.
2096
+ const version = binaryVersion(state.binary) ?? getInstalledProcessosVersion() ?? 'unknown';
1993
2097
 
1994
2098
  console.log('ProcessOS status:');
1995
2099
  console.log('');
1996
2100
  console.log(` pid: ${state.pid} ${alive ? '(alive)' : '(dead — stale state)'}`);
2101
+ console.log(` version: ${version}`);
1997
2102
  console.log(` url: ${state.url}`);
1998
2103
  console.log(` health: ${healthy ? 'ok' : 'unreachable'} (${state.url}/health)`);
1999
2104
  console.log(` target: ${state.nanoUrl}`);
@@ -2055,7 +2160,7 @@ function printProcessosSetUsage() {
2055
2160
  const logger = getLogger();
2056
2161
  logger.info('Usage: c8ctl processos set <field> <value>');
2057
2162
  logger.info(' bin <path> Path to the downloaded ProcessOS binary');
2058
- logger.info(' download-url <url> Closed-alpha binary download URL (enables ProcessOS)');
2163
+ logger.info(' download-url <url> Closed-beta binary download URL (enables ProcessOS)');
2059
2164
  logger.info(' port <n> Listen port (default 8090)');
2060
2165
  logger.info(' nano-url <url> Target Nano BPM engine URL (default http://localhost:8080)');
2061
2166
  logger.info(' data-dir <path> ProcessOS data directory');
@@ -2170,14 +2275,14 @@ function showProcessosConfig() {
2170
2275
  }
2171
2276
  }
2172
2277
  console.log('');
2173
- console.log(' closed-alpha channel:');
2278
+ console.log(' closed-beta channel:');
2174
2279
  const dlUrl = getProcessosDownloadUrl();
2175
2280
  const dlSource = process.env.PROCESSOS_DOWNLOAD_URL && String(process.env.PROCESSOS_DOWNLOAD_URL).trim()
2176
2281
  ? ' (from $PROCESSOS_DOWNLOAD_URL)'
2177
2282
  : cfg.downloadUrl
2178
2283
  ? ' (from "processos set download-url")'
2179
2284
  : '';
2180
- console.log(` download url ${dlUrl ? dlUrl + dlSource : '(not set — ProcessOS is a closed alpha; "c8ctl processos set download-url <url>" to enable)'}`);
2285
+ console.log(` download url ${dlUrl ? dlUrl + dlSource : '(not set — ProcessOS is a closed beta; "c8ctl processos set download-url <url>" to enable)'}`);
2181
2286
  const cached = getProcessosCachedBinaryPath();
2182
2287
  const meta = readProcessosBinaryMeta();
2183
2288
  console.log(` cached binary ${existsSync(cached) ? cached : '(none — downloaded on first "processos start")'}`);
@@ -2203,7 +2308,7 @@ function printProcessosUsage() {
2203
2308
  console.log(' c8ctl processos set bin <path> | download-url <url> | port <n> | nano-url <url> | data-dir <path> | env KEY=VALUE');
2204
2309
  console.log(' c8ctl processos config');
2205
2310
  console.log('');
2206
- console.log('ProcessOS is a closed alpha. Enable it with the download URL you were given:');
2311
+ console.log('ProcessOS is a closed beta. Enable it with the download URL you were given:');
2207
2312
  console.log(' c8ctl processos set download-url <url> # plugin downloads + runs the matching binary');
2208
2313
  console.log('or point the plugin at a binary you already have: "c8ctl processos set bin <path>".');
2209
2314
  console.log('By default ProcessOS spawns its own internal pilot Nano engine (the plugin auto-wires the nano');
@@ -2252,6 +2357,7 @@ export const metadata = {
2252
2357
  },
2253
2358
  { command: 'c8ctl nano start 3 --port 9000', description: 'Start 3 nodes on ports 9000..9002' },
2254
2359
  { command: 'c8ctl nano start --capture', description: 'Start with trace capture for historical replay/analysis' },
2360
+ { command: 'c8ctl nano start --in-memory --history-max 50000', description: 'Stress mode: no disk journal, cap retained instances in RAM' },
2255
2361
  { command: 'c8ctl nano status', description: 'Show cluster status and per-node health' },
2256
2362
  { command: 'c8ctl nano pause 1', description: 'Freeze node 1 (SIGSTOP) to simulate a node failure' },
2257
2363
  { command: 'c8ctl nano resume 1', description: 'Resume node 1 (SIGCONT) to bring it back online' },
@@ -2269,7 +2375,7 @@ export const metadata = {
2269
2375
  processos: {
2270
2376
  description: 'Manage a local ProcessOS instance — start, status, stop, logs, config',
2271
2377
  examples: [
2272
- { command: 'c8ctl processos set download-url <url>', description: 'Enable the closed alpha + auto-download the matching binary' },
2378
+ { command: 'c8ctl processos set download-url <url>', description: 'Enable the closed beta + auto-download the matching binary' },
2273
2379
  { command: 'c8ctl processos set bin <path>', description: 'Point the plugin at a ProcessOS binary you already have' },
2274
2380
  { command: 'c8ctl processos start', description: 'Start ProcessOS against the local Nano BPM engine' },
2275
2381
  { command: 'c8ctl processos start --nano-url http://localhost:8080', description: 'Start against a specific engine' },
@@ -2294,6 +2400,9 @@ export const commands = {
2294
2400
  rf: { type: 'string', description: 'Replication factor; >1 enables Raft (default 1)' },
2295
2401
  raft: { type: 'boolean', description: 'Force per-partition Raft on/off (default: on when rf>1)' },
2296
2402
  capture: { type: 'boolean', description: 'start: enable trace capture (recorded-input replay) on every node' },
2403
+ 'in-memory': { type: 'boolean', description: 'start: run with NO on-disk journal/read-model (in-memory engine; state lost on restart). Alias: --no-journal' },
2404
+ 'no-journal': { type: 'boolean', description: 'start: alias for --in-memory' },
2405
+ 'history-max': { type: 'string', description: 'start: cap retained terminal instances in the read model (NANOBPMN_HISTORY_MAX_INSTANCES; 0/unset = unbounded)' },
2297
2406
  follow: { type: 'boolean', description: 'logs: stream output (tail -F)', short: 'f' },
2298
2407
  purge: { type: 'boolean', description: 'stop: also delete per-node engine data' },
2299
2408
  force: { type: 'boolean', description: 'start: stop any existing cluster first' },
@@ -2376,12 +2485,12 @@ export const commands = {
2376
2485
  return;
2377
2486
  }
2378
2487
 
2379
- // ProcessOS is a closed alpha: gate the operational commands until the
2488
+ // ProcessOS is a closed beta: gate the operational commands until the
2380
2489
  // user has opted in (download URL set or a binary on their system).
2381
2490
  // `set`/`config` stay open so users can configure/inspect at any time.
2382
2491
  const ungated = req.subcommand === 'set' || req.subcommand === 'config';
2383
2492
  if (!ungated && !processosEnabled(req)) {
2384
- printProcessosClosedAlpha();
2493
+ printProcessosClosedBeta();
2385
2494
  process.exit(1);
2386
2495
  }
2387
2496
 
@@ -2425,7 +2534,7 @@ export const commands = {
2425
2534
 
2426
2535
  function printUsage() {
2427
2536
  console.log('Usage:');
2428
- console.log(' c8ctl nano start [<nodes>] [--port <basePort>] [--partitions <n>] [--rf <n>] [--raft] [--capture] [--binary <path>]');
2537
+ console.log(' c8ctl nano start [<nodes>] [--port <basePort>] [--partitions <n>] [--rf <n>] [--raft] [--capture] [--in-memory] [--history-max <n>] [--binary <path>]');
2429
2538
  console.log(' c8ctl nano status [--port <port>]');
2430
2539
  console.log(' c8ctl nano stop [--purge]');
2431
2540
  console.log(' c8ctl nano logs [<nodeId>] [--follow]');
@@ -2457,6 +2566,8 @@ function printUsage() {
2457
2566
  console.log(' --rf <n> Replication factor; >1 enables Raft (default 1)');
2458
2567
  console.log(' --raft Force Raft on (default: on iff rf>1)');
2459
2568
  console.log(' --capture start: enable trace capture (recorded-input replay) on every node');
2569
+ console.log(' --in-memory start: run with NO on-disk journal/read-model (alias --no-journal; state lost on restart)');
2570
+ console.log(' --history-max <n> start: cap retained terminal instances in the read model (0/unset = unbounded)');
2460
2571
  console.log(' --binary <path> Path to the nanobpmn server binary (overrides "set bin")');
2461
2572
  console.log(' --purge stop: also delete per-node engine data');
2462
2573
  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.2",
3
+ "version": "1.4.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,10 +49,10 @@
49
49
  "semantic-release": "^25.0.3"
50
50
  },
51
51
  "optionalDependencies": {
52
- "@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.3.2",
53
- "@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.3.2",
54
- "@nanobpm/c8ctl-plugin-nano-linux-x64": "1.3.2",
55
- "@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.3.2",
56
- "@nanobpm/c8ctl-plugin-nano-win32-x64": "1.3.2"
52
+ "@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.4.0",
53
+ "@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.4.0",
54
+ "@nanobpm/c8ctl-plugin-nano-linux-x64": "1.4.0",
55
+ "@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.4.0",
56
+ "@nanobpm/c8ctl-plugin-nano-win32-x64": "1.4.0"
57
57
  }
58
58
  }