@ours.network/install 0.17.0 → 0.17.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,6 +20,32 @@ Re-run (or update / add a skipped piece) any time with just `ours-install`.
20
20
  npx @ours.network/install
21
21
  ```
22
22
 
23
+ ### Stable and nightly channels
24
+
25
+ The installer package selects the channel for the stack it installs. No extra environment
26
+ variable is required:
27
+
28
+ ```sh
29
+ # Stable (the default)
30
+ npm i -g @ours.network/install@latest && ours-install
31
+
32
+ # Nightly
33
+ npm i -g @ours.network/install@nightly && ours-install
34
+ ```
35
+
36
+ A clean `X.Y.Z` installer resolves the `latest` dist-tags; an
37
+ `X.Y.Z-nightly.N` installer resolves the `nightly` dist-tags. Before changing the machine,
38
+ `ours-install` resolves `@ours.network/mcp`, `@ours.network/claude-code`, and
39
+ `@ours.network/codex`, validates that npm returned exact versions from the selected channel,
40
+ and requires all three to be the same lockstep version. It then installs MCP and the Codex
41
+ launcher by exact version and generates exact-version local marketplace sources for Claude Code
42
+ and Codex under `~/.ours/install/marketplaces/`. A moving `latest` or `nightly` selector is never
43
+ left in a plugin installation source after resolution.
44
+
45
+ Existing automation may still set `OURS_CHANNEL=latest|nightly` (or the legacy
46
+ `OURS_INSTALL_CHANNEL`) explicitly; that override continues to win. With no override, stable is
47
+ still the safe default for a local checkout or an unreadable package version.
48
+
23
49
  **Fallback for machines without npm** (least secure — pipes a script straight into your shell):
24
50
 
25
51
  ```sh
@@ -49,7 +75,8 @@ dependency on the things it installs): an ASCII banner, tasteful colour (degrade
49
75
  `3051`, reserved for the Telegram connector). Applied once, then the stack is built with it.
50
76
  3. **Four consent gates**, each paced with a clean `✓ … No problems.` line + an explicit
51
77
  **Continue?** — never a start-twice-then-ask, never a silent failure:
52
- - **1/4 ours core (the daemon)** — write config optional voice setup install/start ONCE
78
+ - **1/4 ours core (the daemon)** — install the exact version resolved for this installer's
79
+ stable/nightly channel → write config → optional voice setup → start ONCE
53
80
  → boot service. On a re-run it reuses the running config (no re-ask) and only updates when
54
81
  you say yes. Complete voice setup is kept without prompting. Missing/incomplete setup is
55
82
  offered before the first start or pending update restart, then delegated to the canonical
