@ours.network/install 0.17.0-nightly.3 → 0.17.0-nightly.5
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 +65 -0
- package/install.mjs +45 -16
- package/lib/nightly-install.mjs +700 -0
- package/lib/nightly-uninstall.mjs +373 -0
- package/lib/profiles.mjs +501 -0
- package/package.json +1 -1
- package/uninstall.mjs +15 -2
package/README.md
CHANGED
|
@@ -87,6 +87,56 @@ The human identity is created idempotently after the daemon becomes reachable. B
|
|
|
87
87
|
`curl … | bash` gives the script its input over the pipe, every prompt is read from the
|
|
88
88
|
controlling terminal (`/dev/tty`), so the flow still works piped.
|
|
89
89
|
|
|
90
|
+
## Nightly-only profile flow
|
|
91
|
+
|
|
92
|
+
The `nightly` channel uses a topology-first flow. This is a hard release boundary: `latest`
|
|
93
|
+
keeps the five-step behavior, prompts, package tags, files, service names, and uninstall output
|
|
94
|
+
described above and never reads the profile registry.
|
|
95
|
+
|
|
96
|
+
Nightly first discovers only configured local candidates: the registry, the historical default
|
|
97
|
+
config, known `~/.ours-tg` / `~/.ours-rooms` configs, Telegram and Rooms daemon blocks, known ours
|
|
98
|
+
systemd/launchd definitions, and an exact manual entry. It never scans a port range. Hosts are
|
|
99
|
+
limited to `localhost` / `127.0.0.1` (stored as `127.0.0.1`); remote names, URLs, credentials,
|
|
100
|
+
`0.0.0.0`, IPv6 loopback, and HTTPS are rejected. A candidate is deduplicated only when both its
|
|
101
|
+
endpoint and canonical state directory agree. Port, state, service, or config collisions stop the
|
|
102
|
+
plan before mutation.
|
|
103
|
+
|
|
104
|
+
The durable registry is `~/.ours/installer-profiles.json` (schema v1, atomic mode `0600`):
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
{
|
|
108
|
+
"version": 1,
|
|
109
|
+
"profiles": {
|
|
110
|
+
"default": {
|
|
111
|
+
"label": "Default ours daemon",
|
|
112
|
+
"host": "127.0.0.1",
|
|
113
|
+
"port": 3050,
|
|
114
|
+
"configPath": "/home/me/.ours/config.json",
|
|
115
|
+
"stateDir": "/home/me/.ours",
|
|
116
|
+
"serviceName": "",
|
|
117
|
+
"ownership": { "config": true, "service": true, "state": true }
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
"harnessAssociations": { "codex": "default" }
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
It stores topology and explicit installer ownership only—never tokens, token sources, status,
|
|
125
|
+
PIDs, versions, or running state. `OURS_INSTALL_PROFILES` is a test/operator override. Telegram
|
|
126
|
+
and Rooms remain authoritative in their own configs; their associations are reverse-indexed from
|
|
127
|
+
those real daemon blocks rather than copied into the registry.
|
|
128
|
+
|
|
129
|
+
After selecting or creating one profile, Nightly selects Claude Code, Codex/`ours-codex`, Hermes,
|
|
130
|
+
Telegram, and Rooms for that exact daemon, shows one review, and confirms once. A harness already
|
|
131
|
+
associated elsewhere requires explicit reassignment. The global `@ours.network/mcp@nightly`
|
|
132
|
+
package is installed/updated at most once; only the selected installer-owned daemon is restarted.
|
|
133
|
+
Snapshotted connector/registry config bytes roll back if validation or service apply fails. Npm changes and
|
|
134
|
+
new identity state are intentionally not destroyed during rollback.
|
|
135
|
+
|
|
136
|
+
`OURS_ASSUME_YES=1` selects or creates only the historical `3050` / `~/.ours` default, preserves
|
|
137
|
+
existing harness assignments, assigns otherwise-unassociated detected harnesses to it, keeps the
|
|
138
|
+
current harness/fleet defaults, and skips Telegram and Rooms.
|
|
139
|
+
|
|
90
140
|
## Daemon topology
|
|
91
141
|
|
|
92
142
|
A clean deployment installs, configures and starts **one shared** ours daemon first, then wires
|
|
@@ -244,6 +294,17 @@ directory (`~/.ours`), and the `ours-mcp` daemon. It removes **only** what the i
|
|
|
244
294
|
created, and guards the two destructive items — the data directory and the daemon — behind
|
|
245
295
|
an explicit typed `yes`.
|
|
246
296
|
|
|
297
|
+
On Nightly, uninstall is profile-aware. Removing a harness removes only its one association and
|
|
298
|
+
managed plugin artifacts. A daemon/profile cannot be forgotten or removed while any harness,
|
|
299
|
+
Telegram, or Rooms still depends on it. A dependent connector can be explicitly detached (stop its
|
|
300
|
+
service and remove only its daemon target), uninstalled, or reassigned to a different retained
|
|
301
|
+
profile; connector secrets and unrelated settings are preserved. These connector changes roll back
|
|
302
|
+
if exact daemon removal or the registry commit fails. Only a profile with `ownership.service: true`
|
|
303
|
+
may have its exact named service uninstalled; external profiles are forgotten as metadata only.
|
|
304
|
+
Data removal is a separate confirmation that requires typing the exact state path and explicitly
|
|
305
|
+
warns about identity/key loss. The global MCP package is retained while any profile/application
|
|
306
|
+
still needs it.
|
|
307
|
+
|
|
247
308
|
Headless (no terminal), drive it with environment variables:
|
|
248
309
|
|
|
249
310
|
```sh
|
|
@@ -258,6 +319,10 @@ OURS_UNINSTALL_DAEMON=yes \
|
|
|
258
319
|
| `OURS_UNINSTALL` | harnesses to remove (space/comma list of `claude-code codex hermes`, or `all`) |
|
|
259
320
|
| `OURS_UNINSTALL_DATA` | `yes` — remove the ours data directory (`~/.ours`) |
|
|
260
321
|
| `OURS_UNINSTALL_DAEMON` | `yes` — remove the `ours-mcp` daemon |
|
|
322
|
+
| `OURS_UNINSTALL_PROFILE` | Nightly profile id to forget/remove |
|
|
323
|
+
| `OURS_UNINSTALL_FORGET_PROFILE` | Nightly `yes` — forget external/profile metadata only |
|
|
324
|
+
| `OURS_UNINSTALL_TELEGRAM` | Nightly dependency action: `detach`, `uninstall`, or `reassign:<profile-id>` |
|
|
325
|
+
| `OURS_UNINSTALL_ROOMS` | Nightly dependency action: `detach`, `uninstall`, or `reassign:<profile-id>` |
|
|
261
326
|
|
|
262
327
|
## Notes
|
|
263
328
|
|
package/install.mjs
CHANGED
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
coworkSupportsExternalDaemon, COWORK_EXTERNAL_MIN_VERSION,
|
|
41
41
|
} from './lib/logic.mjs';
|
|
42
42
|
import { atomicWriteConfig } from './lib/config.mjs';
|
|
43
|
+
import { runNightlyInstaller } from './lib/nightly-install.mjs';
|
|
43
44
|
|
|
44
45
|
const NPM = process.env.OURS_NPM || 'npm';
|
|
45
46
|
// Release channel: OURS_CHANNEL=nightly installs each package's PRERELEASE dist-tag —
|
|
@@ -102,13 +103,23 @@ async function actSpin(label, desc, fn) {
|
|
|
102
103
|
|
|
103
104
|
// --- daemon probes (always safe to run — read-only) --------------------------------------------
|
|
104
105
|
const daemonVersionLine = () => (run('ours-mcp', ['--version'], { capture: true }).out.split('\n')[0] || '').trim();
|
|
105
|
-
const daemonStatusText = () => run('ours-mcp', ['status'], { capture: true }).out;
|
|
106
|
+
const daemonStatusText = (env) => run('ours-mcp', ['status'], { capture: true, ...(env ? { env } : {}) }).out;
|
|
106
107
|
function daemonLifecycleState() {
|
|
107
108
|
const status = run('ours-mcp', ['status'], { capture: true });
|
|
108
109
|
if (!status.ok) return 'stopped';
|
|
109
110
|
return /^\s*pid:\s*\d+/m.test(status.out) ? 'managed' : 'external';
|
|
110
111
|
}
|
|
111
112
|
const daemonRunning = () => daemonLifecycleState() !== 'stopped';
|
|
113
|
+
// LISTENING, not merely alive — and the difference is not academic. `ours-mcp start`
|
|
114
|
+
// exits 0 when it only FINDS a live pid in the state directory (core cmdStart: it
|
|
115
|
+
// checks `isAlive(pid)` and nothing else, so a pid file left by a previous boot whose
|
|
116
|
+
// number now belongs to any other process reads as "already running"). `ours-mcp
|
|
117
|
+
// status` then exits 0 for that same pid while printing "(port not answering!)" —
|
|
118
|
+
// core cmdStatus only sets a non-zero exit for the no-pid case. So neither exit code
|
|
119
|
+
// proves a daemon is there, and a readiness line built from one can tell the user
|
|
120
|
+
// "ready — no problems" about a port nothing is bound to. The status line the daemon
|
|
121
|
+
// prints only when the port actually answered is the honest signal, so use it.
|
|
122
|
+
const daemonReachable = (env) => /\(reachable\)/.test(daemonStatusText(env));
|
|
112
123
|
// The installed version of a global package, INCLUDING any prerelease suffix. The
|
|
113
124
|
// suffix is not cosmetic here: the Rooms daemon guard compares against an exact
|
|
114
125
|
// `0.4.1-nightly.<date>.<sha>` floor, and truncating at the dash would make every
|
|
@@ -306,6 +317,16 @@ async function main() {
|
|
|
306
317
|
finish(ttyFd); return;
|
|
307
318
|
}
|
|
308
319
|
|
|
320
|
+
// HARD RELEASE BOUNDARY. Nightly owns the topology-first profile flow. The
|
|
321
|
+
// latest/stable consumer-first implementation below remains untouched and
|
|
322
|
+
// never reads or writes installer-profiles.json or emits --application.
|
|
323
|
+
if (CHANNEL === 'nightly') {
|
|
324
|
+
return runNightlyInstaller({
|
|
325
|
+
harnesses, ttyFd, interactive, write, yes, ask, cont, dry: DRY,
|
|
326
|
+
npm: NPM, run, runAsync, act, actSpin, finish,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
309
330
|
// Daemon state up front (decides first-install vs update, and whether Step 0 runs at all).
|
|
310
331
|
const versionBefore = daemonVersionLine();
|
|
311
332
|
const daemonInstalled = !!versionBefore;
|
|
@@ -511,13 +532,13 @@ async function main() {
|
|
|
511
532
|
// WORKING daemon into no daemon at all, while the line below still said "ready": the
|
|
512
533
|
// human identity and every MCP client then fail against a port nothing is listening
|
|
513
534
|
// on. Put the one shared daemon back up and report what is actually true.
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
535
|
+
// `started.ok` is the exit code of `ours-mcp start`, which is not evidence that
|
|
536
|
+
// anything is listening (see daemonReachable). Ask the daemon instead.
|
|
537
|
+
let running = DRY ? started.ok : daemonReachable();
|
|
538
|
+
if (!DRY && !svc.ok && !running) {
|
|
539
|
+
line(info('the boot-service step stopped the daemon before it failed — restarting it.'));
|
|
540
|
+
run('ours-mcp', ['start']);
|
|
541
|
+
running = daemonReachable();
|
|
521
542
|
}
|
|
522
543
|
if (running) line(ok(`ours core ready — running on port ${chosenPort}. No problems.`));
|
|
523
544
|
else line(warn(`could not auto-start — run '${c.cyan('ours-mcp start')}' to bring it up.`));
|
|
@@ -598,9 +619,11 @@ async function main() {
|
|
|
598
619
|
line(ok(`Your human identity "${name}" is created.`));
|
|
599
620
|
record({ key: 'identity', label: 'Human identity', state: 'installed', note: name });
|
|
600
621
|
} else {
|
|
601
|
-
// A freshly-started daemon may need a moment to
|
|
602
|
-
|
|
603
|
-
|
|
622
|
+
// A freshly-started daemon may need a moment to BIND ITS PORT before create-root
|
|
623
|
+
// can reach it — which is the one thing a liveness check cannot see, so this
|
|
624
|
+
// waits on the port answering rather than on a process existing.
|
|
625
|
+
let reachable = daemonReachable();
|
|
626
|
+
for (let i = 0; i < 6 && !reachable; i++) { sleepMs(400); reachable = daemonReachable(); }
|
|
604
627
|
const r = reachable ? run('ours-mcp', ['create-root', name], { capture: true }) : { ok: false, out: '', err: 'daemon not running' };
|
|
605
628
|
const outText = `${r.out} ${r.err}`;
|
|
606
629
|
const existing = outText.match(/already exists \("([^"]+)"\)/);
|
|
@@ -704,7 +727,12 @@ async function main() {
|
|
|
704
727
|
line(info('over Telegram so they talk to each other — and it all configures maximally easily.'));
|
|
705
728
|
const goFleet = yes(' Install it?', true);
|
|
706
729
|
if (goFleet) {
|
|
707
|
-
// ours-fleet
|
|
730
|
+
// ours-fleet FOLLOWS the channel, like everything else here: it publishes its own
|
|
731
|
+
// nightly dist-tag from adapt-toolkit/ours-fleet, and lib/logic.mjs maps it
|
|
732
|
+
// accordingly. (This comment used to claim the opposite — that fleet had no nightly
|
|
733
|
+
// tag and was pinned to @latest even under OURS_CHANNEL=nightly — which stopped
|
|
734
|
+
// being true when the map gained its `fleet` entry. The code always followed the
|
|
735
|
+
// map; only the comment was stale, which is the more dangerous of the two.)
|
|
708
736
|
await actSpin(`installing ${spec('fleet')}…`, `npm i -g ${spec('fleet')}`, () => runAsync(NPM, ['i', '-g', spec('fleet')]));
|
|
709
737
|
const init = await act('ours-fleet init (one-time host setup: units, dirs, linger)', async () => run('ours-fleet', ['init']));
|
|
710
738
|
if (!init.ok) line(warn(`ours-fleet host setup didn't finish — retry '${c.cyan('ours-fleet init')}'.`));
|
|
@@ -753,7 +781,7 @@ async function main() {
|
|
|
753
781
|
// Nothing to change AND it is already up: do not touch it. `install-service` STOPS
|
|
754
782
|
// the daemon before rewriting the unit, so re-running it here would bounce a healthy
|
|
755
783
|
// daemon for no reason.
|
|
756
|
-
if (!DRY &&
|
|
784
|
+
if (!DRY && daemonReachable(env)) {
|
|
757
785
|
line(ok(`Dedicated ${label} daemon already running on port ${port} — left alone.`));
|
|
758
786
|
return { endpoint: daemonEndpoint(port), stateDir, serviceName, configPath: cfgPath, running: true, port };
|
|
759
787
|
}
|
|
@@ -767,12 +795,13 @@ async function main() {
|
|
|
767
795
|
const svc = await act(`ours-mcp install-service (dedicated ${label} daemon, unit ours-${serviceName})`, async () => run('ours-mcp', ['install-service'], { env }));
|
|
768
796
|
// Same recovery as the shared daemon: install-service STOPS the daemon before it
|
|
769
797
|
// writes the unit, so a failure there leaves nothing listening on this port.
|
|
770
|
-
|
|
798
|
+
// Same rule as the shared daemon: an exit code is not evidence of a bound port.
|
|
799
|
+
let running = DRY ? started.ok : daemonReachable(env);
|
|
771
800
|
if (!DRY && !svc.ok) {
|
|
772
|
-
running = run('ours-mcp', ['status'], { capture: true, env }).ok;
|
|
773
801
|
if (!running) {
|
|
774
802
|
line(info(`the boot-service step stopped the ${label} daemon before it failed — restarting it.`));
|
|
775
|
-
|
|
803
|
+
run('ours-mcp', ['start'], { env });
|
|
804
|
+
running = daemonReachable(env);
|
|
776
805
|
}
|
|
777
806
|
line(warn(`the ${label} daemon has no boot service — retry '${c.cyan(`OURS_CONFIG=${cfgPath} OURS_SERVICE_NAME=${serviceName} ours-mcp install-service`)}'.`));
|
|
778
807
|
}
|