@ours.network/install 1.2.1-nightly.4 → 1.2.1-nightly.41
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/GATEWAY_SETUP.md +178 -0
- package/README.md +471 -9
- package/ROOTLESS_PODMAN_VALIDATION.md +210 -0
- package/assets/Dockerfile +14 -12
- package/assets/Dockerfile.gateway +5 -0
- package/assets/docker-compose.yaml +8 -1
- package/assets/fleet-settings.example.json +48 -0
- package/assets/gateway-entrypoint.sh +37 -0
- package/assets/release-lock.json +1177 -4508
- package/assets/release.json +37 -19
- package/assets/scripts/build/build-common.mjs +2 -1
- package/assets/scripts/build/build-sdk.mjs +6 -1
- 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 +7 -3
- package/assets/scripts/maintenance/recover-notifications.mjs +26 -0
- package/assets/scripts/maintenance/release-graph.mjs +18 -1
- package/assets/scripts/maintenance/state-operation.mjs +62 -5
- package/assets/scripts/runtime/README.md +7 -0
- package/assets/scripts/runtime/client-setup.mjs +80 -6
- package/assets/scripts/runtime/dependency-ready.mjs +13 -0
- package/assets/scripts/runtime/diagnostics.mjs +72 -0
- package/assets/scripts/runtime/entrypoint.sh +5 -1
- package/assets/scripts/runtime/health-messenger.mjs +7 -6
- package/assets/scripts/runtime/notifications-gateway.mjs +90 -0
- package/assets/scripts/runtime/recover.mjs +165 -0
- package/assets/scripts/runtime/start-cowork.sh +5 -0
- package/assets/scripts/runtime/start-messenger.sh +29 -4
- package/assets/scripts/runtime/start-telegram.sh +5 -0
- package/assets/scripts/runtime/supervise.mjs +35 -0
- package/assets/sources.json +54 -28
- package/install.mjs +7 -2
- package/lib/boot-readiness.mjs +43 -0
- package/lib/build-transition.mjs +16 -0
- package/lib/client-cli.mjs +127 -0
- package/lib/container-engine.mjs +127 -0
- package/lib/diagnostics.mjs +1 -0
- package/lib/docker-conversion-runtime.mjs +3 -2
- package/lib/docker-layout-installation.mjs +2 -1
- package/lib/docker-recovery.mjs +55 -0
- package/lib/docker-runtime-repair.mjs +160 -0
- package/lib/effects.mjs +357 -133
- package/lib/gateway-transition.mjs +87 -0
- package/lib/gateway.mjs +158 -0
- package/lib/legacy-migration.mjs +13 -2
- package/lib/orchestrate-uninstall.mjs +18 -6
- package/lib/orchestrate.mjs +193 -43
- package/lib/plan.mjs +27 -11
- package/lib/podman-preflight.mjs +40 -0
- package/lib/server-onboarding.mjs +164 -16
- package/lib/server-ports.mjs +17 -0
- package/lib/setup-options.mjs +99 -28
- package/lib/setup.mjs +35 -17
- package/lib/target.mjs +24 -8
- package/lib/uninstall.mjs +2 -2
- package/lib/usage.mjs +21 -7
- package/lib/workspace-setup.mjs +83 -0
- package/package.json +4 -2
package/lib/orchestrate.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { validateGatewayClientProfile } from './target.mjs';
|
|
2
|
+
import { enableGateway } from './gateway-transition.mjs';
|
|
1
3
|
import { executeLegacyMigration } from './legacy-migration.mjs';
|
|
2
4
|
// ours-install v3 — the orchestrator.
|
|
3
5
|
//
|
|
@@ -26,7 +28,8 @@ import { executeLegacyMigration } from './legacy-migration.mjs';
|
|
|
26
28
|
|
|
27
29
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
28
30
|
import { parseNetworkArgs, validateHostProfile, parseInstallArgs, resolveTarget, resolveProfileSelection, profileEnv, InstallUsageError } from './target.mjs';
|
|
29
|
-
import {
|
|
31
|
+
import { NOTIFICATIONS_PRODUCER_FILE } from './server-onboarding.mjs';
|
|
32
|
+
import { clientPackageNames, selectSourcePackages, validateInstallation, SERVER_SERVICES, planDaemonConfig, planServiceInstall, serviceInstallCommand } from './plan.mjs';
|
|
30
33
|
import {
|
|
31
34
|
COMPONENTS,
|
|
32
35
|
planComponentSelection, planMcpAttachment, planTgAttachment, planCoworkAttachment,
|
|
@@ -43,6 +46,7 @@ import {
|
|
|
43
46
|
validateChannelVersion,
|
|
44
47
|
} from './marketplace.mjs';
|
|
45
48
|
import { USAGE } from './usage.mjs';
|
|
49
|
+
import { installerFailure } from './diagnostics.mjs';
|
|
46
50
|
import { ok, info, warn, heading, banner, box, c, progress } from './ui.mjs';
|
|
47
51
|
|
|
48
52
|
export const EXIT_OK = 0;
|
|
@@ -86,6 +90,37 @@ async function perform(effects, dryRun, label, thunk) {
|
|
|
86
90
|
|
|
87
91
|
const reason = (error) => (error instanceof Error ? error.message : String(error));
|
|
88
92
|
|
|
93
|
+
function clientDiagnostic(error) {
|
|
94
|
+
return installerFailure(error);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function clientRetry(effects, imported, integrations) {
|
|
98
|
+
effects.out(info(`Saved server connection: ${imported.configPath}.`));
|
|
99
|
+
if (integrations.includes('fleet') && !imported.settings.disableFleetAgentsSetup && !imported.settings.fleetSettingsPath) {
|
|
100
|
+
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.'));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const args = ['client', 'install', '--config', imported.configPath, '--integrations', integrations.join(',') || 'none', '--sources', imported.settings.sourcesPath];
|
|
104
|
+
if (imported.settings.disableFleetAgentsSetup) args.push('--disable-fleet-agents-setup');
|
|
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
|
+
|
|
89
124
|
function semverMajor(version) {
|
|
90
125
|
const match = /^(?:[~^<>= ]*)(\d+)\./.exec(String(version ?? '').trim());
|
|
91
126
|
return match ? Number(match[1]) : null;
|
|
@@ -110,6 +145,7 @@ function incompatibleUpgrade(target, cliDependencies) {
|
|
|
110
145
|
async function prepareIncompatibleUpgrade(args, effects, target, cliPkg, mismatch) {
|
|
111
146
|
const dir = target.stateDir;
|
|
112
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'];
|
|
113
149
|
if (mismatch.unknown) {
|
|
114
150
|
effects.out(warn(`ours: cannot verify whether ${cliPkg} can restore daemon v${mismatch.runningVersion}. Nothing was changed.`));
|
|
115
151
|
effects.out(info('Check npm registry access and re-run; compatibility checks fail closed.'));
|
|
@@ -153,13 +189,13 @@ async function prepareIncompatibleUpgrade(args, effects, target, cliPkg, mismatc
|
|
|
153
189
|
}
|
|
154
190
|
|
|
155
191
|
await perform(effects, false, 'stop the incompatible daemon', () => effects.run(
|
|
156
|
-
|
|
192
|
+
owner[0], [...owner.slice(1), 'stop', '--state-dir', dir, '--config', configPath], { stream: true },
|
|
157
193
|
));
|
|
158
194
|
try {
|
|
159
195
|
await perform(effects, false, `back up complete daemon state to ${backupPath}`, () => effects.copyDir(dir, backupPath));
|
|
160
196
|
} catch (error) {
|
|
161
197
|
try {
|
|
162
|
-
await effects.run(
|
|
198
|
+
await effects.run(owner[0], [...owner.slice(1), 'start', '--state-dir', dir, '--config', configPath], { stream: true });
|
|
163
199
|
effects.out(ok('backup failed, but the old daemon was started again'));
|
|
164
200
|
} catch {
|
|
165
201
|
effects.out(warn(`backup failed and the old daemon did not restart; its state is still untouched at ${dir}`));
|
|
@@ -168,11 +204,11 @@ async function prepareIncompatibleUpgrade(args, effects, target, cliPkg, mismatc
|
|
|
168
204
|
}
|
|
169
205
|
try {
|
|
170
206
|
await perform(effects, false, 'remove the incompatible daemon boot service', () => effects.run(
|
|
171
|
-
|
|
207
|
+
owner[0], [...owner.slice(1), 'uninstall-service', '--yes', '--state-dir', dir, '--config', configPath], { stream: true },
|
|
172
208
|
));
|
|
173
209
|
} catch (error) {
|
|
174
210
|
try {
|
|
175
|
-
await effects.run(
|
|
211
|
+
await effects.run(owner[0], [...owner.slice(1), 'start', '--state-dir', dir, '--config', configPath], { stream: true });
|
|
176
212
|
effects.out(ok(`service removal failed, but the old daemon was started again; backup retained at ${backupPath}`));
|
|
177
213
|
} catch {
|
|
178
214
|
effects.out(warn(`service removal failed and the old daemon did not restart; state remains at ${dir} and the backup is at ${backupPath}`));
|
|
@@ -193,11 +229,11 @@ async function prepareIncompatibleUpgrade(args, effects, target, cliPkg, mismatc
|
|
|
193
229
|
* rather than one harness. So a failure here is one honest line plus whatever
|
|
194
230
|
* the caller wants to say about retrying, and the walk continues.
|
|
195
231
|
*/
|
|
196
|
-
async function attempt(effects, dryRun, label, thunk) {
|
|
232
|
+
async function attempt(effects, dryRun, label, thunk, formatReason = reason) {
|
|
197
233
|
try {
|
|
198
234
|
return { ok: true, ...(await perform(effects, dryRun, label, thunk)) };
|
|
199
235
|
} catch (error) {
|
|
200
|
-
effects.out(warn(`${label} — did not complete: ${
|
|
236
|
+
effects.out(warn(`${label} — did not complete: ${formatReason(error)}`));
|
|
201
237
|
return { ok: false, error };
|
|
202
238
|
}
|
|
203
239
|
}
|
|
@@ -333,14 +369,13 @@ export async function runDaemonPhase(args, effects, exactSuite = null) {
|
|
|
333
369
|
const mcpPkg = exactSuite?.packages?.mcp
|
|
334
370
|
? `@ours.network/mcp@${exactSuite.packages.mcp}`
|
|
335
371
|
: componentSpec(componentByKey('mcp'), args.channel);
|
|
336
|
-
//
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
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';
|
|
340
375
|
if (!creating && target.daemonVersion) {
|
|
341
|
-
const mismatch = incompatibleUpgrade(target, effects.packageDependencies(
|
|
376
|
+
const mismatch = incompatibleUpgrade(target, effects.packageDependencies(daemonPkg));
|
|
342
377
|
if (mismatch) {
|
|
343
|
-
const prepared = await prepareIncompatibleUpgrade(args, effects, target,
|
|
378
|
+
const prepared = await prepareIncompatibleUpgrade(args, effects, target, daemonPkg, mismatch);
|
|
344
379
|
if (prepared.refused) return { target, refused: prepared.refused, steps };
|
|
345
380
|
creating = prepared.purged === true;
|
|
346
381
|
target.backupPath = prepared.backupPath;
|
|
@@ -349,6 +384,18 @@ export async function runDaemonPhase(args, effects, exactSuite = null) {
|
|
|
349
384
|
}
|
|
350
385
|
await perform(effects, args.dryRun, `MCP server installed (npm i -g ${mcpPkg})`, () => effects.run('npm', ['i', '-g', mcpPkg]));
|
|
351
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
|
+
}
|
|
352
399
|
await perform(effects, args.dryRun, `ours CLI installed (npm i -g ${cliPkg})`, () => effects.run('npm', ['i', '-g', cliPkg]));
|
|
353
400
|
steps.push({ id: 'cli', changed: true, packageRefresh: true });
|
|
354
401
|
|
|
@@ -383,14 +430,14 @@ export async function runDaemonPhase(args, effects, exactSuite = null) {
|
|
|
383
430
|
|
|
384
431
|
try {
|
|
385
432
|
if (creating) {
|
|
386
|
-
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 }));
|
|
387
434
|
steps.push({ id: 'start', changed: true });
|
|
388
435
|
} else {
|
|
389
|
-
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 }));
|
|
390
437
|
steps.push({ id: 'restart', changed: true });
|
|
391
438
|
}
|
|
392
439
|
|
|
393
|
-
const service = await runServicePhase(args, effects, dir, target.port);
|
|
440
|
+
const service = migratedService ?? await runServicePhase(args, effects, dir, target.port);
|
|
394
441
|
if (service.unsupported) serviceUnsupported = service.unsupported;
|
|
395
442
|
if (service.refused) {
|
|
396
443
|
// A REFUSAL IS A FAILURE TO REACH THE STATE, not a special case. An unknown
|
|
@@ -420,7 +467,7 @@ export async function runDaemonPhase(args, effects, exactSuite = null) {
|
|
|
420
467
|
effects.out(recovery.recovered
|
|
421
468
|
? ok('your daemon is running again — nothing was committed, and the service is unchanged')
|
|
422
469
|
: warn('and the daemon did NOT come back up — start it yourself before anything else: '
|
|
423
|
-
+ `ours
|
|
470
|
+
+ `ours-daemon start --config ${configPath}`));
|
|
424
471
|
}
|
|
425
472
|
throw error;
|
|
426
473
|
}
|
|
@@ -443,7 +490,7 @@ export async function runDaemonPhase(args, effects, exactSuite = null) {
|
|
|
443
490
|
async function recoverDaemon(args, effects, dir, configPath, port) {
|
|
444
491
|
if (args.dryRun) return null;
|
|
445
492
|
try {
|
|
446
|
-
await effects.run('ours', [
|
|
493
|
+
await effects.run('ours-daemon', [ 'start', '--config', configPath]);
|
|
447
494
|
return { recovered: true };
|
|
448
495
|
} catch (recoveryError) {
|
|
449
496
|
return { recovered: false, reason: reason(recoveryError) };
|
|
@@ -794,7 +841,7 @@ export async function runIdentityPhase(args, effects, { target, mcpReady }) {
|
|
|
794
841
|
}
|
|
795
842
|
if (/not running|not reachable|ECONNREFUSED|connect/i.test(text)) {
|
|
796
843
|
effects.out(warn("The daemon isn't reachable yet — couldn't create your human identity."));
|
|
797
|
-
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}"'.`));
|
|
798
845
|
return { key: 'identity', label: 'Human identity', state: 'failed', note: 'daemon not reachable' };
|
|
799
846
|
}
|
|
800
847
|
effects.out(warn(`Couldn't create your human identity: ${text.split('\n')[0]}`));
|
|
@@ -817,6 +864,9 @@ export async function runIdentityPhase(args, effects, { target, mcpReady }) {
|
|
|
817
864
|
export async function runHarnessPhase(args, effects, { target, isDefaultStateDir, exactSuite = null }) {
|
|
818
865
|
effects.out(heading('Harness plugins'));
|
|
819
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
|
+
}
|
|
820
870
|
for (const h of detected) {
|
|
821
871
|
if (h.status === 'ok') effects.out(ok(`'${h.command ?? h.name}' → ${h.detail ?? 'real program'} (its plugin can be installed)`));
|
|
822
872
|
else if (h.status === 'alias') effects.out(warn(`'${h.command ?? h.name}' → ${h.detail} (I won't call it — manual steps below)`));
|
|
@@ -826,7 +876,7 @@ export async function runHarnessPhase(args, effects, { target, isDefaultStateDir
|
|
|
826
876
|
if (detected.every((h) => h.status === 'absent')) {
|
|
827
877
|
effects.out(info('No Claude Code, Codex or Hermes found — install one and re-run to wire it up.'));
|
|
828
878
|
effects.out(info('Your daemon is unaffected; nothing else in this run depends on a harness.'));
|
|
829
|
-
return [];
|
|
879
|
+
return args.clientIntegrations ? detected.map(h => ({ key: h.name, state: 'skipped', note: 'not installed' })) : [];
|
|
830
880
|
}
|
|
831
881
|
|
|
832
882
|
const plans = planHarnessPlugins({
|
|
@@ -851,7 +901,7 @@ export async function runHarnessPhase(args, effects, { target, isDefaultStateDir
|
|
|
851
901
|
if (plan.action === 'manual' && exactSuite?.localPackages?.[plan.name]) {
|
|
852
902
|
const root = await effects.prepareClientMarketplace(plan.name, exactSuite.localPackages[plan.name]);
|
|
853
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}`}.`));
|
|
854
|
-
rows.push({ ...row, state: 'failed', note:
|
|
904
|
+
rows.push({ ...row, state: 'failed', note: plan.reason, manual: true });
|
|
855
905
|
continue;
|
|
856
906
|
}
|
|
857
907
|
if (plan.action === 'manual') {
|
|
@@ -873,7 +923,7 @@ export async function runHarnessPhase(args, effects, { target, isDefaultStateDir
|
|
|
873
923
|
effects.out(warn(`${plan.label} — ${plan.reason}; install it yourself with:`));
|
|
874
924
|
for (const step of manual) effects.out(info(` ${step}`));
|
|
875
925
|
if (plan.envLine) effects.out(info(`Before later native launches, set: ${plan.envLine}`));
|
|
876
|
-
rows.push({ ...row, state: 'skipped', note: plan.reason });
|
|
926
|
+
rows.push({ ...row, state: 'skipped', note: plan.reason, manual: true });
|
|
877
927
|
continue;
|
|
878
928
|
}
|
|
879
929
|
|
|
@@ -884,13 +934,16 @@ export async function runHarnessPhase(args, effects, { target, isDefaultStateDir
|
|
|
884
934
|
const steps = plan.name === 'codex'
|
|
885
935
|
? [['codex', 'plugin', 'marketplace', 'add', registration], ['codex', 'plugin', 'add', 'ours@ours-codex-marketplace']]
|
|
886
936
|
: [['claude', 'plugin', 'marketplace', 'add', registration], ['claude', 'plugin', await effects.hasClaudePlugin() ? 'update' : 'install', 'ours@ours.network']];
|
|
887
|
-
let
|
|
937
|
+
let failure = null;
|
|
888
938
|
for (const step of steps) {
|
|
889
|
-
const outcome = await attempt(effects, false, step.join(' '), () => effects.run(step[0], step.slice(1), { env: profileEnv(target) }));
|
|
890
|
-
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
|
+
}
|
|
891
944
|
}
|
|
892
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}.`));
|
|
893
|
-
rows.push({ ...row, state:
|
|
946
|
+
rows.push({ ...row, state: failure ? 'failed' : 'installed', ...(failure ?? {}) });
|
|
894
947
|
continue;
|
|
895
948
|
}
|
|
896
949
|
|
|
@@ -940,6 +993,58 @@ export async function runHarnessPhase(args, effects, { target, isDefaultStateDir
|
|
|
940
993
|
return rows;
|
|
941
994
|
}
|
|
942
995
|
|
|
996
|
+
export const FLEET_MANAGED_CLI_CAPABILITY = 'managed-cli.setup-v1';
|
|
997
|
+
|
|
998
|
+
/**
|
|
999
|
+
* Ask the Fleet release that was just installed to prepare and validate its own
|
|
1000
|
+
* managed CLI policy. Fleet owns the commands, paths and harness rules; nothing
|
|
1001
|
+
* about them is duplicated here. The command is static: it starts no agent
|
|
1002
|
+
* session and no model. Its findings are reported, never turned into an
|
|
1003
|
+
* installation failure, and never widened on the operator's behalf.
|
|
1004
|
+
*/
|
|
1005
|
+
export async function runFleetManagedCliSetup(args, effects, plan, target) {
|
|
1006
|
+
const fleet = args.acquiredFleet || plan.init[0];
|
|
1007
|
+
const env = target.mode === 'host-profile' ? profileEnv(target) : daemonEnv(target.stateDir, target.port);
|
|
1008
|
+
const requested = args.fleetTaskWorkflowAgents ?? [];
|
|
1009
|
+
const manual = `${fleet === plan.init[0] ? fleet : shellQuote(fleet)} managed-cli setup --configuration ${shellQuote(plan.configPath)}`;
|
|
1010
|
+
// Version-matched: the capability is asked of the exact Fleet that was installed.
|
|
1011
|
+
const capabilities = await effects.fleetCapabilities(fleet, env);
|
|
1012
|
+
if (!capabilities.includes(FLEET_MANAGED_CLI_CAPABILITY)) {
|
|
1013
|
+
if (requested.length) effects.out(warn(`The installed Fleet does not provide managed CLI setup (${FLEET_MANAGED_CLI_CAPABILITY}); --fleet-task-workflow was not applied to ${requested.join(', ')} and nothing was changed.`));
|
|
1014
|
+
return { state: 'unavailable' };
|
|
1015
|
+
}
|
|
1016
|
+
let report;
|
|
1017
|
+
try {
|
|
1018
|
+
const result = await effects.run(fleet, ['managed-cli', 'setup', '--configuration', plan.configPath, '--json',
|
|
1019
|
+
...requested.flatMap(agent => ['--enable', agent])], { env, allowCodes: [0, 1] });
|
|
1020
|
+
report = JSON.parse(result.stdout ?? '');
|
|
1021
|
+
if (report?.version !== 1 || !Array.isArray(report.roles)) throw new Error('unrecognised report');
|
|
1022
|
+
} catch (error) {
|
|
1023
|
+
effects.out(warn(`Fleet managed CLI setup did not complete (${installerFailure(error)}). Nothing else was changed; retry: ${manual}`));
|
|
1024
|
+
return { state: 'failed' };
|
|
1025
|
+
}
|
|
1026
|
+
for (const item of report.enabled ?? [])
|
|
1027
|
+
effects.out(ok(`${item.changed ? 'Enabled' : 'Already enabled:'} Fleet task commands for ${item.agent} (${item.file}). Its permissions were not changed.`));
|
|
1028
|
+
const declared = report.roles.filter(role => role.setup?.state !== 'not-declared');
|
|
1029
|
+
if (!declared.length) {
|
|
1030
|
+
effects.out(info('No Fleet agent is set up to run Fleet task commands from a command sandbox; nothing was generated and no permission changed.'));
|
|
1031
|
+
effects.out(info(`To allow it for one agent, re-run with --fleet-task-workflow <Agent> or run: ${manual} --enable <Agent>`));
|
|
1032
|
+
return { state: 'none', report };
|
|
1033
|
+
}
|
|
1034
|
+
const good = ['prepared', 'generated-at-launch', 'not-required'];
|
|
1035
|
+
for (const role of declared) {
|
|
1036
|
+
const line = `${role.role} (${role.harness}/${role.session}): ${role.setup.state} — ${role.setup.detail}`;
|
|
1037
|
+
effects.out((good.includes(role.setup.state) ? ok : warn)(`Fleet task commands, ${line}`));
|
|
1038
|
+
for (const scope of role.scope ?? []) effects.out(info(` ${scope}`));
|
|
1039
|
+
for (const note of role.warnings ?? []) effects.out(info(` note: ${note}`));
|
|
1040
|
+
if (role.session_policy === 'restart-required')
|
|
1041
|
+
effects.out(warn(` ${role.role} is still running the previous policy; restart it (ours-fleet restart ${role.role}) to load this one.`));
|
|
1042
|
+
}
|
|
1043
|
+
for (const item of report.removed ?? []) effects.out(info(`Removed obsolete Fleet-generated policy ${item.path} (${item.detail ?? 'no longer needed'}).`));
|
|
1044
|
+
effects.out(info('This is generated configuration only. It was not executed: no agent session or model was started, and it is not evidence that a command reached a supervisor. Check with: ours-fleet doctor'));
|
|
1045
|
+
return { state: declared.every(role => good.includes(role.setup.state)) ? 'prepared' : 'attention', report };
|
|
1046
|
+
}
|
|
1047
|
+
|
|
943
1048
|
/** Install Fleet and let its interactive wizard publish the owned configuration. */
|
|
944
1049
|
export async function runFleetPhase(args, effects, { target, isDefaultStateDir }) {
|
|
945
1050
|
effects.out(heading('ours-fleet (your always-online agent team)'));
|
|
@@ -956,6 +1061,13 @@ export async function runFleetPhase(args, effects, { target, isDefaultStateDir }
|
|
|
956
1061
|
return { key: 'fleet', label: plan.label, state: 'skipped' };
|
|
957
1062
|
}
|
|
958
1063
|
const install = args.acquiredFleet ? { ok: true } : await attempt(effects, args.dryRun, plan.install.join(' '), () => effects.run(plan.install[0], plan.install.slice(1)));
|
|
1064
|
+
if (args.disableFleetAgentsSetup) {
|
|
1065
|
+
if (!install.ok) return { key: 'fleet', label: plan.label, state: 'failed', note: 'Fleet installation failed' };
|
|
1066
|
+
effects.out(ok('Fleet CLI installed; agent and model setup deferred to the App. Existing Fleet configuration was retained.'));
|
|
1067
|
+
// A retained configuration may already declare the workflow; its generated policy names the package path this run just replaced.
|
|
1068
|
+
if (!args.dryRun && effects.readText(plan.configPath) !== null) await runFleetManagedCliSetup(args, effects, plan, target);
|
|
1069
|
+
return { key: 'fleet', label: plan.label, state: 'installed', note: 'agents setup deferred' };
|
|
1070
|
+
}
|
|
959
1071
|
// Fleet owns its v2 manifest, roles, models and permissions. With no prepared
|
|
960
1072
|
// settings, its wizard keeps the user's terminal while this invocation carries
|
|
961
1073
|
// the selected daemon; --settings makes the same call strictly noninteractive.
|
|
@@ -979,6 +1091,7 @@ export async function runFleetPhase(args, effects, { target, isDefaultStateDir }
|
|
|
979
1091
|
return { key: 'fleet', label: plan.label, state: 'failed', note: 'Fleet configuration was not published' };
|
|
980
1092
|
}
|
|
981
1093
|
effects.out(ok('ours-fleet installed and initialized; no fleet roles were started.'));
|
|
1094
|
+
if (!args.dryRun) await runFleetManagedCliSetup(args, effects, plan, target);
|
|
982
1095
|
if (plan.instruction) effects.out(info(plan.instruction));
|
|
983
1096
|
return { key: 'fleet', label: plan.label, state: 'installed', note: `configured at ${plan.configPath}; stopped` };
|
|
984
1097
|
}
|
|
@@ -1262,15 +1375,22 @@ async function executeServerCommand(args, effects) {
|
|
|
1262
1375
|
const existing = record !== null;
|
|
1263
1376
|
if (existing) {
|
|
1264
1377
|
record = validateInstallation(record, args.stateDir);
|
|
1378
|
+
if (args.containerEngine && args.containerEngine !== (record.containerEngine ?? 'docker')) throw new Error('Conflicting container engine; explicit migration is required');
|
|
1265
1379
|
if (args.mode && args.mode !== record.mode) throw new Error('Conflicting runtime mode; retained installation was not changed');
|
|
1266
1380
|
if (args.operation !== 'update' && args.sources && (record.sourcePolicyHash
|
|
1267
1381
|
? effects.sourcePolicyHash(args.sources) !== record.sourcePolicyHash
|
|
1268
1382
|
: effects.readText(args.sources) !== effects.readText(record.sourcesPath))) throw new Error('Conflicting source selection; use explicit server update');
|
|
1269
1383
|
} else {
|
|
1270
1384
|
if (args.operation !== 'install' || !args.mode) throw new Error('First server install requires --mode');
|
|
1271
|
-
record = effects.newInstallation(args.stateDir, args.mode);
|
|
1385
|
+
record = effects.newInstallation(args.stateDir, args.mode, args);
|
|
1272
1386
|
for (const key of ['port', 'coworkPort', 'messengerPort']) if (args[key] !== undefined) record[key] = args[key];
|
|
1273
1387
|
}
|
|
1388
|
+
if (effects.readJson(join(record.root, 'gateway-transition.json')) && args.operation !== 'gateway-enable' && args.operation !== 'status') throw new Error('Interrupted gateway migration; run server gateway-enable before other changes');
|
|
1389
|
+
if (args.serverUrl && args.operation === 'install') {
|
|
1390
|
+
if (!record.gateway) throw new Error('Use server gateway-enable to migrate an existing Docker installation');
|
|
1391
|
+
if (existing && record.gateway.serverUrl !== args.serverUrl) throw new Error('Conflicting gateway server URL');
|
|
1392
|
+
record.gateway = { ...record.gateway, serverUrl: args.serverUrl };
|
|
1393
|
+
}
|
|
1274
1394
|
if (record.layoutConversion && !['install', 'start', 'stop', 'status'].includes(args.operation)) {
|
|
1275
1395
|
throw new Error('Layout conversion is incomplete; resume with server install or server start before changing state or authority');
|
|
1276
1396
|
}
|
|
@@ -1278,7 +1398,8 @@ async function executeServerCommand(args, effects) {
|
|
|
1278
1398
|
throw new Error(`Server build activation is incomplete; repeat server ${record.buildTransition.operation} before other mutations`);
|
|
1279
1399
|
}
|
|
1280
1400
|
const showInstallProgress = args.operation === 'install' && !(existing && (record.schema === 1 || record.layoutConversion));
|
|
1281
|
-
const installStageCount = (existing ? 7 : 9) + (args.identityName ? 2 : 0)
|
|
1401
|
+
const installStageCount = (existing ? 7 : 9) + (args.identityName ? 2 : 0)
|
|
1402
|
+
+ (args.identityName && args.name && args.surname && (!existing || [undefined, null, args.identityName, `${args.name} ${args.surname}`].includes(record.messengerIdentity)) ? 1 : 0);
|
|
1282
1403
|
let completedInstallStages = 0;
|
|
1283
1404
|
const installStage = async (label, explanation, action) => {
|
|
1284
1405
|
if (!showInstallProgress) return action();
|
|
@@ -1301,7 +1422,7 @@ async function executeServerCommand(args, effects) {
|
|
|
1301
1422
|
}
|
|
1302
1423
|
if (!existing && args.sources) record.sourcePolicyHash = effects.sourcePolicyHash(args.sources);
|
|
1303
1424
|
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, {
|
|
1304
|
-
existing, sourcePath: args.sources ?? record.sourcesPath, sourceManifest: args.resolvedSources, identityName: args.identityName,
|
|
1425
|
+
existing, sourcePath: args.sources ?? record.sourcesPath, sourceManifest: args.resolvedSources, identityName: args.identityName, explicitPorts: args.explicitPorts,
|
|
1305
1426
|
}));
|
|
1306
1427
|
if (existing && (record.schema === 1 || record.layoutConversion)
|
|
1307
1428
|
&& ['install', 'start', 'restart', 'update', 'rebuild'].includes(args.operation)) {
|
|
@@ -1331,13 +1452,28 @@ async function executeServerCommand(args, effects) {
|
|
|
1331
1452
|
if (args.identityName) {
|
|
1332
1453
|
await installStage('Daemon startup', 'Start the daemon and restore its retained identities.', () => effects.serverLifecycle(record, 'start', ['daemon']));
|
|
1333
1454
|
const identity = await installStage('Human identity', 'Keep the existing Human identity, or create it on a fresh daemon.', () => effects.serverEnsureIdentity(record, args.identityName));
|
|
1334
|
-
|
|
1455
|
+
// Messenger runs as the person's own identity under the Human identity when Name and Surname are known.
|
|
1456
|
+
// An installation whose Messenger already runs as some other identity under it keeps that identity.
|
|
1457
|
+
const person = args.name && args.surname ? `${args.name} ${args.surname}` : undefined;
|
|
1458
|
+
const selected = existing ? record.messengerIdentity : undefined;
|
|
1459
|
+
if (person && (!selected || selected === identity.name || selected === person)) {
|
|
1460
|
+
const own = await installStage('Messenger identity', 'Keep or create your own identity under the Human identity; Messenger runs as it.', () => effects.serverEnsureMessengerIdentity(record, person));
|
|
1461
|
+
record.messengerIdentity = own.name;
|
|
1462
|
+
} else if (selected && selected !== identity.name
|
|
1463
|
+
&& (await effects.serverListIdentities(record)).some(row => row.name === selected && row.kind === 'role' && row.temp === null)) {
|
|
1464
|
+
record.messengerIdentity = selected;
|
|
1465
|
+
} else record.messengerIdentity = identity.name;
|
|
1335
1466
|
effects.writeJson(recordPath, JSON.stringify(record, null, 2) + '\n');
|
|
1336
1467
|
await installStage('Application startup', 'Start the selected applications and check readiness.', () => effects.serverLifecycle(record, 'start', record.services.filter(name => name !== 'daemon')));
|
|
1337
1468
|
} else {
|
|
1338
1469
|
await installStage('Service startup', 'Start the daemon and selected services, then check readiness.', () => effects.serverLifecycle(record, 'start'));
|
|
1339
1470
|
}
|
|
1471
|
+
if (record.gateway) await effects.verifyGateway(record);
|
|
1340
1472
|
if (showInstallProgress) effects.out(progress(installStageCount, installStageCount, 'Installation complete', 'The selected services are ready.'));
|
|
1473
|
+
} else if (args.operation === 'uninstall') {
|
|
1474
|
+
await effects.serverUninstall(record);
|
|
1475
|
+
} else if (args.operation === 'gateway-enable') {
|
|
1476
|
+
await enableGateway(record, args, effects);
|
|
1341
1477
|
} else if (['backup', 'restore', 'reset'].includes(args.operation)) {
|
|
1342
1478
|
await effects.serverMaintenance(record, args);
|
|
1343
1479
|
} else if (['update', 'rebuild'].includes(args.operation)) {
|
|
@@ -1385,22 +1521,21 @@ export async function runClientCommand(command, effects) {
|
|
|
1385
1521
|
configPath = managedPath;
|
|
1386
1522
|
}
|
|
1387
1523
|
let profile;
|
|
1388
|
-
if (configPath) profile =
|
|
1524
|
+
if (configPath) profile = validateGatewayClientProfile(effects.readProfile(configPath));
|
|
1389
1525
|
else if (!command.preset && effects.interactive) {
|
|
1390
|
-
const endpoint = await effects.askLine('Server HTTP endpoint: ', 'http://127.0.0.1:3050');
|
|
1526
|
+
const endpoint = await effects.askLine('Server HTTP or HTTPS endpoint: ', 'http://127.0.0.1:3050');
|
|
1391
1527
|
const credentialPath = await effects.askLine('Private issued-token file: ', '');
|
|
1392
1528
|
if (!credentialPath) throw new InstallUsageError('Client setup requires an issued-token file');
|
|
1393
1529
|
profile = await effects.discoverClientProfile(endpoint, credentialPath);
|
|
1394
1530
|
}
|
|
1395
1531
|
if (!profile) throw new InstallUsageError('client install requires a complete network profile; supply --config or use the interactive client setup');
|
|
1396
|
-
if (saved &&
|
|
1532
|
+
if (saved && saved.expectedInstanceId !== profile.expectedInstanceId)
|
|
1397
1533
|
throw new InstallUsageError('Managed client already selects another server; existing default was not changed');
|
|
1398
1534
|
for (const name of ['OURS_API_TOKEN', 'OURS_PORT', 'OURS_STATE_DIR', 'OURS_DAEMON_ID']) {
|
|
1399
1535
|
if (effects.env[name]?.trim()) throw new InstallUsageError(`${name} conflicts with client profile selection`);
|
|
1400
1536
|
}
|
|
1401
1537
|
effects.out(info(`Selected server ${profile.endpoint} (instance ${profile.expectedInstanceId}).`));
|
|
1402
1538
|
await effects.verifyHostProfile(configPath || profile);
|
|
1403
|
-
await effects.verifyPackagedMcp(configPath || profile);
|
|
1404
1539
|
const settings = saved?.installer ?? (configPath ? effects.readJson(configPath)?.installer : undefined);
|
|
1405
1540
|
const settingsBase = saved ? dirname(managedPath) : configPath ? dirname(configPath) : process.cwd();
|
|
1406
1541
|
let integrations = command.integrations ?? settings?.integrations;
|
|
@@ -1408,13 +1543,14 @@ export async function runClientCommand(command, effects) {
|
|
|
1408
1543
|
integrations = [];
|
|
1409
1544
|
for (const name of ['codex', 'claude-code', 'fleet']) if (await effects.ask(`Install ${name}?`, name !== 'fleet')) integrations.push(name);
|
|
1410
1545
|
}
|
|
1411
|
-
if (!Array.isArray(integrations) ||
|
|
1546
|
+
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');
|
|
1412
1547
|
let fleetSettingsPath = command.preset ? command.fleetSettingsPath : settings?.fleetSettingsPath;
|
|
1413
|
-
|
|
1548
|
+
const disableFleetAgentsSetup = command.disableFleetAgentsSetup ?? settings?.disableFleetAgentsSetup ?? false;
|
|
1549
|
+
if (command.nonInteractive && !disableFleetAgentsSetup && integrations.includes('fleet') && !fleetSettingsPath) throw new InstallUsageError('Fleet in CLI mode requires --fleet-settings; no interactive wizard will be opened');
|
|
1414
1550
|
if (fleetSettingsPath !== undefined && (typeof fleetSettingsPath !== 'string' || !fleetSettingsPath))
|
|
1415
1551
|
throw new InstallUsageError('installer.fleetSettingsPath must be a non-empty path when supplied');
|
|
1416
1552
|
if (fleetSettingsPath) fleetSettingsPath = resolve(settingsBase, fleetSettingsPath);
|
|
1417
|
-
const selectedClients =
|
|
1553
|
+
const selectedClients = clientPackageNames(integrations);
|
|
1418
1554
|
let sourcesPath = settings?.sourcesPath;
|
|
1419
1555
|
let resolvedSources;
|
|
1420
1556
|
if (command.sourcePolicy) {
|
|
@@ -1427,30 +1563,44 @@ export async function runClientCommand(command, effects) {
|
|
|
1427
1563
|
resolvedSources = await effects.resolveSourcePolicy(policy, 'client', selectedClients);
|
|
1428
1564
|
}
|
|
1429
1565
|
effects.out(progress(0, 4, 'Client configuration', 'Prepare the selected integrations and private connection profile.'));
|
|
1430
|
-
|
|
1566
|
+
if (profile.serverUrl && integrations.includes('fleet')) await effects.qualifyGatewayClient({ profile, sourcesPath, sources: resolvedSources, integrations, refresh: !!command.preset });
|
|
1567
|
+
// A prepared local profile may carry the Fleet notification producer credential beside it.
|
|
1568
|
+
const notificationsProducerPath = configPath && configPath !== managedPath && effects.exists?.(join(dirname(configPath), NOTIFICATIONS_PRODUCER_FILE)) ? join(dirname(configPath), NOTIFICATIONS_PRODUCER_FILE) : undefined;
|
|
1569
|
+
const imported = effects.importClientProfile({ profile, sourcesPath, sources: resolvedSources, integrations, fleetSettingsPath, disableFleetAgentsSetup, refresh: !!command.preset, notificationsProducerPath });
|
|
1570
|
+
let phase = 'Validate saved server connection';
|
|
1431
1571
|
try {
|
|
1432
1572
|
await effects.verifyHostProfile(imported.configPath);
|
|
1433
1573
|
effects.out(progress(1, 4, 'Client packages', 'Acquire and verify the selected client package versions.'));
|
|
1574
|
+
phase = 'Acquire client packages';
|
|
1434
1575
|
const exactSuite = await effects.acquireClientPackages(imported.configPath, imported.settings.sourcesPath, integrations, { refresh: !!command.preset });
|
|
1435
1576
|
const args = { assumeYes: true, dryRun: false, channel: 'latest', clientIntegrations: integrations,
|
|
1436
|
-
acquiredFleet: exactSuite.fleetBin, fleetSettingsPath: imported.settings.fleetSettingsPath
|
|
1577
|
+
acquiredFleet: exactSuite.fleetBin, fleetSettingsPath: imported.settings.fleetSettingsPath, disableFleetAgentsSetup: imported.settings.disableFleetAgentsSetup,
|
|
1578
|
+
fleetTaskWorkflowAgents: command.fleetTaskWorkflowAgents };
|
|
1437
1579
|
const target = { mode: 'host-profile', managed: true, configPath: imported.configPath, profile: imported.profile, endpoint: imported.profile.endpoint };
|
|
1438
1580
|
effects.out(progress(2, 4, 'Client integrations', 'Register the selected agent integrations.'));
|
|
1581
|
+
phase = 'Register client integrations';
|
|
1439
1582
|
const summary = await runHarnessPhase(args, effects, { target, isDefaultStateDir: false, exactSuite });
|
|
1440
1583
|
if (integrations.includes('fleet')) {
|
|
1441
1584
|
effects.out(progress(3, 4, 'Fleet configuration', 'Apply prepared settings or open the selected Fleet wizard.'));
|
|
1585
|
+
phase = 'Configure Fleet';
|
|
1442
1586
|
summary.push(await runFleetPhase(args, effects, { target, isDefaultStateDir: false }));
|
|
1443
1587
|
}
|
|
1444
1588
|
const incomplete = integrations.filter(name => !summary.some(row => row.key === name && row.state === 'installed'));
|
|
1445
1589
|
if (effects.env.OURS_CONFIG && resolve(effects.env.OURS_CONFIG) !== imported.configPath)
|
|
1446
1590
|
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.'));
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1591
|
+
if (incomplete.length) {
|
|
1592
|
+
effects.out(warn(`Client setup incomplete (${incomplete.join(', ')}). The selected client integrations need attention:`));
|
|
1593
|
+
for (const name of incomplete) explainClientFailure(effects, name, summary.find(row => row.key === name));
|
|
1594
|
+
clientRetry(effects, imported, integrations);
|
|
1595
|
+
} else effects.out(ok(`Client setup complete. New clients discover ${imported.configPath}; no OURS_CONFIG export is required.`));
|
|
1596
|
+
// Fleet's web service reads its producer credential when it starts; agents keep running across a web restart.
|
|
1597
|
+
if (imported.notificationsProducerChanged && integrations.includes('fleet'))
|
|
1598
|
+
effects.out(info('Notifications are ready for Fleet. Run `ours-fleet web restart` so a running Fleet web console starts producing them.'));
|
|
1450
1599
|
if (!incomplete.length) effects.out(progress(4, 4, 'Client setup complete', 'All selected integrations are configured.'));
|
|
1451
1600
|
return incomplete.length ? EXIT_REFUSED : EXIT_OK;
|
|
1452
1601
|
} catch (error) {
|
|
1453
|
-
effects.out(warn(`Client setup incomplete: ${
|
|
1602
|
+
effects.out(warn(`Client setup incomplete: ${phase} failed — ${clientDiagnostic(error)}. Fix this error before retrying.`));
|
|
1603
|
+
clientRetry(effects, imported, integrations);
|
|
1454
1604
|
return EXIT_REFUSED;
|
|
1455
1605
|
}
|
|
1456
1606
|
}
|