c8ctl-plugin-nano 1.0.0 → 1.2.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 +93 -19
- package/c8ctl-plugin.js +619 -27
- package/nanobpmn-binary.json +4 -4
- package/package.json +6 -6
- package/platforms.mjs +8 -6
package/README.md
CHANGED
|
@@ -124,6 +124,43 @@ 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
|
+
|
|
148
|
+
### Automatic "update available" notice
|
|
149
|
+
|
|
150
|
+
You don't have to remember to run `update --check`: any `nano` or `processos`
|
|
151
|
+
command also surfaces a one-line notice when a newer release is published. It is
|
|
152
|
+
deliberately unobtrusive:
|
|
153
|
+
|
|
154
|
+
- The registry lookup runs in a **detached background process**, so a command is
|
|
155
|
+
never slowed down — the fresh result is used on the next invocation.
|
|
156
|
+
- npm is queried at most **once per day**, and the notice is shown at most **once
|
|
157
|
+
per day** (state is cached under the plugin's state home in `update-check.json`).
|
|
158
|
+
- The notice prints to **stderr**, so it never corrupts machine-readable stdout,
|
|
159
|
+
and is suppressed when stdout is not a TTY (piped/scripted) or when `CI` is set.
|
|
160
|
+
|
|
161
|
+
To turn it off entirely, set `NANO_NO_UPDATE_NOTIFIER=1` (or the conventional
|
|
162
|
+
`NO_UPDATE_NOTIFIER=1`). The explicit `c8ctl nano update` command is unaffected.
|
|
163
|
+
|
|
127
164
|
## Checking status
|
|
128
165
|
|
|
129
166
|
`c8ctl nano status` queries each node's always-on `GET /v2/topology`, which is the
|
|
@@ -261,8 +298,9 @@ The plugin needs a built `nanobpmn` server binary. Resolution order:
|
|
|
261
298
|
1. `--binary <path>`
|
|
262
299
|
2. configured path (`c8ctl nano set bin <path>`)
|
|
263
300
|
3. `NANOBPMN_BINARY=<path>`
|
|
264
|
-
4. the matching **platform package** (
|
|
265
|
-
automatically as an `optionalDependency` when you install the plugin
|
|
301
|
+
4. the matching **platform package** (`@nanobpm/c8ctl-plugin-nano-<os>-<arch>`),
|
|
302
|
+
installed automatically as an `optionalDependency` when you install the plugin
|
|
303
|
+
from npm
|
|
266
304
|
5. `release` build under the nanobpmn repo
|
|
267
305
|
6. `debug` build under the nanobpmn repo
|
|
268
306
|
|
|
@@ -332,14 +370,29 @@ ProcessOS is the optimization-plane server that analyses a running Nano BPM
|
|
|
332
370
|
engine. The plugin can manage a single local ProcessOS instance with the same
|
|
333
371
|
start/stop/status/logs lifecycle as `nano`.
|
|
334
372
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
373
|
+
> **ProcessOS is a closed alpha.** The operational commands (`start`, `stop`,
|
|
374
|
+
> `status`, `logs`, `restart`) stay locked with a *"not available yet"* notice
|
|
375
|
+
> until you opt in. Only `set` and `config` work before then. Opt in either by
|
|
376
|
+
> setting the download URL you were given by the Nano BPM team, or by pointing
|
|
377
|
+
> the plugin at a binary you already have.
|
|
338
378
|
|
|
339
379
|
```bash
|
|
340
|
-
#
|
|
380
|
+
# Closed-alpha channel: the plugin downloads + caches the matching binary
|
|
381
|
+
export PROCESSOS_DOWNLOAD_URL=<url you were given>
|
|
382
|
+
c8ctl processos start # fetches processos-<os>-<arch> on first run
|
|
383
|
+
|
|
384
|
+
# …or point the plugin at a binary you already have
|
|
341
385
|
c8ctl processos set bin ~/Downloads/processos
|
|
386
|
+
c8ctl processos start
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
`PROCESSOS_DOWNLOAD_URL` is the prefix the release binaries live under (e.g. the
|
|
390
|
+
`…/processos/latest/` bucket URL). The plugin appends the per-platform asset name
|
|
391
|
+
(`processos-darwin-arm64`, `processos-linux-x64`, `processos-win32-x64.exe`, …),
|
|
392
|
+
downloads it to `<stateHome>/bin/`, marks it executable, and runs it. The cached
|
|
393
|
+
download is reused on subsequent starts.
|
|
342
394
|
|
|
395
|
+
```bash
|
|
343
396
|
# Start ProcessOS against the local Nano BPM engine (http://localhost:8080)
|
|
344
397
|
c8ctl processos start
|
|
345
398
|
|
|
@@ -352,6 +405,18 @@ c8ctl processos logs --follow
|
|
|
352
405
|
c8ctl processos stop
|
|
353
406
|
```
|
|
354
407
|
|
|
408
|
+
### Automatic update notice
|
|
409
|
+
|
|
410
|
+
When you're on the closed-alpha channel (`PROCESSOS_DOWNLOAD_URL` set), the
|
|
411
|
+
plugin checks for newer ProcessOS builds in the background and prints a short
|
|
412
|
+
one-line notice (at most **once per day**) when the published version is newer
|
|
413
|
+
than the one you're running. It compares your installed binary's version against
|
|
414
|
+
the `version.json` the release pipeline publishes next to the binaries, never
|
|
415
|
+
blocks the command (the check runs detached), and is suppressed on
|
|
416
|
+
non-interactive shells, in CI, and when `NO_UPDATE_NOTIFIER` /
|
|
417
|
+
`NANO_NO_UPDATE_NOTIFIER` is set. To update, stop and start ProcessOS again — a
|
|
418
|
+
downloaded binary re-fetches the latest build; a `set bin` binary updates itself.
|
|
419
|
+
|
|
355
420
|
On a successful `start` the summary leads with the landing page:
|
|
356
421
|
|
|
357
422
|
```
|
|
@@ -401,9 +466,10 @@ c8ctl processos config # show current settings and on-disk path
|
|
|
401
466
|
```
|
|
402
467
|
|
|
403
468
|
The binary is resolved in this order: `--binary` flag → `set bin` →
|
|
404
|
-
`$PROCESSOS_BINARY` → a
|
|
405
|
-
|
|
406
|
-
|
|
469
|
+
`$PROCESSOS_BINARY` → a cached download under `<stateHome>/bin/` → a local
|
|
470
|
+
`processos/target/{release,debug}/processos` build → a fresh download from
|
|
471
|
+
`PROCESSOS_DOWNLOAD_URL`. Typed settings (`port`, `nano-url`, `data-dir`) always
|
|
472
|
+
win over generic `env` passthrough values when launching.
|
|
407
473
|
|
|
408
474
|
## Installing
|
|
409
475
|
|
|
@@ -426,15 +492,16 @@ publishes to npm, and creates a GitHub Release.
|
|
|
426
492
|
### Platform packages
|
|
427
493
|
|
|
428
494
|
The server binary is shipped as a set of platform-specific npm packages, one per
|
|
429
|
-
target, gated by npm's `os`/`cpu` fields
|
|
495
|
+
target, gated by npm's `os`/`cpu` fields. They are **scoped under `@nanobpm`** (a
|
|
496
|
+
scope we own) so the names can never be squatted or npm-security-held:
|
|
430
497
|
|
|
431
|
-
| package
|
|
432
|
-
|
|
433
|
-
|
|
|
434
|
-
|
|
|
435
|
-
|
|
|
436
|
-
|
|
|
437
|
-
|
|
|
498
|
+
| package | os | cpu |
|
|
499
|
+
|--------------------------------------------|--------|-------|
|
|
500
|
+
| `@nanobpm/c8ctl-plugin-nano-darwin-arm64` | darwin | arm64 |
|
|
501
|
+
| `@nanobpm/c8ctl-plugin-nano-darwin-x64` | darwin | x64 |
|
|
502
|
+
| `@nanobpm/c8ctl-plugin-nano-linux-x64` | linux | x64 |
|
|
503
|
+
| `@nanobpm/c8ctl-plugin-nano-linux-arm64` | linux | arm64 |
|
|
504
|
+
| `@nanobpm/c8ctl-plugin-nano-win32-x64` | win32 | x64 |
|
|
438
505
|
|
|
439
506
|
The root `c8ctl-plugin-nano` lists all five as `optionalDependencies` (pinned to
|
|
440
507
|
the exact release version, injected into the published tarball at release time).
|
|
@@ -487,7 +554,14 @@ with provenance (`NPM_CONFIG_PROVENANCE: true`, requires this repo to be public)
|
|
|
487
554
|
Trusted Publishing is per-package and requires the package to already exist, so:
|
|
488
555
|
|
|
489
556
|
1. **Bootstrap** the first release with a granular-automation `NPM_TOKEN` secret —
|
|
490
|
-
it is used automatically and creates all six packages.
|
|
557
|
+
it is used automatically and creates all six packages. The token must have
|
|
558
|
+
**publish rights to the `@nanobpm` scope** (the platform packages are scoped).
|
|
491
559
|
2. On npmjs.com, add a **Trusted Publisher** (this repo + `release.yml`) for the
|
|
492
|
-
root package and each of the five platform
|
|
560
|
+
root package and each of the five `@nanobpm/c8ctl-plugin-nano-*` platform
|
|
561
|
+
packages.
|
|
493
562
|
3. Remove the `NPM_TOKEN` secret; subsequent releases authenticate via OIDC.
|
|
563
|
+
|
|
564
|
+
> Note: because the platform packages are **scoped** (`@nanobpm/…`), they sidestep
|
|
565
|
+
> the unscoped-name squatting/`0.0.1-security` hold that previously blocked
|
|
566
|
+
> `c8ctl-plugin-nano-win32-x64`. If you ever add a new platform, its scoped name is
|
|
567
|
+
> 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,
|
|
@@ -34,6 +34,8 @@ import {
|
|
|
34
34
|
writeFileSync,
|
|
35
35
|
rmSync,
|
|
36
36
|
readdirSync,
|
|
37
|
+
chmodSync,
|
|
38
|
+
renameSync,
|
|
37
39
|
} from 'node:fs';
|
|
38
40
|
import { homedir, platform as osPlatform } from 'node:os';
|
|
39
41
|
import { join, isAbsolute, resolve as resolvePath, dirname } from 'node:path';
|
|
@@ -63,8 +65,9 @@ function readBundledBinaryInfo() {
|
|
|
63
65
|
|
|
64
66
|
/**
|
|
65
67
|
* Locate the nanobpmn binary shipped by the matching platform package
|
|
66
|
-
* (an optionalDependency such as c8ctl-plugin-nano-darwin-arm64).
|
|
67
|
-
* absolute path, or undefined if the package isn't installed for
|
|
68
|
+
* (an optionalDependency such as @nanobpm/c8ctl-plugin-nano-darwin-arm64).
|
|
69
|
+
* Returns the absolute path, or undefined if the package isn't installed for
|
|
70
|
+
* this host.
|
|
68
71
|
*/
|
|
69
72
|
function findPlatformPackageBinary() {
|
|
70
73
|
const p = platformForHost();
|
|
@@ -93,6 +96,23 @@ const PROCESSOS_STATE_FILE = 'processos.json';
|
|
|
93
96
|
const PROCESSOS_DEFAULT_PORT = 8090;
|
|
94
97
|
const DEFAULT_NANO_URL = 'http://localhost:8080';
|
|
95
98
|
|
|
99
|
+
// Passive update notifier (npm-style): refresh the latest published version
|
|
100
|
+
// from the registry in a detached background process at most once per day, and
|
|
101
|
+
// surface a one-line "update available" notice at most once per day. Never
|
|
102
|
+
// blocks a command and never fails one.
|
|
103
|
+
const UPDATE_CACHE_FILE = 'update-check.json';
|
|
104
|
+
const UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1000;
|
|
105
|
+
const UPDATE_NOTIFY_TTL_MS = 24 * 60 * 60 * 1000;
|
|
106
|
+
|
|
107
|
+
// ProcessOS is a closed alpha distributed out-of-band: the binary lives in an
|
|
108
|
+
// S3 bucket whose base URL is handed to enabled users via PROCESSOS_DOWNLOAD_URL.
|
|
109
|
+
// `<base>/processos-<os>-<arch>[.exe]` is the per-platform binary and
|
|
110
|
+
// `<base>/version.json` is the {version,commit,updated} metadata the CI writes
|
|
111
|
+
// next to it (the analogue of npm's latest-version lookup for the nano plugin).
|
|
112
|
+
const PROCESSOS_VERSION_META = 'version.json';
|
|
113
|
+
const PROCESSOS_BINARY_META_FILE = 'processos-binary.json';
|
|
114
|
+
const PROCESSOS_UPDATE_CACHE_FILE = 'processos-update-check.json';
|
|
115
|
+
|
|
96
116
|
function getLogger() {
|
|
97
117
|
if (globalThis.c8ctl) {
|
|
98
118
|
return globalThis.c8ctl.getLogger();
|
|
@@ -257,7 +277,7 @@ function findBinary(flags) {
|
|
|
257
277
|
// Argument parsing
|
|
258
278
|
// ---------------------------------------------------------------------------
|
|
259
279
|
|
|
260
|
-
const VALID_SUBCOMMANDS = ['start', 'stop', 'status', 'logs', 'log', 'restart', 'pause', 'resume', 'clean', 'set', 'config'];
|
|
280
|
+
const VALID_SUBCOMMANDS = ['start', 'stop', 'status', 'logs', 'log', 'restart', 'pause', 'resume', 'clean', 'set', 'config', 'update'];
|
|
261
281
|
|
|
262
282
|
/**
|
|
263
283
|
* Parse positional args + flags into a normalized request.
|
|
@@ -288,6 +308,7 @@ function parseRequest(args, flags) {
|
|
|
288
308
|
force: Boolean(flags?.force),
|
|
289
309
|
capture: Boolean(flags?.capture),
|
|
290
310
|
workspace: Boolean(flags?.workspace),
|
|
311
|
+
check: Boolean(flags?.check),
|
|
291
312
|
binary: flags?.binary,
|
|
292
313
|
};
|
|
293
314
|
}
|
|
@@ -1106,6 +1127,232 @@ function showConfig() {
|
|
|
1106
1127
|
console.log(' Change with: c8ctl nano set bin <path> | c8ctl nano set model-dir <path>');
|
|
1107
1128
|
}
|
|
1108
1129
|
|
|
1130
|
+
// ---------------------------------------------------------------------------
|
|
1131
|
+
// update — pull a new nanobpmn release onto a machine with an existing install.
|
|
1132
|
+
// The plugin (and the bundled server binary, shipped via the matching platform
|
|
1133
|
+
// package) is distributed on npm as c8ctl-plugin-nano, so a release is pulled by
|
|
1134
|
+
// reinstalling the package globally. We only ever drive npm here — never touch
|
|
1135
|
+
// the private upstream source — so this works for any npm-installed user.
|
|
1136
|
+
// ---------------------------------------------------------------------------
|
|
1137
|
+
|
|
1138
|
+
/** This plugin package's identity, read from its own package.json. */
|
|
1139
|
+
function pluginPackage() {
|
|
1140
|
+
try {
|
|
1141
|
+
const pkg = JSON.parse(readFileSync(join(pluginDir, 'package.json'), 'utf8'));
|
|
1142
|
+
return { name: pkg.name || 'c8ctl-plugin-nano', version: pkg.version || null };
|
|
1143
|
+
} catch {
|
|
1144
|
+
return { name: 'c8ctl-plugin-nano', version: null };
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
/**
|
|
1149
|
+
* Numeric semver comparison (major.minor.patch), ignoring any pre-release/build
|
|
1150
|
+
* suffix. Returns -1 if a<b, 0 if equal, 1 if a>b.
|
|
1151
|
+
*/
|
|
1152
|
+
function compareSemver(a, b) {
|
|
1153
|
+
const norm = (v) =>
|
|
1154
|
+
String(v)
|
|
1155
|
+
.replace(/^v/, '')
|
|
1156
|
+
.split(/[-+]/)[0]
|
|
1157
|
+
.split('.')
|
|
1158
|
+
.map((n) => Number.parseInt(n, 10) || 0);
|
|
1159
|
+
const av = norm(a);
|
|
1160
|
+
const bv = norm(b);
|
|
1161
|
+
for (let i = 0; i < 3; i++) {
|
|
1162
|
+
const x = av[i] || 0;
|
|
1163
|
+
const y = bv[i] || 0;
|
|
1164
|
+
if (x !== y) return x < y ? -1 : 1;
|
|
1165
|
+
}
|
|
1166
|
+
return 0;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
/** Latest published version of `name` per the npm registry (throws on failure). */
|
|
1170
|
+
function npmLatestVersion(name) {
|
|
1171
|
+
const res = spawnSync('npm', ['view', name, 'version'], { encoding: 'utf8' });
|
|
1172
|
+
if (res.error) throw new Error(res.error.message);
|
|
1173
|
+
if (res.status !== 0) {
|
|
1174
|
+
throw new Error((res.stderr || '').trim() || `npm view exited ${res.status}`);
|
|
1175
|
+
}
|
|
1176
|
+
return res.stdout.trim();
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
/** True when this plugin lives under npm's global node_modules (so `-g` updates it). */
|
|
1180
|
+
function isGlobalInstall() {
|
|
1181
|
+
const res = spawnSync('npm', ['root', '-g'], { encoding: 'utf8' });
|
|
1182
|
+
if (res.status !== 0) return false;
|
|
1183
|
+
const root = res.stdout.trim();
|
|
1184
|
+
return Boolean(root) && pluginDir.startsWith(root);
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function updatePlugin(req) {
|
|
1188
|
+
const { name, version: current } = pluginPackage();
|
|
1189
|
+
const bundled = readBundledBinaryInfo();
|
|
1190
|
+
const nanoNote = bundled ? ` (bundled nano ${bundled.version})` : '';
|
|
1191
|
+
const manual = ` npm install -g ${name}@latest`;
|
|
1192
|
+
|
|
1193
|
+
console.log(`Installed: ${name} v${current ?? '?'}${nanoNote}`);
|
|
1194
|
+
|
|
1195
|
+
let latest;
|
|
1196
|
+
try {
|
|
1197
|
+
latest = npmLatestVersion(name);
|
|
1198
|
+
} catch (err) {
|
|
1199
|
+
console.log(`Could not check npm for updates: ${err.message}`);
|
|
1200
|
+
console.log('Pull the latest release manually with:');
|
|
1201
|
+
console.log(manual);
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
console.log(`Latest: ${name} v${latest} (npm)`);
|
|
1205
|
+
console.log('');
|
|
1206
|
+
|
|
1207
|
+
if (current && compareSemver(current, latest) >= 0) {
|
|
1208
|
+
console.log('Already on the latest release — nothing to do.');
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
console.log(`Update available: v${current ?? '?'} -> v${latest}`);
|
|
1213
|
+
|
|
1214
|
+
if (req.check) {
|
|
1215
|
+
console.log('Run `c8ctl nano update` to pull it (or manually):');
|
|
1216
|
+
console.log(manual);
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
if (!isGlobalInstall()) {
|
|
1221
|
+
console.log('This plugin is not a global npm install, so it cannot self-update in place.');
|
|
1222
|
+
console.log('Pull the latest release with:');
|
|
1223
|
+
console.log(manual);
|
|
1224
|
+
console.log('(or, for a local checkout, `git pull` then reload the plugin).');
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
console.log(`Pulling ${name}@${latest} via npm...`);
|
|
1229
|
+
console.log('');
|
|
1230
|
+
const res = spawnSync('npm', ['install', '-g', `${name}@${latest}`], { stdio: 'inherit' });
|
|
1231
|
+
if (res.error) throw new Error(res.error.message);
|
|
1232
|
+
if (res.status !== 0) {
|
|
1233
|
+
throw new Error(
|
|
1234
|
+
`npm install -g ${name}@${latest} failed (exit ${res.status}). ` +
|
|
1235
|
+
`You may need elevated permissions: sudo ${manual.trim()}`,
|
|
1236
|
+
);
|
|
1237
|
+
}
|
|
1238
|
+
console.log('');
|
|
1239
|
+
console.log(`Updated to v${latest}. Restart any running cluster to use the new binary:`);
|
|
1240
|
+
console.log(' c8ctl nano restart');
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
// ---------------------------------------------------------------------------
|
|
1244
|
+
// Passive "update available" notice. Modelled on npm's update-notifier: the
|
|
1245
|
+
// actual registry lookup runs in a detached background process (so a command is
|
|
1246
|
+
// never slowed), and we only print a notice — at most once per day — from a
|
|
1247
|
+
// cached result. The explicit `c8ctl nano update[ --check]` path is unchanged.
|
|
1248
|
+
// ---------------------------------------------------------------------------
|
|
1249
|
+
|
|
1250
|
+
function getUpdateCacheFile() {
|
|
1251
|
+
return join(getStateHome(), UPDATE_CACHE_FILE);
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
function readUpdateCache() {
|
|
1255
|
+
try {
|
|
1256
|
+
return JSON.parse(readFileSync(getUpdateCacheFile(), 'utf8'));
|
|
1257
|
+
} catch {
|
|
1258
|
+
return {};
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
function writeUpdateCache(obj) {
|
|
1263
|
+
try {
|
|
1264
|
+
mkdirSync(getStateHome(), { recursive: true });
|
|
1265
|
+
writeFileSync(getUpdateCacheFile(), JSON.stringify(obj));
|
|
1266
|
+
} catch {
|
|
1267
|
+
/* a best-effort cache; ignore write failures */
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
/**
|
|
1272
|
+
* True when the notifier should stay silent: an explicit opt-out, CI, or a
|
|
1273
|
+
* non-interactive stdout (piped/scripted), so we never pollute machine-read
|
|
1274
|
+
* output or nag in automation.
|
|
1275
|
+
*/
|
|
1276
|
+
function updateNotifierDisabled() {
|
|
1277
|
+
if (process.env.NANO_NO_UPDATE_NOTIFIER || process.env.NO_UPDATE_NOTIFIER) return true;
|
|
1278
|
+
if (process.env.CI) return true;
|
|
1279
|
+
if (!process.stdout.isTTY) return true;
|
|
1280
|
+
return false;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
/**
|
|
1284
|
+
* Refresh the cached latest version in the background. Spawns a detached Node
|
|
1285
|
+
* process that runs `npm view <name> version` and writes the result to the
|
|
1286
|
+
* cache file, then exits — the current command does not wait on it, so the
|
|
1287
|
+
* fresh result is used on the *next* invocation.
|
|
1288
|
+
*/
|
|
1289
|
+
function spawnUpdateRefresh(name, cacheFile) {
|
|
1290
|
+
const script =
|
|
1291
|
+
'const{spawnSync}=require("child_process");' +
|
|
1292
|
+
'const{readFileSync,writeFileSync}=require("fs");' +
|
|
1293
|
+
`let prev={};try{prev=JSON.parse(readFileSync(${JSON.stringify(cacheFile)},"utf8"))}catch{}` +
|
|
1294
|
+
'const out=Object.assign({},prev,{lastCheck:Date.now()});' +
|
|
1295
|
+
`const r=spawnSync("npm",["view",${JSON.stringify(name)},"version"],{encoding:"utf8"});` +
|
|
1296
|
+
'if(r.status===0){out.latest=String(r.stdout||"").trim()}' +
|
|
1297
|
+
`try{writeFileSync(${JSON.stringify(cacheFile)},JSON.stringify(out))}catch{}`;
|
|
1298
|
+
try {
|
|
1299
|
+
const child = spawn(process.execPath, ['-e', script], { detached: true, stdio: 'ignore' });
|
|
1300
|
+
child.unref();
|
|
1301
|
+
} catch {
|
|
1302
|
+
/* if we can't spawn, just skip this cycle */
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
function printUpdateNotice(name, current, latest) {
|
|
1307
|
+
const lines = [
|
|
1308
|
+
'',
|
|
1309
|
+
`╭─ Update available: ${name} v${current} → v${latest}`,
|
|
1310
|
+
'│ A newer nano release (plugin + bundled server) is published on npm.',
|
|
1311
|
+
'│ Install it: c8ctl nano update',
|
|
1312
|
+
`│ Or manually: npm install -g ${name}@latest`,
|
|
1313
|
+
'╰─ Then restart any running cluster: c8ctl nano restart',
|
|
1314
|
+
'',
|
|
1315
|
+
];
|
|
1316
|
+
// stderr so it never corrupts parseable stdout.
|
|
1317
|
+
for (const l of lines) console.error(l);
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
/**
|
|
1321
|
+
* Best-effort, non-blocking update check run at the end of a command. Triggers
|
|
1322
|
+
* a background registry refresh when the cache is stale, and prints a notice
|
|
1323
|
+
* (at most once per day) when the cached latest version is newer than installed.
|
|
1324
|
+
*/
|
|
1325
|
+
function maybeNotifyUpdate(subcommand) {
|
|
1326
|
+
try {
|
|
1327
|
+
if (updateNotifierDisabled()) return;
|
|
1328
|
+
if (subcommand === 'update') return; // the explicit command reports its own state
|
|
1329
|
+
const { name, version: current } = pluginPackage();
|
|
1330
|
+
if (!current || current === '0.0.0-dev') return;
|
|
1331
|
+
|
|
1332
|
+
const cacheFile = getUpdateCacheFile();
|
|
1333
|
+
const cache = readUpdateCache();
|
|
1334
|
+
const now = Date.now();
|
|
1335
|
+
|
|
1336
|
+
if (!cache.lastCheck || now - cache.lastCheck > UPDATE_CHECK_TTL_MS) {
|
|
1337
|
+
try {
|
|
1338
|
+
mkdirSync(getStateHome(), { recursive: true });
|
|
1339
|
+
} catch {
|
|
1340
|
+
/* ignore */
|
|
1341
|
+
}
|
|
1342
|
+
spawnUpdateRefresh(name, cacheFile);
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
const latest = cache.latest;
|
|
1346
|
+
if (!latest || compareSemver(current, latest) >= 0) return;
|
|
1347
|
+
if (cache.lastNotified && now - cache.lastNotified <= UPDATE_NOTIFY_TTL_MS) return;
|
|
1348
|
+
|
|
1349
|
+
printUpdateNotice(name, current, latest);
|
|
1350
|
+
writeUpdateCache({ ...cache, lastNotified: now });
|
|
1351
|
+
} catch {
|
|
1352
|
+
/* the notifier must never break a command */
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1109
1356
|
// ---------------------------------------------------------------------------
|
|
1110
1357
|
// processos — manage a single local ProcessOS instance (the optimization-plane
|
|
1111
1358
|
// server that analyses a running Nano BPM engine). Unlike nano, the ProcessOS
|
|
@@ -1182,15 +1429,17 @@ function clearProcessosState() {
|
|
|
1182
1429
|
}
|
|
1183
1430
|
|
|
1184
1431
|
/**
|
|
1185
|
-
* Locate
|
|
1432
|
+
* Locate a ProcessOS binary the user already has, WITHOUT downloading. Order:
|
|
1186
1433
|
* 1. --binary flag
|
|
1187
1434
|
* 2. configured path ("processos set bin <path>")
|
|
1188
1435
|
* 3. PROCESSOS_BINARY env var
|
|
1189
|
-
* 4.
|
|
1190
|
-
* 5. debug build under the nanobpmn repo
|
|
1191
|
-
*
|
|
1436
|
+
* 4. a previously auto-downloaded binary cached under the state home
|
|
1437
|
+
* 5. release / debug build under the nanobpmn repo (local dev)
|
|
1438
|
+
* Returns an absolute path, or null when nothing is configured/present. Throws
|
|
1439
|
+
* only when an *explicitly* configured source points at a missing file (so the
|
|
1440
|
+
* user gets an actionable error rather than a silent fallthrough).
|
|
1192
1441
|
*/
|
|
1193
|
-
function
|
|
1442
|
+
function findConfiguredProcessosBinary(req) {
|
|
1194
1443
|
const cfg = readProcessosConfig();
|
|
1195
1444
|
const sources = [
|
|
1196
1445
|
{ val: req?.binary && String(req.binary), from: '--binary' },
|
|
@@ -1206,24 +1455,331 @@ function findProcessosBinary(req) {
|
|
|
1206
1455
|
return abs;
|
|
1207
1456
|
}
|
|
1208
1457
|
|
|
1209
|
-
const
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1458
|
+
const cached = getProcessosCachedBinaryPath();
|
|
1459
|
+
if (existsSync(cached)) return cached;
|
|
1460
|
+
|
|
1461
|
+
let repo = null;
|
|
1462
|
+
try {
|
|
1463
|
+
repo = getRepoRoot();
|
|
1464
|
+
} catch {
|
|
1465
|
+
repo = null;
|
|
1466
|
+
}
|
|
1467
|
+
if (repo) {
|
|
1468
|
+
const candidates = [
|
|
1469
|
+
join(repo, 'processos', 'target', 'release', 'processos'),
|
|
1470
|
+
join(repo, 'processos', 'target', 'debug', 'processos'),
|
|
1471
|
+
];
|
|
1472
|
+
for (const c of candidates) {
|
|
1473
|
+
if (existsSync(c)) return c;
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
return null;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
/** The state-home directory that holds an auto-downloaded ProcessOS binary. */
|
|
1480
|
+
function getProcessosBinDir() {
|
|
1481
|
+
return join(getStateHome(), 'bin');
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
function getProcessosCachedBinaryPath() {
|
|
1485
|
+
const name = process.platform === 'win32' ? 'processos.exe' : 'processos';
|
|
1486
|
+
return join(getProcessosBinDir(), name);
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
/** Sidecar recording the version of the auto-downloaded binary (for update checks). */
|
|
1490
|
+
function getProcessosBinaryMetaPath() {
|
|
1491
|
+
return join(getProcessosBinDir(), PROCESSOS_BINARY_META_FILE);
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
function readProcessosBinaryMeta() {
|
|
1495
|
+
try {
|
|
1496
|
+
return JSON.parse(readFileSync(getProcessosBinaryMetaPath(), 'utf8'));
|
|
1497
|
+
} catch {
|
|
1498
|
+
return {};
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
/**
|
|
1503
|
+
* The S3 asset name for the host platform, matching the names the nanobpmn CI
|
|
1504
|
+
* uploads (`processos-<os>-<arch>`, `.exe` on Windows). Null on an unsupported
|
|
1505
|
+
* platform.
|
|
1506
|
+
*/
|
|
1507
|
+
function processosAssetName(platform = process.platform, arch = process.arch) {
|
|
1508
|
+
const map = {
|
|
1509
|
+
'darwin:arm64': 'processos-darwin-arm64',
|
|
1510
|
+
'darwin:x64': 'processos-darwin-x64',
|
|
1511
|
+
'linux:x64': 'processos-linux-x64',
|
|
1512
|
+
'linux:arm64': 'processos-linux-arm64',
|
|
1513
|
+
'win32:x64': 'processos-win32-x64.exe',
|
|
1514
|
+
};
|
|
1515
|
+
return map[`${platform}:${arch}`] || null;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
/**
|
|
1519
|
+
* Join a PROCESSOS_DOWNLOAD_URL base with a leaf (`processos-<arch>` or
|
|
1520
|
+
* `version.json`). The base is normally a directory/prefix (e.g. the S3
|
|
1521
|
+
* `.../processos/latest/` URL); if it already points straight at a binary
|
|
1522
|
+
* asset, we treat its parent directory as the base so siblings resolve too.
|
|
1523
|
+
*/
|
|
1524
|
+
function processosDownloadBase(rawUrl) {
|
|
1525
|
+
const t = String(rawUrl || '').trim();
|
|
1526
|
+
if (!t) return '';
|
|
1527
|
+
if (t.endsWith('/')) return t.slice(0, -1);
|
|
1528
|
+
const lastSeg = t.split('/').pop();
|
|
1529
|
+
// A direct link to a binary asset -> use its parent as the base.
|
|
1530
|
+
if (lastSeg.startsWith('processos-') || lastSeg === 'processos' || lastSeg.endsWith('.exe')) {
|
|
1531
|
+
return t.slice(0, t.length - lastSeg.length - 1);
|
|
1532
|
+
}
|
|
1533
|
+
return t;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
function processosBinaryUrl(rawUrl) {
|
|
1537
|
+
const asset = processosAssetName();
|
|
1538
|
+
if (!asset) {
|
|
1539
|
+
throw new Error(
|
|
1540
|
+
`No prebuilt ProcessOS binary is published for this platform (${process.platform}/${process.arch}).`,
|
|
1541
|
+
);
|
|
1542
|
+
}
|
|
1543
|
+
return `${processosDownloadBase(rawUrl)}/${asset}`;
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
function processosVersionMetaUrl(rawUrl) {
|
|
1547
|
+
return `${processosDownloadBase(rawUrl)}/${PROCESSOS_VERSION_META}`;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
/** Fetch and parse the remote version.json (best-effort; null on any failure). */
|
|
1551
|
+
async function fetchProcessosVersionMeta(rawUrl, timeoutMs = 4000) {
|
|
1552
|
+
try {
|
|
1553
|
+
const ctrl = new AbortController();
|
|
1554
|
+
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
1555
|
+
const res = await fetch(processosVersionMetaUrl(rawUrl), { redirect: 'follow', signal: ctrl.signal });
|
|
1556
|
+
clearTimeout(t);
|
|
1557
|
+
if (!res.ok) return null;
|
|
1558
|
+
const j = await res.json();
|
|
1559
|
+
return j && typeof j === 'object' ? j : null;
|
|
1560
|
+
} catch {
|
|
1561
|
+
return null;
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
/** Download a binary to `dest` (atomic via temp + rename; +x on unix). */
|
|
1566
|
+
async function downloadProcessosBinary(url, dest) {
|
|
1567
|
+
const logger = getLogger();
|
|
1568
|
+
logger.info(`Downloading ProcessOS for ${process.platform}/${process.arch} from ${url} ...`);
|
|
1569
|
+
const res = await fetch(url, { redirect: 'follow' });
|
|
1570
|
+
if (!res.ok) {
|
|
1571
|
+
throw new Error(`ProcessOS download failed: HTTP ${res.status} ${res.statusText} for ${url}`);
|
|
1572
|
+
}
|
|
1573
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
1574
|
+
mkdirSync(getProcessosBinDir(), { recursive: true });
|
|
1575
|
+
const tmp = `${dest}.download`;
|
|
1576
|
+
writeFileSync(tmp, buf);
|
|
1577
|
+
if (process.platform !== 'win32') chmodSync(tmp, 0o755);
|
|
1578
|
+
renameSync(tmp, dest);
|
|
1579
|
+
logger.info(`Saved ProcessOS to ${dest} (${(buf.length / 1_000_000).toFixed(1)} MB).`);
|
|
1580
|
+
return dest;
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
/**
|
|
1584
|
+
* Resolve the ProcessOS binary to run, downloading it on demand when the user
|
|
1585
|
+
* has a PROCESSOS_DOWNLOAD_URL but no local copy yet. Resolution:
|
|
1586
|
+
* configured/local binary -> cached download -> fresh download -> error.
|
|
1587
|
+
*/
|
|
1588
|
+
async function resolveProcessosBinary(req) {
|
|
1589
|
+
const configured = findConfiguredProcessosBinary(req); // may throw on a missing configured path
|
|
1590
|
+
if (configured) return configured;
|
|
1591
|
+
|
|
1592
|
+
const dlUrl = process.env.PROCESSOS_DOWNLOAD_URL;
|
|
1593
|
+
if (dlUrl) {
|
|
1594
|
+
const dest = getProcessosCachedBinaryPath();
|
|
1595
|
+
const meta = await fetchProcessosVersionMeta(dlUrl);
|
|
1596
|
+
await downloadProcessosBinary(processosBinaryUrl(dlUrl), dest);
|
|
1597
|
+
// Record what we fetched so the update notifier can compare later.
|
|
1598
|
+
try {
|
|
1599
|
+
mkdirSync(getProcessosBinDir(), { recursive: true });
|
|
1600
|
+
writeFileSync(
|
|
1601
|
+
getProcessosBinaryMetaPath(),
|
|
1602
|
+
JSON.stringify({
|
|
1603
|
+
version: meta?.version ?? null,
|
|
1604
|
+
commit: meta?.commit ?? null,
|
|
1605
|
+
updated: meta?.updated ?? null,
|
|
1606
|
+
source: processosDownloadBase(dlUrl),
|
|
1607
|
+
downloaded: new Date().toISOString(),
|
|
1608
|
+
}),
|
|
1609
|
+
);
|
|
1610
|
+
} catch {
|
|
1611
|
+
/* sidecar is best-effort */
|
|
1612
|
+
}
|
|
1613
|
+
return dest;
|
|
1217
1614
|
}
|
|
1615
|
+
|
|
1218
1616
|
throw new Error(
|
|
1219
|
-
`Could not find the ProcessOS binary.\n` +
|
|
1220
|
-
`
|
|
1221
|
-
`
|
|
1222
|
-
`
|
|
1223
|
-
`Looked for a local build in:\n ${candidates.join('\n ')}`,
|
|
1617
|
+
`Could not find or download the ProcessOS binary.\n` +
|
|
1618
|
+
`Set the download URL you were given (PROCESSOS_DOWNLOAD_URL), point the plugin at a\n` +
|
|
1619
|
+
`local binary ("c8ctl processos set bin <path>" / --binary / PROCESSOS_BINARY), or build\n` +
|
|
1620
|
+
`from source under the nanobpmn repo.`,
|
|
1224
1621
|
);
|
|
1225
1622
|
}
|
|
1226
1623
|
|
|
1624
|
+
/**
|
|
1625
|
+
* Whether ProcessOS is enabled for this user. It is a closed alpha, so the
|
|
1626
|
+
* operational commands stay locked until the user either has the binary on
|
|
1627
|
+
* their system (configured path / cached download / local build) or has been
|
|
1628
|
+
* given a PROCESSOS_DOWNLOAD_URL to fetch it from.
|
|
1629
|
+
*/
|
|
1630
|
+
function processosEnabled(req) {
|
|
1631
|
+
if (process.env.PROCESSOS_DOWNLOAD_URL) return true;
|
|
1632
|
+
try {
|
|
1633
|
+
if (findConfiguredProcessosBinary(req)) return true;
|
|
1634
|
+
} catch {
|
|
1635
|
+
// A configured-but-missing path still means the user opted in; let the real
|
|
1636
|
+
// not-found error surface from the command rather than the closed-alpha gate.
|
|
1637
|
+
return true;
|
|
1638
|
+
}
|
|
1639
|
+
return false;
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
function printProcessosClosedAlpha() {
|
|
1643
|
+
const logger = getLogger();
|
|
1644
|
+
logger.error(
|
|
1645
|
+
'ProcessOS is in closed alpha and is not available yet.\n' +
|
|
1646
|
+
'\n' +
|
|
1647
|
+
'To enable it, set the download URL you were given by the Nano BPM team:\n' +
|
|
1648
|
+
' export PROCESSOS_DOWNLOAD_URL=<url>\n' +
|
|
1649
|
+
' c8ctl processos start # downloads + runs the matching binary\n' +
|
|
1650
|
+
'\n' +
|
|
1651
|
+
'or, if you already have the binary, point the plugin at it:\n' +
|
|
1652
|
+
' c8ctl processos set bin <path>',
|
|
1653
|
+
);
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
// --- ProcessOS update notifier ---------------------------------------------
|
|
1657
|
+
// Mirrors the nano plugin notifier, but the "latest version" comes from the
|
|
1658
|
+
// version.json the nanobpmn CI publishes next to the S3 binaries rather than
|
|
1659
|
+
// from npm. Throttled to one background fetch + one notice per day.
|
|
1660
|
+
|
|
1661
|
+
function getProcessosUpdateCacheFile() {
|
|
1662
|
+
return join(getStateHome(), PROCESSOS_UPDATE_CACHE_FILE);
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
function readProcessosUpdateCache() {
|
|
1666
|
+
try {
|
|
1667
|
+
return JSON.parse(readFileSync(getProcessosUpdateCacheFile(), 'utf8'));
|
|
1668
|
+
} catch {
|
|
1669
|
+
return {};
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
function writeProcessosUpdateCache(obj) {
|
|
1674
|
+
try {
|
|
1675
|
+
mkdirSync(getStateHome(), { recursive: true });
|
|
1676
|
+
writeFileSync(getProcessosUpdateCacheFile(), JSON.stringify(obj));
|
|
1677
|
+
} catch {
|
|
1678
|
+
/* best-effort */
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
/**
|
|
1683
|
+
* The installed ProcessOS version: the recorded version of an auto-downloaded
|
|
1684
|
+
* binary, else `processos --version` against the resolved binary. Null when no
|
|
1685
|
+
* binary is present or it can't report a version.
|
|
1686
|
+
*/
|
|
1687
|
+
function getInstalledProcessosVersion(req) {
|
|
1688
|
+
const meta = readProcessosBinaryMeta();
|
|
1689
|
+
if (meta.version) return String(meta.version);
|
|
1690
|
+
let binary = null;
|
|
1691
|
+
try {
|
|
1692
|
+
binary = findConfiguredProcessosBinary(req);
|
|
1693
|
+
} catch {
|
|
1694
|
+
binary = null;
|
|
1695
|
+
}
|
|
1696
|
+
if (!binary) return null;
|
|
1697
|
+
try {
|
|
1698
|
+
const res = spawnSync(binary, ['--version'], { encoding: 'utf8', timeout: 3000 });
|
|
1699
|
+
if (res.status === 0) {
|
|
1700
|
+
const m = String(res.stdout || '').match(/(\d+\.\d+\.\d+[^\s]*)/);
|
|
1701
|
+
if (m) return m[1];
|
|
1702
|
+
}
|
|
1703
|
+
} catch {
|
|
1704
|
+
/* ignore */
|
|
1705
|
+
}
|
|
1706
|
+
return null;
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
/**
|
|
1710
|
+
* Refresh the cached latest ProcessOS version in a detached background process
|
|
1711
|
+
* (fetches version.json), so the current command never waits on the network.
|
|
1712
|
+
*/
|
|
1713
|
+
function spawnProcessosVersionRefresh(metaUrl, cacheFile) {
|
|
1714
|
+
const script =
|
|
1715
|
+
'const{readFileSync,writeFileSync}=require("fs");' +
|
|
1716
|
+
`let prev={};try{prev=JSON.parse(readFileSync(${JSON.stringify(cacheFile)},"utf8"))}catch{}` +
|
|
1717
|
+
'const out=Object.assign({},prev,{lastCheck:Date.now()});' +
|
|
1718
|
+
'const ac=new AbortController();const t=setTimeout(()=>ac.abort(),5000);' +
|
|
1719
|
+
`fetch(${JSON.stringify(metaUrl)},{redirect:"follow",signal:ac.signal})` +
|
|
1720
|
+
'.then(r=>r.ok?r.json():null).then(j=>{clearTimeout(t);' +
|
|
1721
|
+
'if(j&&j.version){out.latest=String(j.version);out.commit=j.commit||null}' +
|
|
1722
|
+
`try{writeFileSync(${JSON.stringify(cacheFile)},JSON.stringify(out))}catch{}})` +
|
|
1723
|
+
`.catch(()=>{try{writeFileSync(${JSON.stringify(cacheFile)},JSON.stringify(out))}catch{}});`;
|
|
1724
|
+
try {
|
|
1725
|
+
const child = spawn(process.execPath, ['-e', script], { detached: true, stdio: 'ignore' });
|
|
1726
|
+
child.unref();
|
|
1727
|
+
} catch {
|
|
1728
|
+
/* skip this cycle */
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
function printProcessosUpdateNotice(current, latest) {
|
|
1733
|
+
const lines = [
|
|
1734
|
+
'',
|
|
1735
|
+
`╭─ ProcessOS update available: v${current ?? '?'} → v${latest}`,
|
|
1736
|
+
'│ A newer ProcessOS build is published.',
|
|
1737
|
+
'│ Get it: c8ctl processos stop && c8ctl processos start',
|
|
1738
|
+
'│ (a configured binary updates itself; a downloaded one re-fetches)',
|
|
1739
|
+
'╰─ Pin a specific build instead with: c8ctl processos set bin <path>',
|
|
1740
|
+
'',
|
|
1741
|
+
];
|
|
1742
|
+
for (const l of lines) console.error(l);
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
/**
|
|
1746
|
+
* Best-effort, non-blocking ProcessOS update check. Triggers a background
|
|
1747
|
+
* version.json fetch when the cache is stale and prints a notice (at most once
|
|
1748
|
+
* per day) when the published version is newer than the installed one. Only
|
|
1749
|
+
* meaningful when a download URL is configured (the closed-alpha channel).
|
|
1750
|
+
*/
|
|
1751
|
+
function maybeNotifyProcessosUpdate(req) {
|
|
1752
|
+
try {
|
|
1753
|
+
if (updateNotifierDisabled()) return;
|
|
1754
|
+
const dlUrl = process.env.PROCESSOS_DOWNLOAD_URL;
|
|
1755
|
+
if (!dlUrl) return; // no published channel to compare against
|
|
1756
|
+
const current = getInstalledProcessosVersion(req);
|
|
1757
|
+
if (!current) return;
|
|
1758
|
+
|
|
1759
|
+
const cacheFile = getProcessosUpdateCacheFile();
|
|
1760
|
+
const cache = readProcessosUpdateCache();
|
|
1761
|
+
const now = Date.now();
|
|
1762
|
+
|
|
1763
|
+
if (!cache.lastCheck || now - cache.lastCheck > UPDATE_CHECK_TTL_MS) {
|
|
1764
|
+
try {
|
|
1765
|
+
mkdirSync(getStateHome(), { recursive: true });
|
|
1766
|
+
} catch {
|
|
1767
|
+
/* ignore */
|
|
1768
|
+
}
|
|
1769
|
+
spawnProcessosVersionRefresh(processosVersionMetaUrl(dlUrl), cacheFile);
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
const latest = cache.latest;
|
|
1773
|
+
if (!latest || compareSemver(current, latest) >= 0) return;
|
|
1774
|
+
if (cache.lastNotified && now - cache.lastNotified <= UPDATE_NOTIFY_TTL_MS) return;
|
|
1775
|
+
|
|
1776
|
+
printProcessosUpdateNotice(current, latest);
|
|
1777
|
+
writeProcessosUpdateCache({ ...cache, lastNotified: now });
|
|
1778
|
+
} catch {
|
|
1779
|
+
/* never break a command over the notifier */
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1227
1783
|
/** Probe ProcessOS's GET /health endpoint for reachability. */
|
|
1228
1784
|
async function probeProcessosHealthy(url) {
|
|
1229
1785
|
return probePath(url, '/health');
|
|
@@ -1253,7 +1809,7 @@ async function startProcessos(req) {
|
|
|
1253
1809
|
await stopProcessos({});
|
|
1254
1810
|
}
|
|
1255
1811
|
|
|
1256
|
-
const binary =
|
|
1812
|
+
const binary = await resolveProcessosBinary(req);
|
|
1257
1813
|
const port = getProcessosPort(req);
|
|
1258
1814
|
const url = `http://127.0.0.1:${port}`;
|
|
1259
1815
|
const nanoUrl = req.nanoUrl || getProcessosNanoUrl();
|
|
@@ -1587,6 +2143,15 @@ function showProcessosConfig() {
|
|
|
1587
2143
|
}
|
|
1588
2144
|
}
|
|
1589
2145
|
console.log('');
|
|
2146
|
+
console.log(' closed-alpha channel:');
|
|
2147
|
+
console.log(` download url ${process.env.PROCESSOS_DOWNLOAD_URL || '(not set — ProcessOS is a closed alpha; set PROCESSOS_DOWNLOAD_URL to enable)'}`);
|
|
2148
|
+
const cached = getProcessosCachedBinaryPath();
|
|
2149
|
+
const meta = readProcessosBinaryMeta();
|
|
2150
|
+
console.log(` cached binary ${existsSync(cached) ? cached : '(none — downloaded on first "processos start")'}`);
|
|
2151
|
+
if (meta.version || meta.commit) {
|
|
2152
|
+
console.log(` version ${meta.version || '?'}${meta.commit ? ` (${String(meta.commit).slice(0, 8)})` : ''}${meta.downloaded ? ` downloaded ${meta.downloaded}` : ''}`);
|
|
2153
|
+
}
|
|
2154
|
+
console.log('');
|
|
1590
2155
|
console.log(` state file ${getProcessosStateFile()}`);
|
|
1591
2156
|
console.log(` log file ${getProcessosLogFile()}`);
|
|
1592
2157
|
console.log('');
|
|
@@ -1605,7 +2170,9 @@ function printProcessosUsage() {
|
|
|
1605
2170
|
console.log(' c8ctl processos set bin <path> | port <n> | nano-url <url> | data-dir <path> | env KEY=VALUE');
|
|
1606
2171
|
console.log(' c8ctl processos config');
|
|
1607
2172
|
console.log('');
|
|
1608
|
-
console.log('ProcessOS is
|
|
2173
|
+
console.log('ProcessOS is a closed alpha. Enable it with the download URL you were given:');
|
|
2174
|
+
console.log(' export PROCESSOS_DOWNLOAD_URL=<url> # plugin downloads + runs the matching binary');
|
|
2175
|
+
console.log('or point the plugin at a binary you already have: "c8ctl processos set bin <path>".');
|
|
1609
2176
|
console.log('By default ProcessOS spawns its own internal pilot Nano engine (the plugin auto-wires the nano');
|
|
1610
2177
|
console.log('binary into PROCESSOS_NANO_BIN). Use --no-spawn-nano to instead use the --nano-url engine for');
|
|
1611
2178
|
console.log('the pilot too. If no nano binary is available, it falls back to --no-spawn-nano automatically.');
|
|
@@ -1662,12 +2229,15 @@ export const metadata = {
|
|
|
1662
2229
|
{ command: 'c8ctl nano set bin <path>', description: 'Set the nanobpmn server binary path' },
|
|
1663
2230
|
{ command: 'c8ctl nano set model-dir <path>', description: 'Set the workspace dir (models + workers)' },
|
|
1664
2231
|
{ command: 'c8ctl nano config', description: 'Show current plugin configuration and paths' },
|
|
2232
|
+
{ command: 'c8ctl nano update', description: 'Pull the latest published nano release (re-installs via npm)' },
|
|
2233
|
+
{ command: 'c8ctl nano update --check', description: 'Check whether a newer nano release is available' },
|
|
1665
2234
|
],
|
|
1666
2235
|
},
|
|
1667
2236
|
processos: {
|
|
1668
2237
|
description: 'Manage a local ProcessOS instance — start, status, stop, logs, config',
|
|
1669
2238
|
examples: [
|
|
1670
|
-
{ command: '
|
|
2239
|
+
{ command: 'export PROCESSOS_DOWNLOAD_URL=<url>', description: 'Enable the closed alpha + auto-download the matching binary' },
|
|
2240
|
+
{ command: 'c8ctl processos set bin <path>', description: 'Point the plugin at a ProcessOS binary you already have' },
|
|
1671
2241
|
{ command: 'c8ctl processos start', description: 'Start ProcessOS against the local Nano BPM engine' },
|
|
1672
2242
|
{ command: 'c8ctl processos start --nano-url http://localhost:8080', description: 'Start against a specific engine' },
|
|
1673
2243
|
{ command: 'c8ctl processos status', description: 'Show ProcessOS status and health' },
|
|
@@ -1695,6 +2265,7 @@ export const commands = {
|
|
|
1695
2265
|
purge: { type: 'boolean', description: 'stop: also delete per-node engine data' },
|
|
1696
2266
|
force: { type: 'boolean', description: 'start: stop any existing cluster first' },
|
|
1697
2267
|
workspace: { type: 'boolean', description: 'clean: also delete the workspace (models + workers)' },
|
|
2268
|
+
check: { type: 'boolean', description: 'update: only report whether a new release is available; do not install' },
|
|
1698
2269
|
binary: { type: 'string', description: 'Path to the nanobpmn server binary' },
|
|
1699
2270
|
},
|
|
1700
2271
|
handler: async (args, flags) => {
|
|
@@ -1706,6 +2277,7 @@ export const commands = {
|
|
|
1706
2277
|
return;
|
|
1707
2278
|
}
|
|
1708
2279
|
|
|
2280
|
+
let failed = false;
|
|
1709
2281
|
try {
|
|
1710
2282
|
switch (req.subcommand) {
|
|
1711
2283
|
case 'start':
|
|
@@ -1740,11 +2312,16 @@ export const commands = {
|
|
|
1740
2312
|
case 'config':
|
|
1741
2313
|
showConfig();
|
|
1742
2314
|
break;
|
|
2315
|
+
case 'update':
|
|
2316
|
+
updatePlugin(req);
|
|
2317
|
+
break;
|
|
1743
2318
|
}
|
|
1744
2319
|
} catch (error) {
|
|
1745
2320
|
logger.error(`nano ${req.subcommand} failed: ${error instanceof Error ? error.message : error}`);
|
|
1746
|
-
|
|
2321
|
+
failed = true;
|
|
1747
2322
|
}
|
|
2323
|
+
maybeNotifyUpdate(req.subcommand);
|
|
2324
|
+
if (failed) process.exit(1);
|
|
1748
2325
|
},
|
|
1749
2326
|
},
|
|
1750
2327
|
processos: {
|
|
@@ -1766,6 +2343,16 @@ export const commands = {
|
|
|
1766
2343
|
return;
|
|
1767
2344
|
}
|
|
1768
2345
|
|
|
2346
|
+
// ProcessOS is a closed alpha: gate the operational commands until the
|
|
2347
|
+
// user has opted in (download URL set or a binary on their system).
|
|
2348
|
+
// `set`/`config` stay open so users can configure/inspect at any time.
|
|
2349
|
+
const ungated = req.subcommand === 'set' || req.subcommand === 'config';
|
|
2350
|
+
if (!ungated && !processosEnabled(req)) {
|
|
2351
|
+
printProcessosClosedAlpha();
|
|
2352
|
+
process.exit(1);
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
let failed = false;
|
|
1769
2356
|
try {
|
|
1770
2357
|
switch (req.subcommand) {
|
|
1771
2358
|
case 'start':
|
|
@@ -1794,8 +2381,11 @@ export const commands = {
|
|
|
1794
2381
|
}
|
|
1795
2382
|
} catch (error) {
|
|
1796
2383
|
logger.error(`processos ${req.subcommand} failed: ${error instanceof Error ? error.message : error}`);
|
|
1797
|
-
|
|
2384
|
+
failed = true;
|
|
1798
2385
|
}
|
|
2386
|
+
maybeNotifyUpdate(req.subcommand);
|
|
2387
|
+
maybeNotifyProcessosUpdate(req);
|
|
2388
|
+
if (failed) process.exit(1);
|
|
1799
2389
|
},
|
|
1800
2390
|
},
|
|
1801
2391
|
};
|
|
@@ -1812,6 +2402,7 @@ function printUsage() {
|
|
|
1812
2402
|
console.log(' c8ctl nano clean [--workspace]');
|
|
1813
2403
|
console.log(' c8ctl nano set <bin|model-dir> <path>');
|
|
1814
2404
|
console.log(' c8ctl nano config');
|
|
2405
|
+
console.log(' c8ctl nano update [--check]');
|
|
1815
2406
|
console.log('');
|
|
1816
2407
|
console.log('Subcommands:');
|
|
1817
2408
|
console.log(' start Spawn an N-node local cluster wired to talk to each other on localhost');
|
|
@@ -1824,6 +2415,7 @@ function printUsage() {
|
|
|
1824
2415
|
console.log(' clean Wipe journal/data + logs on disk (keeps models/workers)');
|
|
1825
2416
|
console.log(' set Persist a setting: "bin <path>" or "model-dir <path>"');
|
|
1826
2417
|
console.log(' config Show current configuration and on-disk locations');
|
|
2418
|
+
console.log(' update Pull the latest published nano release (--check to only report)');
|
|
1827
2419
|
console.log('');
|
|
1828
2420
|
console.log('Options:');
|
|
1829
2421
|
console.log(' <nodes> Number of nodes to start (default 1)');
|
package/nanobpmn-binary.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.0.
|
|
3
|
-
"commit": "
|
|
4
|
-
"updated": "2026-06-
|
|
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.
|
|
3
|
+
"version": "1.2.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.
|
|
53
|
-
"c8ctl-plugin-nano-darwin-x64": "1.
|
|
54
|
-
"c8ctl-plugin-nano-linux-x64": "1.
|
|
55
|
-
"c8ctl-plugin-nano-linux-arm64": "1.
|
|
56
|
-
"c8ctl-plugin-nano-win32-x64": "1.
|
|
52
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.2.0",
|
|
53
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.2.0",
|
|
54
|
+
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.2.0",
|
|
55
|
+
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.2.0",
|
|
56
|
+
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.2.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',
|