@ours.network/install 0.17.0 → 0.17.1
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 +35 -7
- package/install.mjs +175 -34
- package/install.sh +3 -0
- package/lib/logic.mjs +12 -5
- package/lib/marketplace.mjs +104 -0
- package/package.json +1 -1
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)** —
|
|
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
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
//
|
|
36
|
-
//
|
|
37
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
const
|
|
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
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
|
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 ${
|
|
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 ${
|
|
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
|
|
699
|
-
line(' ' + c.cyan(
|
|
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(
|
|
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
|
|
706
|
-
line(' ' + c.cyan(
|
|
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(
|
|
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
|
-
|
|
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
|
|
198
|
-
//
|
|
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.
|
|
3
|
+
"version": "0.17.1",
|
|
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",
|