c8ctl-plugin-nano 1.0.0 → 1.1.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
@@ -124,6 +124,27 @@ Persistent settings are stored in `<state home>/config.json`:
124
124
 
125
125
  Show the effective configuration and all on-disk locations with `c8ctl nano config`.
126
126
 
127
+ ## Updating to a new release (`update`)
128
+
129
+ The plugin and the bundled server binary (delivered via the matching platform
130
+ package) ship together on npm as `c8ctl-plugin-nano`. To pull a new nanobpmn
131
+ release onto a machine that already has nano installed:
132
+
133
+ ```bash
134
+ c8ctl nano update # check npm for a newer release and install it
135
+ c8ctl nano update --check # only report whether an update is available
136
+ ```
137
+
138
+ `update` compares the installed plugin version against the latest published on
139
+ npm. When a newer release exists it reinstalls the package globally
140
+ (`npm install -g c8ctl-plugin-nano@latest`), which brings the new server binary
141
+ with it. It only ever drives npm — it never touches the private upstream source —
142
+ so it works for any npm-installed user. After updating, restart any running
143
+ cluster (`c8ctl nano restart`) so it picks up the new binary.
144
+
145
+ If the plugin is running from a local checkout rather than a global npm install,
146
+ `update` prints the manual command instead of reinstalling in place.
147
+
127
148
  ## Checking status
128
149
 
129
150
  `c8ctl nano status` queries each node's always-on `GET /v2/topology`, which is the
@@ -261,8 +282,9 @@ The plugin needs a built `nanobpmn` server binary. Resolution order:
261
282
  1. `--binary <path>`
262
283
  2. configured path (`c8ctl nano set bin <path>`)
263
284
  3. `NANOBPMN_BINARY=<path>`
264
- 4. the matching **platform package** (`c8ctl-plugin-nano-<os>-<arch>`), installed
265
- automatically as an `optionalDependency` when you install the plugin from npm
285
+ 4. the matching **platform package** (`@nanobpm/c8ctl-plugin-nano-<os>-<arch>`),
286
+ installed automatically as an `optionalDependency` when you install the plugin
287
+ from npm
266
288
  5. `release` build under the nanobpmn repo
267
289
  6. `debug` build under the nanobpmn repo
268
290
 
@@ -426,15 +448,16 @@ publishes to npm, and creates a GitHub Release.
426
448
  ### Platform packages
427
449
 
428
450
  The server binary is shipped as a set of platform-specific npm packages, one per
429
- target, gated by npm's `os`/`cpu` fields:
451
+ target, gated by npm's `os`/`cpu` fields. They are **scoped under `@nanobpm`** (a
452
+ scope we own) so the names can never be squatted or npm-security-held:
430
453
 
431
- | package | os | cpu |
432
- |----------------------------------|--------|-------|
433
- | `c8ctl-plugin-nano-darwin-arm64` | darwin | arm64 |
434
- | `c8ctl-plugin-nano-darwin-x64` | darwin | x64 |
435
- | `c8ctl-plugin-nano-linux-x64` | linux | x64 |
436
- | `c8ctl-plugin-nano-linux-arm64` | linux | arm64 |
437
- | `c8ctl-plugin-nano-win32-x64` | win32 | x64 |
454
+ | package | os | cpu |
455
+ |--------------------------------------------|--------|-------|
456
+ | `@nanobpm/c8ctl-plugin-nano-darwin-arm64` | darwin | arm64 |
457
+ | `@nanobpm/c8ctl-plugin-nano-darwin-x64` | darwin | x64 |
458
+ | `@nanobpm/c8ctl-plugin-nano-linux-x64` | linux | x64 |
459
+ | `@nanobpm/c8ctl-plugin-nano-linux-arm64` | linux | arm64 |
460
+ | `@nanobpm/c8ctl-plugin-nano-win32-x64` | win32 | x64 |
438
461
 
439
462
  The root `c8ctl-plugin-nano` lists all five as `optionalDependencies` (pinned to
440
463
  the exact release version, injected into the published tarball at release time).
