@ours.network/install 1.2.1-nightly.6 → 1.2.1-nightly.8
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 +19 -0
- package/assets/release-lock.json +3 -3
- package/assets/release.json +1 -1
- package/assets/sources.json +1 -1
- package/lib/effects.mjs +2 -2
- package/lib/orchestrate.mjs +62 -14
- package/lib/target.mjs +4 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -295,3 +295,22 @@ deletion is required; repeat the original setup with the updated installer.
|
|
|
295
295
|
The repair runs isolated verification containers without state mounts or network
|
|
296
296
|
access. Other image verification failures stop setup without replacing the image.
|
|
297
297
|
An interrupted repair can be retried, including after the image tag was replaced.
|
|
298
|
+
|
|
299
|
+
### Remote clients over HTTPS
|
|
300
|
+
|
|
301
|
+
A client may select an `https://` origin backed by a TLS reverse proxy. Keep the
|
|
302
|
+
daemon listener private and configure the proxy separately with a certificate
|
|
303
|
+
trusted by the client and matching the hostname. Node uses its normal trust
|
|
304
|
+
store; a private CA may be supplied through `NODE_EXTRA_CA_CERTS` before starting
|
|
305
|
+
the client. Certificate verification must remain enabled.
|
|
306
|
+
|
|
307
|
+
The existing issued client credential works over either transport; changing the
|
|
308
|
+
URL scheme does not require a new credential. Keep the server's master on the
|
|
309
|
+
server. Forward `x-ours-api-token` and the `x-ours-*` session headers unchanged,
|
|
310
|
+
and support streamed request/response bodies and long polling. Client requests
|
|
311
|
+
refuse redirects, including HTTPS-to-HTTP redirects: configure the final HTTPS
|
|
312
|
+
origin directly. UUID/capability checks still precede credential-bearing calls.
|
|
313
|
+
|
|
314
|
+
This adds client HTTPS support, not an HTTPS daemon listener, certificate
|
|
315
|
+
provisioning or automatic reverse-proxy configuration. Existing local HTTP and
|
|
316
|
+
SSH-tunnel profiles continue to work.
|
package/assets/release-lock.json
CHANGED
|
@@ -4285,9 +4285,9 @@
|
|
|
4285
4285
|
}
|
|
4286
4286
|
},
|
|
4287
4287
|
"node_modules/lru-cache": {
|
|
4288
|
-
"version": "11.5.
|
|
4289
|
-
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.
|
|
4290
|
-
"integrity": "sha512-
|
|
4288
|
+
"version": "11.5.3",
|
|
4289
|
+
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz",
|
|
4290
|
+
"integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==",
|
|
4291
4291
|
"license": "BlueOak-1.0.0",
|
|
4292
4292
|
"engines": {
|
|
4293
4293
|
"node": "20 || >=22"
|
package/assets/release.json
CHANGED
package/assets/sources.json
CHANGED
package/lib/effects.mjs
CHANGED
|
@@ -1155,8 +1155,8 @@ export function networkEffects(effects) {
|
|
|
1155
1155
|
},
|
|
1156
1156
|
async discoverClientProfile(endpoint, credentialPath) {
|
|
1157
1157
|
const url = new URL(endpoint);
|
|
1158
|
-
if (url.protocol !== 'http:' || url.username || url.password || url.pathname !== '/' || url.search || url.hash)
|
|
1159
|
-
throw new Error('Client endpoint must be an HTTP origin');
|
|
1158
|
+
if ((url.protocol !== 'http:' && url.protocol !== 'https:') || url.username || url.password || url.pathname !== '/' || url.search || url.hash)
|
|
1159
|
+
throw new Error('Client endpoint must be an HTTP or HTTPS origin');
|
|
1160
1160
|
const response = await fetch(`${url.origin}/selection`, { redirect: 'error', signal: AbortSignal.timeout(5000) });
|
|
1161
1161
|
if (!response.ok) throw new Error(`Daemon selection answered HTTP ${response.status}`);
|
|
1162
1162
|
const selection = await response.json();
|
package/lib/orchestrate.mjs
CHANGED
|
@@ -86,6 +86,41 @@ async function perform(effects, dryRun, label, thunk) {
|
|
|
86
86
|
|
|
87
87
|
const reason = (error) => (error instanceof Error ? error.message : String(error));
|
|
88
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(','), '--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
|
+
|
|
89
124
|
function semverMajor(version) {
|
|
90
125
|
const match = /^(?:[~^<>= ]*)(\d+)\./.exec(String(version ?? '').trim());
|
|
91
126
|
return match ? Number(match[1]) : null;
|
|
@@ -193,11 +228,11 @@ async function prepareIncompatibleUpgrade(args, effects, target, cliPkg, mismatc
|
|
|
193
228
|
* rather than one harness. So a failure here is one honest line plus whatever
|
|
194
229
|
* the caller wants to say about retrying, and the walk continues.
|
|
195
230
|
*/
|
|
196
|
-
async function attempt(effects, dryRun, label, thunk) {
|
|
231
|
+
async function attempt(effects, dryRun, label, thunk, formatReason = reason) {
|
|
197
232
|
try {
|
|
198
233
|
return { ok: true, ...(await perform(effects, dryRun, label, thunk)) };
|
|
199
234
|
} catch (error) {
|
|
200
|
-
effects.out(warn(`${label} — did not complete: ${
|
|
235
|
+
effects.out(warn(`${label} — did not complete: ${formatReason(error)}`));
|
|
201
236
|
return { ok: false, error };
|
|
202
237
|
}
|
|
203
238
|
}
|
|
@@ -817,6 +852,9 @@ export async function runIdentityPhase(args, effects, { target, mcpReady }) {
|
|
|
817
852
|
export async function runHarnessPhase(args, effects, { target, isDefaultStateDir, exactSuite = null }) {
|
|
818
853
|
effects.out(heading('Harness plugins'));
|
|
819
854
|
const detected = (await effects.detectHarnesses()).filter(h => !args.clientIntegrations || args.clientIntegrations.includes(h.name));
|
|
855
|
+
for (const name of args.clientIntegrations ?? []) {
|
|
856
|
+
if (name !== 'fleet' && !detected.some(h => h.name === name)) detected.push({ name, status: 'absent' });
|
|
857
|
+
}
|
|
820
858
|
for (const h of detected) {
|
|
821
859
|
if (h.status === 'ok') effects.out(ok(`'${h.command ?? h.name}' → ${h.detail ?? 'real program'} (its plugin can be installed)`));
|
|
822
860
|
else if (h.status === 'alias') effects.out(warn(`'${h.command ?? h.name}' → ${h.detail} (I won't call it — manual steps below)`));
|
|
@@ -826,7 +864,7 @@ export async function runHarnessPhase(args, effects, { target, isDefaultStateDir
|
|
|
826
864
|
if (detected.every((h) => h.status === 'absent')) {
|
|
827
865
|
effects.out(info('No Claude Code, Codex or Hermes found — install one and re-run to wire it up.'));
|
|
828
866
|
effects.out(info('Your daemon is unaffected; nothing else in this run depends on a harness.'));
|
|
829
|
-
return [];
|
|
867
|
+
return args.clientIntegrations ? detected.map(h => ({ key: h.name, state: 'skipped', note: 'not installed' })) : [];
|
|
830
868
|
}
|
|
831
869
|
|
|
832
870
|
const plans = planHarnessPlugins({
|
|
@@ -851,7 +889,7 @@ export async function runHarnessPhase(args, effects, { target, isDefaultStateDir
|
|
|
851
889
|
if (plan.action === 'manual' && exactSuite?.localPackages?.[plan.name]) {
|
|
852
890
|
const root = await effects.prepareClientMarketplace(plan.name, exactSuite.localPackages[plan.name]);
|
|
853
891
|
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:
|
|
892
|
+
rows.push({ ...row, state: 'failed', note: plan.reason, manual: true });
|
|
855
893
|
continue;
|
|
856
894
|
}
|
|
857
895
|
if (plan.action === 'manual') {
|
|
@@ -873,7 +911,7 @@ export async function runHarnessPhase(args, effects, { target, isDefaultStateDir
|
|
|
873
911
|
effects.out(warn(`${plan.label} — ${plan.reason}; install it yourself with:`));
|
|
874
912
|
for (const step of manual) effects.out(info(` ${step}`));
|
|
875
913
|
if (plan.envLine) effects.out(info(`Before later native launches, set: ${plan.envLine}`));
|
|
876
|
-
rows.push({ ...row, state: 'skipped', note: plan.reason });
|
|
914
|
+
rows.push({ ...row, state: 'skipped', note: plan.reason, manual: true });
|
|
877
915
|
continue;
|
|
878
916
|
}
|
|
879
917
|
|
|
@@ -884,13 +922,16 @@ export async function runHarnessPhase(args, effects, { target, isDefaultStateDir
|
|
|
884
922
|
const steps = plan.name === 'codex'
|
|
885
923
|
? [['codex', 'plugin', 'marketplace', 'add', registration], ['codex', 'plugin', 'add', 'ours@ours-codex-marketplace']]
|
|
886
924
|
: [['claude', 'plugin', 'marketplace', 'add', registration], ['claude', 'plugin', await effects.hasClaudePlugin() ? 'update' : 'install', 'ours@ours.network']];
|
|
887
|
-
let
|
|
925
|
+
let failure = null;
|
|
888
926
|
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) {
|
|
927
|
+
const outcome = await attempt(effects, false, step.join(' '), () => effects.run(step[0], step.slice(1), { env: profileEnv(target) }), clientDiagnostic);
|
|
928
|
+
if (!outcome.ok) {
|
|
929
|
+
failure = { failedCommand: step, failedStep: step[2] === 'marketplace' ? `Register ${plan.label} marketplace` : `Install ${plan.label} plugin`, detail: clientDiagnostic(outcome.error) };
|
|
930
|
+
break;
|
|
931
|
+
}
|
|
891
932
|
}
|
|
892
933
|
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:
|
|
934
|
+
rows.push({ ...row, state: failure ? 'failed' : 'installed', ...(failure ?? {}) });
|
|
894
935
|
continue;
|
|
895
936
|
}
|
|
896
937
|
|
|
@@ -1387,7 +1428,7 @@ export async function runClientCommand(command, effects) {
|
|
|
1387
1428
|
let profile;
|
|
1388
1429
|
if (configPath) profile = validateHostProfile(effects.readProfile(configPath));
|
|
1389
1430
|
else if (!command.preset && effects.interactive) {
|
|
1390
|
-
const endpoint = await effects.askLine('Server HTTP endpoint: ', 'http://127.0.0.1:3050');
|
|
1431
|
+
const endpoint = await effects.askLine('Server HTTP or HTTPS endpoint: ', 'http://127.0.0.1:3050');
|
|
1391
1432
|
const credentialPath = await effects.askLine('Private issued-token file: ', '');
|
|
1392
1433
|
if (!credentialPath) throw new InstallUsageError('Client setup requires an issued-token file');
|
|
1393
1434
|
profile = await effects.discoverClientProfile(endpoint, credentialPath);
|
|
@@ -1428,29 +1469,36 @@ export async function runClientCommand(command, effects) {
|
|
|
1428
1469
|
}
|
|
1429
1470
|
effects.out(progress(0, 4, 'Client configuration', 'Prepare the selected integrations and private connection profile.'));
|
|
1430
1471
|
const imported = effects.importClientProfile({ profile, sourcesPath, sources: resolvedSources, integrations, fleetSettingsPath, refresh: !!command.preset });
|
|
1472
|
+
let phase = 'Validate saved server connection';
|
|
1431
1473
|
try {
|
|
1432
1474
|
await effects.verifyHostProfile(imported.configPath);
|
|
1433
1475
|
effects.out(progress(1, 4, 'Client packages', 'Acquire and verify the selected client package versions.'));
|
|
1476
|
+
phase = 'Acquire client packages';
|
|
1434
1477
|
const exactSuite = await effects.acquireClientPackages(imported.configPath, imported.settings.sourcesPath, integrations, { refresh: !!command.preset });
|
|
1435
1478
|
const args = { assumeYes: true, dryRun: false, channel: 'latest', clientIntegrations: integrations,
|
|
1436
1479
|
acquiredFleet: exactSuite.fleetBin, fleetSettingsPath: imported.settings.fleetSettingsPath };
|
|
1437
1480
|
const target = { mode: 'host-profile', managed: true, configPath: imported.configPath, profile: imported.profile, endpoint: imported.profile.endpoint };
|
|
1438
1481
|
effects.out(progress(2, 4, 'Client integrations', 'Register the selected agent integrations.'));
|
|
1482
|
+
phase = 'Register client integrations';
|
|
1439
1483
|
const summary = await runHarnessPhase(args, effects, { target, isDefaultStateDir: false, exactSuite });
|
|
1440
1484
|
if (integrations.includes('fleet')) {
|
|
1441
1485
|
effects.out(progress(3, 4, 'Fleet configuration', 'Apply prepared settings or open the selected Fleet wizard.'));
|
|
1486
|
+
phase = 'Configure Fleet';
|
|
1442
1487
|
summary.push(await runFleetPhase(args, effects, { target, isDefaultStateDir: false }));
|
|
1443
1488
|
}
|
|
1444
1489
|
const incomplete = integrations.filter(name => !summary.some(row => row.key === name && row.state === 'installed'));
|
|
1445
1490
|
if (effects.env.OURS_CONFIG && resolve(effects.env.OURS_CONFIG) !== imported.configPath)
|
|
1446
1491
|
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
|
-
|
|
1492
|
+
if (incomplete.length) {
|
|
1493
|
+
effects.out(warn(`Client setup incomplete (${incomplete.join(', ')}). The selected client integrations need attention:`));
|
|
1494
|
+
for (const name of incomplete) explainClientFailure(effects, name, summary.find(row => row.key === name));
|
|
1495
|
+
clientRetry(effects, imported, integrations);
|
|
1496
|
+
} else effects.out(ok(`Client setup complete. New clients discover ${imported.configPath}; no OURS_CONFIG export is required.`));
|
|
1450
1497
|
if (!incomplete.length) effects.out(progress(4, 4, 'Client setup complete', 'All selected integrations are configured.'));
|
|
1451
1498
|
return incomplete.length ? EXIT_REFUSED : EXIT_OK;
|
|
1452
1499
|
} catch (error) {
|
|
1453
|
-
effects.out(warn(`Client setup incomplete: ${
|
|
1500
|
+
effects.out(warn(`Client setup incomplete: ${phase} failed — ${clientDiagnostic(error)}. Fix this error before retrying.`));
|
|
1501
|
+
clientRetry(effects, imported, integrations);
|
|
1454
1502
|
return EXIT_REFUSED;
|
|
1455
1503
|
}
|
|
1456
1504
|
}
|
package/lib/target.mjs
CHANGED
|
@@ -85,15 +85,15 @@ export function validateHostProfile(value) {
|
|
|
85
85
|
const mixed = LEGACY_PROFILE_KEYS.filter((key) => Object.hasOwn(value, key));
|
|
86
86
|
if (mixed.length) throw profileError(`legacy selection keys cannot be mixed with a host profile (${mixed.join(', ')}).`);
|
|
87
87
|
const { endpoint, expectedInstanceId, credentialPath } = value;
|
|
88
|
-
if (typeof endpoint !== 'string' || endpoint.trim() !== endpoint || endpoint === '') throw profileError('endpoint must be a non-empty HTTP origin.');
|
|
88
|
+
if (typeof endpoint !== 'string' || endpoint.trim() !== endpoint || endpoint === '') throw profileError('endpoint must be a non-empty HTTP or HTTPS origin.');
|
|
89
89
|
if (typeof expectedInstanceId !== 'string' || !PROFILE_UUID.test(expectedInstanceId)) throw profileError('expectedInstanceId must be a lowercase UUID.');
|
|
90
90
|
if (typeof credentialPath !== 'string' || credentialPath === '' || !credentialPath.startsWith('/') || resolve(credentialPath) !== credentialPath) {
|
|
91
91
|
throw profileError('credentialPath must be a normalized absolute path.');
|
|
92
92
|
}
|
|
93
93
|
let url;
|
|
94
|
-
try { url = new URL(endpoint); } catch { throw profileError('endpoint must be an HTTP origin.'); }
|
|
95
|
-
if (url.protocol !== 'http:' || url.username || url.password || url.pathname !== '/' || url.search || url.hash) {
|
|
96
|
-
throw profileError('endpoint must be an HTTP origin without credentials, path, query, or fragment.');
|
|
94
|
+
try { url = new URL(endpoint); } catch { throw profileError('endpoint must be an HTTP or HTTPS origin.'); }
|
|
95
|
+
if ((url.protocol !== 'http:' && url.protocol !== 'https:') || url.username || url.password || url.pathname !== '/' || url.search || url.hash) {
|
|
96
|
+
throw profileError('endpoint must be an HTTP or HTTPS origin without credentials, path, query, or fragment.');
|
|
97
97
|
}
|
|
98
98
|
return { endpoint: url.origin, expectedInstanceId, credentialPath };
|
|
99
99
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ours.network/install",
|
|
3
|
-
"version": "1.2.1-nightly.
|
|
3
|
+
"version": "1.2.1-nightly.8",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "The all-in-one ours.network installer: one shared daemon, MCP, cowork, Telegram, Fleet initialization, harness plugins, Human identity, progress UI, and guided next steps.",
|
|
6
6
|
"type": "module",
|