@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
@@ -0,0 +1,21 @@
1
+ {
2
+ "sources": {
3
+ "sdk": { "type": "git", "url": "https://github.com/adapt-toolkit/ours-sdk.git", "commit": "9cb51add34888e6ae2cc3b554c97362bb1ca2d33" },
4
+ "telegram": { "type": "git", "url": "https://github.com/adapt-toolkit/ours-tg-connector.git", "commit": "81798b836b15ac4ba9b32ac180f94fb258a54a41" },
5
+ "cowork": { "type": "git", "url": "https://github.com/adapt-toolkit/ours-cowork.git", "commit": "1e17f7666c1682a858101c98a8f371aac7b737e3" },
6
+ "messenger": { "type": "git", "url": "https://github.com/adapt-toolkit/ours-messenger-server.git", "commit": "fc852e49eb3b43c077f59cf4919536be6f6d03b1" },
7
+ "fleet": { "type": "git", "url": "https://github.com/adapt-toolkit/ours-fleet.git", "commit": "b8444a7aac2eb6148a3ab534e90308bfc76447d7" },
8
+ "mcp": { "type": "git", "url": "https://github.com/adapt-toolkit/ours-mcp.git", "commit": "3d970d0cb7baf52351d5cd44dd59d4a2f705207b" }
9
+ },
10
+ "packages": {
11
+ "@ours.network/sdk": { "source": "sdk" },
12
+ "@ours.network/cli": { "source": "sdk" },
13
+ "@ours.network/tg-connector": { "source": "telegram" },
14
+ "@ours.network/cowork": { "source": "cowork" },
15
+ "@ours.network/messenger-server": { "source": "messenger" },
16
+ "@ours.network/fleet": { "source": "fleet" },
17
+ "@ours.network/mcp": { "source": "mcp" },
18
+ "@ours.network/codex": { "source": "mcp" },
19
+ "@ours.network/claude-code": { "source": "mcp" }
20
+ }
21
+ }
package/install.sh CHANGED
@@ -30,7 +30,8 @@
30
30
  # OURS_INSTALL_DRY_RUN=1 walk the whole flow WITHOUT installing/changing anything
31
31
  # (prints exactly what it WOULD do — safe on any machine)
32
32
  # OURS_NPM="npm" npm binary to use
33
- # OURS_CONFIG=/path/config.json daemon config file (default ~/.ours/config.json)
33
+ # OURS_CONFIG=/path/config.json legacy daemon config or prepared private host profile;
34
+ # a complete host profile performs client-only setup
34
35
  # OURS_INSTALLER_MJS=/path/install.mjs run this Node installer directly (dev/testing)
35
36
  # OURS_INSTALL_PKG=@ours.network/install the package to install for the command (override for dev)
36
37
  set -euo pipefail