@@ -487,7 +510,14 @@ with provenance (`NPM_CONFIG_PROVENANCE: true`, requires this repo to be public)
487
510
  Trusted Publishing is per-package and requires the package to already exist, so:
488
511
 
489
512
  1. **Bootstrap** the first release with a granular-automation `NPM_TOKEN` secret —
490
- it is used automatically and creates all six packages.
513
+ it is used automatically and creates all six packages. The token must have
514
+ **publish rights to the `@nanobpm` scope** (the platform packages are scoped).
491
515
  2. On npmjs.com, add a **Trusted Publisher** (this repo + `release.yml`) for the
492
- root package and each of the five platform packages.
516
+ root package and each of the five `@nanobpm/c8ctl-plugin-nano-*` platform
517
+ packages.
493
518
  3. Remove the `NPM_TOKEN` secret; subsequent releases authenticate via OIDC.
519
+
520
+ > Note: because the platform packages are **scoped** (`@nanobpm/…`), they sidestep
521
+ > the unscoped-name squatting/`0.0.1-security` hold that previously blocked
522
+ > `c8ctl-plugin-nano-win32-x64`. If you ever add a new platform, its scoped name is
523
+ > yours to publish immediately.
package/c8ctl-plugin.js CHANGED
@@ -25,7 +25,7 @@
25
25
  * c8ctl nano restart [<nodes>] ...
26
26
  */
27
27
 
