@ours.network/install 0.12.0-nightly.9 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/install.mjs +61 -10
  2. 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 and/or Codex.
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';
@@ -135,6 +136,21 @@ function detectHarness(name) {
135
136
  return { name, ...verdict };
136
137
  }
137
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
+
138
154
  // Set by main() so the top-level catch can route a Ctrl+C (InstallCancelled) through the same
139
155
  // clean-exit path as the SIGINT handler.
140
156
  let cancelHandler = null;
@@ -152,7 +168,7 @@ const USAGE = `ours-install — the unified ours.network stack installer.
152
168
  ours-install [--dry-run] [--help] [--version]
153
169
 
154
170
  Guided ~3-minute setup for the whole stack: ours core (the daemon), the harness
155
- 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
156
172
  copy-paste hand-off prompt. You approve each step; re-run any time to add a piece
157
173
  or update.
158
174
 
@@ -225,7 +241,10 @@ async function main() {
225
241
  { name: 'claude', label: 'Claude Code' },
226
242
  { name: 'codex', label: 'Codex' },
227
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.
228
246
  const harnesses = harnessSpecs.map((h) => ({ ...h, ...detectHarness(h.name) }));
247
+ harnesses.push(detectHermes());
229
248
  for (const h of harnesses) {
230
249
  if (h.status === 'ok') line(ok(`'${h.name}' → real program (its plugin can be installed)`));
231
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)`));
@@ -236,7 +255,7 @@ async function main() {
236
255
  const anyHarness = harnesses.some((h) => h.status !== 'absent');
237
256
  if (!anyHarness) {
238
257
  line('');
239
- line(warn('No Claude Code or Codex found on this machine.'));
258
+ line(warn('No Claude Code, Codex, or Hermes found on this machine.'));
240
259
  line(info('Install one of them first, then re-run ours-install to wire it up.'));
241
260
  finish(ttyFd); return;
242
261
  }
@@ -431,17 +450,39 @@ async function main() {
431
450
  } else { failCodex(); record({ key: 'codex', label: 'Codex plugin + ours-codex', state: 'failed', note: 'marketplace/install step failed' }); }
432
451
  return true;
433
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
+ }
434
472
 
435
473
  // ============================================================================================
436
- // STEP 2 / 4 — harness plugins (Claude Code + Codex). The installer drives the plugin CLIs.
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).
437
476
  // ============================================================================================
438
477
  line(heading('2/4 — harness plugins'));
439
- line(info('These teach Claude Code and Codex the ours skills, so you can just talk to your agent'));
440
- 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."));
441
480
  for (const h of harnesses) {
442
481
  if (h.status === 'absent') continue; // nothing to offer; pre-flight already noted it
443
482
  line('');
444
- const acted = h.name === 'claude' ? await installClaude(h) : await installCodex(h);
483
+ const acted = h.name === 'claude' ? await installClaude(h)
484
+ : h.name === 'codex' ? await installCodex(h)
485
+ : await installHermes(h);
445
486
  cont(acted);
446
487
  }
447
488
 
@@ -472,7 +513,7 @@ async function main() {
472
513
  const r = await act(`claude plugin install ${CLAUDE_FLEET_PLUGIN}`, async () => run('claude', ['plugin', 'install', CLAUDE_FLEET_PLUGIN], { capture: true }));
473
514
  if (r.ok) { line(ok('Claude Code fleet plugin installed — you can spawn agents from Claude Code. No problems.')); fleetIn.push('Claude Code'); }
474
515
  else failClaudeFleet();
475
- } else { // codex
516
+ } else if (h.name === 'codex') {
476
517
  if (h.status !== 'ok') { manualCodexFleet(h); continue; }
477
518
  await act(`codex plugin marketplace add ${CODEX_MARKET}`, async () => run('codex', ['plugin', 'marketplace', 'add', CODEX_MARKET], { capture: true }));
478
519
  const r = await act(`codex plugin add ${CODEX_FLEET_PLUGIN}`, async () => run('codex', ['plugin', 'add', CODEX_FLEET_PLUGIN], { capture: true }));
@@ -483,7 +524,10 @@ async function main() {
483
524
  // Only claim the skill is present where the fleet plugin actually installed.
484
525
  if (init.ok && fleetIn.length) line(ok(`ours-fleet ready — ${fleetIn.join(' + ')} now know the fleet skill. No problems.`));
485
526
  else if (init.ok) line(info('ours-fleet CLI is installed; add the fleet plugin to your harness with the commands above.'));
486
- const fleetOk = init.ok && (fleetIn.length > 0 || harnesses.every((h) => h.status === 'absent'));
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);
487
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') });
488
532
  } else {
489
533
  line(info('skipped cleanly — re-run ours-install any time to add it.'));
@@ -560,6 +604,13 @@ function failCodex() {
560
604
  line(' ' + c.cyan('npm i -g @ours.network/codex'));
561
605
  line(info('Your daemon and other steps are intact. Continuing.'));
562
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
+ }
563
614
  // --- fleet HARNESS PLUGIN never-dead-end messaging ---------------------------------------------
564
615
  function manualClaudeFleet(h) {
565
616
  line(warn(h.status === 'alias'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/install",
3
- "version": "0.12.0-nightly.9",
3
+ "version": "0.12.0",
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",