@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.
Files changed (49) hide show
  1. package/README.md +196 -6
  2. package/assets/Dockerfile +41 -0
  3. package/assets/docker-compose.yaml +222 -0
  4. package/assets/scripts/README.md +20 -0
  5. package/assets/scripts/build/README.md +39 -0
  6. package/assets/scripts/build/build-common.mjs +37 -0
  7. package/assets/scripts/build/build-cowork.mjs +2 -0
  8. package/assets/scripts/build/build-fleet.mjs +2 -0
  9. package/assets/scripts/build/build-mcp.mjs +9 -0
  10. package/assets/scripts/build/build-messenger.mjs +2 -0
  11. package/assets/scripts/build/build-sdk.mjs +9 -0
  12. package/assets/scripts/build/build-telegram.mjs +2 -0
  13. package/assets/scripts/build/build.mjs +49 -0
  14. package/assets/scripts/build/record-build.mjs +31 -0
  15. package/assets/scripts/maintenance/README.md +35 -0
  16. package/assets/scripts/maintenance/build-context.mjs +162 -0
  17. package/assets/scripts/maintenance/docker-layout-conversion.mjs +238 -0
  18. package/assets/scripts/maintenance/provenance-compare.mjs +160 -0
  19. package/assets/scripts/maintenance/state-archive.mjs +238 -0
  20. package/assets/scripts/maintenance/state-native.mjs +56 -0
  21. package/assets/scripts/maintenance/state-operation.mjs +249 -0
  22. package/assets/scripts/runtime/README.md +24 -0
  23. package/assets/scripts/runtime/check-client.mjs +21 -0
  24. package/assets/scripts/runtime/check-start.mjs +15 -0
  25. package/assets/scripts/runtime/client-setup.mjs +197 -0
  26. package/assets/scripts/runtime/entrypoint.sh +13 -0
  27. package/assets/scripts/runtime/health-cowork.sh +11 -0
  28. package/assets/scripts/runtime/health-messenger.mjs +6 -0
  29. package/assets/scripts/runtime/health-telegram.sh +8 -0
  30. package/assets/scripts/runtime/healthcheck.mjs +17 -0
  31. package/assets/scripts/runtime/runtime-common.mjs +47 -0
  32. package/assets/scripts/runtime/start-cowork.sh +6 -0
  33. package/assets/scripts/runtime/start-messenger.sh +6 -0
  34. package/assets/scripts/runtime/start-telegram.sh +6 -0
  35. package/assets/sources.json +21 -0
  36. package/install.sh +2 -1
  37. package/lib/build-transition.mjs +56 -0
  38. package/lib/docker-conversion-runtime.mjs +96 -0
  39. package/lib/docker-layout-installation.mjs +62 -0
  40. package/lib/effects.mjs +945 -11
  41. package/lib/extras.mjs +23 -68
  42. package/lib/layout-conversion.mjs +297 -0
  43. package/lib/orchestrate-uninstall.mjs +30 -1
  44. package/lib/orchestrate.mjs +265 -18
  45. package/lib/plan.mjs +194 -1
  46. package/lib/target.mjs +100 -0
  47. package/lib/usage.mjs +27 -2
  48. package/package.json +9 -2
  49. 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 · OURS_BROKER_URL · OURS_NPM. Docs: https://ours.network`;
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.1.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, stopped Fleet starter, progress UI, and guided next steps.",
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