28
- import { spawn } from 'node:child_process';
28
+ import { spawn, spawnSync } from 'node:child_process';
29
29
  import {
30
30
  existsSync,
31
31
  mkdirSync,
@@ -63,8 +63,9 @@ function readBundledBinaryInfo() {
63
63
 
64
64
  /**
65
65
  * Locate the nanobpmn binary shipped by the matching platform package
66
- * (an optionalDependency such as c8ctl-plugin-nano-darwin-arm64). Returns the
67
- * absolute path, or undefined if the package isn't installed for this host.
66
+ * (an optionalDependency such as @nanobpm/c8ctl-plugin-nano-darwin-arm64).
67
+ * Returns the absolute path, or undefined if the package isn't installed for
68
+ * this host.
68
69
  */
69
70
  function findPlatformPackageBinary() {
70
71
  const p = platformForHost();
@@ -257,7 +258,7 @@ function findBinary(flags) {
257
258
  // Argument parsing
258
259
  // ---------------------------------------------------------------------------
259
260
 
260
- const VALID_SUBCOMMANDS = ['start', 'stop', 'status', 'logs', 'log', 'restart', 'pause', 'resume', 'clean', 'set', 'config'];
261
+ const VALID_SUBCOMMANDS = ['start', 'stop', 'status', 'logs', 'log', 'restart', 'pause', 'resume', 'clean', 'set', 'config', 'update'];
261
262
 
262
263
  /**
263
264
  * Parse positional args + flags into a normalized request.
@@ -288,6 +289,7 @@ function parseRequest(args, flags) {
288
289
  force: Boolean(flags?.force),
289
290
  capture: Boolean(flags?.capture),
290
291
  workspace: Boolean(flags?.workspace),
292
+ check: Boolean(flags?.check),
291
293
  binary: flags?.binary,
292
294
  };
293
295
  }
@@ -1106,6 +1108,119 @@ function showConfig() {
1106
1108
  console.log(' Change with: c8ctl nano set bin <path> | c8ctl nano set model-dir <path>');
1107
1109
  }
1108
1110
 
1111
+ // ---------------------------------------------------------------------------
1112
+ // update — pull a new nanobpmn release onto a machine with an existing install.
1113
+ // The plugin (and the bundled server binary, shipped via the matching platform
1114
+ // package) is distributed on npm as c8ctl-plugin-nano, so a release is pulled by
1115
+ // reinstalling the package globally. We only ever drive npm here — never touch
1116
+ // the private upstream source — so this works for any npm-installed user.
1117
+ // ---------------------------------------------------------------------------
1118
+
1119
+ /** This plugin package's identity, read from its own package.json. */
1120
+ function pluginPackage() {
1121
+ try {
1122
+ const pkg = JSON.parse(readFileSync(join(pluginDir, 'package.json'), 'utf8'));
1123
+ return { name: pkg.name || 'c8ctl-plugin-nano', version: pkg.version || null };
1124
+ } catch {
1125
+ return { name: 'c8ctl-plugin-nano', version: null };
1126
+ }
1127
+ }
1128
+
1129
+ /**
1130
+ * Numeric semver comparison (major.minor.patch), ignoring any pre-release/build
1131
+ * suffix. Returns -1 if a<b, 0 if equal, 1 if a>b.
1132
+ */
1133
+ function compareSemver(a, b) {
1134
+ const norm = (v) =>
1135
+ String(v)
1136
+ .replace(/^v/, '')
1137
+ .split(/[-+]/)[0]
1138
+ .split('.')
1139
+ .map((n) => Number.parseInt(n, 10) || 0);
1140
+ const av = norm(a);
1141
+ const bv = norm(b);
1142
+ for (let i = 0; i < 3; i++) {
1143
+ const x = av[i] || 0;
1144
+ const y = bv[i] || 0;
1145
+ if (x !== y) return x < y ? -1 : 1;
1146
+ }
1147
+ return 0;
1148
+ }
1149
+
1150
+ /** Latest published version of `name` per the npm registry (throws on failure). */
1151
+ function npmLatestVersion(name) {
1152
+ const res = spawnSync('npm', ['view', name, 'version'], { encoding: 'utf8' });
1153
+ if (res.error) throw new Error(res.error.message);
1154
+ if (res.status !== 0) {
1155
+ throw new Error((res.stderr || '').trim() || `npm view exited ${res.status}`);
1156
+ }
1157
+ return res.stdout.trim();
1158
+ }
1159
+
1160
+ /** True when this plugin lives under npm's global node_modules (so `-g` updates it). */
1161
+ function isGlobalInstall() {
1162
+ const res = spawnSync('npm', ['root', '-g'], { encoding: 'utf8' });
1163
+ if (res.status !== 0) return false;
1164
+ const root = res.stdout.trim();
1165
+ return Boolean(root) && pluginDir.startsWith(root);
1166
+ }
1167
+
1168
+ function updatePlugin(req) {
1169
+ const { name, version: current } = pluginPackage();
1170
+ const bundled = readBundledBinaryInfo();
1171
+ const nanoNote = bundled ? ` (bundled nano ${bundled.version})` : '';
1172
+ const manual = ` npm install -g ${name}@latest`;
1173
+
1174
+ console.log(`Installed: ${name} v${current ?? '?'}${nanoNote}`);
1175
+
1176
+ let latest;
1177
+ try {
1178
+ latest = npmLatestVersion(name);
1179
+ } catch (err) {
1180
+ console.log(`Could not check npm for updates: ${err.message}`);
1181
+ console.log('Pull the latest release manually with:');
1182
+ console.log(manual);
1183
+ return;
1184
+ }
1185
+ console.log(`Latest: ${name} v${latest} (npm)`);
1186
+ console.log('');
1187
+
1188
+ if (current && compareSemver(current, latest) >= 0) {
1189
+ console.log('Already on the latest release — nothing to do.');
1190
+ return;
1191
+ }
1192
+
1193
+ console.log(`Update available: v${current ?? '?'} -> v${latest}`);
1194
+
1195
+ if (req.check) {
1196
+ console.log('Run `c8ctl nano update` to pull it (or manually):');
1197
+ console.log(manual);
1198
+ return;
1199
+ }
1200
+
1201
+ if (!isGlobalInstall()) {
1202
+ console.log('This plugin is not a global npm install, so it cannot self-update in place.');
1203
+ console.log('Pull the latest release with:');
1204
+ console.log(manual);
1205
+ console.log('(or, for a local checkout, `git pull` then reload the plugin).');
1206
+ return;
1207
+ }
1208
+
1209
+ console.log(`Pulling ${name}@${latest} via npm...`);
1210
+ console.log('');
1211
+ const res = spawnSync('npm', ['install', '-g', `${name}@${latest}`], { stdio: 'inherit' });
1212
+ if (res.error) throw new Error(res.error.message);
1213
+ if (res.status !== 0) {
1214
+ throw new Error(
1215
+ `npm install -g ${name}@${latest} failed (exit ${res.status}). ` +
1216
+ `You may need elevated permissions: sudo ${manual.trim()}`,
1217
+ );
1218
+ }
1219
+ console.log('');
1220
+ console.log(`Updated to v${latest}. Restart any running cluster to use the new binary:`);
1221
+ console.log(' c8ctl nano restart');
1222
+ }
1223
+
1109
1224
  // ---------------------------------------------------------------------------
1110
1225
  // processos — manage a single local ProcessOS instance (the optimization-plane
1111
1226
  // server that analyses a running Nano BPM engine). Unlike nano, the ProcessOS
@@ -1662,6 +1777,8 @@ export const metadata = {
1662
1777
  { command: 'c8ctl nano set bin <path>', description: 'Set the nanobpmn server binary path' },
1663
1778
  { command: 'c8ctl nano set model-dir <path>', description: 'Set the workspace dir (models + workers)' },
1664
1779
  { command: 'c8ctl nano config', description: 'Show current plugin configuration and paths' },
1780
+ { command: 'c8ctl nano update', description: 'Pull the latest published nano release (re-installs via npm)' },
1781
+ { command: 'c8ctl nano update --check', description: 'Check whether a newer nano release is available' },
1665
1782
  ],
1666
1783
  },
1667
1784
  processos: {
@@ -1695,6 +1812,7 @@ export const commands = {
1695
1812
  purge: { type: 'boolean', description: 'stop: also delete per-node engine data' },
1696
1813
  force: { type: 'boolean', description: 'start: stop any existing cluster first' },
1697
1814
  workspace: { type: 'boolean', description: 'clean: also delete the workspace (models + workers)' },
1815
+ check: { type: 'boolean', description: 'update: only report whether a new release is available; do not install' },
1698
1816
  binary: { type: 'string', description: 'Path to the nanobpmn server binary' },
1699
1817
  },
1700
1818
  handler: async (args, flags) => {
@@ -1740,6 +1858,9 @@ export const commands = {
1740
1858
  case 'config':
1741
1859
  showConfig();
1742
1860
  break;
1861
+ case 'update':
1862
+ updatePlugin(req);
1863
+ break;
1743
1864
  }
1744
1865
  } catch (error) {
1745
1866
  logger.error(`nano ${req.subcommand} failed: ${error instanceof Error ? error.message : error}`);
@@ -1812,6 +1933,7 @@ function printUsage() {
1812
1933
  console.log(' c8ctl nano clean [--workspace]');
1813
1934
  console.log(' c8ctl nano set <bin|model-dir> <path>');
1814
1935
  console.log(' c8ctl nano config');
1936
+ console.log(' c8ctl nano update [--check]');
1815
1937
  console.log('');
1816
1938
  console.log('Subcommands:');
1817
1939
  console.log(' start Spawn an N-node local cluster wired to talk to each other on localhost');
@@ -1824,6 +1946,7 @@ function printUsage() {
1824
1946
  console.log(' clean Wipe journal/data + logs on disk (keeps models/workers)');
1825
1947
  console.log(' set Persist a setting: "bin <path>" or "model-dir <path>"');
1826
1948
  console.log(' config Show current configuration and on-disk locations');
1949
+ console.log(' update Pull the latest published nano release (--check to only report)');
1827
1950
  console.log('');
1828
1951
  console.log('Options:');
1829
1952
  console.log(' <nodes> Number of nodes to start (default 1)');
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.0.1",
3
- "commit": "ed49485",
4
- "updated": "2026-06-28T06:34:54Z"
5
- }
2
+ "version": "0.0.2",
3
+ "commit": "44ad803",
4
+ "updated": "2026-06-30T06:04:34Z"
5
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c8ctl-plugin-nano",
3
- "version": "1.0.0",
3
+ "version": "1.1.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
- "c8ctl-plugin-nano-darwin-arm64": "1.0.0",
53
- "c8ctl-plugin-nano-darwin-x64": "1.0.0",
54
- "c8ctl-plugin-nano-linux-x64": "1.0.0",
55
- "c8ctl-plugin-nano-linux-arm64": "1.0.0",
56
- "c8ctl-plugin-nano-win32-x64": "1.0.0"
52
+ "@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.1.0",
53
+ "@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.1.0",
54
+ "@nanobpm/c8ctl-plugin-nano-linux-x64": "1.1.0",
55
+ "@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.1.0",
56
+ "@nanobpm/c8ctl-plugin-nano-win32-x64": "1.1.0"
57
57
  }
58
58
  }
package/platforms.mjs CHANGED
@@ -2,7 +2,9 @@
2
2
  * Single source of truth for the platform-specific binary packages.
3
3
  *
4
4
  * Each entry maps a Node platform/arch pair to:
5
- * - pkg: the npm package name that carries the binary for that platform
5
+ * - pkg: the npm package name that carries the binary for that platform.
6
+ * Scoped under @nanobpm (a scope we own) so the names can never be
7
+ * squatted or npm-security-held the way an unscoped name can.
6
8
  * - os: the npm "os" field value (process.platform)
7
9
  * - cpu: the npm "cpu" field value (process.arch)
8
10
  * - triple: the Rust target triple (informational; used by the Nano BPM CI)
@@ -19,7 +21,7 @@ export const BIN_BASENAME = 'nanobpm-gateway-rest-server';
19
21
 
20
22
  export const PLATFORMS = [
21
23
  {
22
- pkg: 'c8ctl-plugin-nano-darwin-arm64',
24
+ pkg: '@nanobpm/c8ctl-plugin-nano-darwin-arm64',
23
25
  os: 'darwin',
24
26
  cpu: 'arm64',
25
27
  triple: 'aarch64-apple-darwin',
@@ -27,7 +29,7 @@ export const PLATFORMS = [
27
29
  bin: BIN_BASENAME,
28
30
  },
29
31
  {
30
- pkg: 'c8ctl-plugin-nano-darwin-x64',
32
+ pkg: '@nanobpm/c8ctl-plugin-nano-darwin-x64',
31
33
  os: 'darwin',
32
34
  cpu: 'x64',
33
35
  triple: 'x86_64-apple-darwin',
@@ -35,7 +37,7 @@ export const PLATFORMS = [
35
37
  bin: BIN_BASENAME,
36
38
  },
37
39
  {
38
- pkg: 'c8ctl-plugin-nano-linux-x64',
40
+ pkg: '@nanobpm/c8ctl-plugin-nano-linux-x64',
39
41
  os: 'linux',
40
42
  cpu: 'x64',
41
43
  triple: 'x86_64-unknown-linux-gnu',
@@ -43,7 +45,7 @@ export const PLATFORMS = [
43
45
  bin: BIN_BASENAME,
44
46
  },
45
47
  {
46
- pkg: 'c8ctl-plugin-nano-linux-arm64',
48
+ pkg: '@nanobpm/c8ctl-plugin-nano-linux-arm64',
47
49
  os: 'linux',
48
50
  cpu: 'arm64',
49
51
  triple: 'aarch64-unknown-linux-gnu',
@@ -51,7 +53,7 @@ export const PLATFORMS = [
51
53
  bin: BIN_BASENAME,
52
54
  },
53
55
  {
54
- pkg: 'c8ctl-plugin-nano-win32-x64',
56
+ pkg: '@nanobpm/c8ctl-plugin-nano-win32-x64',
55
57
  os: 'win32',
56
58
  cpu: 'x64',
57
59
  triple: 'x86_64-pc-windows-msvc',