@ours.network/install 1.1.1 → 1.2.0-nightly.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +196 -6
- package/assets/Dockerfile +41 -0
- package/assets/docker-compose.yaml +222 -0
- package/assets/scripts/README.md +20 -0
- package/assets/scripts/build/README.md +39 -0
- package/assets/scripts/build/build-common.mjs +37 -0
- package/assets/scripts/build/build-cowork.mjs +2 -0
- package/assets/scripts/build/build-fleet.mjs +2 -0
- package/assets/scripts/build/build-mcp.mjs +9 -0
- package/assets/scripts/build/build-messenger.mjs +2 -0
- package/assets/scripts/build/build-sdk.mjs +9 -0
- package/assets/scripts/build/build-telegram.mjs +2 -0
- package/assets/scripts/build/build.mjs +49 -0
- package/assets/scripts/build/record-build.mjs +31 -0
- package/assets/scripts/maintenance/README.md +35 -0
- package/assets/scripts/maintenance/build-context.mjs +162 -0
- package/assets/scripts/maintenance/docker-layout-conversion.mjs +238 -0
- package/assets/scripts/maintenance/provenance-compare.mjs +160 -0
- package/assets/scripts/maintenance/state-archive.mjs +238 -0
- package/assets/scripts/maintenance/state-native.mjs +56 -0
- package/assets/scripts/maintenance/state-operation.mjs +249 -0
- package/assets/scripts/runtime/README.md +24 -0
- package/assets/scripts/runtime/check-client.mjs +21 -0
- package/assets/scripts/runtime/check-start.mjs +15 -0
- package/assets/scripts/runtime/client-setup.mjs +197 -0
- package/assets/scripts/runtime/entrypoint.sh +13 -0
- package/assets/scripts/runtime/health-cowork.sh +11 -0
- package/assets/scripts/runtime/health-messenger.mjs +6 -0
- package/assets/scripts/runtime/health-telegram.sh +8 -0
- package/assets/scripts/runtime/healthcheck.mjs +17 -0
- package/assets/scripts/runtime/runtime-common.mjs +47 -0
- package/assets/scripts/runtime/start-cowork.sh +6 -0
- package/assets/scripts/runtime/start-messenger.sh +6 -0
- package/assets/scripts/runtime/start-telegram.sh +6 -0
- package/assets/sources.json +21 -0
- package/install.sh +2 -1
- package/lib/build-transition.mjs +56 -0
- package/lib/docker-conversion-runtime.mjs +96 -0
- package/lib/docker-layout-installation.mjs +62 -0
- package/lib/effects.mjs +945 -11
- package/lib/extras.mjs +23 -68
- package/lib/layout-conversion.mjs +297 -0
- package/lib/orchestrate-uninstall.mjs +30 -1
- package/lib/orchestrate.mjs +265 -18
- package/lib/plan.mjs +194 -1
- package/lib/target.mjs +100 -0
- package/lib/usage.mjs +27 -2
- package/package.json +9 -2
- package/uninstall.sh +2 -0
package/lib/target.mjs
CHANGED
|
@@ -66,6 +66,59 @@ export class InstallUsageError extends Error {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
const PROFILE_KEYS = ['endpoint', 'expectedInstanceId', 'credentialPath'];
|
|
70
|
+
const LEGACY_PROFILE_KEYS = ['port', 'stateDir', 'apiToken', 'apiVisibility'];
|
|
71
|
+
const PROFILE_CONFLICT_ENV = ['OURS_API_TOKEN', 'OURS_PORT', 'OURS_STATE_DIR', 'OURS_DAEMON_ID'];
|
|
72
|
+
const PROFILE_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
73
|
+
|
|
74
|
+
const profileError = (message) => new InstallUsageError(`Invalid external host profile: ${message}`);
|
|
75
|
+
|
|
76
|
+
export function validateHostProfile(value) {
|
|
77
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
78
|
+
throw profileError('expected a complete host profile object.');
|
|
79
|
+
}
|
|
80
|
+
const present = PROFILE_KEYS.filter((key) => Object.hasOwn(value, key));
|
|
81
|
+
if (present.length !== 0 && present.length !== PROFILE_KEYS.length) {
|
|
82
|
+
throw profileError('expected a complete host profile tuple: endpoint, expectedInstanceId, credentialPath.');
|
|
83
|
+
}
|
|
84
|
+
if (present.length === 0) return null;
|
|
85
|
+
const mixed = LEGACY_PROFILE_KEYS.filter((key) => Object.hasOwn(value, key));
|
|
86
|
+
if (mixed.length) throw profileError(`legacy selection keys cannot be mixed with a host profile (${mixed.join(', ')}).`);
|
|
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.');
|
|
89
|
+
if (typeof expectedInstanceId !== 'string' || !PROFILE_UUID.test(expectedInstanceId)) throw profileError('expectedInstanceId must be a lowercase UUID.');
|
|
90
|
+
if (typeof credentialPath !== 'string' || credentialPath === '' || !credentialPath.startsWith('/') || resolve(credentialPath) !== credentialPath) {
|
|
91
|
+
throw profileError('credentialPath must be a normalized absolute path.');
|
|
92
|
+
}
|
|
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.');
|
|
97
|
+
}
|
|
98
|
+
return { endpoint: url.origin, expectedInstanceId, credentialPath };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function resolveProfileSelection({ args, env = {}, home = homedir(), exists, readProfile }) {
|
|
102
|
+
const explicit = (env.OURS_CONFIG ?? '').trim();
|
|
103
|
+
const configPath = explicit ? resolve(explicit) : resolve(home, DEFAULT_STATE_DIR_NAME, DAEMON_CONFIG);
|
|
104
|
+
if (!explicit && !exists(configPath)) return { mode: 'local' };
|
|
105
|
+
if (explicit && !exists(configPath)) throw new InstallUsageError(`Cannot read host profile ${JSON.stringify(configPath)}.`);
|
|
106
|
+
let value;
|
|
107
|
+
try { value = readProfile(configPath); }
|
|
108
|
+
catch (error) { throw new InstallUsageError(error instanceof Error ? error.message : String(error)); }
|
|
109
|
+
const profile = value === null ? null : validateHostProfile(value);
|
|
110
|
+
if (profile === null) return { mode: 'local' };
|
|
111
|
+
if (args.stateDirExplicit || args.portExplicit) throw profileError('--state-dir or --port conflicts with host-profile mode.');
|
|
112
|
+
const conflicting = PROFILE_CONFLICT_ENV.filter((key) => (env[key] ?? '').trim() !== '');
|
|
113
|
+
if (conflicting.length) throw profileError(`${conflicting.join(', ')} conflicts with host-profile mode.`);
|
|
114
|
+
return { mode: 'host-profile', configPath, profile };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function profileEnv(selection) {
|
|
118
|
+
if (selection?.mode !== 'host-profile' || typeof selection.configPath !== 'string') throw new Error('profileEnv requires a host-profile selection');
|
|
119
|
+
return { OURS_CONFIG: selection.configPath };
|
|
120
|
+
}
|
|
121
|
+
|
|
69
122
|
// Lexical path comparison, matching how the SDK compares a reported state
|
|
70
123
|
// directory against a selected one. Resolving symlinks would mean touching the
|
|
71
124
|
// filesystem before validation, which is what this comparison exists to avoid.
|
|
@@ -396,3 +449,50 @@ export function searchFreePort(isTaken, { floor = FREE_PORT_FLOOR, reserved = IN
|
|
|
396
449
|
}
|
|
397
450
|
return null;
|
|
398
451
|
}
|
|
452
|
+
|
|
453
|
+
/** Explicit network operations never fall through to legacy Human provisioning. */
|
|
454
|
+
export function parseNetworkArgs(argv) {
|
|
455
|
+
if (!['server', 'client'].includes(argv[0])) return null;
|
|
456
|
+
const [role, operation] = argv;
|
|
457
|
+
const operations = role === 'client' ? ['install'] : ['install', 'status', 'start', 'stop', 'restart', 'access-issue', 'access-replace', 'backup', 'restore', 'reset', 'update', 'rebuild'];
|
|
458
|
+
if (!operations.includes(operation)) throw new InstallUsageError(`Unsupported ${role} operation: ${operation ?? '(missing)'}`);
|
|
459
|
+
const allowed = role === 'client' ? ['config'] : ['state-dir'];
|
|
460
|
+
if (role === 'server' && operation === 'install') allowed.push('mode', 'sources', 'migrate');
|
|
461
|
+
if (operation === 'access-issue') allowed.push('output');
|
|
462
|
+
if (['access-replace', 'reset'].includes(operation)) allowed.push('confirm');
|
|
463
|
+
if (['restore', 'update'].includes(operation)) allowed.push('compatible');
|
|
464
|
+
if (operation === 'update') allowed.push('sources');
|
|
465
|
+
const result = { role, operation };
|
|
466
|
+
let optionsStart = 2;
|
|
467
|
+
if (['backup', 'restore', 'reset'].includes(operation)) {
|
|
468
|
+
result.domain = argv[optionsStart++];
|
|
469
|
+
if (!['server', 'daemon', 'telegram', 'cowork', 'messenger'].includes(result.domain)) throw new InstallUsageError('Select server, daemon, telegram, cowork or messenger');
|
|
470
|
+
if (result.domain === 'server' && operation === 'reset') throw new InstallUsageError('Full-server reset is not supported');
|
|
471
|
+
if (operation !== 'reset') {
|
|
472
|
+
result.label = argv[optionsStart++];
|
|
473
|
+
if (typeof result.label !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(result.label)) throw new InstallUsageError('Backup label must be a plain basename');
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
const seen = new Set();
|
|
477
|
+
for (let i = optionsStart; i < argv.length; i++) {
|
|
478
|
+
const match = /^--([a-z-]+)(?:=(.*))?$/.exec(argv[i]);
|
|
479
|
+
if (!match || !allowed.includes(match[1]) || seen.has(match[1])) throw new InstallUsageError(`Unexpected or repeated option: ${argv[i]}`);
|
|
480
|
+
const [, name, inline] = match;
|
|
481
|
+
seen.add(name);
|
|
482
|
+
const key = name.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
483
|
+
if (['confirm', 'migrate', 'compatible'].includes(name)) {
|
|
484
|
+
if (inline !== undefined) throw new InstallUsageError(`--${name} does not take a value`);
|
|
485
|
+
result[key] = true;
|
|
486
|
+
} else {
|
|
487
|
+
const value = inline ?? argv[++i];
|
|
488
|
+
if (!value || value.startsWith('--')) throw new InstallUsageError(`--${name} requires a value`);
|
|
489
|
+
result[key] = name === 'mode' ? value : resolve(value);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
const required = role === 'client' ? [] : ['stateDir'];
|
|
493
|
+
if (operation === 'access-issue') required.push('output');
|
|
494
|
+
if (['access-replace', 'reset'].includes(operation)) required.push('confirm');
|
|
495
|
+
for (const key of required) if (!result[key]) throw new InstallUsageError(`${operation} requires --${key.replace(/[A-Z]/g, c => '-' + c.toLowerCase())}`);
|
|
496
|
+
if (result.mode && !['packages', 'docker'].includes(result.mode)) throw new InstallUsageError('--mode must be packages or docker');
|
|
497
|
+
return result;
|
|
498
|
+
}
|
package/lib/usage.mjs
CHANGED
|
@@ -6,6 +6,25 @@
|
|
|
6
6
|
|
|
7
7
|
export const USAGE = `ours-install — the unified ours.network stack installer.
|
|
8
8
|
|
|
9
|
+
Selected network installation (uses the packaged compatible source policy):
|
|
10
|
+
ours-install server install --mode packages|docker [--sources PATH] --state-dir PATH
|
|
11
|
+
ours-install server status|start|stop|restart --state-dir PATH
|
|
12
|
+
ours-install server rebuild --state-dir PATH
|
|
13
|
+
ours-install server update --state-dir PATH [--sources PATH] --compatible
|
|
14
|
+
ours-install server access-issue --state-dir PATH --output PATH
|
|
15
|
+
ours-install server access-replace --state-dir PATH --confirm
|
|
16
|
+
ours-install server backup|restore server LABEL --state-dir PATH
|
|
17
|
+
ours-install server backup|restore daemon|telegram|cowork|messenger LABEL --state-dir PATH
|
|
18
|
+
ours-install server reset daemon|telegram|cowork|messenger --confirm --state-dir PATH
|
|
19
|
+
ours-install client install --config PATH
|
|
20
|
+
Client profile settings select installer.integrations and may override installer.sourcesPath,
|
|
21
|
+
and optional installer.fleetSettingsPath (relative paths use the profile directory).
|
|
22
|
+
Repeat selected server install repairs setup without replacing existing authority.
|
|
23
|
+
Daemon maintenance includes MCP state. Full-server reset is not supported.
|
|
24
|
+
Update requires reviewed storage compatibility; --compatible records that attestation.
|
|
25
|
+
Existing managed layouts are converted before service startup.
|
|
26
|
+
|
|
27
|
+
|
|
9
28
|
Install: npm i -g @ours.network/install && ours-install (recommended)
|
|
10
29
|
npx @ours.network/install (one-off)
|
|
11
30
|
|
|
@@ -28,7 +47,9 @@ ends with exact next commands plus a copy-paste agent hand-off prompt.
|
|
|
28
47
|
--version print the installer version and exit
|
|
29
48
|
|
|
30
49
|
Env: OURS_ASSUME_YES=1 (accept defaults, no prompts) · OURS_INSTALL_DRY_RUN=1 ·
|
|
31
|
-
OURS_CHANNEL=nightly ·
|
|
50
|
+
OURS_CONFIG=/private/host-profile.json · OURS_CHANNEL=nightly ·
|
|
51
|
+
OURS_BROKER_URL · OURS_NPM. A complete host profile selects client-only
|
|
52
|
+
setup for an existing Compose-owned daemon. Docs: https://ours.network`;
|
|
32
53
|
|
|
33
54
|
export const UNINSTALL_USAGE = `ours-uninstall — remove one ours daemon and what attaches to it.
|
|
34
55
|
|
|
@@ -46,4 +67,8 @@ run that refuses leaves the daemon whole rather than half-dismantled.
|
|
|
46
67
|
identity keys exist nowhere else and no peer can give them back.
|
|
47
68
|
--dry-run print what it WOULD remove and remove nothing
|
|
48
69
|
--help show this help and exit
|
|
49
|
-
--version print the version and exit
|
|
70
|
+
--version print the version and exit
|
|
71
|
+
|
|
72
|
+
When OURS_CONFIG selects a prepared host profile, remove selected client
|
|
73
|
+
attachments only. The shared profile/credential and Compose daemon are kept,
|
|
74
|
+
including under --purge.`;
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ours.network/install",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0-nightly.1",
|
|
4
4
|
"private": false,
|
|
5
|
-
"description": "The all-in-one ours.network installer: one shared daemon, MCP, cowork, Telegram, Fleet, harness plugins, Human identity,
|
|
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",
|
|
7
7
|
"bin": {
|
|
8
8
|
"ours-install": "install.mjs"
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"install.mjs",
|
|
12
12
|
"install.sh",
|
|
13
13
|
"lib",
|
|
14
|
+
"assets",
|
|
14
15
|
"uninstall.mjs",
|
|
15
16
|
"uninstall.sh",
|
|
16
17
|
"README.md",
|
|
@@ -29,5 +30,11 @@
|
|
|
29
30
|
},
|
|
30
31
|
"scripts": {
|
|
31
32
|
"test": "node --test"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"koffi": "3.2.1",
|
|
36
|
+
"tar-stream": "3.1.7",
|
|
37
|
+
"jsonc-parser": "3.3.1",
|
|
38
|
+
"semver": "7.8.5"
|
|
32
39
|
}
|
|
33
40
|
}
|
package/uninstall.sh
CHANGED
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
# from a terminal and type the full path.
|
|
26
26
|
# OURS_ASSUME_YES=1 accept defaults; skip the typed confirmations (implies no tty)
|
|
27
27
|
# OURS_NPM="npm" npm binary to use
|
|
28
|
+
# OURS_CONFIG=/path/profile.json remove selected client attachments for this prepared host
|
|
29
|
+
# profile; its shared credential and Compose daemon are retained
|
|
28
30
|
# OURS_UNINSTALLER_MJS / OURS_INSTALLER_BASE run/fetch overrides (dev/testing)
|
|
29
31
|
set -euo pipefail
|
|
30
32
|
|