@ours.network/install 1.2.0-nightly.2 → 1.2.1-nightly.10
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 +151 -131
- package/assets/Dockerfile +6 -3
- package/assets/docker-compose.yaml +21 -1
- package/assets/release-lock.json +7161 -0
- package/assets/release.json +57 -0
- package/assets/scripts/build/build-common.mjs +1 -1
- package/assets/scripts/build/build-sdk.mjs +6 -1
- package/assets/scripts/build/record-build.mjs +2 -0
- package/assets/scripts/maintenance/build-context.mjs +1 -1
- package/assets/scripts/maintenance/daemon-owner.mjs +15 -0
- package/assets/scripts/maintenance/docker-layout-conversion.mjs +3 -1
- package/assets/scripts/maintenance/release-graph.mjs +111 -0
- package/assets/scripts/maintenance/state-operation.mjs +4 -2
- package/assets/scripts/runtime/client-setup.mjs +3 -3
- package/assets/scripts/runtime/entrypoint.sh +1 -1
- package/assets/scripts/runtime/legacy-import.mjs +103 -0
- package/assets/scripts/runtime/runtime-common.mjs +2 -0
- package/assets/sources.json +96 -16
- package/install.mjs +2 -2
- package/install.sh +1 -1
- package/lib/build-transition.mjs +13 -7
- package/lib/client-cli.mjs +117 -0
- package/lib/docker-conversion-runtime.mjs +1 -0
- package/lib/docker-runtime-repair.mjs +125 -0
- package/lib/effects.mjs +163 -85
- package/lib/fleet-settings.mjs +43 -0
- package/lib/legacy-migration.mjs +202 -0
- package/lib/legacy-state.mjs +205 -0
- package/lib/managed-cli.mjs +164 -0
- package/lib/orchestrate.mjs +162 -58
- package/lib/plan.mjs +13 -7
- package/lib/prompt.mjs +113 -119
- package/lib/server-onboarding.mjs +114 -0
- package/lib/setup-options.mjs +253 -0
- package/lib/setup.mjs +154 -0
- package/lib/target.mjs +4 -4
- package/lib/uninstall.mjs +2 -2
- package/lib/usage.mjs +49 -44
- package/package.json +5 -4
package/lib/orchestrate.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { executeLegacyMigration } from './legacy-migration.mjs';
|
|
1
2
|
// ours-install v3 — the orchestrator.
|
|
2
3
|
//
|
|
3
4
|
// This is the part that cannot be pure: it walks the flow, renders the screens
|
|
@@ -25,7 +26,7 @@
|
|
|
25
26
|
|
|
26
27
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
27
28
|
import { parseNetworkArgs, validateHostProfile, parseInstallArgs, resolveTarget, resolveProfileSelection, profileEnv, InstallUsageError } from './target.mjs';
|
|
28
|
-
import { selectSourcePackages, validateInstallation, SERVER_SERVICES, planDaemonConfig, planServiceInstall, serviceInstallCommand } from './plan.mjs';
|
|
29
|
+
import { clientPackageNames, selectSourcePackages, validateInstallation, SERVER_SERVICES, planDaemonConfig, planServiceInstall, serviceInstallCommand } from './plan.mjs';
|
|
29
30
|
import {
|
|
30
31
|
COMPONENTS,
|
|
31
32
|
planComponentSelection, planMcpAttachment, planTgAttachment, planCoworkAttachment,
|
|
@@ -85,6 +86,41 @@ async function perform(effects, dryRun, label, thunk) {
|
|
|
85
86
|
|
|
86
87
|
const reason = (error) => (error instanceof Error ? error.message : String(error));
|
|
87
88
|
|
|
89
|
+
function clientDiagnostic(error) {
|
|
90
|
+
return reason(error)
|
|
91
|
+
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '')
|
|
92
|
+
.replace(/\bBearer\s+[^\s,;]+/gi, 'Bearer [redacted]')
|
|
93
|
+
.replace(/((?:api[_-]?key|api[_-]?token|access[_-]?token|refresh[_-]?token|token|password|secret|authorization|credential)\s*["']?\s*[:=]\s*["']?)[^\s"',;]+/gi, '$1[redacted]')
|
|
94
|
+
.replace(/(https?:\/\/)[^\s/@]+:[^\s/@]+@/gi, '$1[redacted]@')
|
|
95
|
+
.replace(/[\x00-\x1f\x7f]/g, ' ').slice(0, 700);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function clientRetry(effects, imported, integrations) {
|
|
99
|
+
effects.out(info(`Saved server connection: ${imported.configPath}.`));
|
|
100
|
+
if (integrations.includes('fleet') && !imported.settings.fleetSettingsPath) {
|
|
101
|
+
effects.out(info('Saved profile and settings retained. Run ours-install, choose Connect to an existing server, and reuse the saved profile to finish the interactive Fleet configuration.'));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const args = ['client', 'install', '--config', imported.configPath, '--integrations', integrations.join(',') || 'none', '--sources', imported.settings.sourcesPath];
|
|
105
|
+
if (imported.settings.fleetSettingsPath) args.push('--fleet-settings', imported.settings.fleetSettingsPath);
|
|
106
|
+
const command = `ours-install client install ${args.slice(2).map((value, i) => i % 2 === 0 ? value : shellQuote(value)).join(' ')}`;
|
|
107
|
+
effects.out(info(`Saved profile and settings retained; re-run ${command}`));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function explainClientFailure(effects, name, row) {
|
|
111
|
+
const label = { codex: 'Codex', 'claude-code': 'Claude Code', fleet: 'Fleet' }[name];
|
|
112
|
+
const command = name === 'claude-code' ? 'claude' : name;
|
|
113
|
+
if (row?.note === 'not installed') {
|
|
114
|
+
effects.out(warn(`${label}: executable not found on PATH. Install ${label} or make its executable available in this shell, then check ${command} --version before retrying.`));
|
|
115
|
+
} else if (row?.manual) {
|
|
116
|
+
effects.out(warn(`${label}: cannot register the plugin automatically — ${clientDiagnostic(row.note)}. Use the real executable (check ${command} --version), or register the displayed local marketplace manually.`));
|
|
117
|
+
} else {
|
|
118
|
+
effects.out(warn(`${label}: ${row?.failedStep ?? 'integration setup'} failed. ${clientDiagnostic(row?.detail ?? row?.note ?? 'The integration did not report successful completion. Review the messages above.')} Fix the reported command error before retrying.`));
|
|
119
|
+
if (row?.failedCommand) effects.out(info(`Failed command: ${row.failedCommand.map(shellQuote).join(' ')}`));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
88
124
|
function semverMajor(version) {
|
|
89
125
|
const match = /^(?:[~^<>= ]*)(\d+)\./.exec(String(version ?? '').trim());
|
|
90
126
|
return match ? Number(match[1]) : null;
|
|
@@ -109,6 +145,7 @@ function incompatibleUpgrade(target, cliDependencies) {
|
|
|
109
145
|
async function prepareIncompatibleUpgrade(args, effects, target, cliPkg, mismatch) {
|
|
110
146
|
const dir = target.stateDir;
|
|
111
147
|
const configPath = join(dir, 'config.json');
|
|
148
|
+
const owner = effects.readJson(join(dir, 'ours-cli-daemon.json'))?.owner === '@ours.network/daemon' ? ['ours-daemon'] : ['ours', 'daemon'];
|
|
112
149
|
if (mismatch.unknown) {
|
|
113
150
|
effects.out(warn(`ours: cannot verify whether ${cliPkg} can restore daemon v${mismatch.runningVersion}. Nothing was changed.`));
|
|
114
151
|
effects.out(info('Check npm registry access and re-run; compatibility checks fail closed.'));
|
|
@@ -152,13 +189,13 @@ async function prepareIncompatibleUpgrade(args, effects, target, cliPkg, mismatc
|
|
|
152
189
|
}
|
|
153
190
|
|
|
154
191
|
await perform(effects, false, 'stop the incompatible daemon', () => effects.run(
|
|
155
|
-
|
|
192
|
+
owner[0], [...owner.slice(1), 'stop', '--state-dir', dir, '--config', configPath], { stream: true },
|
|
156
193
|
));
|
|
157
194
|
try {
|
|
158
195
|
await perform(effects, false, `back up complete daemon state to ${backupPath}`, () => effects.copyDir(dir, backupPath));
|
|
159
196
|
} catch (error) {
|
|
160
197
|
try {
|
|
161
|
-
await effects.run(
|
|
198
|
+
await effects.run(owner[0], [...owner.slice(1), 'start', '--state-dir', dir, '--config', configPath], { stream: true });
|
|
162
199
|
effects.out(ok('backup failed, but the old daemon was started again'));
|
|
163
200
|
} catch {
|
|
164
201
|
effects.out(warn(`backup failed and the old daemon did not restart; its state is still untouched at ${dir}`));
|
|
@@ -167,11 +204,11 @@ async function prepareIncompatibleUpgrade(args, effects, target, cliPkg, mismatc
|
|
|
167
204
|
}
|
|
168
205
|
try {
|
|
169
206
|
await perform(effects, false, 'remove the incompatible daemon boot service', () => effects.run(
|
|
170
|
-
|
|
207
|
+
owner[0], [...owner.slice(1), 'uninstall-service', '--yes', '--state-dir', dir, '--config', configPath], { stream: true },
|
|
171
208
|
));
|
|
172
209
|
} catch (error) {
|
|
173
210
|
try {
|
|
174
|
-
await effects.run(
|
|
211
|
+
await effects.run(owner[0], [...owner.slice(1), 'start', '--state-dir', dir, '--config', configPath], { stream: true });
|
|
175
212
|
effects.out(ok(`service removal failed, but the old daemon was started again; backup retained at ${backupPath}`));
|
|
176
213
|
} catch {
|
|
177
214
|
effects.out(warn(`service removal failed and the old daemon did not restart; state remains at ${dir} and the backup is at ${backupPath}`));
|
|
@@ -192,11 +229,11 @@ async function prepareIncompatibleUpgrade(args, effects, target, cliPkg, mismatc
|
|
|
192
229
|
* rather than one harness. So a failure here is one honest line plus whatever
|
|
193
230
|
* the caller wants to say about retrying, and the walk continues.
|
|
194
231
|
*/
|
|
195
|
-
async function attempt(effects, dryRun, label, thunk) {
|
|
232
|
+
async function attempt(effects, dryRun, label, thunk, formatReason = reason) {
|
|
196
233
|
try {
|
|
197
234
|
return { ok: true, ...(await perform(effects, dryRun, label, thunk)) };
|
|
198
235
|
} catch (error) {
|
|
199
|
-
effects.out(warn(`${label} — did not complete: ${
|
|
236
|
+
effects.out(warn(`${label} — did not complete: ${formatReason(error)}`));
|
|
200
237
|
return { ok: false, error };
|
|
201
238
|
}
|
|
202
239
|
}
|
|
@@ -332,14 +369,13 @@ export async function runDaemonPhase(args, effects, exactSuite = null) {
|
|
|
332
369
|
const mcpPkg = exactSuite?.packages?.mcp
|
|
333
370
|
? `@ours.network/mcp@${exactSuite.packages.mcp}`
|
|
334
371
|
: componentSpec(componentByKey('mcp'), args.channel);
|
|
335
|
-
//
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
const cliPkg = '@ours.network/cli';
|
|
372
|
+
// Inspect the runtime package SDK compatibility before replacing the owner.
|
|
373
|
+
const cliPkg = args.channel === 'nightly' ? '@ours.network/cli@nightly' : '@ours.network/cli';
|
|
374
|
+
const daemonPkg = args.channel === 'nightly' ? '@ours.network/daemon@nightly' : '@ours.network/daemon';
|
|
339
375
|
if (!creating && target.daemonVersion) {
|
|
340
|
-
const mismatch = incompatibleUpgrade(target, effects.packageDependencies(
|
|
376
|
+
const mismatch = incompatibleUpgrade(target, effects.packageDependencies(daemonPkg));
|
|
341
377
|
if (mismatch) {
|
|
342
|
-
const prepared = await prepareIncompatibleUpgrade(args, effects, target,
|
|
378
|
+
const prepared = await prepareIncompatibleUpgrade(args, effects, target, daemonPkg, mismatch);
|
|
343
379
|
if (prepared.refused) return { target, refused: prepared.refused, steps };
|
|
344
380
|
creating = prepared.purged === true;
|
|
345
381
|
target.backupPath = prepared.backupPath;
|
|
@@ -348,6 +384,18 @@ export async function runDaemonPhase(args, effects, exactSuite = null) {
|
|
|
348
384
|
}
|
|
349
385
|
await perform(effects, args.dryRun, `MCP server installed (npm i -g ${mcpPkg})`, () => effects.run('npm', ['i', '-g', mcpPkg]));
|
|
350
386
|
steps.push({ id: 'mcp-package', changed: true, packageRefresh: true });
|
|
387
|
+
await perform(effects, args.dryRun, 'daemon runtime installed', () => effects.run('npm', ['i', '-g', daemonPkg]));
|
|
388
|
+
// A retained global unit still points into the old CLI package. Rewrite and
|
|
389
|
+
// activate that owned unit before npm replaces its executable with thin CLI.
|
|
390
|
+
let migratedService;
|
|
391
|
+
if (!creating) {
|
|
392
|
+
const retained = planServiceInstall({ stateDir: dir, home: effects.home, readText: effects.readText, platform: effects.platform?.platform });
|
|
393
|
+
if (retained.unitPath && effects.readText(retained.unitPath) !== null) {
|
|
394
|
+
try { migratedService = await runServicePhase(args, effects, dir, target.port); }
|
|
395
|
+
catch (error) { await recoverDaemon(args, effects, dir, join(dir, 'config.json'), target.port); throw error; }
|
|
396
|
+
if (migratedService.refused) return { target, refused: migratedService.refused, steps };
|
|
397
|
+
}
|
|
398
|
+
}
|
|
351
399
|
await perform(effects, args.dryRun, `ours CLI installed (npm i -g ${cliPkg})`, () => effects.run('npm', ['i', '-g', cliPkg]));
|
|
352
400
|
steps.push({ id: 'cli', changed: true, packageRefresh: true });
|
|
353
401
|
|
|
@@ -382,14 +430,14 @@ export async function runDaemonPhase(args, effects, exactSuite = null) {
|
|
|
382
430
|
|
|
383
431
|
try {
|
|
384
432
|
if (creating) {
|
|
385
|
-
await perform(effects, args.dryRun, `start the daemon on port ${target.port}`, () => effects.run('ours', [
|
|
433
|
+
await perform(effects, args.dryRun, `start the daemon on port ${target.port}`, () => effects.run('ours-daemon', [ 'start', '--config', configPath], { stream: true }));
|
|
386
434
|
steps.push({ id: 'start', changed: true });
|
|
387
435
|
} else {
|
|
388
|
-
await perform(effects, args.dryRun, `restart the daemon on port ${target.port}`, () => effects.run('ours', [
|
|
436
|
+
await perform(effects, args.dryRun, `restart the daemon on port ${target.port}`, () => effects.run('ours-daemon', [ 'restart', '--config', configPath], { stream: true }));
|
|
389
437
|
steps.push({ id: 'restart', changed: true });
|
|
390
438
|
}
|
|
391
439
|
|
|
392
|
-
const service = await runServicePhase(args, effects, dir, target.port);
|
|
440
|
+
const service = migratedService ?? await runServicePhase(args, effects, dir, target.port);
|
|
393
441
|
if (service.unsupported) serviceUnsupported = service.unsupported;
|
|
394
442
|
if (service.refused) {
|
|
395
443
|
// A REFUSAL IS A FAILURE TO REACH THE STATE, not a special case. An unknown
|
|
@@ -419,7 +467,7 @@ export async function runDaemonPhase(args, effects, exactSuite = null) {
|
|
|
419
467
|
effects.out(recovery.recovered
|
|
420
468
|
? ok('your daemon is running again — nothing was committed, and the service is unchanged')
|
|
421
469
|
: warn('and the daemon did NOT come back up — start it yourself before anything else: '
|
|
422
|
-
+ `ours
|
|
470
|
+
+ `ours-daemon start --config ${configPath}`));
|
|
423
471
|
}
|
|
424
472
|
throw error;
|
|
425
473
|
}
|
|
@@ -442,7 +490,7 @@ export async function runDaemonPhase(args, effects, exactSuite = null) {
|
|
|
442
490
|
async function recoverDaemon(args, effects, dir, configPath, port) {
|
|
443
491
|
if (args.dryRun) return null;
|
|
444
492
|
try {
|
|
445
|
-
await effects.run('ours', [
|
|
493
|
+
await effects.run('ours-daemon', [ 'start', '--config', configPath]);
|
|
446
494
|
return { recovered: true };
|
|
447
495
|
} catch (recoveryError) {
|
|
448
496
|
return { recovered: false, reason: reason(recoveryError) };
|
|
@@ -793,7 +841,7 @@ export async function runIdentityPhase(args, effects, { target, mcpReady }) {
|
|
|
793
841
|
}
|
|
794
842
|
if (/not running|not reachable|ECONNREFUSED|connect/i.test(text)) {
|
|
795
843
|
effects.out(warn("The daemon isn't reachable yet — couldn't create your human identity."));
|
|
796
|
-
effects.out(info(`Fix: run 'ours
|
|
844
|
+
effects.out(info(`Fix: run 'ours-daemon start --config ${env.OURS_CONFIG}', then 'ours identity create-root --config ${env.OURS_CONFIG} --name "${name}"'.`));
|
|
797
845
|
return { key: 'identity', label: 'Human identity', state: 'failed', note: 'daemon not reachable' };
|
|
798
846
|
}
|
|
799
847
|
effects.out(warn(`Couldn't create your human identity: ${text.split('\n')[0]}`));
|
|
@@ -816,6 +864,9 @@ export async function runIdentityPhase(args, effects, { target, mcpReady }) {
|
|
|
816
864
|
export async function runHarnessPhase(args, effects, { target, isDefaultStateDir, exactSuite = null }) {
|
|
817
865
|
effects.out(heading('Harness plugins'));
|
|
818
866
|
const detected = (await effects.detectHarnesses()).filter(h => !args.clientIntegrations || args.clientIntegrations.includes(h.name));
|
|
867
|
+
for (const name of args.clientIntegrations ?? []) {
|
|
868
|
+
if (name !== 'fleet' && !detected.some(h => h.name === name)) detected.push({ name, status: 'absent' });
|
|
869
|
+
}
|
|
819
870
|
for (const h of detected) {
|
|
820
871
|
if (h.status === 'ok') effects.out(ok(`'${h.command ?? h.name}' → ${h.detail ?? 'real program'} (its plugin can be installed)`));
|
|
821
872
|
else if (h.status === 'alias') effects.out(warn(`'${h.command ?? h.name}' → ${h.detail} (I won't call it — manual steps below)`));
|
|
@@ -825,7 +876,7 @@ export async function runHarnessPhase(args, effects, { target, isDefaultStateDir
|
|
|
825
876
|
if (detected.every((h) => h.status === 'absent')) {
|
|
826
877
|
effects.out(info('No Claude Code, Codex or Hermes found — install one and re-run to wire it up.'));
|
|
827
878
|
effects.out(info('Your daemon is unaffected; nothing else in this run depends on a harness.'));
|
|
828
|
-
return [];
|
|
879
|
+
return args.clientIntegrations ? detected.map(h => ({ key: h.name, state: 'skipped', note: 'not installed' })) : [];
|
|
829
880
|
}
|
|
830
881
|
|
|
831
882
|
const plans = planHarnessPlugins({
|
|
@@ -850,7 +901,7 @@ export async function runHarnessPhase(args, effects, { target, isDefaultStateDir
|
|
|
850
901
|
if (plan.action === 'manual' && exactSuite?.localPackages?.[plan.name]) {
|
|
851
902
|
const root = await effects.prepareClientMarketplace(plan.name, exactSuite.localPackages[plan.name]);
|
|
852
903
|
effects.out(warn(`${plan.label} requires manual registration of exact local marketplace ${root}; ${target.managed ? 'the saved client default is retained' : `keep OURS_CONFIG=${target.configPath}`}.`));
|
|
853
|
-
rows.push({ ...row, state: 'failed', note:
|
|
904
|
+
rows.push({ ...row, state: 'failed', note: plan.reason, manual: true });
|
|
854
905
|
continue;
|
|
855
906
|
}
|
|
856
907
|
if (plan.action === 'manual') {
|
|
@@ -872,7 +923,7 @@ export async function runHarnessPhase(args, effects, { target, isDefaultStateDir
|
|
|
872
923
|
effects.out(warn(`${plan.label} — ${plan.reason}; install it yourself with:`));
|
|
873
924
|
for (const step of manual) effects.out(info(` ${step}`));
|
|
874
925
|
if (plan.envLine) effects.out(info(`Before later native launches, set: ${plan.envLine}`));
|
|
875
|
-
rows.push({ ...row, state: 'skipped', note: plan.reason });
|
|
926
|
+
rows.push({ ...row, state: 'skipped', note: plan.reason, manual: true });
|
|
876
927
|
continue;
|
|
877
928
|
}
|
|
878
929
|
|
|
@@ -883,13 +934,16 @@ export async function runHarnessPhase(args, effects, { target, isDefaultStateDir
|
|
|
883
934
|
const steps = plan.name === 'codex'
|
|
884
935
|
? [['codex', 'plugin', 'marketplace', 'add', registration], ['codex', 'plugin', 'add', 'ours@ours-codex-marketplace']]
|
|
885
936
|
: [['claude', 'plugin', 'marketplace', 'add', registration], ['claude', 'plugin', await effects.hasClaudePlugin() ? 'update' : 'install', 'ours@ours.network']];
|
|
886
|
-
let
|
|
937
|
+
let failure = null;
|
|
887
938
|
for (const step of steps) {
|
|
888
|
-
const outcome = await attempt(effects, false, step.join(' '), () => effects.run(step[0], step.slice(1), { env: profileEnv(target) }));
|
|
889
|
-
if (!outcome.ok) {
|
|
939
|
+
const outcome = await attempt(effects, false, step.join(' '), () => effects.run(step[0], step.slice(1), { env: profileEnv(target) }), clientDiagnostic);
|
|
940
|
+
if (!outcome.ok) {
|
|
941
|
+
failure = { failedCommand: step, failedStep: step[2] === 'marketplace' ? `Register ${plan.label} marketplace` : `Install ${plan.label} plugin`, detail: clientDiagnostic(outcome.error) };
|
|
942
|
+
break;
|
|
943
|
+
}
|
|
890
944
|
}
|
|
891
945
|
effects.out(info(target.managed ? `Native ${plan.name} launches use the saved client default.` : `Native ${plan.name} launches must retain OURS_CONFIG=${target.configPath}.`));
|
|
892
|
-
rows.push({ ...row, state:
|
|
946
|
+
rows.push({ ...row, state: failure ? 'failed' : 'installed', ...(failure ?? {}) });
|
|
893
947
|
continue;
|
|
894
948
|
}
|
|
895
949
|
|
|
@@ -1250,7 +1304,9 @@ export async function runInstall(argv, effects) {
|
|
|
1250
1304
|
|
|
1251
1305
|
export async function runServerCommand(args, effects) {
|
|
1252
1306
|
if (args.operation === 'status') return executeServerCommand(args, effects);
|
|
1253
|
-
return effects.withInstallationLock(args.stateDir, () =>
|
|
1307
|
+
return effects.withInstallationLock(args.stateDir, () => args.migrateFrom
|
|
1308
|
+
? executeLegacyMigration(args, effects, executeServerCommand)
|
|
1309
|
+
: executeServerCommand(args, effects));
|
|
1254
1310
|
}
|
|
1255
1311
|
|
|
1256
1312
|
async function executeServerCommand(args, effects) {
|
|
@@ -1266,6 +1322,7 @@ async function executeServerCommand(args, effects) {
|
|
|
1266
1322
|
} else {
|
|
1267
1323
|
if (args.operation !== 'install' || !args.mode) throw new Error('First server install requires --mode');
|
|
1268
1324
|
record = effects.newInstallation(args.stateDir, args.mode);
|
|
1325
|
+
for (const key of ['port', 'coworkPort', 'messengerPort']) if (args[key] !== undefined) record[key] = args[key];
|
|
1269
1326
|
}
|
|
1270
1327
|
if (record.layoutConversion && !['install', 'start', 'stop', 'status'].includes(args.operation)) {
|
|
1271
1328
|
throw new Error('Layout conversion is incomplete; resume with server install or server start before changing state or authority');
|
|
@@ -1273,14 +1330,32 @@ async function executeServerCommand(args, effects) {
|
|
|
1273
1330
|
if (record.buildTransition && !['stop', 'status', record.buildTransition.operation].includes(args.operation)) {
|
|
1274
1331
|
throw new Error(`Server build activation is incomplete; repeat server ${record.buildTransition.operation} before other mutations`);
|
|
1275
1332
|
}
|
|
1333
|
+
const showInstallProgress = args.operation === 'install' && !(existing && (record.schema === 1 || record.layoutConversion));
|
|
1334
|
+
const installStageCount = (existing ? 7 : 9) + (args.identityName ? 2 : 0);
|
|
1335
|
+
let completedInstallStages = 0;
|
|
1336
|
+
const installStage = async (label, explanation, action) => {
|
|
1337
|
+
if (!showInstallProgress) return action();
|
|
1338
|
+
effects.out(progress(completedInstallStages, installStageCount, label, explanation));
|
|
1339
|
+
try {
|
|
1340
|
+
const result = await action();
|
|
1341
|
+
completedInstallStages += 1;
|
|
1342
|
+
effects.out(ok(`${label} complete`));
|
|
1343
|
+
return result;
|
|
1344
|
+
} catch (error) {
|
|
1345
|
+
effects.out(warn(`Server installation stopped during ${label.toLowerCase()}.`));
|
|
1346
|
+
throw error;
|
|
1347
|
+
}
|
|
1348
|
+
};
|
|
1276
1349
|
if ((!existing || args.operation === 'update') && !record.buildTransition) {
|
|
1277
|
-
|
|
1278
|
-
|
|
1350
|
+
args.resolvedSources = await installStage('Package selection', 'Resolve the selected server packages before installation.', async () => {
|
|
1351
|
+
const policy = args.sourcePolicy ?? (args.sources ? effects.readJson(args.sources) : effects.packagedSourcePolicy());
|
|
1352
|
+
return effects.resolveSourcePolicy(policy, 'server');
|
|
1353
|
+
});
|
|
1279
1354
|
}
|
|
1280
1355
|
if (!existing && args.sources) record.sourcePolicyHash = effects.sourcePolicyHash(args.sources);
|
|
1281
|
-
await effects.serverPreflight(record, args.operation, {
|
|
1282
|
-
existing, sourcePath: args.sources ?? record.sourcesPath, sourceManifest: args.resolvedSources,
|
|
1283
|
-
});
|
|
1356
|
+
await installStage('Prerequisite checks', record.mode === 'docker' ? 'Check Docker Engine and Docker Compose.' : 'Check native tools and the user service manager.', () => effects.serverPreflight(record, args.operation, {
|
|
1357
|
+
existing, sourcePath: args.sources ?? record.sourcesPath, sourceManifest: args.resolvedSources, identityName: args.identityName,
|
|
1358
|
+
}));
|
|
1284
1359
|
if (existing && (record.schema === 1 || record.layoutConversion)
|
|
1285
1360
|
&& ['install', 'start', 'restart', 'update', 'rebuild'].includes(args.operation)) {
|
|
1286
1361
|
record = record.mode === 'docker'
|
|
@@ -1295,16 +1370,27 @@ async function executeServerCommand(args, effects) {
|
|
|
1295
1370
|
await effects.stopPendingConversion(record);
|
|
1296
1371
|
} else if (args.operation === 'install') {
|
|
1297
1372
|
if (!existing) {
|
|
1298
|
-
await
|
|
1299
|
-
|
|
1373
|
+
await installStage('Installation setup', 'Save the selected packages and installation settings.', async () => {
|
|
1374
|
+
await effects.initializeSelection(record, args.resolvedSources);
|
|
1375
|
+
effects.writeJson(recordPath, JSON.stringify(record, null, 2) + '\n');
|
|
1376
|
+
});
|
|
1300
1377
|
}
|
|
1301
|
-
await effects.prepareInstallation(record);
|
|
1378
|
+
await installStage('Runtime preparation', record.mode === 'docker' ? 'Prepare the Docker runtime. Downloads and builds may take several minutes.' : 'Download and prepare the native runtime packages. This may take several minutes.', () => effects.prepareInstallation(record));
|
|
1302
1379
|
// A repeated setup repairs delivery with the retained master and exact packages.
|
|
1303
|
-
await effects.serverLifecycle(record, 'stop');
|
|
1304
|
-
await effects.serverAccess(record, 'access-init', { migrate: !!args.migrate });
|
|
1305
|
-
await effects.serverAccess(record, 'access-issue');
|
|
1306
|
-
await effects.recordInstallationBuild(record);
|
|
1307
|
-
|
|
1380
|
+
await installStage('Service shutdown', 'Stop managed services before configuring access.', () => effects.serverLifecycle(record, 'stop'));
|
|
1381
|
+
await installStage('Credential initialization', 'Initialize or retain the installation credentials.', () => effects.serverAccess(record, 'access-init', { migrate: !!args.migrate }));
|
|
1382
|
+
await installStage('Credential delivery', 'Prepare access for the selected services.', () => effects.serverAccess(record, 'access-issue'));
|
|
1383
|
+
await installStage('Build verification', 'Record the installed runtime and selected package versions.', () => effects.recordInstallationBuild(record));
|
|
1384
|
+
if (args.identityName) {
|
|
1385
|
+
await installStage('Daemon startup', 'Start the daemon and restore its retained identities.', () => effects.serverLifecycle(record, 'start', ['daemon']));
|
|
1386
|
+
const identity = await installStage('Human identity', 'Keep the existing Human identity, or create it on a fresh daemon.', () => effects.serverEnsureIdentity(record, args.identityName));
|
|
1387
|
+
record.messengerIdentity = identity.name;
|
|
1388
|
+
effects.writeJson(recordPath, JSON.stringify(record, null, 2) + '\n');
|
|
1389
|
+
await installStage('Application startup', 'Start the selected applications and check readiness.', () => effects.serverLifecycle(record, 'start', record.services.filter(name => name !== 'daemon')));
|
|
1390
|
+
} else {
|
|
1391
|
+
await installStage('Service startup', 'Start the daemon and selected services, then check readiness.', () => effects.serverLifecycle(record, 'start'));
|
|
1392
|
+
}
|
|
1393
|
+
if (showInstallProgress) effects.out(progress(installStageCount, installStageCount, 'Installation complete', 'The selected services are ready.'));
|
|
1308
1394
|
} else if (['backup', 'restore', 'reset'].includes(args.operation)) {
|
|
1309
1395
|
await effects.serverMaintenance(record, args);
|
|
1310
1396
|
} else if (['update', 'rebuild'].includes(args.operation)) {
|
|
@@ -1336,24 +1422,25 @@ async function executeServerCommand(args, effects) {
|
|
|
1336
1422
|
layoutConversion: record.schema === 1 ? 'preparation-pending' : 'activation-pending',
|
|
1337
1423
|
} : {}),
|
|
1338
1424
|
}));
|
|
1425
|
+
if (args.operation === 'status') return EXIT_OK;
|
|
1339
1426
|
}
|
|
1340
1427
|
effects.out(ok(`Server ${args.operation} completed for ${record.root}`));
|
|
1341
1428
|
return EXIT_OK;
|
|
1342
1429
|
}
|
|
1343
1430
|
|
|
1344
|
-
async function runClientCommand(command, effects) {
|
|
1431
|
+
export async function runClientCommand(command, effects) {
|
|
1345
1432
|
const managedPath = join(effects.home, '.ours-client', 'profile.json');
|
|
1346
1433
|
const saved = effects.readManagedClientProfile();
|
|
1347
1434
|
let configPath = command.config;
|
|
1348
1435
|
if (!configPath && saved) {
|
|
1349
|
-
if (effects.interactive && !await effects.ask(`Reuse saved client server ${saved.endpoint}?`, true))
|
|
1436
|
+
if (!command.preset && effects.interactive && !await effects.ask(`Reuse saved client server ${saved.endpoint}?`, true))
|
|
1350
1437
|
throw new InstallUsageError('Saved client default retained; use an explicit prepared profile to validate replacement input');
|
|
1351
1438
|
configPath = managedPath;
|
|
1352
1439
|
}
|
|
1353
1440
|
let profile;
|
|
1354
1441
|
if (configPath) profile = validateHostProfile(effects.readProfile(configPath));
|
|
1355
|
-
else if (effects.interactive) {
|
|
1356
|
-
const endpoint = await effects.askLine('Server HTTP endpoint: ', 'http://127.0.0.1:3050');
|
|
1442
|
+
else if (!command.preset && effects.interactive) {
|
|
1443
|
+
const endpoint = await effects.askLine('Server HTTP or HTTPS endpoint: ', 'http://127.0.0.1:3050');
|
|
1357
1444
|
const credentialPath = await effects.askLine('Private issued-token file: ', '');
|
|
1358
1445
|
if (!credentialPath) throw new InstallUsageError('Client setup requires an issued-token file');
|
|
1359
1446
|
profile = await effects.discoverClientProfile(endpoint, credentialPath);
|
|
@@ -1366,46 +1453,63 @@ async function runClientCommand(command, effects) {
|
|
|
1366
1453
|
}
|
|
1367
1454
|
effects.out(info(`Selected server ${profile.endpoint} (instance ${profile.expectedInstanceId}).`));
|
|
1368
1455
|
await effects.verifyHostProfile(configPath || profile);
|
|
1369
|
-
await effects.verifyPackagedMcp(configPath || profile);
|
|
1370
1456
|
const settings = saved?.installer ?? (configPath ? effects.readJson(configPath)?.installer : undefined);
|
|
1371
1457
|
const settingsBase = saved ? dirname(managedPath) : configPath ? dirname(configPath) : process.cwd();
|
|
1372
|
-
let integrations = settings?.integrations;
|
|
1373
|
-
if (!integrations && effects.interactive) {
|
|
1458
|
+
let integrations = command.integrations ?? settings?.integrations;
|
|
1459
|
+
if (!integrations && !command.preset && effects.interactive) {
|
|
1374
1460
|
integrations = [];
|
|
1375
1461
|
for (const name of ['codex', 'claude-code', 'fleet']) if (await effects.ask(`Install ${name}?`, name !== 'fleet')) integrations.push(name);
|
|
1376
1462
|
}
|
|
1377
|
-
if (!Array.isArray(integrations) ||
|
|
1378
|
-
let fleetSettingsPath = settings?.fleetSettingsPath;
|
|
1463
|
+
if (!Array.isArray(integrations) || integrations.some(name => !['codex', 'claude-code', 'fleet'].includes(name)) || new Set(integrations).size !== integrations.length) throw new InstallUsageError('installer.integrations must be an array of codex, claude-code and/or fleet; use an empty array for CLI only');
|
|
1464
|
+
let fleetSettingsPath = command.preset ? command.fleetSettingsPath : settings?.fleetSettingsPath;
|
|
1465
|
+
if (command.nonInteractive && integrations.includes('fleet') && !fleetSettingsPath) throw new InstallUsageError('Fleet in CLI mode requires --fleet-settings; no interactive wizard will be opened');
|
|
1379
1466
|
if (fleetSettingsPath !== undefined && (typeof fleetSettingsPath !== 'string' || !fleetSettingsPath))
|
|
1380
1467
|
throw new InstallUsageError('installer.fleetSettingsPath must be a non-empty path when supplied');
|
|
1381
1468
|
if (fleetSettingsPath) fleetSettingsPath = resolve(settingsBase, fleetSettingsPath);
|
|
1382
|
-
const selectedClients =
|
|
1469
|
+
const selectedClients = clientPackageNames(integrations);
|
|
1383
1470
|
let sourcesPath = settings?.sourcesPath;
|
|
1384
1471
|
let resolvedSources;
|
|
1385
|
-
if (
|
|
1472
|
+
if (command.sourcePolicy) {
|
|
1473
|
+
resolvedSources = await effects.resolveSourcePolicy(command.sourcePolicy, 'client', selectedClients);
|
|
1474
|
+
sourcesPath = undefined;
|
|
1475
|
+
} else if (saved) sourcesPath = resolve(settingsBase, sourcesPath);
|
|
1386
1476
|
else {
|
|
1387
1477
|
if (sourcesPath) sourcesPath = resolve(settingsBase, sourcesPath);
|
|
1388
1478
|
const policy = sourcesPath ? effects.readJson(sourcesPath) : effects.packagedSourcePolicy();
|
|
1389
1479
|
resolvedSources = await effects.resolveSourcePolicy(policy, 'client', selectedClients);
|
|
1390
1480
|
}
|
|
1391
|
-
|
|
1481
|
+
effects.out(progress(0, 4, 'Client configuration', 'Prepare the selected integrations and private connection profile.'));
|
|
1482
|
+
const imported = effects.importClientProfile({ profile, sourcesPath, sources: resolvedSources, integrations, fleetSettingsPath, refresh: !!command.preset });
|
|
1483
|
+
let phase = 'Validate saved server connection';
|
|
1392
1484
|
try {
|
|
1393
1485
|
await effects.verifyHostProfile(imported.configPath);
|
|
1394
|
-
|
|
1486
|
+
effects.out(progress(1, 4, 'Client packages', 'Acquire and verify the selected client package versions.'));
|
|
1487
|
+
phase = 'Acquire client packages';
|
|
1488
|
+
const exactSuite = await effects.acquireClientPackages(imported.configPath, imported.settings.sourcesPath, integrations, { refresh: !!command.preset });
|
|
1395
1489
|
const args = { assumeYes: true, dryRun: false, channel: 'latest', clientIntegrations: integrations,
|
|
1396
1490
|
acquiredFleet: exactSuite.fleetBin, fleetSettingsPath: imported.settings.fleetSettingsPath };
|
|
1397
1491
|
const target = { mode: 'host-profile', managed: true, configPath: imported.configPath, profile: imported.profile, endpoint: imported.profile.endpoint };
|
|
1492
|
+
effects.out(progress(2, 4, 'Client integrations', 'Register the selected agent integrations.'));
|
|
1493
|
+
phase = 'Register client integrations';
|
|
1398
1494
|
const summary = await runHarnessPhase(args, effects, { target, isDefaultStateDir: false, exactSuite });
|
|
1399
|
-
if (integrations.includes('fleet'))
|
|
1495
|
+
if (integrations.includes('fleet')) {
|
|
1496
|
+
effects.out(progress(3, 4, 'Fleet configuration', 'Apply prepared settings or open the selected Fleet wizard.'));
|
|
1497
|
+
phase = 'Configure Fleet';
|
|
1498
|
+
summary.push(await runFleetPhase(args, effects, { target, isDefaultStateDir: false }));
|
|
1499
|
+
}
|
|
1400
1500
|
const incomplete = integrations.filter(name => !summary.some(row => row.key === name && row.state === 'installed'));
|
|
1401
1501
|
if (effects.env.OURS_CONFIG && resolve(effects.env.OURS_CONFIG) !== imported.configPath)
|
|
1402
1502
|
effects.out(warn('This shell has an explicit OURS_CONFIG override. Unset it for new clients to use the saved default; installer did not edit your shell.'));
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1503
|
+
if (incomplete.length) {
|
|
1504
|
+
effects.out(warn(`Client setup incomplete (${incomplete.join(', ')}). The selected client integrations need attention:`));
|
|
1505
|
+
for (const name of incomplete) explainClientFailure(effects, name, summary.find(row => row.key === name));
|
|
1506
|
+
clientRetry(effects, imported, integrations);
|
|
1507
|
+
} else effects.out(ok(`Client setup complete. New clients discover ${imported.configPath}; no OURS_CONFIG export is required.`));
|
|
1508
|
+
if (!incomplete.length) effects.out(progress(4, 4, 'Client setup complete', 'All selected integrations are configured.'));
|
|
1406
1509
|
return incomplete.length ? EXIT_REFUSED : EXIT_OK;
|
|
1407
1510
|
} catch (error) {
|
|
1408
|
-
effects.out(warn(`Client setup incomplete: ${
|
|
1511
|
+
effects.out(warn(`Client setup incomplete: ${phase} failed — ${clientDiagnostic(error)}. Fix this error before retrying.`));
|
|
1512
|
+
clientRetry(effects, imported, integrations);
|
|
1409
1513
|
return EXIT_REFUSED;
|
|
1410
1514
|
}
|
|
1411
1515
|
}
|
package/lib/plan.mjs
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { join, resolve, basename, dirname } from 'node:path';
|
|
8
8
|
import { valid, validRange, satisfies } from 'semver';
|
|
9
|
+
import { releaseBinding } from '../assets/scripts/maintenance/release-graph.mjs';
|
|
9
10
|
|
|
10
11
|
export const CLI_UNIT_MARKER = '# Managed by @ours.network/cli';
|
|
11
12
|
export const SYSTEMD_USER_DIR = ['.config', 'systemd', 'user'];
|
|
@@ -96,7 +97,7 @@ export function launchdLabelForStateDir(stateDir) {
|
|
|
96
97
|
export function classifyUnit(text) {
|
|
97
98
|
if (text === null || text === undefined) return { kind: 'absent' };
|
|
98
99
|
const s = String(text);
|
|
99
|
-
if (s.startsWith(CLI_UNIT_MARKER)) return { kind: 'cli-managed' };
|
|
100
|
+
if ((s.startsWith(CLI_UNIT_MARKER) || s.startsWith('# Managed by @ours.network/daemon'))) return { kind: 'cli-managed' };
|
|
100
101
|
const looksLikeOursMcp = /ExecStart=.*\bours-mcp\b/.test(s)
|
|
101
102
|
|| /^Description=ours MCP daemon\b/m.test(s)
|
|
102
103
|
|| (/^Environment=OURS_STATE_DIR=/m.test(s) && /^Environment=OURS_TRANSPORT=http$/m.test(s));
|
|
@@ -137,7 +138,7 @@ export function planServiceInstall({ stateDir, home, readText, platform = 'linux
|
|
|
137
138
|
platform,
|
|
138
139
|
reason: 'no-service-manager',
|
|
139
140
|
message: `installing a boot service is not available on ${platform} — the ours CLI supports Linux user systemd and macOS launchd`,
|
|
140
|
-
manual: ['ours
|
|
141
|
+
manual: ['ours-daemon', 'serve', '--config'],
|
|
141
142
|
};
|
|
142
143
|
}
|
|
143
144
|
|
|
@@ -223,7 +224,7 @@ export function serviceInstallCommand({ stateDir, adoptLegacyUnit = false }) {
|
|
|
223
224
|
// --json so the caller can read back whether the unit actually CHANGED rather
|
|
224
225
|
// than assuming it did. --force is reachable only through the explicit argument
|
|
225
226
|
// above, and the CLI refuses to overwrite a unit it did not write.
|
|
226
|
-
const cmd = ['ours
|
|
227
|
+
const cmd = ['ours-daemon', 'install-service', '--yes', '--json', '--state-dir', dir, '--config', join(dir, 'config.json')];
|
|
227
228
|
if (adoptLegacyUnit) cmd.push('--force');
|
|
228
229
|
return cmd;
|
|
229
230
|
}
|
|
@@ -264,20 +265,20 @@ export function planDaemonSteps(target, { cliVersionChanged = false, cliStartedI
|
|
|
264
265
|
const steps = [{ id: 'cli', label: 'install the ours-sdk CLI', command: ['npm', 'i', '-g', '@ours.network/cli'] }];
|
|
265
266
|
steps.push({ id: 'config', label: `write ${join(dir, 'config.json')}`, port: target.port });
|
|
266
267
|
if (target.action === 'create') {
|
|
267
|
-
steps.push({ id: 'start', label: `start the daemon on port ${target.port}`, command: ['ours
|
|
268
|
+
steps.push({ id: 'start', label: `start the daemon on port ${target.port}`, command: ['ours-daemon', 'start', '--config', join(dir, 'config.json')] });
|
|
268
269
|
} else if (cliVersionChanged) {
|
|
269
270
|
// `ours daemon stop` refuses to signal a daemon it did not start, so a
|
|
270
271
|
// daemon under another launcher is left running and the caller says which
|
|
271
272
|
// launcher must be restarted instead.
|
|
272
273
|
steps.push(cliStartedIt
|
|
273
|
-
? { id: 'restart', label: 'restart the daemon (package version changed)', command: ['ours
|
|
274
|
+
? { id: 'restart', label: 'restart the daemon (package version changed)', command: ['ours-daemon', 'restart', '--config', join(dir, 'config.json')] }
|
|
274
275
|
: { id: 'restart-external', label: 'daemon was not started by the CLI — restart it with its own launcher', command: null });
|
|
275
276
|
}
|
|
276
277
|
steps.push({ id: 'service', label: 'install the boot service', command: serviceInstallCommand({ stateDir: dir }) });
|
|
277
278
|
return steps;
|
|
278
279
|
}
|
|
279
280
|
|
|
280
|
-
export const SERVER_PACKAGES = ['sdk', 'cli', '
|
|
281
|
+
export const SERVER_PACKAGES = ['sdk', 'cli', 'daemon', 'tg-connector', 'cowork', 'messenger-server'].map(n => `@ours.network/${n}`);
|
|
281
282
|
export const SERVER_DEPENDENCIES = {
|
|
282
283
|
daemon: [], telegram: ['daemon'], cowork: ['daemon'], messenger: ['daemon'],
|
|
283
284
|
};
|
|
@@ -310,6 +311,7 @@ export function selectSourcePackages(manifest, role, clients = []) {
|
|
|
310
311
|
|
|
311
312
|
/** Resolve a packaged compatibility policy into a role-filtered exact selection. */
|
|
312
313
|
export async function resolveSourcePolicy(manifest, role, clients = [], resolveNpm) {
|
|
314
|
+
const release = releaseBinding(manifest);
|
|
313
315
|
const names = role === 'server' ? SERVER_PACKAGES : clients.map(name => `@ours.network/${name}`);
|
|
314
316
|
const packages = {};
|
|
315
317
|
const sourceNames = new Set();
|
|
@@ -332,7 +334,7 @@ export async function resolveSourcePolicy(manifest, role, clients = [], resolveN
|
|
|
332
334
|
} else throw new Error(`Missing source policy for ${name}`);
|
|
333
335
|
}
|
|
334
336
|
const sources = Object.fromEntries([...sourceNames].map(name => [name, manifest.sources[name]]));
|
|
335
|
-
const exact = { ...(sourceNames.size ? { sources } : {}), packages };
|
|
337
|
+
const exact = { ...(sourceNames.size ? { sources } : {}), packages, ...(release ? { release } : {}) };
|
|
336
338
|
selectSourcePackages(exact, role, clients);
|
|
337
339
|
return exact;
|
|
338
340
|
}
|
|
@@ -468,3 +470,7 @@ export function consumerServiceState(text, service, platform) {
|
|
|
468
470
|
}
|
|
469
471
|
return matches === 1 ? selected : undefined;
|
|
470
472
|
}
|
|
473
|
+
|
|
474
|
+
export function clientPackageNames(integrations) {
|
|
475
|
+
return [...new Set(['sdk', 'cli', ...(integrations.some(name => ['codex', 'claude-code'].includes(name)) ? ['mcp'] : []), ...integrations])];
|
|
476
|
+
}
|