@ours.network/install 0.12.0-nightly.2 → 0.12.0-nightly.20
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/install.mjs +72 -16
- package/lib/logic.mjs +39 -0
- package/package.json +1 -1
package/install.mjs
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// bootstrap, and the `ours-install` command once the stack is on the machine).
|
|
4
4
|
//
|
|
5
5
|
// ONE installer for the WHOLE stack — ours core (the daemon) + the harness plugins (Claude Code /
|
|
6
|
-
// Codex) + ours-fleet + the Telegram connector — for someone who ALREADY has Claude
|
|
6
|
+
// Codex / Hermes) + ours-fleet + the Telegram connector — for someone who ALREADY has Claude,
|
|
7
|
+
// Codex, and/or Hermes.
|
|
7
8
|
// Its whole job: install the stack cleanly, then hand back ONE copy-paste prompt the user drops
|
|
8
9
|
// into their agent to finish all real configuration conversationally. No tokens, no port editing,
|
|
9
10
|
// no config files. See packages/installer/README.md and the UX spec for the full contract.
|
|
@@ -16,7 +17,7 @@
|
|
|
16
17
|
// installed/started/restarted — it prints exactly what it WOULD do. That is the safe way to walk
|
|
17
18
|
// the whole flow on a machine you don't want to touch (and how the tests drive it).
|
|
18
19
|
import { spawn, spawnSync } from 'node:child_process';
|
|
19
|
-
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
20
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
20
21
|
import { homedir, userInfo, platform as osPlatform, release as osRelease } from 'node:os';
|
|
21
22
|
import { join, dirname } from 'node:path';
|
|
22
23
|
import { banner, heading, ok, info, warn, c, box, withSpinner, openTty, makeWriter, closeSync } from './lib/ui.mjs';
|
|
@@ -24,10 +25,14 @@ import { askLine, askYesNo, isCancel } from './lib/prompt.mjs';
|
|
|
24
25
|
import {
|
|
25
26
|
suggestPort, parsePort, validateBroker, mergeConfig, parseVersion, parseStatus,
|
|
26
27
|
detectPlatform, classifyHarnessProbe, buildHandoffPrompt,
|
|
27
|
-
DEFAULT_PORT,
|
|
28
|
+
DEFAULT_PORT, resolveChannel, pkgSpec,
|
|
28
29
|
} from './lib/logic.mjs';
|
|
29
30
|
|
|
30
31
|
const NPM = process.env.OURS_NPM || 'npm';
|
|
32
|
+
// Release channel: OURS_CHANNEL=nightly installs @nightly for mcp/tg-connector/plugin
|
|
33
|
+
// launchers but keeps @ours.network/fleet at @latest (fleet has no nightly). Default: latest.
|
|
34
|
+
const CHANNEL = resolveChannel(process.env.OURS_CHANNEL || process.env.OURS_INSTALL_CHANNEL);
|
|
35
|
+
const spec = (pkgKey) => pkgSpec(pkgKey, CHANNEL); // → "@ours.network/<key>@<tag>"
|
|
31
36
|
let DRY = !!process.env.OURS_INSTALL_DRY_RUN;
|
|
32
37
|
const SELFHOST_URL = 'ours.network';
|
|
33
38
|
const CLAUDE_MARKET = 'adapt-toolkit/ours-claude-marketplace';
|
|
@@ -131,6 +136,21 @@ function detectHarness(name) {
|
|
|
131
136
|
return { name, ...verdict };
|
|
132
137
|
}
|
|
133
138
|
|
|
139
|
+
// Hermes detection is DIFFERENT from Claude Code / Codex: Hermes has no driven CLI. Its ours plugin
|
|
140
|
+
// (`ours-hermes-install`) never calls a `hermes` binary — it just writes ~/.hermes/config.yaml + the
|
|
141
|
+
// skills — so "is it drivable?" is the wrong question. Per the Hermes plugin's own prerequisites,
|
|
142
|
+
// presence == the config dir (~/.hermes, override with HERMES_DIR) exists. We still run the alias-safe
|
|
143
|
+
// CLI probe in case a real `hermes` command is on PATH, purely to enrich detection; either signal
|
|
144
|
+
// makes it installable. No config dir and no CLI → absent (skipped, like an uninstalled harness).
|
|
145
|
+
function detectHermes() {
|
|
146
|
+
const dir = process.env.HERMES_DIR || join(homedir(), '.hermes');
|
|
147
|
+
const dirPresent = existsSync(dir);
|
|
148
|
+
const cli = detectHarness('hermes'); // best-effort — Hermes usually has no `--version` CLI
|
|
149
|
+
const status = dirPresent || cli.status === 'ok' ? 'ok' : 'absent';
|
|
150
|
+
const detail = dirPresent ? `config dir ${dir} present` : cli.detail;
|
|
151
|
+
return { name: 'hermes', label: 'Hermes', status, detail };
|
|
152
|
+
}
|
|
153
|
+
|
|
134
154
|
// Set by main() so the top-level catch can route a Ctrl+C (InstallCancelled) through the same
|
|
135
155
|
// clean-exit path as the SIGINT handler.
|
|
136
156
|
let cancelHandler = null;
|
|
@@ -148,7 +168,7 @@ const USAGE = `ours-install — the unified ours.network stack installer.
|
|
|
148
168
|
ours-install [--dry-run] [--help] [--version]
|
|
149
169
|
|
|
150
170
|
Guided ~3-minute setup for the whole stack: ours core (the daemon), the harness
|
|
151
|
-
plugins (Claude Code + Codex), ours-fleet, and the Telegram connector — then one
|
|
171
|
+
plugins (Claude Code + Codex + Hermes), ours-fleet, and the Telegram connector — then one
|
|
152
172
|
copy-paste hand-off prompt. You approve each step; re-run any time to add a piece
|
|
153
173
|
or update.
|
|
154
174
|
|
|
@@ -221,7 +241,10 @@ async function main() {
|
|
|
221
241
|
{ name: 'claude', label: 'Claude Code' },
|
|
222
242
|
{ name: 'codex', label: 'Codex' },
|
|
223
243
|
];
|
|
244
|
+
// Claude Code + Codex are driven CLIs (alias-safe --version probe); Hermes is config-dir based
|
|
245
|
+
// (see detectHermes) so it gets its own detector, appended after them.
|
|
224
246
|
const harnesses = harnessSpecs.map((h) => ({ ...h, ...detectHarness(h.name) }));
|
|
247
|
+
harnesses.push(detectHermes());
|
|
225
248
|
for (const h of harnesses) {
|
|
226
249
|
if (h.status === 'ok') line(ok(`'${h.name}' → real program (its plugin can be installed)`));
|
|
227
250
|
else if (h.status === 'alias') line(warn(`'${h.name}' → ${h.detail} (I won't call it — see the note below; you can still install it by hand)`));
|
|
@@ -232,7 +255,7 @@ async function main() {
|
|
|
232
255
|
const anyHarness = harnesses.some((h) => h.status !== 'absent');
|
|
233
256
|
if (!anyHarness) {
|
|
234
257
|
line('');
|
|
235
|
-
line(warn('No Claude Code or
|
|
258
|
+
line(warn('No Claude Code, Codex, or Hermes found on this machine.'));
|
|
236
259
|
line(info('Install one of them first, then re-run ours-install to wire it up.'));
|
|
237
260
|
finish(ttyFd); return;
|
|
238
261
|
}
|
|
@@ -317,7 +340,7 @@ async function main() {
|
|
|
317
340
|
// Without a daemon the rest is moot; go straight to the summary.
|
|
318
341
|
return endScreen({ ttyFd, summary, chosenPort, chosenBroker });
|
|
319
342
|
}
|
|
320
|
-
await actSpin(
|
|
343
|
+
await actSpin(`ensuring ${spec('mcp')}…`, `npm i -g ${spec('mcp')}`, () => runAsync(NPM, ['i', '-g', spec('mcp')]));
|
|
321
344
|
const patch = { port: chosenPort };
|
|
322
345
|
if (chosenBroker) patch.brokerUrl = chosenBroker;
|
|
323
346
|
await act(`write config (${configPath()}) with port ${chosenPort}${chosenBroker ? ' + custom broker' : ''}`, async () => { writeConfigPatch(patch); return { ok: true }; });
|
|
@@ -332,7 +355,7 @@ async function main() {
|
|
|
332
355
|
const running = daemonRunning();
|
|
333
356
|
const upd = yes(` ours core is installed (${before || '?'}) — check for an update now?`, false);
|
|
334
357
|
if (upd) {
|
|
335
|
-
await actSpin(
|
|
358
|
+
await actSpin(`updating ${spec('mcp')}…`, `npm i -g ${spec('mcp')}`, () => runAsync(NPM, ['i', '-g', spec('mcp')]));
|
|
336
359
|
const after = parseVersion(daemonVersionLine());
|
|
337
360
|
if (before && after && before !== after) {
|
|
338
361
|
await act(`ours-mcp restart (now v${after})`, async () => { if (!run('ours-mcp', ['restart']).ok) run('ours-mcp', ['start']); return { ok: true }; });
|
|
@@ -413,7 +436,7 @@ async function main() {
|
|
|
413
436
|
const add = await act(`codex plugin marketplace add ${CODEX_MARKET}`, async () => run('codex', ['plugin', 'marketplace', 'add', CODEX_MARKET], { capture: true }));
|
|
414
437
|
const inst = add.ok ? await act('codex plugin add ours@ours-codex-marketplace', async () => run('codex', ['plugin', 'add', 'ours@ours-codex-marketplace'], { capture: true })) : add;
|
|
415
438
|
// Owner-mandated: choosing the Codex plugin ALSO installs the ours-codex live launcher, same step.
|
|
416
|
-
const wrap = inst.ok ? await actSpin('installing the ours-codex live launcher…',
|
|
439
|
+
const wrap = inst.ok ? await actSpin('installing the ours-codex live launcher…', `npm i -g ${spec('codex')} (provides ours-codex)`, () => runAsync(NPM, ['i', '-g', spec('codex')])) : inst;
|
|
417
440
|
if (inst.ok && wrap.ok) {
|
|
418
441
|
line(ok(`Codex plugin + ours-codex live launcher installed — pointed at port ${chosenPort}. No problems.`));
|
|
419
442
|
// Plain-language: what ours-codex is and why you'd use it (background wake vs blocking).
|
|
@@ -427,17 +450,39 @@ async function main() {
|
|
|
427
450
|
} else { failCodex(); record({ key: 'codex', label: 'Codex plugin + ours-codex', state: 'failed', note: 'marketplace/install step failed' }); }
|
|
428
451
|
return true;
|
|
429
452
|
}
|
|
453
|
+
// Hermes: NOT a driven CLI. Its plugin install is `npm i -g @ours.network/hermes@<channel>` then
|
|
454
|
+
// `ours-hermes-install`, which writes ~/.hermes/config.yaml (the ours MCP server) + the skills. No
|
|
455
|
+
// marketplace/plugin-add, no alias-safety gate — nothing here calls the `hermes` binary. We pass
|
|
456
|
+
// --skip-daemon because the unified installer already owns the daemon (Step 1, chosen port);
|
|
457
|
+
// ours-hermes-install would otherwise re-ensure/restart it. Same never-dead-end contract as above.
|
|
458
|
+
async function installHermes() {
|
|
459
|
+
const go = yes(' Install the ours plugin into Hermes?', true);
|
|
460
|
+
if (!go) { line(info('skipped — re-run ours-install to add it.')); record({ key: 'hermes', label: 'Hermes plugin', state: 'skipped' }); return false; }
|
|
461
|
+
const npmOk = await actSpin(`installing ${spec('hermes')}…`, `npm i -g ${spec('hermes')} (provides ours-hermes-install)`, () => runAsync(NPM, ['i', '-g', spec('hermes')]));
|
|
462
|
+
const inst = npmOk.ok
|
|
463
|
+
? await act('ours-hermes-install --skip-daemon (writes ~/.hermes: ours MCP server + skills)', async () => run('ours-hermes-install', ['--skip-daemon'], { capture: true }))
|
|
464
|
+
: npmOk;
|
|
465
|
+
if (inst.ok) {
|
|
466
|
+
line(ok('Hermes plugin installed — the ours MCP server + skills are registered in ~/.hermes. No problems.'));
|
|
467
|
+
line(info("(run '/reload-mcp' in Hermes to load the ours tools.)"));
|
|
468
|
+
record({ key: 'hermes', label: 'Hermes plugin', state: 'installed', note: 'run /reload-mcp' });
|
|
469
|
+
} else { failHermes(); record({ key: 'hermes', label: 'Hermes plugin', state: 'failed', note: 'npm/ours-hermes-install step failed' }); }
|
|
470
|
+
return true;
|
|
471
|
+
}
|
|
430
472
|
|
|
431
473
|
// ============================================================================================
|
|
432
|
-
// STEP 2 / 4 — harness plugins (Claude Code + Codex). The installer drives the plugin
|
|
474
|
+
// STEP 2 / 4 — harness plugins (Claude Code + Codex + Hermes). The installer drives the plugin
|
|
475
|
+
// CLIs for Claude/Codex; Hermes installs via npm + ours-hermes-install (no CLI driving).
|
|
433
476
|
// ============================================================================================
|
|
434
477
|
line(heading('2/4 — harness plugins'));
|
|
435
|
-
line(info('These teach Claude Code and
|
|
436
|
-
line(info("to message people and set things up. I'll install them for you — no commands to type."));
|
|
478
|
+
line(info('These teach Claude Code, Codex, and Hermes the ours skills, so you can just talk to your'));
|
|
479
|
+
line(info("agent to message people and set things up. I'll install them for you — no commands to type."));
|
|
437
480
|
for (const h of harnesses) {
|
|
438
481
|
if (h.status === 'absent') continue; // nothing to offer; pre-flight already noted it
|
|
439
482
|
line('');
|
|
440
|
-
const acted = h.name === 'claude' ? await installClaude(h)
|
|
483
|
+
const acted = h.name === 'claude' ? await installClaude(h)
|
|
484
|
+
: h.name === 'codex' ? await installCodex(h)
|
|
485
|
+
: await installHermes(h);
|
|
441
486
|
cont(acted);
|
|
442
487
|
}
|
|
443
488
|
|
|
@@ -451,7 +496,8 @@ async function main() {
|
|
|
451
496
|
line(info('over Telegram so they talk to each other — and it all configures maximally easily.'));
|
|
452
497
|
const goFleet = yes(' Install it?', true);
|
|
453
498
|
if (goFleet) {
|
|
454
|
-
|
|
499
|
+
// ours-fleet is ALWAYS @latest — it has no nightly tag (pkgSpec pins it even under OURS_CHANNEL=nightly).
|
|
500
|
+
await actSpin(`installing ${spec('fleet')}…`, `npm i -g ${spec('fleet')}`, () => runAsync(NPM, ['i', '-g', spec('fleet')]));
|
|
455
501
|
const init = await act('ours-fleet init (one-time host setup: units, dirs, linger)', async () => run('ours-fleet', ['init']));
|
|
456
502
|
if (!init.ok) line(warn(`ours-fleet host setup didn't finish — retry '${c.cyan('ours-fleet init')}'.`));
|
|
457
503
|
|
|
@@ -467,7 +513,7 @@ async function main() {
|
|
|
467
513
|
const r = await act(`claude plugin install ${CLAUDE_FLEET_PLUGIN}`, async () => run('claude', ['plugin', 'install', CLAUDE_FLEET_PLUGIN], { capture: true }));
|
|
468
514
|
if (r.ok) { line(ok('Claude Code fleet plugin installed — you can spawn agents from Claude Code. No problems.')); fleetIn.push('Claude Code'); }
|
|
469
515
|
else failClaudeFleet();
|
|
470
|
-
} else
|
|
516
|
+
} else if (h.name === 'codex') {
|
|
471
517
|
if (h.status !== 'ok') { manualCodexFleet(h); continue; }
|
|
472
518
|
await act(`codex plugin marketplace add ${CODEX_MARKET}`, async () => run('codex', ['plugin', 'marketplace', 'add', CODEX_MARKET], { capture: true }));
|
|
473
519
|
const r = await act(`codex plugin add ${CODEX_FLEET_PLUGIN}`, async () => run('codex', ['plugin', 'add', CODEX_FLEET_PLUGIN], { capture: true }));
|
|
@@ -478,7 +524,10 @@ async function main() {
|
|
|
478
524
|
// Only claim the skill is present where the fleet plugin actually installed.
|
|
479
525
|
if (init.ok && fleetIn.length) line(ok(`ours-fleet ready — ${fleetIn.join(' + ')} now know the fleet skill. No problems.`));
|
|
480
526
|
else if (init.ok) line(info('ours-fleet CLI is installed; add the fleet plugin to your harness with the commands above.'));
|
|
481
|
-
|
|
527
|
+
// Fleet plugins target Claude Code + Codex only (Hermes has none) — so "no fleet-capable harness
|
|
528
|
+
// to install into" means every claude/codex is absent, NOT every harness (Hermes doesn't count).
|
|
529
|
+
const fleetCapableAbsent = harnesses.filter((h) => h.name === 'claude' || h.name === 'codex').every((h) => h.status === 'absent');
|
|
530
|
+
const fleetOk = init.ok && (fleetIn.length > 0 || fleetCapableAbsent);
|
|
482
531
|
record({ key: 'fleet', label: 'ours-fleet', state: fleetOk ? 'installed' : 'failed', version: globalVersion('@ours.network/fleet'), note: fleetIn.length ? fleetIn.join(' + ') : (init.ok ? 'CLI only — add plugin manually' : 'ours-fleet init failed') });
|
|
483
532
|
} else {
|
|
484
533
|
line(info('skipped cleanly — re-run ours-install any time to add it.'));
|
|
@@ -494,7 +543,7 @@ async function main() {
|
|
|
494
543
|
line(info("Telegram. (You'll set up the actual bot later, with your agent — not here.)"));
|
|
495
544
|
const goTg = yes(' Install it?', false);
|
|
496
545
|
if (goTg) {
|
|
497
|
-
await actSpin(
|
|
546
|
+
await actSpin(`installing ${spec('tg-connector')}…`, `npm i -g ${spec('tg-connector')}`, () => runAsync(NPM, ['i', '-g', spec('tg-connector')]));
|
|
498
547
|
const asService = yes(' Keep it running in the background so it starts automatically on boot?', true);
|
|
499
548
|
if (asService) {
|
|
500
549
|
const svc = await act('ours-tg-connector install-service (starts on boot)', async () => run('ours-tg-connector', ['install-service']));
|
|
@@ -555,6 +604,13 @@ function failCodex() {
|
|
|
555
604
|
line(' ' + c.cyan('npm i -g @ours.network/codex'));
|
|
556
605
|
line(info('Your daemon and other steps are intact. Continuing.'));
|
|
557
606
|
}
|
|
607
|
+
function failHermes() {
|
|
608
|
+
line(warn('Couldn\'t install the Hermes plugin automatically (network or npm).'));
|
|
609
|
+
line(info('Install it by hand — run these two, then run /reload-mcp in Hermes:'));
|
|
610
|
+
line(' ' + c.cyan('npm i -g @ours.network/hermes'));
|
|
611
|
+
line(' ' + c.cyan('ours-hermes-install'));
|
|
612
|
+
line(info('Your daemon and other steps are intact. Continuing.'));
|
|
613
|
+
}
|
|
558
614
|
// --- fleet HARNESS PLUGIN never-dead-end messaging ---------------------------------------------
|
|
559
615
|
function manualClaudeFleet(h) {
|
|
560
616
|
line(warn(h.status === 'alias'
|
package/lib/logic.mjs
CHANGED
|
@@ -26,6 +26,45 @@ export function canonHarnesses(raw) {
|
|
|
26
26
|
return { names, unknown };
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
// ── Release CHANNEL / npm dist-tag selection (owner 2026-07-17) ─────────────────
|
|
30
|
+
// The installer normally installs everything at @latest (stable). Setting
|
|
31
|
+
// OURS_CHANNEL=nightly (or OURS_INSTALL_CHANNEL) makes it install the NIGHTLY tag
|
|
32
|
+
// for the packages that HAVE a nightly (mcp, tg-connector, and the harness-plugin
|
|
33
|
+
// launchers claude-code/codex/hermes — all lockstep-published to the `nightly` tag),
|
|
34
|
+
// but keep @ours.network/fleet at @latest ALWAYS: ours-fleet lives in its own repo
|
|
35
|
+
// and publishes NO nightly tag, so `@nightly` there would 404 the whole install.
|
|
36
|
+
export const DEFAULT_CHANNEL = 'latest';
|
|
37
|
+
|
|
38
|
+
// Packages that follow the selected channel (nightly ⇒ @nightly). Short keys map to
|
|
39
|
+
// the @ours.network/<key> npm name. NOTE fleet is deliberately ABSENT — it is pinned.
|
|
40
|
+
const CHANNEL_TRACKING_PKGS = new Set(['mcp', 'tg-connector', 'claude-code', 'codex', 'hermes']);
|
|
41
|
+
// Packages ALWAYS pinned to @latest regardless of channel (no nightly tag exists).
|
|
42
|
+
const STABLE_ONLY_PKGS = new Set(['fleet']);
|
|
43
|
+
|
|
44
|
+
// Normalize a raw channel selection to 'latest' | 'nightly'. Anything unrecognized
|
|
45
|
+
// (incl. undefined/'') falls back to the safe default 'latest' — never guesses a tag.
|
|
46
|
+
export function resolveChannel(raw) {
|
|
47
|
+
const v = String(raw || '').trim().toLowerCase();
|
|
48
|
+
if (v === 'nightly' || v === 'prerelease' || v === 'next') return 'nightly';
|
|
49
|
+
return DEFAULT_CHANNEL; // 'latest' and everything else
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// The npm dist-tag to install for one package key under a channel. fleet is ALWAYS
|
|
53
|
+
// 'latest'; channel-tracking packages take the channel; anything else defaults to 'latest'.
|
|
54
|
+
export function pkgTag(pkgKey, channel = DEFAULT_CHANNEL) {
|
|
55
|
+
const key = String(pkgKey || '').replace(/^@ours\.network\//, '');
|
|
56
|
+
if (STABLE_ONLY_PKGS.has(key)) return 'latest';
|
|
57
|
+
const ch = resolveChannel(channel);
|
|
58
|
+
if (ch === 'nightly' && CHANNEL_TRACKING_PKGS.has(key)) return 'nightly';
|
|
59
|
+
return 'latest';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Full `@ours.network/<key>@<tag>` spec for `npm i -g`, honoring the channel.
|
|
63
|
+
export function pkgSpec(pkgKey, channel = DEFAULT_CHANNEL) {
|
|
64
|
+
const key = String(pkgKey || '').replace(/^@ours\.network\//, '');
|
|
65
|
+
return `@ours.network/${key}@${pkgTag(key, channel)}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
29
68
|
// The Telegram connector owns 3051 — the installer must never hand a daemon that port.
|
|
30
69
|
export const RESERVED_PORTS = [3051];
|
|
31
70
|
export const DEFAULT_PORT = 3050;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ours.network/install",
|
|
3
|
-
"version": "0.12.0-nightly.
|
|
3
|
+
"version": "0.12.0-nightly.20",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "The unified ours.network stack installer (ours-install): one guided ~3-minute flow for ours core (the daemon) + the harness plugins (Claude Code / Codex) + ours-fleet + the Telegram connector, then a single copy-paste hand-off prompt. Self-contained (Node built-ins only); run as `ours-install` or via curl|bash (install.sh).",
|
|
6
6
|
"type": "module",
|