@@ -57,7 +84,8 @@ dependency on the things it installs): an ASCII banner, tasteful colour (degrade
57
84
  one restart/readiness transaction; declining or already-ready setup preserves the normal
58
85
  core lifecycle. The secret is written atomically to mode-`0600` config; a failed daemon
59
86
  reload rolls back.
60
- - **2/4 harness plugins** — the installer **drives the plugin CLIs itself**
87
+ - **2/4 harness plugins** — the installer **drives the plugin CLIs itself** using generated,
88
+ exact-version local marketplace sources
61
89
  (`claude plugin marketplace add …` + `claude plugin install ours@ours.network`;
62
90
  `codex plugin marketplace add …` + `codex plugin add ours@ours-codex-marketplace`). Choosing
63
91
  Codex also installs the `ours-codex` live launcher in the same step. Any failure / alias
@@ -96,6 +124,8 @@ is reported and left unchanged.
96
124
  | `OURS_INSTALL_DRY_RUN` | walk the flow without installing or changing anything |
97
125
  | `OURS_NPM` | npm binary to use (default `npm`) |
98
126
  | `OURS_CONFIG` | daemon config file location (default `~/.ours/config.json`) |
127
+ | `OURS_CHANNEL` | optional explicit `latest` or `nightly` override; otherwise follows the installer package version |
128
+ | `OURS_INSTALL_CHANNEL` | legacy alias for `OURS_CHANNEL` |
99
129
 
100
130
  ## Uninstall
101
131
 
@@ -135,11 +165,9 @@ OURS_UNINSTALL_DAEMON=yes \
135
165
 
136
166
  ## Notes
137
167
 
138
- - This package is **not published to npm** (`private: true`); it ships as the hosted
139
- `install.sh` bootstrap plus the `install.mjs` Node installer (and its `lib/`), and exposes the
140
- `ours-install` bin. The pieces it installs — the daemon (`@ours.network/mcp`), the harness
141
- plugins via each marketplace, `@ours.network/fleet`, and `@ours.network/tg-connector` — are the
142
- published components.
168
+ - This package is published to npm as `@ours.network/install` and exposes the `ours-install` bin.
169
+ It remains self-contained (Node built-ins only). The hosted `install.sh` is the stable/latest
170
+ bootstrap; install `@ours.network/install@nightly` directly for the nightly channel.
143
171
  - **Idempotent + safe to re-run.** A re-run adds a skipped piece, re-points the plugins, or (only
144
172
  when you say yes) updates a component; an already-current daemon is left untouched, its running
145
173
  port and complete voice setup are reused everywhere. Bot tokens and fleet roles remain in the
package/install.mjs CHANGED
@@ -30,16 +30,23 @@ import {
30
30
  DEFAULT_PORT, resolveChannel, pkgSpec,
31
31
  } from './lib/logic.mjs';
32
32
  import { atomicWriteConfig } from './lib/config.mjs';
33
+ import {
34
+ buildClaudeMarketplace, buildCodexMarketplace, marketplaceJson, marketplacePaths,
35
+ parseNpmVersion, validateChannelVersion,
36
+ } from './lib/marketplace.mjs';
33
37
 
34
38
  const NPM = process.env.OURS_NPM || 'npm';
35
- // Release channel: OURS_CHANNEL=nightly installs @nightly for mcp/tg-connector/plugin
36
- // launchers but keeps @ours.network/fleet at @latest (fleet has no nightly). Default: latest.
37
- const CHANNEL = resolveChannel(process.env.OURS_CHANNEL || process.env.OURS_INSTALL_CHANNEL);
39
+ // A published installer selects its OWN channel when the environment is silent:
40
+ // X.Y.Z-nightly.N follows @nightly; a clean X.Y.Z follows @latest. Explicit legacy
41
+ // channel overrides still win in both directions.
42
+ const CHANNEL = resolveChannel(
43
+ process.env.OURS_CHANNEL || process.env.OURS_INSTALL_CHANNEL,
44
+ pkgVersion(),
45
+ );
38
46
  const spec = (pkgKey) => pkgSpec(pkgKey, CHANNEL); // → "@ours.network/<key>@<tag>"
39
47
  let DRY = !!process.env.OURS_INSTALL_DRY_RUN;
40
48
  const SELFHOST_URL = 'ours.network';
41
- const CLAUDE_MARKET = 'adapt-toolkit/ours-claude-marketplace';
42
- const CODEX_MARKET = 'adapt-toolkit/ours-codex-marketplace';
49
+ const MARKETPLACE_PATHS = marketplacePaths(homedir());
43
50
 
44
51
  const sink = (s) => process.stdout.write(s);
45
52
  const line = (s = '') => sink(`${s}\n`);
@@ -100,6 +107,107 @@ const globalVersion = (pkg) => {
100
107
  return m ? m[1] : '';
101
108
  };
102
109
 
110
+ function resolveExactPackage(pkgKey) {
111
+ const packageName = `@ours.network/${pkgKey}`;
112
+ const tag = CHANNEL === 'nightly' ? 'nightly' : 'latest';
113
+ const viewed = run(NPM, ['view', `${packageName}@${tag}`, 'version', '--json'], { capture: true, timeout: 15000 });
114
+ if (!viewed.ok) {
115
+ return { ok: false, packageName, tag, reason: `npm could not resolve ${packageName}@${tag}` };
116
+ }
117
+ const checked = validateChannelVersion(parseNpmVersion(viewed.out), CHANNEL);
118
+ if (!checked.ok) return { ...checked, packageName, tag };
119
+ return { ok: true, packageName, tag, version: checked.version };
120
+ }
121
+
122
+ function resolveExactSuite() {
123
+ const packages = {};
124
+ for (const key of ['mcp', 'claude-code', 'codex']) {
125
+ const resolved = resolveExactPackage(key);
126
+ if (!resolved.ok) return resolved;
127
+ packages[key] = resolved;
128
+ }
129
+ const versions = new Set(Object.values(packages).map((entry) => entry.version));
130
+ if (versions.size !== 1) {
131
+ const detail = Object.values(packages).map((entry) => `${entry.packageName}=${entry.version}`).join(', ');
132
+ return {
133
+ ok: false,
134
+ packageName: '@ours.network/{mcp,claude-code,codex}',
135
+ tag: CHANNEL,
136
+ reason: `the ${CHANNEL} dist-tags are not lockstep (${detail})`,
137
+ };
138
+ }
139
+ return { ok: true, channel: CHANNEL, version: versions.values().next().value, packages };
140
+ }
141
+
142
+ async function prepareExactMarketplace(pkgKey, resolved) {
143
+ const claude = pkgKey === 'claude-code';
144
+ const root = claude ? MARKETPLACE_PATHS.claudeRoot : MARKETPLACE_PATHS.codexRoot;
145
+ const manifest = claude ? MARKETPLACE_PATHS.claudeManifest : MARKETPLACE_PATHS.codexManifest;
146
+ const value = claude
147
+ ? buildClaudeMarketplace(resolved.version, CHANNEL)
148
+ : buildCodexMarketplace(resolved.version, CHANNEL);
149
+ const written = await act(
150
+ `write exact ${pkgKey} marketplace (${resolved.packageName}@${resolved.version}) to ${manifest}`,
151
+ async () => {
152
+ try {
153
+ atomicWriteConfig(manifest, marketplaceJson(value));
154
+ return { ok: true };
155
+ } catch (error) {
156
+ return { ok: false, error };
157
+ }
158
+ },
159
+ );
160
+ if (!written.ok) {
161
+ return { ...resolved, ok: false, reason: `could not write the exact marketplace at ${manifest}` };
162
+ }
163
+ return { ...resolved, root, manifest };
164
+ }
165
+
166
+ function exactResolutionFailure(label, result) {
167
+ line(warn(`Couldn't safely resolve ${label} on the ${CHANNEL} channel to an exact version.`));
168
+ line(info(`${result.reason || 'The npm registry returned an invalid version.'} Existing plugin setup was left unchanged.`));
169
+ line(info(`Check '${c.cyan(`npm view ${result.packageName}@${result.tag} version`)}', then re-run ours-install.`));
170
+ }
171
+
172
+ function configuredCodexMarketplace() {
173
+ const listed = run('codex', ['plugin', 'marketplace', 'list', '--json'], { capture: true, timeout: 6000 });
174
+ if (!listed.ok) return null;
175
+ try {
176
+ const parsed = JSON.parse(listed.out);
177
+ return parsed?.marketplaces?.find((marketplace) => marketplace?.name === 'ours-codex-marketplace') || null;
178
+ } catch {
179
+ return null;
180
+ }
181
+ }
182
+
183
+ function hasClaudePlugin(pluginId = 'ours@ours.network') {
184
+ const listed = run('claude', ['plugin', 'list', '--json'], { capture: true, timeout: 6000 });
185
+ if (!listed.ok) return false;
186
+ try {
187
+ const parsed = JSON.parse(listed.out);
188
+ return Array.isArray(parsed) && parsed.some((plugin) => plugin?.id === pluginId);
189
+ } catch {
190
+ return false;
191
+ }
192
+ }
193
+
194
+ async function addExactCodexMarketplace(exact) {
195
+ const current = configuredCodexMarketplace();
196
+ const source = current?.marketplaceSource;
197
+ const alreadyExact = source?.sourceType === 'local' && source?.source === exact.root;
198
+ if (current && !alreadyExact) {
199
+ const removed = await act(
200
+ 'codex plugin marketplace remove ours-codex-marketplace (replace moving source with exact local source)',
201
+ async () => run('codex', ['plugin', 'marketplace', 'remove', 'ours-codex-marketplace'], { capture: true }),
202
+ );
203
+ if (!removed.ok) return removed;
204
+ }
205
+ return act(
206
+ `codex plugin marketplace add ${exact.root} (${exact.packageName}@${exact.version})`,
207
+ async () => run('codex', ['plugin', 'marketplace', 'add', exact.root], { capture: true }),
208
+ );
209
+ }
210
+
103
211
  // --- config file (fixed home location; mirrors core/config.ts) ---------------------------------
104
212
  function configPath() { return process.env.OURS_CONFIG || join(homedir(), '.ours', 'config.json'); }
105
213
  function readConfigObject() { try { return JSON.parse(readFileSync(configPath(), 'utf8')); } catch { return {}; } }
@@ -189,7 +297,8 @@ or update.
189
297
  --version print the installer version and exit
190
298
 
191
299
  Env: OURS_ASSUME_YES=1 (accept defaults, no prompts) · OURS_INSTALL_DRY_RUN=1 ·
192
- OURS_NPM · OURS_CONFIG (default ~/.ours/config.json). Docs: https://ours.network`;
300
+ OURS_CHANNEL=latest|nightly (optional override) · OURS_NPM ·
301
+ OURS_CONFIG (default ~/.ours/config.json). Docs: https://ours.network`;
193
302
 
194
303
  // ===============================================================================================
195
304
  async function main() {
@@ -272,6 +381,18 @@ async function main() {
272
381
  finish(ttyFd); return;
273
382
  }
274
383
 
384
+ // Resolve the release boundary before making any change. The install package is
385
+ // published last in the lockstep suite, so its selected channel must already expose
386
+ // one identical exact MCP/Claude/Codex version. Partial or malformed registry state
387
+ // fails closed instead of creating a mixed installation.
388
+ const exactSuite = resolveExactSuite();
389
+ if (!exactSuite.ok) {
390
+ line('');
391
+ exactResolutionFailure('ours.network suite', exactSuite);
392
+ finish(ttyFd); return;
393
+ }
394
+ line(ok(`Release channel: ${exactSuite.channel} → exact lockstep suite v${exactSuite.version}`));
395
+
275
396
  // Daemon state up front (decides first-install vs update, and whether Step 0 runs at all).
276
397
  const versionBefore = daemonVersionLine();
277
398
  const daemonInstalled = !!versionBefore;
@@ -432,7 +553,8 @@ async function main() {
432
553
  // Without a daemon the rest is moot; go straight to the summary.
433
554
  return endScreen({ ttyFd, summary, chosenPort, chosenBroker });
434
555
  }
435
- await actSpin(`ensuring ${spec('mcp')}…`, `npm i -g ${spec('mcp')}`, () => runAsync(NPM, ['i', '-g', spec('mcp')]));
556
+ const exactMcp = `${exactSuite.packages.mcp.packageName}@${exactSuite.packages.mcp.version}`;
557
+ await actSpin(`ensuring ${exactMcp}…`, `npm i -g ${exactMcp}`, () => runAsync(NPM, ['i', '-g', exactMcp]));
436
558
  const patch = { port: chosenPort };
437
559
  if (chosenBroker) patch.brokerUrl = chosenBroker;
438
560
  await act(`write config (${configPath()}) with port ${chosenPort}${chosenBroker ? ' + custom broker' : ''}`, async () => { writeConfigPatch(patch); return { ok: true }; });
@@ -464,7 +586,8 @@ async function main() {
464
586
  let pendingUpdateRestart = false;
465
587
  let after = before;
466
588
  if (upd) {
467
- await actSpin(`updating ${spec('mcp')}…`, `npm i -g ${spec('mcp')}`, () => runAsync(NPM, ['i', '-g', spec('mcp')]));
589
+ const exactMcp = `${exactSuite.packages.mcp.packageName}@${exactSuite.packages.mcp.version}`;
590
+ await actSpin(`updating ${exactMcp}…`, `npm i -g ${exactMcp}`, () => runAsync(NPM, ['i', '-g', exactMcp]));
468
591
  after = parseVersion(daemonVersionLine());
469
592
  pendingUpdateRestart = !!(before && after && before !== after);
470
593
  }
@@ -542,23 +665,39 @@ async function main() {
542
665
  // NEVER dead-end (owner edit #3): plain reason + manual path, always. Each returns whether it
543
666
  // ACTED (so a plain user-No skip shows no Continue pause).
544
667
  async function installClaude(h) {
545
- if (h.status !== 'ok') { manualClaude(h); record({ key: 'claude', label: 'Claude Code plugin', state: 'skipped', note: h.status === 'alias' ? 'installed as an alias' : 'not drivable' }); return true; }
546
- const go = yes(' Install the ours plugin into Claude Code?', true);
547
- if (!go) { line(info('skipped — re-run ours-install to add it.')); record({ key: 'claude', label: 'Claude Code plugin', state: 'skipped' }); return false; }
548
- const add = await act(`claude plugin marketplace add ${CLAUDE_MARKET}`, async () => run('claude', ['plugin', 'marketplace', 'add', CLAUDE_MARKET], { capture: true }));
549
- const inst = add.ok ? await act('claude plugin install ours@ours.network', async () => run('claude', ['plugin', 'install', 'ours@ours.network'], { capture: true })) : add;
668
+ if (h.status === 'ok') {
669
+ const go = yes(' Install the ours plugin into Claude Code?', true);
670
+ if (!go) { line(info('skipped — re-run ours-install to add it.')); record({ key: 'claude', label: 'Claude Code plugin', state: 'skipped' }); return false; }
671
+ }
672
+ const exact = await prepareExactMarketplace('claude-code', exactSuite.packages['claude-code']);
673
+ if (!exact.ok) { exactResolutionFailure('Claude Code', exact); record({ key: 'claude', label: 'Claude Code plugin', state: 'failed', note: 'exact version resolution failed' }); return true; }
674
+ line(info(`Claude Code plugin channel: ${exact.tag} → exact ${exact.version}.`));
675
+ if (h.status !== 'ok') { manualClaude(h, exact); record({ key: 'claude', label: 'Claude Code plugin', state: 'skipped', note: h.status === 'alias' ? 'installed as an alias' : 'not drivable' }); return true; }
676
+ const add = await act(`claude plugin marketplace add ${exact.root}`, async () => run('claude', ['plugin', 'marketplace', 'add', exact.root], { capture: true }));
677
+ // Claude's `plugin install` reports success without changing an already-installed
678
+ // plugin. Use its explicit update command on reruns/channel switches so the newly
679
+ // written exact marketplace version is actually applied.
680
+ const claudeInstalled = add.ok && hasClaudePlugin();
681
+ const verb = claudeInstalled ? 'update' : 'install';
682
+ const inst = add.ok ? await act(`claude plugin ${verb} ours@ours.network`, async () => run('claude', ['plugin', verb, 'ours@ours.network'], { capture: true })) : add;
550
683
  if (inst.ok) { line(ok(`Claude Code plugin installed — pointed at port ${chosenPort}. No problems.`)); line(info('(restart Claude Code to load it.)')); record({ key: 'claude', label: 'Claude Code plugin', state: 'installed', note: 'restart Claude Code' }); }
551
- else { failClaude(); record({ key: 'claude', label: 'Claude Code plugin', state: 'failed', note: 'marketplace/install step failed' }); }
684
+ else { failClaude(exact); record({ key: 'claude', label: 'Claude Code plugin', state: 'failed', note: 'marketplace/install step failed' }); }
552
685
  return true;
553
686
  }
554
687
  async function installCodex(h) {
555
- if (h.status !== 'ok') { manualCodex(h); record({ key: 'codex', label: 'Codex plugin + ours-codex', state: 'skipped', note: h.status === 'alias' ? 'installed as an alias' : 'not drivable' }); return true; }
556
- const go = yes(' Install the ours plugin into Codex?', true);
557
- if (!go) { line(info('skipped — re-run ours-install to add it.')); record({ key: 'codex', label: 'Codex plugin + ours-codex', state: 'skipped' }); return false; }
558
- const add = await act(`codex plugin marketplace add ${CODEX_MARKET}`, async () => run('codex', ['plugin', 'marketplace', 'add', CODEX_MARKET], { capture: true }));
688
+ if (h.status === 'ok') {
689
+ const go = yes(' Install the ours plugin into Codex?', true);
690
+ if (!go) { line(info('skipped — re-run ours-install to add it.')); record({ key: 'codex', label: 'Codex plugin + ours-codex', state: 'skipped' }); return false; }
691
+ }
692
+ const exact = await prepareExactMarketplace('codex', exactSuite.packages.codex);
693
+ if (!exact.ok) { exactResolutionFailure('Codex', exact); record({ key: 'codex', label: 'Codex plugin + ours-codex', state: 'failed', note: 'exact version resolution failed' }); return true; }
694
+ line(info(`Codex plugin channel: ${exact.tag} → exact ${exact.version}.`));
695
+ if (h.status !== 'ok') { manualCodex(h, exact); record({ key: 'codex', label: 'Codex plugin + ours-codex', state: 'skipped', note: h.status === 'alias' ? 'installed as an alias' : 'not drivable' }); return true; }
696
+ const add = await addExactCodexMarketplace(exact);
559
697
  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;
560
698
  // Owner-mandated: choosing the Codex plugin ALSO installs the ours-codex live launcher, same step.
561
- 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;
699
+ const exactSpec = `${exact.packageName}@${exact.version}`;
700
+ const wrap = inst.ok ? await actSpin('installing the ours-codex live launcher…', `npm i -g ${exactSpec} (provides ours-codex)`, () => runAsync(NPM, ['i', '-g', exactSpec])) : inst;
562
701
  if (inst.ok && wrap.ok) {
563
702
  line(ok(`Codex plugin + ours-codex live launcher installed — pointed at port ${chosenPort}. No problems.`));
564
703
  // Plain-language: what ours-codex is and why you'd use it (background wake vs blocking).
@@ -569,7 +708,7 @@ async function main() {
569
708
  line(info(' app server to watch for new mail in the BACKGROUND while you keep typing, so a'));
570
709
  line(info(" reply wakes it without interrupting you. Use 'ours-codex' for hands-off replies."));
571
710
  record({ key: 'codex', label: 'Codex plugin + ours-codex', state: 'installed', note: 'new Codex thread' });
572
- } else { failCodex(); record({ key: 'codex', label: 'Codex plugin + ours-codex', state: 'failed', note: 'marketplace/install step failed' }); }
711
+ } else { failCodex(exact); record({ key: 'codex', label: 'Codex plugin + ours-codex', state: 'failed', note: 'marketplace/install step failed' }); }
573
712
  return true;
574
713
  }
575
714
  // Hermes: NOT a driven CLI. Its plugin install is `npm i -g @ours.network/hermes@<channel>` then
@@ -668,7 +807,7 @@ async function main() {
668
807
  }
669
808
 
670
809
  // --- never-dead-end messaging (owner edit #3) --------------------------------------------------
671
- function manualClaude(h) {
810
+ function manualClaude(h, exact) {
672
811
  if (h.status === 'alias') {
673
812
  line(warn('Heads-up: on your machine, "claude" is installed as an alias, not the real command,'));
674
813
  line(info('so I can\'t drive it safely. To fix it: run ' + c.cyan('type claude') + ' , remove/rename that'));
@@ -677,17 +816,17 @@ function manualClaude(h) {
677
816
  line(warn('I couldn\'t safely drive the "claude" command on this machine.'));
678
817
  }
679
818
  line(info('You can still install the plugin yourself — inside Claude Code, run these two:'));
680
- line(' ' + c.cyan(`/plugin marketplace add ${CLAUDE_MARKET}`));
681
- line(' ' + c.cyan('/plugin install ours'));
819
+ line(' ' + c.cyan(`/plugin marketplace add ${exact.root}`));
820
+ line(' ' + c.cyan('/plugin install ours@ours.network'));
682
821
  }
683
- function failClaude() {
822
+ function failClaude(exact) {
684
823
  line(warn('Couldn\'t install the Claude Code plugin automatically (network or plugin cache).'));
685
824
  line(info('Install it by hand — inside Claude Code, run these two, then re-run ours-install:'));
686
- line(' ' + c.cyan(`/plugin marketplace add ${CLAUDE_MARKET}`));
687
- line(' ' + c.cyan('/plugin install ours'));
825
+ line(' ' + c.cyan(`/plugin marketplace add ${exact.root}`));
826
+ line(' ' + c.cyan('/plugin install ours@ours.network'));
688
827
  line(info('Your daemon and other steps are intact. Continuing.'));
689
828
  }
690
- function manualCodex(h) {
829
+ function manualCodex(h, exact) {
691
830
  if (h.status === 'alias') {
692
831
  line(warn('Heads-up: on your machine, "codex" is installed as an alias, not the real command,'));
693
832
  line(info('so I can\'t drive it safely. To fix it: run ' + c.cyan('type codex') + ' , remove/rename that'));
@@ -695,17 +834,19 @@ function manualCodex(h) {
695
834
  } else {
696
835
  line(warn('I couldn\'t safely drive the "codex" command on this machine.'));
697
836
  }
698
- line(info('You can still install it yourself — run these three in your terminal:'));
699
- line(' ' + c.cyan(`codex plugin marketplace add ${CODEX_MARKET}`));
837
+ line(info('You can still install it yourself — run these four in your terminal:'));
838
+ line(' ' + c.cyan('codex plugin marketplace remove ours-codex-marketplace') + c.gray(' (okay if absent)'));
839
+ line(' ' + c.cyan(`codex plugin marketplace add ${exact.root}`));
700
840
  line(' ' + c.cyan('codex plugin add ours@ours-codex-marketplace'));
701
- line(' ' + c.cyan('npm i -g @ours.network/codex') + c.gray(' (adds the ours-codex live launcher)'));
841
+ line(' ' + c.cyan(`npm i -g ${exact.packageName}@${exact.version}`) + c.gray(' (adds the ours-codex live launcher)'));
702
842
  }
703
- function failCodex() {
843
+ function failCodex(exact) {
704
844
  line(warn('Couldn\'t install the Codex plugin automatically (network or plugin cache).'));
705
- line(info('Install it by hand — run these three, then re-run ours-install:'));
706
- line(' ' + c.cyan(`codex plugin marketplace add ${CODEX_MARKET}`));
845
+ line(info('Install it by hand — run these four, then re-run ours-install:'));
846
+ line(' ' + c.cyan('codex plugin marketplace remove ours-codex-marketplace') + c.gray(' (okay if absent)'));
847
+ line(' ' + c.cyan(`codex plugin marketplace add ${exact.root}`));
707
848
  line(' ' + c.cyan('codex plugin add ours@ours-codex-marketplace'));
708
- line(' ' + c.cyan('npm i -g @ours.network/codex'));
849
+ line(' ' + c.cyan(`npm i -g ${exact.packageName}@${exact.version}`));
709
850
  line(info('Your daemon and other steps are intact. Continuing.'));
710
851
  }
711
852
  function failHermes() {
package/install.sh CHANGED
@@ -21,6 +21,9 @@
21
21
  # From a CLONE (a sibling install.mjs is present) it runs that directly. Piped as `curl … | bash`
22
22
  # it installs the published command globally — `npm i -g @ours.network/install` — then runs
23
23
  # `ours-install`. Idempotent: a re-run updates to @latest and runs again.
24
+ # The bootstrap is deliberately stable. For nightly, install the published nightly command
25
+ # directly: `npm i -g @ours.network/install@nightly && ours-install`; that package's own
26
+ # X.Y.Z-nightly.N version selects and exactly resolves the matching stack channel.
24
27
  #
25
28
  # Non-interactive env overrides (all optional) — consumed by the Node installer:
26
29
  # OURS_ASSUME_YES=1 accept every default; never prompt (no tty needed)
package/lib/logic.mjs CHANGED
@@ -43,10 +43,17 @@ const STABLE_ONLY_PKGS = new Set(['fleet']);
43
43
 
44
44
  // Normalize a raw channel selection to 'latest' | 'nightly'. Anything unrecognized
45
45
  // (incl. undefined/'') falls back to the safe default 'latest' — never guesses a tag.
46
- export function resolveChannel(raw) {
46
+ export function resolveChannel(raw, selfVersion = '') {
47
47
  const v = String(raw || '').trim().toLowerCase();
48
48
  if (v === 'nightly' || v === 'prerelease' || v === 'next') return 'nightly';
49
- return DEFAULT_CHANNEL; // 'latest' and everything else
49
+ if (v === 'latest' || v === 'stable') return DEFAULT_CHANNEL;
50
+ if (v) return DEFAULT_CHANNEL; // unknown explicit values never inherit a prerelease
51
+ return isNightlyVersion(selfVersion) ? 'nightly' : DEFAULT_CHANNEL;
52
+ }
53
+
54
+ // Nightly packages are stamped by the release workflow as X.Y.Z-nightly.N.
55
+ export function isNightlyVersion(version) {
56
+ return /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)-nightly\.(?:0|[1-9]\d*)$/.test(String(version || ''));
50
57
  }
51
58
 
52
59
  // The npm dist-tag to install for one package key under a channel. fleet is ALWAYS
@@ -194,10 +201,10 @@ export function redactSensitive(text, secrets = []) {
194
201
  .replace(/((?:api[_ -]?key|token)\s*[=:]\s*)\S+/gi, '$1[redacted]');
195
202
  }
196
203
 
197
- // parseVersion: pull the first x.y.z out of a version string (e.g. `ours-mcp v0.9.9`), matching
198
- // install.sh's `grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1`. Returns '' when none is present.
204
+ // parseVersion: pull the first stable or release-workflow nightly version from a CLI line.
205
+ // Keeping -nightly.N makes a stable↔nightly channel switch visible to restart detection.
199
206
  export function parseVersion(text) {
200
- const m = String(text || '').match(/[0-9]+\.[0-9]+\.[0-9]+/);
207
+ const m = String(text || '').match(/[0-9]+\.[0-9]+\.[0-9]+(?:-nightly\.[0-9]+)?/);
201
208
  return m ? m[0] : '';
202
209
  }
203
210
 
@@ -0,0 +1,104 @@
1
+ import { join } from 'node:path';
2
+
3
+ const NUM = '(?:0|[1-9]\\d*)';
4
+ const STABLE_VERSION = new RegExp(`^${NUM}\\.${NUM}\\.${NUM}$`);
5
+ const NIGHTLY_VERSION = new RegExp(`^${NUM}\\.${NUM}\\.${NUM}-nightly\\.${NUM}$`);
6
+
7
+ // npm view --json normally returns a JSON string, while older/custom npm wrappers may
8
+ // return the plain version. Accept exactly one scalar either way; arrays/objects are not
9
+ // a deliberate dist-tag resolution and therefore fail closed.
10
+ export function parseNpmVersion(text) {
11
+ const raw = String(text ?? '').trim();
12
+ if (!raw) return '';
13
+ try {
14
+ const parsed = JSON.parse(raw);
15
+ return typeof parsed === 'string' ? parsed.trim() : '';
16
+ } catch {
17
+ return /^\S+$/.test(raw) ? raw : '';
18
+ }
19
+ }
20
+
21
+ export function validateChannelVersion(version, channel) {
22
+ const value = String(version ?? '').trim();
23
+ const selected = channel === 'nightly' ? 'nightly' : 'latest';
24
+ const valid = selected === 'nightly'
25
+ ? NIGHTLY_VERSION.test(value)
26
+ : STABLE_VERSION.test(value);
27
+ return valid
28
+ ? { ok: true, version: value, channel: selected }
29
+ : {
30
+ ok: false,
31
+ version: value,
32
+ channel: selected,
33
+ reason: selected === 'nightly'
34
+ ? `expected an exact X.Y.Z-nightly.N version, got ${value || '<empty>'}`
35
+ : `expected an exact stable X.Y.Z version, got ${value || '<empty>'}`,
36
+ };
37
+ }
38
+
39
+ function exactVersion(version, channel) {
40
+ const checked = validateChannelVersion(version, channel);
41
+ if (!checked.ok) throw new Error(checked.reason);
42
+ return checked.version;
43
+ }
44
+
45
+ export function buildClaudeMarketplace(version, channel) {
46
+ const pinned = exactVersion(version, channel);
47
+ return {
48
+ $schema: 'https://json.schemastore.org/claude-code-marketplace.json',
49
+ name: 'ours.network',
50
+ owner: {
51
+ name: 'Adapt Toolkit',
52
+ url: 'https://github.com/adapt-toolkit/ours-claude-marketplace',
53
+ },
54
+ plugins: [{
55
+ name: 'ours',
56
+ displayName: 'ours',
57
+ description: 'Secure agent-to-agent communication channel over ADAPT: self-sovereign pubkey identity, end-to-end encryption.',
58
+ author: { name: 'Adapt Toolkit' },
59
+ homepage: 'https://github.com/adapt-toolkit/ours-claude-marketplace',
60
+ repository: 'https://github.com/adapt-toolkit/ours-claude-marketplace',
61
+ keywords: ['mcp', 'a2a', 'adapt', 'e2e', 'messaging'],
62
+ source: {
63
+ source: 'npm',
64
+ package: '@ours.network/claude-code',
65
+ version: pinned,
66
+ },
67
+ }],
68
+ };
69
+ }
70
+
71
+ export function buildCodexMarketplace(version, channel) {
72
+ const pinned = exactVersion(version, channel);
73
+ return {
74
+ name: 'ours-codex-marketplace',
75
+ interface: { displayName: 'ours.network for Codex' },
76
+ plugins: [{
77
+ name: 'ours',
78
+ source: {
79
+ source: 'npm',
80
+ package: '@ours.network/codex',
81
+ version: pinned,
82
+ registry: 'https://registry.npmjs.org',
83
+ },
84
+ policy: { installation: 'AVAILABLE', authentication: 'ON_INSTALL' },
85
+ category: 'Productivity',
86
+ }],
87
+ };
88
+ }
89
+
90
+ export function marketplacePaths(home) {
91
+ const root = join(home, '.ours', 'install', 'marketplaces');
92
+ const claudeRoot = join(root, 'claude-code');
93
+ const codexRoot = join(root, 'codex');
94
+ return {
95
+ claudeRoot,
96
+ claudeManifest: join(claudeRoot, '.claude-plugin', 'marketplace.json'),
97
+ codexRoot,
98
+ codexManifest: join(codexRoot, '.agents', 'plugins', 'marketplace.json'),
99
+ };
100
+ }
101
+
102
+ export function marketplaceJson(value) {
103
+ return JSON.stringify(value, null, 2) + '\n';
104
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/install",
3
- "version": "0.17.0",
3
+ "version": "0.17.2",
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",