@@ -0,0 +1,56 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { createHash } from 'node:crypto';
3
+ import { join } from 'node:path';
4
+ import { validateInstallation } from './plan.mjs';
5
+
6
+ /** The caller holds the installation lock; installation.json owns retry state. */
7
+ export async function serverBuildTransition(record, args, effects) {
8
+ validateInstallation(record, record.root);
9
+ const path = join(record.root, 'installation.json');
10
+ const save = transition => {
11
+ record = { ...record, buildTransition: transition };
12
+ validateInstallation(record, record.root);
13
+ effects.writeJson(path, JSON.stringify(record, null, 2) + '\n');
14
+ };
15
+ let transition = record.buildTransition;
16
+ if (!transition) {
17
+ const candidate = await effects.prepareServerBuild(record, args);
18
+ try {
19
+ await effects.checkServerBuild(record, candidate, !!args.compatible, args.operation);
20
+ transition = { operation: args.operation, candidate, compatible: !!args.compatible,
21
+ ...(args.sources ? { sourcePolicyHash: createHash('sha256').update(readFileSync(args.sources)).digest('hex') } : {}),
22
+ runningServices: await effects.serverLifecycle(record, 'status'), phase: 'prepared' };
23
+ save(transition);
24
+ } catch (error) {
25
+ await effects.discardServerBuild(candidate);
26
+ throw error;
27
+ }
28
+ } else {
29
+ const suppliedHash = args.sources ? createHash('sha256').update(readFileSync(args.sources)).digest('hex') : null;
30
+ if (args.operation !== transition.operation || (args.sources && suppliedHash !== transition.sourcePolicyHash)) {
31
+ throw new Error('Resume the retained server update/rebuild without substituting sources');
32
+ }
33
+ }
34
+ const { candidate, runningServices } = transition;
35
+ // A failed readiness check can leave some new services running. Every retry
36
+ // excludes those writers again and retains the original requested running set.
37
+ await effects.retireServerBuildRuntime(record);
38
+ if (transition.phase === 'prepared') {
39
+ await effects.updateServerBuildState(record, candidate, transition.compatible, transition.operation);
40
+ transition = { ...transition, phase: 'state-updated' };
41
+ save(transition);
42
+ }
43
+ if (transition.phase === 'state-updated') {
44
+ await effects.publishServerBuild(record, candidate);
45
+ transition = { ...transition, phase: 'runtime-activated' };
46
+ save(transition);
47
+ }
48
+ await effects.validateServerBuildState(record);
49
+ await effects.serverLifecycle(record, 'start', runningServices);
50
+ const completed = { ...record, sourcePolicyHash: candidate.sourcePolicyHash };
51
+ delete completed.buildTransition;
52
+ effects.writeJson(path, JSON.stringify(completed, null, 2) + '\n');
53
+ try { await effects.discardServerBuild(candidate); }
54
+ catch { effects.out('Server update completed; temporary build artifacts could not be removed.'); }
55
+ return completed;
56
+ }
@@ -0,0 +1,96 @@
1
+ import * as fs from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { atomicWriteConfig } from './config.mjs';
5
+
6
+ /** Update installer assets while retaining the selected application build. */
7
+ export async function prepareDockerConversionRuntime(record, effects, assets) {
8
+ if (record.schema !== 1 || record.mode !== 'docker' || !/^ours-[a-z0-9]+$/.test(record.project)) {
9
+ throw new Error('Select the legacy Docker runtime for conversion');
10
+ }
11
+ const compose = join(record.workDir, 'docker-compose.yaml');
12
+ const legacy = join(record.workDir, 'docker-compose.legacy.yaml');
13
+ if (!fs.existsSync(legacy)) {
14
+ atomicWriteConfig(legacy, fs.readFileSync(compose));
15
+ }
16
+ const context = fs.mkdtempSync(join(record.root, '.conversion-build-'));
17
+ try {
18
+ fs.cpSync(join(assets, 'scripts/runtime'), join(context, 'runtime'), { recursive: true });
19
+ fs.cpSync(join(assets, 'scripts/maintenance'), join(context, 'maintenance'), { recursive: true });
20
+ const { dependencies } = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url)));
21
+ fs.writeFileSync(join(context, 'package.json'), JSON.stringify({ private: true, type: 'module', dependencies }));
22
+ fs.writeFileSync(join(context, 'Dockerfile'), `# syntax=docker/dockerfile:1
23
+ FROM ${record.project}:runtime AS runtime
24
+ COPY --chmod=644 runtime/ /opt/ours/docker/
25
+ FROM runtime AS maintenance
26
+ USER 0:0
27
+ COPY --chmod=644 maintenance/ /opt/ours/docker/
28
+ COPY --chmod=644 package.json /opt/ours/docker/package.json
29
+ RUN npm install --prefix /opt/ours/docker --omit=dev --ignore-scripts --no-audit --no-fund
30
+ USER 1000:1000
31
+ ENTRYPOINT ["node", "/opt/ours/docker/state-operation.mjs"]
32
+ `);
33
+ // Build maintenance first: both targets inherit the same retained runtime.
34
+ for (const target of ['maintenance', 'runtime']) {
35
+ await effects.run('docker', ['build', '--platform', 'linux/amd64', '--target', target,
36
+ '--tag', `${record.project}:${target}`, context]);
37
+ }
38
+ atomicWriteConfig(compose, fs.readFileSync(join(assets, 'docker-compose.yaml')));
39
+ } finally {
40
+ fs.rmSync(context, { recursive: true, force: true });
41
+ }
42
+ }
43
+
44
+ /** Run the existing maintenance service with the volumes selected by the installer. */
45
+ export async function runDockerConversion(record, selected, operation, label, effects) {
46
+ if (!['prepare', 'validate', 'cleanup'].includes(operation)) throw new Error('Unsupported Docker conversion operation');
47
+ const volumes = { storage: { external: true, name: selected.target } };
48
+ const mounts = [{ type: 'volume', source: 'storage', target: '/storage', volume: { nocopy: true } }];
49
+ const addMount = (source, target, subpath, readOnly) => mounts.push({
50
+ type: 'volume', source, target, read_only: readOnly,
51
+ volume: { nocopy: true, ...(subpath ? { subpath } : {}) },
52
+ });
53
+ if (operation !== 'validate') {
54
+ for (const [alias, name] of Object.entries(selected.sources)) {
55
+ const source = `source-${alias}`;
56
+ volumes[source] = { external: true, name };
57
+ addMount(source, `/source/${alias}`, undefined, operation === 'prepare' && alias !== 'cowork');
58
+ }
59
+ if (operation === 'prepare') {
60
+ addMount('source-daemon', '/var/lib/ours', 'data', true);
61
+ addMount('source-cowork', '/var/lib/ours-cowork', 'data', false);
62
+ }
63
+ } else {
64
+ addMount('storage', '/var/lib/ours', 'state/daemon', true);
65
+ addMount('storage', '/var/lib/ours-cowork', 'state/cowork', false);
66
+ }
67
+ const definition = {
68
+ services: {
69
+ 'state-operation': {
70
+ image: `${record.project}:maintenance`, platform: 'linux/amd64',
71
+ user: `${record.uid ?? 1000}:${record.gid ?? 1000}`,
72
+ network_mode: 'none', read_only: true, cap_drop: ['ALL'],
73
+ security_opt: ['no-new-privileges:true'], tmpfs: ['/tmp:rw,nosuid,nodev,noexec,size=64m,mode=1777'],
74
+ entrypoint: ['node', '/opt/ours/docker/docker-layout-conversion.mjs'],
75
+ environment: { HOME: '/tmp', OURS_DAEMON_ID: record.instanceId },
76
+ volumes: mounts,
77
+ },
78
+ },
79
+ volumes,
80
+ };
81
+ const path = join(record.root, `.conversion-compose-${randomUUID()}.json`);
82
+ try {
83
+ fs.writeFileSync(path, JSON.stringify(definition), { mode: 0o600, flag: 'wx' });
84
+ const result = await effects.run('docker', ['compose', '--file', path, '--project-name', record.project,
85
+ 'run', '--rm', '--no-deps', '-T', 'state-operation', operation, ...(label ? [label] : [])]);
86
+ if (operation === 'cleanup') {
87
+ const summary = JSON.parse(result.stdout);
88
+ if (!Array.isArray(summary.emptyVolumes) || summary.emptyVolumes.some(alias => !Object.hasOwn(selected.sources, alias))) {
89
+ throw new Error('Invalid retired-volume cleanup result');
90
+ }
91
+ return summary;
92
+ }
93
+ } finally {
94
+ fs.rmSync(path, { force: true });
95
+ }
96
+ }
@@ -0,0 +1,62 @@
1
+ import * as fs from 'node:fs';
2
+ import { join, basename } from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { installationPaths, validateInstallation, SERVER_SERVICES } from './plan.mjs';
5
+
6
+ /** The installation lock is held by the caller; installation.json selects the live side. */
7
+ export async function convertDockerInstallation(record, operation, effects) {
8
+ validateInstallation(record, record.root);
9
+ if (record.mode !== 'docker' || (record.schema !== 1 && !record.layoutConversion)) {
10
+ throw new Error('Select a legacy or pending Docker installation');
11
+ }
12
+ const source = record.layoutConversion?.sourceRecord ?? record;
13
+ const published = record.schema === 2;
14
+ const writeSelection = value => effects.writeJson(join(record.root, 'installation.json'), JSON.stringify(value, null, 2) + '\n');
15
+ const volumes = await effects.selectConversionVolumes(
16
+ { ...source, layoutConversion: record.layoutConversion }, { allowMissingSources: published });
17
+ if (published && !volumes.targetExists) throw new Error('Published Docker state volume is missing');
18
+
19
+ if (!published) {
20
+ await effects.prepareDockerConversionRuntime(source);
21
+ if (!record.layoutConversion) {
22
+ record = { ...source, layoutConversion: {
23
+ version: 1, sourceRecord: source,
24
+ runningServices: await effects.serverLifecycle(source, 'status'),
25
+ backupPath: join(source.root, 'storage/backups', `before-layout-2-${randomUUID()}`),
26
+ } };
27
+ writeSelection(record);
28
+ }
29
+ // The full tree includes all four component volumes, even when a component
30
+ // is not selected for restart. Exclude every former managed writer.
31
+ await effects.retireLegacyServices({ ...source, services: [...SERVER_SERVICES] });
32
+ const target = { ...source, schema: 2 };
33
+ target.configPath = installationPaths(target).config;
34
+ // Preparation initializes only owned storage; it does not issue authority.
35
+ await effects.prepareInstallation(target);
36
+ await effects.runDockerConversion(source, volumes, 'prepare', basename(record.layoutConversion.backupPath));
37
+ record = { ...target, layoutConversion: record.layoutConversion };
38
+ writeSelection(record);
39
+ } else {
40
+ await effects.serverLifecycle(record, 'stop');
41
+ await effects.confirmDockerWritersStopped(record);
42
+ }
43
+
44
+ await effects.runDockerConversion(record, volumes, 'validate');
45
+ const running = ['install', 'start'].includes(operation) ? record.services : record.layoutConversion.runningServices;
46
+ await effects.serverLifecycle(record, 'start', running);
47
+ const cleanup = await effects.runDockerConversion(record, volumes, 'cleanup');
48
+ if (cleanup.emptyVolumes.length) {
49
+ await effects.run('docker', ['volume', 'rm', ...cleanup.emptyVolumes.map(alias => volumes.sources[alias])]);
50
+ }
51
+ try {
52
+ const stat = fs.lstatSync(source.configPath);
53
+ // Private host files may inherit a directory's group on macOS.
54
+ if (!stat.isFile() || stat.uid !== process.getuid()
55
+ || (stat.mode & 0o7777) !== 0o600) throw new Error('Unsafe former installer configuration');
56
+ fs.unlinkSync(source.configPath);
57
+ } catch (error) { if (error.code !== 'ENOENT') throw error; }
58
+ const finished = { ...record };
59
+ delete finished.layoutConversion;
60
+ writeSelection(finished);
61
+ return finished;
62
+ }