@ours.network/install 0.17.0-nightly.9 → 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 +55 -220
- package/install.mjs +252 -503
- package/install.sh +3 -0
- package/lib/logic.mjs +25 -416
- package/lib/marketplace.mjs +104 -0
- package/package.json +1 -1
- package/uninstall.mjs +4 -36
- package/lib/components.mjs +0 -351
- package/lib/detect.mjs +0 -182
- package/lib/effects.mjs +0 -331
- package/lib/extras.mjs +0 -400
- package/lib/journal.mjs +0 -113
- package/lib/nightly-install.mjs +0 -739
- package/lib/nightly-uninstall.mjs +0 -396
- package/lib/orchestrate-uninstall.mjs +0 -293
- package/lib/orchestrate.mjs +0 -1035
- package/lib/plan.mjs +0 -238
- package/lib/profiles.mjs +0 -524
- package/lib/rerun.mjs +0 -119
- package/lib/target.mjs +0 -357
- package/lib/uninstall.mjs +0 -467
- package/lib/usage.mjs +0 -47
package/install.mjs
CHANGED
|
@@ -2,16 +2,9 @@
|
|
|
2
2
|
// ours.network — the unified `ours-install` experience (the real UX behind install.sh's thin
|
|
3
3
|
// bootstrap, and the `ours-install` command once the stack is on the machine).
|
|
4
4
|
//
|
|
5
|
-
// ONE installer for the WHOLE stack —
|
|
6
|
-
// Codex / Hermes) + ours-fleet + the Telegram connector
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// TOPOLOGY. Step 1 installs and configures ONE shared daemon and the user picks its listen port
|
|
10
|
-
// there; every consumer below is wired to that endpoint. The Telegram connector may instead be
|
|
11
|
-
// given its OWN daemon — its own port, state directory and boot unit — chosen independently, and
|
|
12
|
-
// defaulting to the shared one so Enter and non-interactive runs keep the historical topology.
|
|
13
|
-
// Rooms answers the same question, plus a third answer the connector has no use for: keeping
|
|
14
|
-
// cowork's own EMBEDDED daemon, which is what every pre-PR#9 cowork install runs (see step 5).
|
|
5
|
+
// ONE installer for the WHOLE stack — ours core (the daemon) + the harness plugins (Claude Code /
|
|
6
|
+
// Codex / Hermes) + ours-fleet + the Telegram connector — for someone who ALREADY has Claude,
|
|
7
|
+
// Codex, and/or Hermes.
|
|
15
8
|
// Its whole job: install the stack cleanly, then hand back ONE copy-paste prompt the user drops
|
|
16
9
|
// into their agent to finish remaining configuration conversationally. Voice API credentials are
|
|
17
10
|
// the one guided secret flow: interactive, masked, optional, and written atomically with mode 0600.
|
|
@@ -27,46 +20,43 @@
|
|
|
27
20
|
import { spawn, spawnSync } from 'node:child_process';
|
|
28
21
|
import { readFileSync, existsSync } from 'node:fs';
|
|
29
22
|
import { homedir, userInfo, platform as osPlatform, release as osRelease } from 'node:os';
|
|
30
|
-
import { join
|
|
23
|
+
import { join } from 'node:path';
|
|
31
24
|
import { banner, heading, ok, info, warn, c, box, withSpinner, openTty, makeWriter, closeSync } from './lib/ui.mjs';
|
|
32
25
|
import { askLine, askYesNo, isCancel } from './lib/prompt.mjs';
|
|
33
26
|
import {
|
|
34
27
|
suggestPort, parsePort, validateBroker, mergeConfig, parseVersion, parseStatus,
|
|
35
28
|
detectPlatform, classifyHarnessProbe, buildHandoffPrompt,
|
|
36
|
-
voiceSetupStatus,
|
|
29
|
+
voiceSetupStatus,
|
|
37
30
|
DEFAULT_PORT, resolveChannel, pkgSpec,
|
|
38
|
-
validateDaemonPort, planPorts, dedicatedDaemonPaths, DEDICATED_INSTANCES,
|
|
39
|
-
coworkConfigPath, planCoworkConfig, COWORK_DEFAULT_PORT, coworkDaemonMode,
|
|
40
|
-
coworkSupportsExternalDaemon, COWORK_EXTERNAL_MIN_VERSION,
|
|
41
31
|
} from './lib/logic.mjs';
|
|
42
32
|
import { atomicWriteConfig } from './lib/config.mjs';
|
|
43
|
-
import {
|
|
44
|
-
|
|
33
|
+
import {
|
|
34
|
+
buildClaudeMarketplace, buildCodexMarketplace, marketplaceJson, marketplacePaths,
|
|
35
|
+
parseNpmVersion, validateChannelVersion,
|
|
36
|
+
} from './lib/marketplace.mjs';
|
|
45
37
|
|
|
46
38
|
const NPM = process.env.OURS_NPM || 'npm';
|
|
47
|
-
//
|
|
48
|
-
// @nightly
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
+
);
|
|
54
46
|
const spec = (pkgKey) => pkgSpec(pkgKey, CHANNEL); // → "@ours.network/<key>@<tag>"
|
|
55
47
|
let DRY = !!process.env.OURS_INSTALL_DRY_RUN;
|
|
56
48
|
const SELFHOST_URL = 'ours.network';
|
|
57
|
-
const
|
|
58
|
-
const CODEX_MARKET = 'adapt-toolkit/ours-codex-marketplace';
|
|
49
|
+
const MARKETPLACE_PATHS = marketplacePaths(homedir());
|
|
59
50
|
|
|
60
51
|
const sink = (s) => process.stdout.write(s);
|
|
61
52
|
const line = (s = '') => sink(`${s}\n`);
|
|
62
53
|
const say = (s) => sink(`ours: ${s}\n`);
|
|
63
54
|
|
|
64
55
|
// --- external command helpers (never throw; the installer degrades, it doesn't crash) ----------
|
|
65
|
-
function run(bin, args, { capture = false, timeout
|
|
56
|
+
function run(bin, args, { capture = false, timeout } = {}) {
|
|
66
57
|
const r = spawnSync(bin, args, {
|
|
67
58
|
encoding: 'utf8',
|
|
68
59
|
timeout,
|
|
69
|
-
env: env ? { ...process.env, ...env } : process.env,
|
|
70
60
|
stdio: capture ? ['ignore', 'pipe', 'pipe'] : 'inherit',
|
|
71
61
|
});
|
|
72
62
|
const timedOut = !!(r.error && (r.error.code === 'ETIMEDOUT' || r.signal === 'SIGTERM'));
|
|
@@ -104,33 +94,120 @@ async function actSpin(label, desc, fn) {
|
|
|
104
94
|
|
|
105
95
|
// --- daemon probes (always safe to run — read-only) --------------------------------------------
|
|
106
96
|
const daemonVersionLine = () => (run('ours-mcp', ['--version'], { capture: true }).out.split('\n')[0] || '').trim();
|
|
107
|
-
const daemonStatusText = (
|
|
97
|
+
const daemonStatusText = () => run('ours-mcp', ['status'], { capture: true }).out;
|
|
108
98
|
function daemonLifecycleState() {
|
|
109
99
|
const status = run('ours-mcp', ['status'], { capture: true });
|
|
110
100
|
if (!status.ok) return 'stopped';
|
|
111
101
|
return /^\s*pid:\s*\d+/m.test(status.out) ? 'managed' : 'external';
|
|
112
102
|
}
|
|
113
103
|
const daemonRunning = () => daemonLifecycleState() !== 'stopped';
|
|
114
|
-
// LISTENING, not merely alive — and the difference is not academic. `ours-mcp start`
|
|
115
|
-
// exits 0 when it only FINDS a live pid in the state directory (core cmdStart: it
|
|
116
|
-
// checks `isAlive(pid)` and nothing else, so a pid file left by a previous boot whose
|
|
117
|
-
// number now belongs to any other process reads as "already running"). `ours-mcp
|
|
118
|
-
// status` then exits 0 for that same pid while printing "(port not answering!)" —
|
|
119
|
-
// core cmdStatus only sets a non-zero exit for the no-pid case. So neither exit code
|
|
120
|
-
// proves a daemon is there, and a readiness line built from one can tell the user
|
|
121
|
-
// "ready — no problems" about a port nothing is bound to. The status line the daemon
|
|
122
|
-
// prints only when the port actually answered is the honest signal, so use it.
|
|
123
|
-
const daemonReachable = (env) => /\(reachable\)/.test(daemonStatusText(env));
|
|
124
|
-
// The installed version of a global package, INCLUDING any prerelease suffix. The
|
|
125
|
-
// suffix is not cosmetic here: the Rooms daemon guard compares against an exact
|
|
126
|
-
// `0.4.1-nightly.<date>.<sha>` floor, and truncating at the dash would make every
|
|
127
|
-
// 0.4.1 nightly look alike — including ones published before the mode existed.
|
|
128
104
|
const globalVersion = (pkg) => {
|
|
129
105
|
const ls = run(NPM, ['ls', '-g', pkg], { capture: true }).out;
|
|
130
|
-
const m = ls.match(new RegExp(pkg.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&') + '@(
|
|
106
|
+
const m = ls.match(new RegExp(pkg.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&') + '@([0-9][0-9.]*)'));
|
|
131
107
|
return m ? m[1] : '';
|
|
132
108
|
};
|
|
133
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
|
+
|
|
134
211
|
// --- config file (fixed home location; mirrors core/config.ts) ---------------------------------
|
|
135
212
|
function configPath() { return process.env.OURS_CONFIG || join(homedir(), '.ours', 'config.json'); }
|
|
136
213
|
function readConfigObject() { try { return JSON.parse(readFileSync(configPath(), 'utf8')); } catch { return {}; } }
|
|
@@ -140,18 +217,6 @@ function writeConfigPatch(patch) {
|
|
|
140
217
|
return p;
|
|
141
218
|
}
|
|
142
219
|
|
|
143
|
-
// The daemon's state directory, resolved exactly the way packages/core/src/config.ts
|
|
144
|
-
// resolves it (env > config file > ~/.ours). The Telegram connector needs the ABSOLUTE
|
|
145
|
-
// path: the SDK reads the daemon's API token from it and refuses to send that token to
|
|
146
|
-
// a separately-chosen endpoint unless the state dir was chosen just as deliberately.
|
|
147
|
-
function daemonStateDir() {
|
|
148
|
-
const fromEnv = process.env.OURS_STATE_DIR?.trim();
|
|
149
|
-
if (fromEnv) return resolve(fromEnv);
|
|
150
|
-
const fromFile = readConfigObject().stateDir;
|
|
151
|
-
if (typeof fromFile === 'string' && fromFile.trim()) return resolve(fromFile.trim());
|
|
152
|
-
return join(homedir(), '.ours');
|
|
153
|
-
}
|
|
154
|
-
|
|
155
220
|
function daemonVoiceCapability() {
|
|
156
221
|
const r = run('ours-mcp', ['voice-status', '--json'], { capture: true, timeout: 6000 });
|
|
157
222
|
if (!r.ok) return null;
|
|
@@ -222,46 +287,22 @@ const USAGE = `ours-install — the unified ours.network stack installer.
|
|
|
222
287
|
|
|
223
288
|
ours-install [--dry-run] [--help] [--version]
|
|
224
289
|
|
|
225
|
-
Guided ~3-minute setup for the whole stack:
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
Telegram can share the daemon from step 1 or be given its own (its own port, state
|
|
231
|
-
directory and boot service); Enter keeps the shared one.
|
|
290
|
+
Guided ~3-minute setup for the whole stack: ours core (the daemon), the harness
|
|
291
|
+
plugins (Claude Code + Codex + Hermes), ours-fleet, and the Telegram connector — then one
|
|
292
|
+
copy-paste hand-off prompt. You approve each step; re-run any time to add a piece
|
|
293
|
+
or update.
|
|
232
294
|
|
|
233
295
|
--dry-run walk the whole flow and print what it WOULD do — install/change nothing
|
|
234
296
|
--help show this help and exit
|
|
235
297
|
--version print the installer version and exit
|
|
236
298
|
|
|
237
299
|
Env: OURS_ASSUME_YES=1 (accept defaults, no prompts) · OURS_INSTALL_DRY_RUN=1 ·
|
|
238
|
-
|
|
300
|
+
OURS_CHANNEL=latest|nightly (optional override) · OURS_NPM ·
|
|
301
|
+
OURS_CONFIG (default ~/.ours/config.json). Docs: https://ours.network`;
|
|
239
302
|
|
|
240
303
|
// ===============================================================================================
|
|
241
304
|
async function main() {
|
|
242
305
|
const argv = process.argv.slice(2);
|
|
243
|
-
|
|
244
|
-
// ─── THE CHANNEL FORK ───────────────────────────────────────────────────────
|
|
245
|
-
// Nightly is the v3 installer, end to end. Owner ruling 2026-08-17: v3 SUBSUMES
|
|
246
|
-
// the nightly flow rather than being hosted by it, so this hands the whole run
|
|
247
|
-
// over — arguments, screens, refusals and exit code — and never returns.
|
|
248
|
-
//
|
|
249
|
-
// FIRST IN main(), BEFORE --help/--version, ON PURPOSE. Those used to be
|
|
250
|
-
// answered by the v2 body above, which would have printed v2's usage describing
|
|
251
|
-
// v2's flags for a run that is about to be v3's. Whatever prints the help must
|
|
252
|
-
// be whatever runs.
|
|
253
|
-
//
|
|
254
|
-
// The latest/stable body below is untouched and still serves that channel byte
|
|
255
|
-
// for byte; nothing a stable user does changes.
|
|
256
|
-
if (CHANNEL === 'nightly') {
|
|
257
|
-
const ttyFd = openTty();
|
|
258
|
-
const code = await runInstallV3(argv, realEffects({
|
|
259
|
-
write: makeWriter(ttyFd), ttyFd, env: process.env, version: pkgVersion(),
|
|
260
|
-
}));
|
|
261
|
-
finish(ttyFd);
|
|
262
|
-
process.exit(code);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
306
|
if (argv.includes('--help') || argv.includes('-h')) { process.stdout.write(USAGE + '\n'); return; }
|
|
266
307
|
if (argv.includes('--version') || argv.includes('-V')) { process.stdout.write(`ours-install v${pkgVersion()}\n`); return; }
|
|
267
308
|
if (argv.includes('--dry-run')) DRY = true;
|
|
@@ -340,6 +381,18 @@ async function main() {
|
|
|
340
381
|
finish(ttyFd); return;
|
|
341
382
|
}
|
|
342
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
|
+
|
|
343
396
|
// Daemon state up front (decides first-install vs update, and whether Step 0 runs at all).
|
|
344
397
|
const versionBefore = daemonVersionLine();
|
|
345
398
|
const daemonInstalled = !!versionBefore;
|
|
@@ -349,44 +402,56 @@ async function main() {
|
|
|
349
402
|
line('');
|
|
350
403
|
cont();
|
|
351
404
|
|
|
405
|
+
// ============================================================================================
|
|
406
|
+
// STEP 0 — the two config questions (asked ONCE, up front). SKIPPED entirely when a daemon is
|
|
407
|
+
// already configured (update path reuses its port/broker; delta #1859).
|
|
408
|
+
// ============================================================================================
|
|
352
409
|
const status0 = parseStatus(daemonStatusText());
|
|
353
410
|
let chosenBroker; // undefined = keep default / existing
|
|
354
411
|
let chosenPort = status0.port || DEFAULT_PORT;
|
|
355
412
|
const configFirst = !daemonInstalled;
|
|
356
413
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
414
|
+
if (configFirst) {
|
|
415
|
+
line(heading('A couple of quick settings'));
|
|
416
|
+
// 0a — broker (owner edit #1: SECURE wording; owner edit #2: self-host → website only).
|
|
417
|
+
line(info('Your agents connect through a "broker" — a shared meeting point that lets them find'));
|
|
418
|
+
line(info("each other. It's secure: your messages are end-to-end encrypted, so the broker never"));
|
|
419
|
+
line(info('sees what they say. Almost everyone uses the standard one — just press Enter.'));
|
|
420
|
+
const custom = yes(' Use a custom broker address?', false);
|
|
421
|
+
if (custom) {
|
|
422
|
+
line(info(`(Only needed if you run your own broker. More at ${SELFHOST_URL}.)`));
|
|
423
|
+
const entered = ask(' Enter the broker address: ', '');
|
|
424
|
+
const v = validateBroker(entered);
|
|
425
|
+
if (entered && v.ok && !v.empty) {
|
|
426
|
+
// Undo safety net: a mistaken custom entry is one keystroke back to the standard broker.
|
|
427
|
+
const keep = yes(` Use "${v.value}"? (No = go back to the standard broker)`, true);
|
|
428
|
+
if (keep) { chosenBroker = v.value; line(ok(`broker set to ${chosenBroker}.`)); }
|
|
429
|
+
else line(ok('using the standard broker.'));
|
|
430
|
+
} else {
|
|
431
|
+
if (entered) line(warn(`"${entered}" doesn't look like a ws:// address — using the standard broker.`));
|
|
432
|
+
else line(ok('using the standard broker.'));
|
|
433
|
+
}
|
|
434
|
+
} else {
|
|
435
|
+
line(ok('using the standard broker.'));
|
|
436
|
+
}
|
|
369
437
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
line(info(`Suggesting ${candidate} instead.`));
|
|
438
|
+
// 0b — port: probe 3050; only ask if busy. Minimize the concept.
|
|
439
|
+
if (!portTakenSync(DEFAULT_PORT)) {
|
|
440
|
+
chosenPort = DEFAULT_PORT;
|
|
441
|
+
line(ok(`Using local port ${DEFAULT_PORT}.`));
|
|
442
|
+
} else {
|
|
443
|
+
line(info(`The standard local port (${DEFAULT_PORT}) is already in use on your machine.`));
|
|
444
|
+
let candidate = suggestPort(DEFAULT_PORT + 1, portTakenSync);
|
|
445
|
+
const raw = ask(` Pick another number for the ours daemon? ${c.gray(`[Enter for ${candidate}]`)}: `, String(candidate));
|
|
446
|
+
const parsed = parsePort(raw, candidate);
|
|
447
|
+
candidate = suggestPort(parsed.ok ? parsed.port : candidate, portTakenSync);
|
|
448
|
+
chosenPort = candidate;
|
|
449
|
+
line(ok(`Using local port ${chosenPort}.`));
|
|
383
450
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
return fallback;
|
|
389
|
-
};
|
|
451
|
+
line('');
|
|
452
|
+
line(ok(`Config ready — broker: ${chosenBroker ? 'custom' : 'standard'}, port: ${chosenPort}.`));
|
|
453
|
+
cont();
|
|
454
|
+
}
|
|
390
455
|
|
|
391
456
|
// Track outcomes for the summary + hand-off.
|
|
392
457
|
const summary = [];
|
|
@@ -473,57 +538,13 @@ async function main() {
|
|
|
473
538
|
};
|
|
474
539
|
|
|
475
540
|
// ============================================================================================
|
|
476
|
-
// STEP 1 /
|
|
477
|
-
// (broker + listen port) rather than a nameless "quick settings" preamble: every consumer below
|
|
478
|
-
// is wired to the endpoint chosen HERE, so the choice belongs to the step that makes it.
|
|
479
|
-
// Config-first within the step: choose → write config → optional voice → start ONCE.
|
|
541
|
+
// STEP 1 / 4 — ours core (the daemon). Config-first: write config → optional voice → start ONCE.
|
|
480
542
|
// ============================================================================================
|
|
481
|
-
line(heading('1/
|
|
482
|
-
line(info('This is the piece that lets your agents talk to each other securely.
|
|
483
|
-
line(info('
|
|
484
|
-
line(info('given its own instead, later — and so can Rooms.'));
|
|
543
|
+
line(heading('1/4 — ours core (the daemon)'));
|
|
544
|
+
line(info('This is the piece that lets your agents talk to each other securely. Everything else'));
|
|
545
|
+
line(info('needs it.'));
|
|
485
546
|
const before = parseVersion(versionBefore);
|
|
486
547
|
|
|
487
|
-
if (configFirst) {
|
|
488
|
-
// Broker (owner edit #1: SECURE wording; owner edit #2: self-host → website only).
|
|
489
|
-
line('');
|
|
490
|
-
line(info('Your agents connect through a "broker" — a shared meeting point that lets them find'));
|
|
491
|
-
line(info("each other. It's secure: your messages are end-to-end encrypted, so the broker never"));
|
|
492
|
-
line(info('sees what they say. Almost everyone uses the standard one — just press Enter.'));
|
|
493
|
-
const custom = yes(' Use a custom broker address?', false);
|
|
494
|
-
if (custom) {
|
|
495
|
-
line(info(`(Only needed if you run your own broker. More at ${SELFHOST_URL}.)`));
|
|
496
|
-
const entered = ask(' Enter the broker address: ', '');
|
|
497
|
-
const v = validateBroker(entered);
|
|
498
|
-
if (entered && v.ok && !v.empty) {
|
|
499
|
-
// Undo safety net: a mistaken custom entry is one keystroke back to the standard broker.
|
|
500
|
-
const keep = yes(` Use "${v.value}"? (No = go back to the standard broker)`, true);
|
|
501
|
-
if (keep) { chosenBroker = v.value; line(ok(`broker set to ${chosenBroker}.`)); }
|
|
502
|
-
else line(ok('using the standard broker.'));
|
|
503
|
-
} else {
|
|
504
|
-
if (entered) line(warn(`"${entered}" doesn't look like a ws:// address — using the standard broker.`));
|
|
505
|
-
else line(ok('using the standard broker.'));
|
|
506
|
-
}
|
|
507
|
-
} else {
|
|
508
|
-
line(ok('using the standard broker.'));
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
// Listen port. ALWAYS asked now, so the shared daemon's endpoint is a decision the
|
|
512
|
-
// user makes rather than one they only hear about when 3050 happens to be busy.
|
|
513
|
-
// The default is still 3050 (the next free port when it is taken), so Enter and
|
|
514
|
-
// every non-interactive run land exactly where they always did.
|
|
515
|
-
line('');
|
|
516
|
-
line(info('The daemon listens on a local port. Everything else in this install is pointed at it.'));
|
|
517
|
-
const portDefault = portTakenSync(DEFAULT_PORT) ? suggestPort(DEFAULT_PORT + 1, portTakenSync) : DEFAULT_PORT;
|
|
518
|
-
if (portDefault !== DEFAULT_PORT) line(info(`The standard port (${DEFAULT_PORT}) is already in use on your machine.`));
|
|
519
|
-
chosenPort = askDaemonPort('Which local port should the shared daemon use?', portDefault);
|
|
520
|
-
line(ok(`Shared daemon: port ${chosenPort}, broker ${chosenBroker ? 'custom' : 'standard'}.`));
|
|
521
|
-
line('');
|
|
522
|
-
} else {
|
|
523
|
-
line(ok(`Shared daemon already configured — port ${chosenPort}. Keeping it.`));
|
|
524
|
-
}
|
|
525
|
-
claimPort(chosenPort, 'the shared ours daemon');
|
|
526
|
-
|
|
527
548
|
if (!daemonInstalled) {
|
|
528
549
|
const goCore = yes(' Install and start it?', true);
|
|
529
550
|
if (!goCore) {
|
|
@@ -532,31 +553,18 @@ async function main() {
|
|
|
532
553
|
// Without a daemon the rest is moot; go straight to the summary.
|
|
533
554
|
return endScreen({ ttyFd, summary, chosenPort, chosenBroker });
|
|
534
555
|
}
|
|
535
|
-
|
|
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]));
|
|
536
558
|
const patch = { port: chosenPort };
|
|
537
559
|
if (chosenBroker) patch.brokerUrl = chosenBroker;
|
|
538
560
|
await act(`write config (${configPath()}) with port ${chosenPort}${chosenBroker ? ' + custom broker' : ''}`, async () => { writeConfigPatch(patch); return { ok: true }; });
|
|
539
561
|
const voice = offerVoiceSetup({ readinessAfterStart: true });
|
|
540
562
|
const started = await act(`ours-mcp start (port ${chosenPort})`, async () => run('ours-mcp', ['start']));
|
|
541
563
|
const svc = await act('ours-mcp install-service (survives reboot)', async () => run('ours-mcp', ['install-service']));
|
|
542
|
-
|
|
543
|
-
// cmdInstallService), then exits non-zero if `systemctl --user enable --now` fails —
|
|
544
|
-
// no linger, no user bus, a container, WSL without systemd. Left alone that turns a
|
|
545
|
-
// WORKING daemon into no daemon at all, while the line below still said "ready": the
|
|
546
|
-
// human identity and every MCP client then fail against a port nothing is listening
|
|
547
|
-
// on. Put the one shared daemon back up and report what is actually true.
|
|
548
|
-
// `started.ok` is the exit code of `ours-mcp start`, which is not evidence that
|
|
549
|
-
// anything is listening (see daemonReachable). Ask the daemon instead.
|
|
550
|
-
let running = DRY ? started.ok : daemonReachable();
|
|
551
|
-
if (!DRY && !svc.ok && !running) {
|
|
552
|
-
line(info('the boot-service step stopped the daemon before it failed — restarting it.'));
|
|
553
|
-
run('ours-mcp', ['start']);
|
|
554
|
-
running = daemonReachable();
|
|
555
|
-
}
|
|
556
|
-
if (running) line(ok(`ours core ready — running on port ${chosenPort}. No problems.`));
|
|
564
|
+
if (started.ok) line(ok(`ours core ready — running on port ${chosenPort}. No problems.`));
|
|
557
565
|
else line(warn(`could not auto-start — run '${c.cyan('ours-mcp start')}' to bring it up.`));
|
|
558
566
|
if (!svc.ok && !svc.dry) line(warn(`boot-service not installed — retry '${c.cyan('ours-mcp install-service')}' later.`));
|
|
559
|
-
if (voice.setupRan && !DRY &&
|
|
567
|
+
if (voice.setupRan && !DRY && started.ok) {
|
|
560
568
|
const verified = daemonVoiceCapability();
|
|
561
569
|
if (verified?.ready) {
|
|
562
570
|
line(ok(`Voice transcription readiness confirmed (${verified.provider}) after the first start.`));
|
|
@@ -569,13 +577,7 @@ async function main() {
|
|
|
569
577
|
}
|
|
570
578
|
}
|
|
571
579
|
}
|
|
572
|
-
record({
|
|
573
|
-
key: 'core',
|
|
574
|
-
label: 'ours core (daemon)',
|
|
575
|
-
state: running ? 'installed' : 'failed',
|
|
576
|
-
version: parseVersion(daemonVersionLine()),
|
|
577
|
-
note: svc.ok ? 'starts on boot' : 'running; no boot service',
|
|
578
|
-
});
|
|
580
|
+
record({ key: 'core', label: 'ours core (daemon)', state: started.ok ? 'installed' : 'failed', version: parseVersion(daemonVersionLine()), note: 'starts on boot' });
|
|
579
581
|
} else {
|
|
580
582
|
// Installed: offer an update; never re-ask config; reuse the running port everywhere.
|
|
581
583
|
const daemonState = daemonLifecycleState();
|
|
@@ -584,7 +586,8 @@ async function main() {
|
|
|
584
586
|
let pendingUpdateRestart = false;
|
|
585
587
|
let after = before;
|
|
586
588
|
if (upd) {
|
|
587
|
-
|
|
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]));
|
|
588
591
|
after = parseVersion(daemonVersionLine());
|
|
589
592
|
pendingUpdateRestart = !!(before && after && before !== after);
|
|
590
593
|
}
|
|
@@ -632,11 +635,9 @@ async function main() {
|
|
|
632
635
|
line(ok(`Your human identity "${name}" is created.`));
|
|
633
636
|
record({ key: 'identity', label: 'Human identity', state: 'installed', note: name });
|
|
634
637
|
} else {
|
|
635
|
-
// A freshly-started daemon may need a moment to
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
let reachable = daemonReachable();
|
|
639
|
-
for (let i = 0; i < 6 && !reachable; i++) { sleepMs(400); reachable = daemonReachable(); }
|
|
638
|
+
// A freshly-started daemon may need a moment to bind its port before create-root can reach it.
|
|
639
|
+
let reachable = daemonRunning();
|
|
640
|
+
for (let i = 0; i < 6 && !reachable; i++) { sleepMs(400); reachable = daemonRunning(); }
|
|
640
641
|
const r = reachable ? run('ours-mcp', ['create-root', name], { capture: true }) : { ok: false, out: '', err: 'daemon not running' };
|
|
641
642
|
const outText = `${r.out} ${r.err}`;
|
|
642
643
|
const existing = outText.match(/already exists \("([^"]+)"\)/);
|
|
@@ -664,23 +665,39 @@ async function main() {
|
|
|
664
665
|
// NEVER dead-end (owner edit #3): plain reason + manual path, always. Each returns whether it
|
|
665
666
|
// ACTED (so a plain user-No skip shows no Continue pause).
|
|
666
667
|
async function installClaude(h) {
|
|
667
|
-
if (h.status
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
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;
|
|
672
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' }); }
|
|
673
|
-
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' }); }
|
|
674
685
|
return true;
|
|
675
686
|
}
|
|
676
687
|
async function installCodex(h) {
|
|
677
|
-
if (h.status
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
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);
|
|
681
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;
|
|
682
698
|
// Owner-mandated: choosing the Codex plugin ALSO installs the ours-codex live launcher, same step.
|
|
683
|
-
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;
|
|
684
701
|
if (inst.ok && wrap.ok) {
|
|
685
702
|
line(ok(`Codex plugin + ours-codex live launcher installed — pointed at port ${chosenPort}. No problems.`));
|
|
686
703
|
// Plain-language: what ours-codex is and why you'd use it (background wake vs blocking).
|
|
@@ -691,7 +708,7 @@ async function main() {
|
|
|
691
708
|
line(info(' app server to watch for new mail in the BACKGROUND while you keep typing, so a'));
|
|
692
709
|
line(info(" reply wakes it without interrupting you. Use 'ours-codex' for hands-off replies."));
|
|
693
710
|
record({ key: 'codex', label: 'Codex plugin + ours-codex', state: 'installed', note: 'new Codex thread' });
|
|
694
|
-
} 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' }); }
|
|
695
712
|
return true;
|
|
696
713
|
}
|
|
697
714
|
// Hermes: NOT a driven CLI. Its plugin install is `npm i -g @ours.network/hermes@<channel>` then
|
|
@@ -715,10 +732,10 @@ async function main() {
|
|
|
715
732
|
}
|
|
716
733
|
|
|
717
734
|
// ============================================================================================
|
|
718
|
-
// STEP 2 /
|
|
735
|
+
// STEP 2 / 4 — harness plugins (Claude Code + Codex + Hermes). The installer drives the plugin
|
|
719
736
|
// CLIs for Claude/Codex; Hermes installs via npm + ours-hermes-install (no CLI driving).
|
|
720
737
|
// ============================================================================================
|
|
721
|
-
line(heading('2/
|
|
738
|
+
line(heading('2/4 — harness plugins'));
|
|
722
739
|
line(info('These teach Claude Code, Codex, and Hermes the ours skills, so you can just talk to your'));
|
|
723
740
|
line(info("agent to message people and set things up. I'll install them for you — no commands to type."));
|
|
724
741
|
for (const h of harnesses) {
|
|
@@ -731,21 +748,16 @@ async function main() {
|
|
|
731
748
|
}
|
|
732
749
|
|
|
733
750
|
// ============================================================================================
|
|
734
|
-
// STEP 3 /
|
|
751
|
+
// STEP 3 / 4 — ours-fleet. Appealing wording (owner edit #4); default YES.
|
|
735
752
|
// ============================================================================================
|
|
736
|
-
line(heading('3/
|
|
753
|
+
line(heading('3/4 — ours-fleet (your always-online agent team)'));
|
|
737
754
|
line(info('This makes your harnesses PERSISTENT: Claude Code and Codex stop being just a terminal'));
|
|
738
755
|
line(info('session and become always-online daemons that survive a reboot. Stand up your own team'));
|
|
739
756
|
line(info('of always-online developers, combine harnesses, run several Claude Codes, and link them'));
|
|
740
757
|
line(info('over Telegram so they talk to each other — and it all configures maximally easily.'));
|
|
741
758
|
const goFleet = yes(' Install it?', true);
|
|
742
759
|
if (goFleet) {
|
|
743
|
-
// ours-fleet
|
|
744
|
-
// nightly dist-tag from adapt-toolkit/ours-fleet, and lib/logic.mjs maps it
|
|
745
|
-
// accordingly. (This comment used to claim the opposite — that fleet had no nightly
|
|
746
|
-
// tag and was pinned to @latest even under OURS_CHANNEL=nightly — which stopped
|
|
747
|
-
// being true when the map gained its `fleet` entry. The code always followed the
|
|
748
|
-
// map; only the comment was stale, which is the more dangerous of the two.)
|
|
760
|
+
// ours-fleet is ALWAYS @latest — it has no nightly tag (pkgSpec pins it even under OURS_CHANNEL=nightly).
|
|
749
761
|
await actSpin(`installing ${spec('fleet')}…`, `npm i -g ${spec('fleet')}`, () => runAsync(NPM, ['i', '-g', spec('fleet')]));
|
|
750
762
|
const init = await act('ours-fleet init (one-time host setup: units, dirs, linger)', async () => run('ours-fleet', ['init']));
|
|
751
763
|
if (!init.ok) line(warn(`ours-fleet host setup didn't finish — retry '${c.cyan('ours-fleet init')}'.`));
|
|
@@ -766,147 +778,24 @@ async function main() {
|
|
|
766
778
|
}
|
|
767
779
|
cont(goFleet);
|
|
768
780
|
|
|
769
|
-
// The broker the WHOLE deployment shares, whichever daemon a consumer talks to.
|
|
770
|
-
const sharedBroker = () => resolveSharedBroker({
|
|
771
|
-
chosenBroker,
|
|
772
|
-
statusBroker: status0.broker,
|
|
773
|
-
configBroker: readConfigObject().brokerUrl,
|
|
774
|
-
});
|
|
775
|
-
|
|
776
|
-
// Provision a daemon that belongs to ONE consumer: its own config file, its own
|
|
777
|
-
// state directory, its own port, and — via core's OURS_SERVICE_NAME — its own boot
|
|
778
|
-
// unit, so `install-service` cannot overwrite the shared daemon's. Returns the
|
|
779
|
-
// endpoint + state dir to wire that consumer to, and whether it came up.
|
|
780
|
-
async function provisionDedicatedDaemon({ instance, port, label }) {
|
|
781
|
-
const { stateDir, configPath: cfgPath, serviceName } = dedicatedDaemonPaths(homedir(), instance);
|
|
782
|
-
const env = { OURS_CONFIG: cfgPath, OURS_STATE_DIR: stateDir, OURS_SERVICE_NAME: serviceName };
|
|
783
|
-
// The dedicated daemon has to exist as a package before it can be started; on a
|
|
784
|
-
// fresh machine step 1 already installed it, but a re-run that skipped core has not.
|
|
785
|
-
await actSpin(`ensuring ${spec('mcp')}…`, `npm i -g ${spec('mcp')}`, () => runAsync(NPM, ['i', '-g', spec('mcp')]));
|
|
786
|
-
const desired = { port, stateDir, serviceName };
|
|
787
|
-
const broker = sharedBroker();
|
|
788
|
-
if (broker) desired.brokerUrl = broker;
|
|
789
|
-
let existing = {};
|
|
790
|
-
try { existing = JSON.parse(readFileSync(cfgPath, 'utf8')); } catch { /* absent or unreadable */ }
|
|
791
|
-
const sameAlready = existing.port === port && existing.stateDir === stateDir && existing.serviceName === serviceName;
|
|
792
|
-
if (sameAlready) {
|
|
793
|
-
line(ok(`The ${label} daemon is already configured on port ${port} — no change.`));
|
|
794
|
-
// Nothing to change AND it is already up: do not touch it. `install-service` STOPS
|
|
795
|
-
// the daemon before rewriting the unit, so re-running it here would bounce a healthy
|
|
796
|
-
// daemon for no reason.
|
|
797
|
-
if (!DRY && daemonReachable(env)) {
|
|
798
|
-
line(ok(`Dedicated ${label} daemon already running on port ${port} — left alone.`));
|
|
799
|
-
return { endpoint: daemonEndpoint(port), stateDir, serviceName, configPath: cfgPath, running: true, port };
|
|
800
|
-
}
|
|
801
|
-
} else {
|
|
802
|
-
await act(`write ${cfgPath} (dedicated ${label} daemon, port ${port}, state ${stateDir})`, async () => {
|
|
803
|
-
atomicWriteConfig(cfgPath, mergeConfig(existing, desired));
|
|
804
|
-
return { ok: true };
|
|
805
|
-
});
|
|
806
|
-
}
|
|
807
|
-
const started = await act(`ours-mcp start (dedicated ${label} daemon, port ${port})`, async () => run('ours-mcp', ['start'], { env }));
|
|
808
|
-
const svc = await act(`ours-mcp install-service (dedicated ${label} daemon, unit ours-${serviceName})`, async () => run('ours-mcp', ['install-service'], { env }));
|
|
809
|
-
// Same recovery as the shared daemon: install-service STOPS the daemon before it
|
|
810
|
-
// writes the unit, so a failure there leaves nothing listening on this port.
|
|
811
|
-
// Same rule as the shared daemon: an exit code is not evidence of a bound port.
|
|
812
|
-
let running = DRY ? started.ok : daemonReachable(env);
|
|
813
|
-
if (!DRY && !svc.ok) {
|
|
814
|
-
if (!running) {
|
|
815
|
-
line(info(`the boot-service step stopped the ${label} daemon before it failed — restarting it.`));
|
|
816
|
-
run('ours-mcp', ['start'], { env });
|
|
817
|
-
running = daemonReachable(env);
|
|
818
|
-
}
|
|
819
|
-
line(warn(`the ${label} daemon has no boot service — retry '${c.cyan(`OURS_CONFIG=${cfgPath} OURS_SERVICE_NAME=${serviceName} ours-mcp install-service`)}'.`));
|
|
820
|
-
}
|
|
821
|
-
if (running || DRY) line(ok(`Dedicated ${label} daemon ready on port ${port} (state ${stateDir}, unit ours-${serviceName}).`));
|
|
822
|
-
else line(warn(`could not start the dedicated ${label} daemon — run '${c.cyan(`OURS_CONFIG=${cfgPath} ours-mcp start`)}'.`));
|
|
823
|
-
return { endpoint: daemonEndpoint(port), stateDir, serviceName, configPath: cfgPath, running: running || DRY, port };
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
// Ask one consumer whether it uses the COMMON daemon or gets its own. Enter and
|
|
827
|
-
// every non-interactive run answer "common" — the historical topology.
|
|
828
|
-
const askDaemonMode = (what) => {
|
|
829
|
-
line(info(`${what} can share the daemon from step 1, or run against its own isolated one.`));
|
|
830
|
-
line(info('Sharing is right for almost everyone — press Enter. A dedicated daemon gets its own'));
|
|
831
|
-
line(info('port, state directory and boot service, and does not see the shared daemon\'s identities.'));
|
|
832
|
-
return yes(` Give ${what} its OWN dedicated daemon?`, false) ? 'dedicated' : 'common';
|
|
833
|
-
};
|
|
834
|
-
|
|
835
|
-
// Give the Telegram connector the daemon it was assigned: that daemon's loopback
|
|
836
|
-
// endpoint, the state directory that endpoint's API token belongs to, and — for a
|
|
837
|
-
// pre-0.3.3 connector that still meets the daemon at a broker instead — that broker.
|
|
838
|
-
// Idempotent: an unchanged selection writes nothing. Returns { changed, hadPrevious }
|
|
839
|
-
// so the caller can warn about a service unit that froze an older selection.
|
|
840
|
-
async function writeTgDaemonConfig({ endpoint, stateDir }) {
|
|
841
|
-
const path = tgConfigPath(process.env, homedir());
|
|
842
|
-
const desired = {
|
|
843
|
-
daemonUrl: endpoint,
|
|
844
|
-
daemonStateDir: stateDir,
|
|
845
|
-
brokerUrl: sharedBroker(),
|
|
846
|
-
};
|
|
847
|
-
let existing = {};
|
|
848
|
-
try { existing = JSON.parse(readFileSync(path, 'utf8')); } catch { /* absent or unreadable */ }
|
|
849
|
-
const plan = planTgDaemonConfig(existing, desired);
|
|
850
|
-
const hadPrevious = !!(plan.previous.daemonUrl || plan.previous.brokerUrl);
|
|
851
|
-
if (!plan.changed) {
|
|
852
|
-
line(ok(`Telegram connector already points at this daemon (${desired.daemonUrl}) — no change.`));
|
|
853
|
-
return { changed: false, hadPrevious };
|
|
854
|
-
}
|
|
855
|
-
await act(`write ${path} (daemon ${desired.daemonUrl}, state ${stateDir})`, async () => {
|
|
856
|
-
atomicWriteConfig(path, plan.text);
|
|
857
|
-
return { ok: true };
|
|
858
|
-
});
|
|
859
|
-
line(ok(`Telegram connector configured to use this daemon (${desired.daemonUrl}).`));
|
|
860
|
-
return { changed: true, hadPrevious };
|
|
861
|
-
}
|
|
862
|
-
|
|
863
781
|
// ============================================================================================
|
|
864
|
-
// STEP 4 /
|
|
782
|
+
// STEP 4 / 4 — Telegram connector. Install-only (no bot tokens here). Then: run as a service?
|
|
865
783
|
// ============================================================================================
|
|
866
|
-
line(heading('4/
|
|
784
|
+
line(heading('4/4 — Telegram connector'));
|
|
867
785
|
line(info('This bridges a Telegram bot to your Ours node, so you can talk to your agent from'));
|
|
868
786
|
line(info("Telegram. (You'll set up the actual bot later, with your agent — not here.)"));
|
|
869
787
|
const goTg = yes(' Install it?', false);
|
|
870
788
|
if (goTg) {
|
|
871
789
|
await actSpin(`installing ${spec('tg-connector')}…`, `npm i -g ${spec('tg-connector')}`, () => runAsync(NPM, ['i', '-g', spec('tg-connector')]));
|
|
872
|
-
// WHICH daemon — asked independently of every other consumer, and answered
|
|
873
|
-
// "common" by Enter / non-interactive so the historical topology is the default.
|
|
874
|
-
line('');
|
|
875
|
-
const tgMode = askDaemonMode('the Telegram connector');
|
|
876
|
-
let tgDaemon = { endpoint: daemonEndpoint(chosenPort), stateDir: daemonStateDir(), port: chosenPort, mode: 'common' };
|
|
877
|
-
if (tgMode === 'dedicated') {
|
|
878
|
-
const instance = DEDICATED_INSTANCES.telegram;
|
|
879
|
-
const suggested = suggestPort(chosenPort + 1, (p) => claimedPorts.includes(p) || portTakenSync(p));
|
|
880
|
-
const port = askDaemonPort('Which local port should the Telegram daemon use?', suggested);
|
|
881
|
-
claimPort(port, 'the dedicated Telegram daemon');
|
|
882
|
-
const provisioned = await provisionDedicatedDaemon({ instance, port, label: 'Telegram' });
|
|
883
|
-
tgDaemon = { ...provisioned, mode: 'dedicated' };
|
|
884
|
-
} else {
|
|
885
|
-
line(ok(`Telegram will use the shared daemon on port ${chosenPort}.`));
|
|
886
|
-
}
|
|
887
|
-
// POINT IT AT THAT DAEMON — BEFORE it is started or installed as a service.
|
|
888
|
-
// The connector never inherits ~/.ours/config.json (its SDK reports configPath:
|
|
889
|
-
// null unless told otherwise), and `install-service` bakes whatever it resolves
|
|
890
|
-
// into the unit as environment variables that outrank the file from then on. So
|
|
891
|
-
// the daemon's identity has to be in its config BEFORE either happens. See
|
|
892
|
-
// planTgDaemonConfig for why all three keys are written.
|
|
893
|
-
const tgConfigured = await writeTgDaemonConfig(tgDaemon);
|
|
894
|
-
const where = tgDaemon.mode === 'dedicated' ? `its own daemon on port ${tgDaemon.port}` : `the shared daemon on port ${tgDaemon.port}`;
|
|
895
790
|
const asService = yes(' Keep it running in the background so it starts automatically on boot?', true);
|
|
896
791
|
if (asService) {
|
|
897
792
|
const svc = await act('ours-tg-connector install-service (starts on boot)', async () => run('ours-tg-connector', ['install-service']));
|
|
898
|
-
if (svc.ok) line(ok(
|
|
793
|
+
if (svc.ok) line(ok('Telegram connector installed and running as a service (starts on boot). No problems.'));
|
|
899
794
|
else line(warn(`connector installed, but the service didn't start — retry '${c.cyan('ours-tg-connector install-service')}'.`));
|
|
900
|
-
record({ key: 'telegram', label: 'Telegram connector', state: 'installed', version: globalVersion('@ours.network/tg-connector'), note:
|
|
795
|
+
record({ key: 'telegram', label: 'Telegram connector', state: 'installed', version: globalVersion('@ours.network/tg-connector'), note: 'service (boot)' });
|
|
901
796
|
} else {
|
|
902
|
-
line(ok(`Telegram connector installed
|
|
903
|
-
|
|
904
|
-
// the unit's environment, which outranks the file we just wrote. Config alone
|
|
905
|
-
// cannot repair that — say so plainly rather than let it look fixed.
|
|
906
|
-
if (tgConfigured.changed && tgConfigured.hadPrevious) {
|
|
907
|
-
line(warn(`if you previously ran '${c.cyan('ours-tg-connector install-service')}', re-run it — the old service froze the previous daemon selection in its unit.`));
|
|
908
|
-
}
|
|
909
|
-
record({ key: 'telegram', label: 'Telegram connector', state: 'installed', version: globalVersion('@ours.network/tg-connector'), note: `start on demand · ${tgDaemon.mode} daemon ${tgDaemon.port}` });
|
|
797
|
+
line(ok(`Telegram connector installed. Start it any time with '${c.cyan('ours-tg-connector start')}'. No problems.`));
|
|
798
|
+
record({ key: 'telegram', label: 'Telegram connector', state: 'installed', version: globalVersion('@ours.network/tg-connector'), note: 'start on demand' });
|
|
910
799
|
}
|
|
911
800
|
} else {
|
|
912
801
|
line(info('skipped cleanly.'));
|
|
@@ -914,151 +803,11 @@ async function main() {
|
|
|
914
803
|
}
|
|
915
804
|
cont(goTg);
|
|
916
805
|
|
|
917
|
-
// ============================================================================================
|
|
918
|
-
// STEP 5 / 5 — Rooms (ours-cowork). Two independent things get configured here.
|
|
919
|
-
//
|
|
920
|
-
// ITS OWN SURFACE — the deployment broker, its private state directory, and its loopback
|
|
921
|
-
// console/REST port. Those it has always had.
|
|
922
|
-
//
|
|
923
|
-
// WHICH DAEMON — ours-cowork used to host its own, always. It now supports an EXTERNAL ours
|
|
924
|
-
// daemon (cowork PR #9), so Rooms answers the same common-vs-dedicated question the Telegram
|
|
925
|
-
// connector does, plus a third state the connector does not have: EMBEDDED, cowork's own.
|
|
926
|
-
// Contract (see logic.mjs): the `daemon` block is optional; absent means embedded; external is
|
|
927
|
-
// { mode:'external', endpoint, stateDir } and REQUIRES both halves, because cowork holds no
|
|
928
|
-
// token and its SDK reads <stateDir>/daemon-token.
|
|
929
|
-
//
|
|
930
|
-
// Boot is FAIL-CLOSED on an unreachable endpoint, a non-ours daemon, or a mismatched state
|
|
931
|
-
// directory — there is no embedded fallback. So an install that is ALREADY running embedded is
|
|
932
|
-
// never migrated behind the user's back: non-interactively it is left exactly as it is, and
|
|
933
|
-
// interactively the question is asked plainly before anything is written.
|
|
934
|
-
// ============================================================================================
|
|
935
|
-
line(heading('5/5 — Rooms (ours-cowork)'));
|
|
936
|
-
line(info('Durable mission rooms: a room keeps its own ordered history, and people and agents'));
|
|
937
|
-
line(info('join it as seats. It serves a local web console, and reaches everyone through the'));
|
|
938
|
-
line(info('same broker as the rest of your install.'));
|
|
939
|
-
const goRooms = yes(' Install it?', false);
|
|
940
|
-
if (goRooms) {
|
|
941
|
-
await actSpin(`installing ${spec('cowork')}…`, `npm i -g ${spec('cowork')}`, () => runAsync(NPM, ['i', '-g', spec('cowork')]));
|
|
942
|
-
const roomsStateDir = join(homedir(), '.ours-cowork');
|
|
943
|
-
const cfgPath = coworkConfigPath(process.env, homedir());
|
|
944
|
-
let existingRooms = {};
|
|
945
|
-
try { existingRooms = JSON.parse(readFileSync(cfgPath, 'utf8')); } catch { /* absent or unreadable */ }
|
|
946
|
-
const restDefault = Number.isInteger(existingRooms.rest?.port) ? existingRooms.rest.port : COWORK_DEFAULT_PORT;
|
|
947
|
-
line('');
|
|
948
|
-
line(info('Rooms serves a console on a loopback port — 127.0.0.1 only, never exposed.'));
|
|
949
|
-
// COWORK_DEFAULT_PORT is in RESERVED_PORTS (so no ours daemon can be handed it),
|
|
950
|
-
// so validate this one against the daemon ports only.
|
|
951
|
-
const roomsPort = (() => {
|
|
952
|
-
let candidate = restDefault;
|
|
953
|
-
for (let attempt = 0; attempt < 3; attempt++) {
|
|
954
|
-
const raw = ask(` Which local port should the Rooms console use? ${c.gray(`[Enter for ${candidate}]`)}: `, String(candidate));
|
|
955
|
-
const v = validateDaemonPort(raw, {
|
|
956
|
-
fallback: candidate, isTaken: (p) => (p === restDefault ? false : portTakenSync(p)),
|
|
957
|
-
taken: claimedPorts, reserved: [],
|
|
958
|
-
});
|
|
959
|
-
if (v.ok) return v.port;
|
|
960
|
-
line(warn(`${v.reason}.`));
|
|
961
|
-
if (!interactive) break;
|
|
962
|
-
candidate = suggestPort(v.port + 1, (p) => claimedPorts.includes(p) || portTakenSync(p));
|
|
963
|
-
line(info(`Suggesting ${candidate} instead.`));
|
|
964
|
-
}
|
|
965
|
-
return candidate;
|
|
966
|
-
})();
|
|
967
|
-
claimPort(roomsPort, 'the Rooms console');
|
|
968
|
-
|
|
969
|
-
// WHICH DAEMON. `undefined` means "leave whatever is there alone" — the answer for an
|
|
970
|
-
// existing embedded install nobody asked to migrate.
|
|
971
|
-
const wasEmbedded = coworkDaemonMode(existingRooms) === 'embedded';
|
|
972
|
-
const hadConfig = existsSync(cfgPath);
|
|
973
|
-
// Ask the BUILD, not the channel: this runs after the install above, so the version
|
|
974
|
-
// read here is the one actually on the machine.
|
|
975
|
-
const coworkVersion = globalVersion('@ours.network/cowork');
|
|
976
|
-
const externalSupported = coworkSupportsExternalDaemon(coworkVersion);
|
|
977
|
-
let roomsDaemon;
|
|
978
|
-
let roomsDaemonLabel;
|
|
979
|
-
if (!externalSupported) {
|
|
980
|
-
// The build we just installed predates cowork's external-daemon mode. Its config
|
|
981
|
-
// is a strict document and its boot fails closed, so writing a selection it cannot
|
|
982
|
-
// honour would break Rooms rather than degrade it.
|
|
983
|
-
line(info(`This Rooms build${coworkVersion ? ` (${coworkVersion})` : ''} hosts its own daemon; pointing it at the shared`));
|
|
984
|
-
line(info(`one needs ${COWORK_EXTERNAL_MIN_VERSION} or newer. Re-run with ${c.cyan('OURS_CHANNEL=nightly')} to get it.`));
|
|
985
|
-
roomsDaemonLabel = 'embedded';
|
|
986
|
-
} else if (hadConfig && wasEmbedded && !interactive) {
|
|
987
|
-
// Fail-closed boot makes this migration a real risk; never do it unasked.
|
|
988
|
-
line(info('Rooms already runs its own embedded daemon — leaving that alone.'));
|
|
989
|
-
line(info(`To point it at this install's daemon, re-run ${c.cyan('ours-install')} in a terminal.`));
|
|
990
|
-
roomsDaemonLabel = 'embedded (unchanged)';
|
|
991
|
-
} else {
|
|
992
|
-
line('');
|
|
993
|
-
const roomsMode = askDaemonMode('Rooms');
|
|
994
|
-
if (roomsMode === 'dedicated') {
|
|
995
|
-
const instance = DEDICATED_INSTANCES.rooms;
|
|
996
|
-
const suggested = suggestPort(chosenPort + 1, (p) => claimedPorts.includes(p) || portTakenSync(p));
|
|
997
|
-
const port = askDaemonPort('Which local port should the Rooms daemon use?', suggested);
|
|
998
|
-
claimPort(port, 'the dedicated Rooms daemon');
|
|
999
|
-
const provisioned = await provisionDedicatedDaemon({ instance, port, label: 'Rooms' });
|
|
1000
|
-
roomsDaemon = { endpoint: provisioned.endpoint, stateDir: provisioned.stateDir };
|
|
1001
|
-
roomsDaemonLabel = `dedicated daemon ${port}`;
|
|
1002
|
-
} else {
|
|
1003
|
-
roomsDaemon = { endpoint: daemonEndpoint(chosenPort), stateDir: daemonStateDir() };
|
|
1004
|
-
roomsDaemonLabel = `common daemon ${chosenPort}`;
|
|
1005
|
-
line(ok(`Rooms will use the shared daemon on port ${chosenPort}.`));
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
const roomsPlan = planCoworkConfig(existingRooms, {
|
|
1010
|
-
brokerUrl: sharedBroker(),
|
|
1011
|
-
stateDir: roomsStateDir,
|
|
1012
|
-
restPort: roomsPort,
|
|
1013
|
-
daemon: roomsDaemon,
|
|
1014
|
-
});
|
|
1015
|
-
if (roomsPlan.error) {
|
|
1016
|
-
// Only reachable if a daemon selection lost half of itself; a half-written block
|
|
1017
|
-
// would fail closed at cowork's boot, so refuse rather than write it.
|
|
1018
|
-
line(warn(`not changing the Rooms daemon selection — ${roomsPlan.error}.`));
|
|
1019
|
-
} else if (roomsPlan.changed) {
|
|
1020
|
-
await act(`write ${cfgPath} (console port ${roomsPort}, state ${roomsStateDir}${roomsDaemon ? `, daemon ${roomsDaemon.endpoint}` : ''})`, async () => {
|
|
1021
|
-
atomicWriteConfig(cfgPath, roomsPlan.text);
|
|
1022
|
-
return { ok: true };
|
|
1023
|
-
});
|
|
1024
|
-
if (roomsDaemon) line(ok(`Rooms configured to use ${roomsDaemonLabel} (${roomsDaemon.endpoint}, state ${roomsDaemon.stateDir}).`));
|
|
1025
|
-
} else {
|
|
1026
|
-
line(ok(`Rooms is already configured for this deployment (console port ${roomsPort}) — no change.`));
|
|
1027
|
-
}
|
|
1028
|
-
const svc = await act('ours-cowork install-service (starts on boot)', async () => run('ours-cowork', ['install-service']));
|
|
1029
|
-
if (svc.ok) {
|
|
1030
|
-
line(ok(`Rooms ready — console at ${c.cyan(`http://127.0.0.1:${roomsPort}/`)}, sharing your broker. No problems.`));
|
|
1031
|
-
} else {
|
|
1032
|
-
line(warn(`Rooms installed, but its service didn't start — retry '${c.cyan('ours-cowork install-service')}'.`));
|
|
1033
|
-
line(info(`You can also run it in the foreground: '${c.cyan('ours-cowork web')}'.`));
|
|
1034
|
-
}
|
|
1035
|
-
record({
|
|
1036
|
-
key: 'rooms',
|
|
1037
|
-
label: 'Rooms (ours-cowork)',
|
|
1038
|
-
state: svc.ok ? 'installed' : 'failed',
|
|
1039
|
-
version: coworkVersion,
|
|
1040
|
-
note: svc.ok ? `console ${roomsPort} · ${roomsDaemonLabel}` : 'ours-cowork install-service failed',
|
|
1041
|
-
});
|
|
1042
|
-
} else {
|
|
1043
|
-
line(info('skipped cleanly — re-run ours-install any time to add it.'));
|
|
1044
|
-
record({ key: 'rooms', label: 'Rooms (ours-cowork)', state: 'skipped' });
|
|
1045
|
-
}
|
|
1046
|
-
cont(goRooms);
|
|
1047
|
-
|
|
1048
|
-
// Last guard on the whole topology: no two daemons in this install may share a port.
|
|
1049
|
-
// Each answer was validated as it was given, but only the finished plan proves the set.
|
|
1050
|
-
const portPlan = planPorts(topology);
|
|
1051
|
-
if (!portPlan.ok) {
|
|
1052
|
-
for (const d of portPlan.duplicates) {
|
|
1053
|
-
line(warn(`port ${d.port} ended up claimed by both ${d.labels[0]} and ${d.labels[1]} — one of them will fail to bind.`));
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
806
|
return endScreen({ ttyFd, summary, chosenPort, chosenBroker });
|
|
1058
807
|
}
|
|
1059
808
|
|
|
1060
809
|
// --- never-dead-end messaging (owner edit #3) --------------------------------------------------
|
|
1061
|
-
function manualClaude(h) {
|
|
810
|
+
function manualClaude(h, exact) {
|
|
1062
811
|
if (h.status === 'alias') {
|
|
1063
812
|
line(warn('Heads-up: on your machine, "claude" is installed as an alias, not the real command,'));
|
|
1064
813
|
line(info('so I can\'t drive it safely. To fix it: run ' + c.cyan('type claude') + ' , remove/rename that'));
|
|
@@ -1067,17 +816,17 @@ function manualClaude(h) {
|
|
|
1067
816
|
line(warn('I couldn\'t safely drive the "claude" command on this machine.'));
|
|
1068
817
|
}
|
|
1069
818
|
line(info('You can still install the plugin yourself — inside Claude Code, run these two:'));
|
|
1070
|
-
line(' ' + c.cyan(`/plugin marketplace add ${
|
|
1071
|
-
line(' ' + c.cyan('/plugin install ours'));
|
|
819
|
+
line(' ' + c.cyan(`/plugin marketplace add ${exact.root}`));
|
|
820
|
+
line(' ' + c.cyan('/plugin install ours@ours.network'));
|
|
1072
821
|
}
|
|
1073
|
-
function failClaude() {
|
|
822
|
+
function failClaude(exact) {
|
|
1074
823
|
line(warn('Couldn\'t install the Claude Code plugin automatically (network or plugin cache).'));
|
|
1075
824
|
line(info('Install it by hand — inside Claude Code, run these two, then re-run ours-install:'));
|
|
1076
|
-
line(' ' + c.cyan(`/plugin marketplace add ${
|
|
1077
|
-
line(' ' + c.cyan('/plugin install ours'));
|
|
825
|
+
line(' ' + c.cyan(`/plugin marketplace add ${exact.root}`));
|
|
826
|
+
line(' ' + c.cyan('/plugin install ours@ours.network'));
|
|
1078
827
|
line(info('Your daemon and other steps are intact. Continuing.'));
|
|
1079
828
|
}
|
|
1080
|
-
function manualCodex(h) {
|
|
829
|
+
function manualCodex(h, exact) {
|
|
1081
830
|
if (h.status === 'alias') {
|
|
1082
831
|
line(warn('Heads-up: on your machine, "codex" is installed as an alias, not the real command,'));
|
|
1083
832
|
line(info('so I can\'t drive it safely. To fix it: run ' + c.cyan('type codex') + ' , remove/rename that'));
|
|
@@ -1085,17 +834,19 @@ function manualCodex(h) {
|
|
|
1085
834
|
} else {
|
|
1086
835
|
line(warn('I couldn\'t safely drive the "codex" command on this machine.'));
|
|
1087
836
|
}
|
|
1088
|
-
line(info('You can still install it yourself — run these
|
|
1089
|
-
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}`));
|
|
1090
840
|
line(' ' + c.cyan('codex plugin add ours@ours-codex-marketplace'));
|
|
1091
|
-
line(' ' + c.cyan(
|
|
841
|
+
line(' ' + c.cyan(`npm i -g ${exact.packageName}@${exact.version}`) + c.gray(' (adds the ours-codex live launcher)'));
|
|
1092
842
|
}
|
|
1093
|
-
function failCodex() {
|
|
843
|
+
function failCodex(exact) {
|
|
1094
844
|
line(warn('Couldn\'t install the Codex plugin automatically (network or plugin cache).'));
|
|
1095
|
-
line(info('Install it by hand — run these
|
|
1096
|
-
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}`));
|
|
1097
848
|
line(' ' + c.cyan('codex plugin add ours@ours-codex-marketplace'));
|
|
1098
|
-
line(' ' + c.cyan(
|
|
849
|
+
line(' ' + c.cyan(`npm i -g ${exact.packageName}@${exact.version}`));
|
|
1099
850
|
line(info('Your daemon and other steps are intact. Continuing.'));
|
|
1100
851
|
}
|
|
1101
852
|
function failHermes() {
|
|
@@ -1132,9 +883,7 @@ function endScreen({ ttyFd, summary, chosenPort, chosenBroker }) {
|
|
|
1132
883
|
const has = (k) => summary.some((r) => r.key === k && (r.state === 'installed' || r.state === 'current'));
|
|
1133
884
|
if (has('core')) {
|
|
1134
885
|
const identityDone = has('identity');
|
|
1135
|
-
const { text, empty } = buildHandoffPrompt({
|
|
1136
|
-
identity: !identityDone, fleet: has('fleet'), telegram: has('telegram'), rooms: has('rooms'),
|
|
1137
|
-
});
|
|
886
|
+
const { text, empty } = buildHandoffPrompt({ identity: !identityDone, fleet: has('fleet'), telegram: has('telegram') });
|
|
1138
887
|
if (empty) {
|
|
1139
888
|
// Nothing left to finish (identity created in-install, no fleet/Telegram). Don't show an empty box.
|
|
1140
889
|
line('');
|