@ours.network/install 1.2.0-nightly.2 → 1.2.1-nightly.10
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 +151 -131
- package/assets/Dockerfile +6 -3
- package/assets/docker-compose.yaml +21 -1
- package/assets/release-lock.json +7161 -0
- package/assets/release.json +57 -0
- package/assets/scripts/build/build-common.mjs +1 -1
- package/assets/scripts/build/build-sdk.mjs +6 -1
- package/assets/scripts/build/record-build.mjs +2 -0
- 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 +3 -1
- package/assets/scripts/maintenance/release-graph.mjs +111 -0
- package/assets/scripts/maintenance/state-operation.mjs +4 -2
- package/assets/scripts/runtime/client-setup.mjs +3 -3
- package/assets/scripts/runtime/entrypoint.sh +1 -1
- package/assets/scripts/runtime/legacy-import.mjs +103 -0
- package/assets/scripts/runtime/runtime-common.mjs +2 -0
- package/assets/sources.json +96 -16
- package/install.mjs +2 -2
- package/install.sh +1 -1
- package/lib/build-transition.mjs +13 -7
- package/lib/client-cli.mjs +117 -0
- package/lib/docker-conversion-runtime.mjs +1 -0
- package/lib/docker-runtime-repair.mjs +125 -0
- package/lib/effects.mjs +163 -85
- package/lib/fleet-settings.mjs +43 -0
- package/lib/legacy-migration.mjs +202 -0
- package/lib/legacy-state.mjs +205 -0
- package/lib/managed-cli.mjs +164 -0
- package/lib/orchestrate.mjs +162 -58
- package/lib/plan.mjs +13 -7
- package/lib/prompt.mjs +113 -119
- package/lib/server-onboarding.mjs +114 -0
- package/lib/setup-options.mjs +253 -0
- package/lib/setup.mjs +154 -0
- package/lib/target.mjs +4 -4
- package/lib/uninstall.mjs +2 -2
- package/lib/usage.mjs +49 -44
- package/package.json +5 -4
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": 1,
|
|
3
|
+
"channel": "nightly",
|
|
4
|
+
"installerVersion": "1.2.1-nightly.10",
|
|
5
|
+
"packages": {
|
|
6
|
+
"@ours.network/sdk": {
|
|
7
|
+
"version": "3.8.1-nightly.9",
|
|
8
|
+
"integrity": "sha512-9xh/7hWZ8zRV+8Vimu7OTf2qUAp4o4MJFxlCVtyi1YO0WyFFgk+dZcT8nWlXY/JWIPFdb8wnYW/OSoqGUAw4Hw=="
|
|
9
|
+
},
|
|
10
|
+
"@ours.network/cli": {
|
|
11
|
+
"version": "2.8.1-nightly.7",
|
|
12
|
+
"integrity": "sha512-Kq3iySBsOgazTxawxY8aFvaMTQ6fnkPZ2KmO/k6NhPaVpZbmBcL/9OWefKYEUlDMvuSOEVzjQLZa0Wac4oopuA=="
|
|
13
|
+
},
|
|
14
|
+
"@ours.network/tg-connector": {
|
|
15
|
+
"version": "1.0.1-nightly.3",
|
|
16
|
+
"integrity": "sha512-UDHEgM5C3mnnyfV3KTM2XFDGZ7vYs7oBE/3r8bKDtxJdlQZ4bxxI44LKrPI3gO2sdGROsDdM8/tvzwbJ8MiBYw=="
|
|
17
|
+
},
|
|
18
|
+
"@ours.network/cowork": {
|
|
19
|
+
"version": "1.3.2-nightly.20260921.0d6bb50",
|
|
20
|
+
"integrity": "sha512-gyUgY7Fw6RQVhONqexLNcS8m0WuBUQiHYIrWqd2FeAyyJOONjYGfbRtgJ67Xin1YTYf8Cxlomuf6j8cEc4ZwZg=="
|
|
21
|
+
},
|
|
22
|
+
"@ours.network/messenger-server": {
|
|
23
|
+
"version": "1.0.31-nightly.3",
|
|
24
|
+
"integrity": "sha512-j86POD2U2yh7jiIqbIixDu8TpEpaVf+dHLs3adtTRfTs6V7ucl//digdkyc1uaZrUliRduxXKBOrb/1Nu648xQ=="
|
|
25
|
+
},
|
|
26
|
+
"@ours.network/fleet": {
|
|
27
|
+
"version": "1.2.0-nightly.3",
|
|
28
|
+
"integrity": "sha512-FzizLkh1fQ9RkzmID1v42QlZs9E0QHWJ44IWIWzYd0DRaetcXg/5FUVTNc82U5yWZyveBZTJq6mvg9H73NSlkw=="
|
|
29
|
+
},
|
|
30
|
+
"@ours.network/mcp": {
|
|
31
|
+
"version": "1.2.0-nightly.5",
|
|
32
|
+
"integrity": "sha512-CsRsHlJwU5WkUeF8mMLysq9GsYLBA2wnl8v+qZhaJZhSJWoNb5TqfXd2v5/Su7VUpOaRwvubv/9Jf86eQgqhGw=="
|
|
33
|
+
},
|
|
34
|
+
"@ours.network/codex": {
|
|
35
|
+
"version": "1.2.0-nightly.5",
|
|
36
|
+
"integrity": "sha512-6E3tE0valPE0FSOsONh3BrrBo9Gxt1IgWm5Hwj0xm00mGQ+qsjGRrzCB8snrOh9OdnGuvPOF20aunmp4g4KoMg=="
|
|
37
|
+
},
|
|
38
|
+
"@ours.network/claude-code": {
|
|
39
|
+
"version": "1.2.0-nightly.5",
|
|
40
|
+
"integrity": "sha512-PxPegFvXx5bkvTprB8dcPjIYvE5bvjQoAtw31onOAZa0YdX2K7yYpesYcZ0sOnFdSQkI4+aaVyTb1Lj9EiMABQ=="
|
|
41
|
+
},
|
|
42
|
+
"@ours.network/daemon": {
|
|
43
|
+
"version": "3.8.1-nightly.1",
|
|
44
|
+
"integrity": "sha512-1+RA0UH/wJUSmKnmZuHS3v6UyH5PsrZWoqwa5rFKl6n2Cd8sWDs++DrwwV0IOFdgFcDOtb/k8mes4sI2J7J8mg=="
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"hostCli": {
|
|
48
|
+
"@ours.network/sdk": {
|
|
49
|
+
"version": "3.8.1-nightly.9",
|
|
50
|
+
"integrity": "sha512-9xh/7hWZ8zRV+8Vimu7OTf2qUAp4o4MJFxlCVtyi1YO0WyFFgk+dZcT8nWlXY/JWIPFdb8wnYW/OSoqGUAw4Hw=="
|
|
51
|
+
},
|
|
52
|
+
"@ours.network/cli": {
|
|
53
|
+
"version": "2.8.1-nightly.7",
|
|
54
|
+
"integrity": "sha512-Kq3iySBsOgazTxawxY8aFvaMTQ6fnkPZ2KmO/k6NhPaVpZbmBcL/9OWefKYEUlDMvuSOEVzjQLZa0Wac4oopuA=="
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -7,7 +7,7 @@ export const ROOT = resolve(process.env.OURS_BUILD_ROOT || '/opt/ours');
|
|
|
7
7
|
export const SOURCE_ROOT = resolve(process.env.OURS_SOURCE_ROOT || '/src');
|
|
8
8
|
export const OUT = join(ROOT, 'docker/vendor');
|
|
9
9
|
export const CONFIG = JSON.parse(readFileSync(join(ROOT, 'sources.json'), 'utf8'));
|
|
10
|
-
export const SELECTED = new Set((process.env.OURS_BUILD_PACKAGES || 'sdk,cli,
|
|
10
|
+
export const SELECTED = new Set((process.env.OURS_BUILD_PACKAGES || 'sdk,cli,daemon,tg-connector,cowork,messenger-server').split(',').map(name => '@ours.network/' + name));
|
|
11
11
|
export const inheritedLock = process.env.OURS_INSTALLER_LOCK_FD === '3' ? [3] : [];
|
|
12
12
|
export const run = (args, cwd) => execFileSync(args[0], args.slice(1), { cwd, stdio: ['inherit', 'inherit', 'inherit', ...inheritedLock] });
|
|
13
13
|
export const capture = (args, cwd, env = process.env) => execFileSync(args[0], args.slice(1), { cwd, env, encoding: 'utf8', stdio: ['ignore', 'pipe', 'inherit', ...inheritedLock] });
|
|
@@ -2,8 +2,13 @@ import { join } from 'node:path';
|
|
|
2
2
|
import { SOURCE_ROOT, SELECTED, pack, run } from './build-common.mjs';
|
|
3
3
|
const source = join(SOURCE_ROOT, 'sdk');
|
|
4
4
|
run(['npm', 'ci', '--no-audit', '--no-fund'], source);
|
|
5
|
-
run(['bash', 'scripts/compile-mufl.sh'], source);
|
|
5
|
+
if (SELECTED.has('@ours.network/daemon')) run(['bash', 'scripts/compile-mufl.sh'], source);
|
|
6
6
|
run(['npm', 'run', 'build'], source);
|
|
7
7
|
if (SELECTED.has('@ours.network/cli')) run(['npm', 'run', 'build:cli'], source);
|
|
8
8
|
pack(source, '@ours.network/sdk', 'sdk');
|
|
9
9
|
pack(join(source, 'packages/cli'), '@ours.network/cli', 'sdk');
|
|
10
|
+
|
|
11
|
+
if (SELECTED.has('@ours.network/daemon')) {
|
|
12
|
+
run(['npm', 'run', 'build:daemon'], source);
|
|
13
|
+
pack(join(source, 'packages/daemon'), '@ours.network/daemon', 'sdk');
|
|
14
|
+
}
|
|
@@ -4,6 +4,7 @@ import { join, resolve } from 'node:path';
|
|
|
4
4
|
import { pathToFileURL } from 'node:url';
|
|
5
5
|
import { execFileSync } from 'node:child_process';
|
|
6
6
|
import { createBuildContext, CONTEXT } from '../maintenance/build-context.mjs';
|
|
7
|
+
import { verifyRuntimeRelease } from '../maintenance/release-graph.mjs';
|
|
7
8
|
export function finalizeBuild(root) {
|
|
8
9
|
if (fs.realpathSync(root) !== root) throw new Error('Build root must be canonical');
|
|
9
10
|
try { fs.lstatSync(join(root, CONTEXT)); throw new Error('Existing context cannot be regenerated'); }
|
|
@@ -20,6 +21,7 @@ export function finalizeBuild(root) {
|
|
|
20
21
|
if (typeof spec !== 'string' || !/^file:docker\/vendor\/ours\.network-[a-z-]+\.tgz$/.test(spec)) throw new Error('Unexpected installer vendor reference');
|
|
21
22
|
protect(join(root, spec.slice(5)));
|
|
22
23
|
}
|
|
24
|
+
verifyRuntimeRelease(root);
|
|
23
25
|
const tree = execFileSync('npm', ['ls', '--omit=dev', '--all', '--json'], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
24
26
|
JSON.parse(tree);
|
|
25
27
|
const path = join(root, 'dependency-tree.json');
|
|
@@ -8,7 +8,7 @@ import { semanticRecordEqual } from './provenance-compare.mjs';
|
|
|
8
8
|
export const BASE_RECORDS = ['package-lock.json', 'dependency-tree.json'];
|
|
9
9
|
export const CONTEXT = 'build-context.json';
|
|
10
10
|
const admitted = new WeakMap();
|
|
11
|
-
const packages = new Set(['sdk', 'cli', 'mcp', 'tg-connector', 'cowork', 'messenger-server', 'fleet', 'codex', 'claude-code', 'install'].map(n => '@ours.network/' + n));
|
|
11
|
+
const packages = new Set(['sdk', 'cli', 'daemon', 'mcp', 'tg-connector', 'cowork', 'messenger-server', 'fleet', 'codex', 'claude-code', 'install'].map(n => '@ours.network/' + n));
|
|
12
12
|
const digest = (bytes, algorithm = 'sha256', encoding = 'hex') => createHash(algorithm).update(bytes).digest(encoding);
|
|
13
13
|
const fail = text => { throw new Error(`Build context: ${text}`); };
|
|
14
14
|
const keys = (v, expected) => v && !Array.isArray(v) && typeof v === 'object' && Object.keys(v).sort().join('\0') === [...expected].sort().join('\0');
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
// Native callers provide an exact selected executable. Compose mounts the
|
|
5
|
+
// selected runtime and supplies its bin directory, including retained releases.
|
|
6
|
+
export function daemonOwner(env = process.env) {
|
|
7
|
+
if (env.OURS_CLI_PATH) return env.OURS_CLI_PATH;
|
|
8
|
+
const bin = env.OURS_DAEMON_BIN_DIR;
|
|
9
|
+
if (!bin) throw new Error('daemon maintenance requires its selected executable or bin directory');
|
|
10
|
+
const daemon = join(bin, 'ours-daemon');
|
|
11
|
+
if (existsSync(daemon)) return daemon;
|
|
12
|
+
const legacy = join(bin, 'ours');
|
|
13
|
+
if (existsSync(legacy)) return legacy;
|
|
14
|
+
throw new Error('selected runtime contains no daemon administrator');
|
|
15
|
+
}
|
|
@@ -6,6 +6,8 @@ import { pathToFileURL } from 'node:url';
|
|
|
6
6
|
import { copyPrivateTree, scanSource, createArchive, validateArchive } from './state-archive.mjs';
|
|
7
7
|
import { publishNoReplace } from './state-native.mjs';
|
|
8
8
|
|
|
9
|
+
import { daemonOwner } from './daemon-owner.mjs';
|
|
10
|
+
|
|
9
11
|
const COMPONENTS = ['daemon', 'telegram', 'cowork', 'messenger'];
|
|
10
12
|
import { recordNames, readBuildRecords, validateBuildRecordSet } from './build-context.mjs';
|
|
11
13
|
const DAEMON_STATE = '/var/lib/ours';
|
|
@@ -207,7 +209,7 @@ export async function runDockerLayoutCommand(argv, env = process.env) {
|
|
|
207
209
|
const build = env.OURS_BUILD_ROOT || '/opt/ours';
|
|
208
210
|
const options = {
|
|
209
211
|
uid: process.getuid(), gid: process.getgid(), instanceId: env.OURS_DAEMON_ID,
|
|
210
|
-
cli:
|
|
212
|
+
cli: daemonOwner({ OURS_DAEMON_BIN_DIR: '/opt/ours/node_modules/.bin', ...env }),
|
|
211
213
|
configPath: env.OURS_DAEMON_CONFIG || '/var/lib/ours/config.json',
|
|
212
214
|
provenance: readBuildRecords(build),
|
|
213
215
|
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/** Enforce the selected ours artifacts; this is not third-party lock replay. */
|
|
2
|
+
import { existsSync, lstatSync, readFileSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { join, resolve, relative, isAbsolute } from 'node:path';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
|
|
6
|
+
const names = ['sdk', 'cli', 'tg-connector', 'cowork', 'messenger-server', 'fleet', 'mcp', 'codex', 'claude-code'].map(name => '@ours.network/' + name);
|
|
7
|
+
const stable = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
8
|
+
const nightly = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)-nightly\.(0|[1-9]\d*)(?:\.[0-9a-f]{7,40})?$/;
|
|
9
|
+
const fail = message => { throw new Error(`Release graph refused: ${message}`); };
|
|
10
|
+
function file(path) {
|
|
11
|
+
if (!lstatSync(path).isFile() || realpathSync(path) !== path) fail(`non-regular or linked file: ${path}`);
|
|
12
|
+
return readFileSync(path);
|
|
13
|
+
}
|
|
14
|
+
const json = path => JSON.parse(file(path));
|
|
15
|
+
|
|
16
|
+
export function releaseBinding(policy) {
|
|
17
|
+
// Only an absent binding denotes explicit development/retained legacy inputs.
|
|
18
|
+
if (!Object.hasOwn(policy ?? {}, 'release')) return null;
|
|
19
|
+
const release = policy.release;
|
|
20
|
+
if (release?.schema !== 1 || !['stable', 'nightly'].includes(release.channel)) fail('invalid release binding');
|
|
21
|
+
const pattern = release.channel === 'nightly' ? nightly : stable;
|
|
22
|
+
if (typeof release.installerVersion !== 'string' || !pattern.test(release.installerVersion)) fail('invalid installer version/channel');
|
|
23
|
+
if (release.scope !== undefined && release.scope !== 'host-cli') fail('invalid release scope');
|
|
24
|
+
const required = release.scope === 'host-cli' ? ['@ours.network/cli', '@ours.network/sdk'] : [...names, ...(release.packages?.['@ours.network/daemon'] ? ['@ours.network/daemon'] : [])];
|
|
25
|
+
if (!release.packages || JSON.stringify(Object.keys(release.packages).sort()) !== JSON.stringify([...required].sort())) fail('release must select exactly the required ours packages');
|
|
26
|
+
if (release.scope === 'host-cli' && Object.hasOwn(release, 'hostCli')) fail('nested host CLI selection');
|
|
27
|
+
for (const [name, entry] of Object.entries(release.packages)) {
|
|
28
|
+
if (typeof entry?.version !== 'string' || !pattern.test(entry.version) || !/^sha512-[A-Za-z0-9+/]{86}==$/.test(entry.integrity ?? '')) fail(`invalid release artifact: ${name}`);
|
|
29
|
+
}
|
|
30
|
+
if (Object.hasOwn(release, 'hostCli')) {
|
|
31
|
+
if (!release.hostCli || Object.keys(release.hostCli).sort().join(',') !== '@ours.network/cli,@ours.network/sdk') fail('host CLI must select exactly CLI and SDK');
|
|
32
|
+
for (const [name, entry] of Object.entries(release.hostCli)) {
|
|
33
|
+
if (typeof entry?.version !== 'string' || !pattern.test(entry.version) || !/^sha512-[A-Za-z0-9+/]{86}==$/.test(entry.integrity ?? '')) fail(`invalid host CLI artifact: ${name}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (!policy.packages || !Object.keys(policy.packages).length) fail('missing release source selection');
|
|
37
|
+
for (const [name, entry] of Object.entries(policy.packages)) {
|
|
38
|
+
if (entry?.type !== 'npm' || entry.version !== release.packages[name]?.version || Object.keys(entry).sort().join(',') !== 'type,version') fail(`source selection differs from release: ${name}`);
|
|
39
|
+
}
|
|
40
|
+
return release;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function verifyReleaseGraph(root, policy, { requiredPackages = Object.keys(policy?.packages ?? {}) } = {}) {
|
|
44
|
+
const release = releaseBinding(policy);
|
|
45
|
+
if (!release) return { verified: false, reason: 'development or retained legacy source selection' };
|
|
46
|
+
root = resolve(root);
|
|
47
|
+
if (realpathSync(root) !== root) fail('build root is not canonical');
|
|
48
|
+
const manifest = json(join(root, 'package.json'));
|
|
49
|
+
if (manifest.name?.startsWith('@ours.network/') && manifest.version !== release.packages[manifest.name]?.version) fail(`root package differs from release: ${manifest.name}`);
|
|
50
|
+
const locks = ['package-lock.json', 'node_modules/.package-lock.json'].map(path => {
|
|
51
|
+
const lock = json(join(root, path));
|
|
52
|
+
if (lock.lockfileVersion !== 3 || !lock.packages || Array.isArray(lock.packages)) fail(`invalid lock: ${path}`);
|
|
53
|
+
const entries = new Map();
|
|
54
|
+
for (const [location, entry] of Object.entries(lock.packages)) {
|
|
55
|
+
const match = location.match(/(?:^|\/)node_modules\/(@ours\.network\/[^/]+)$/);
|
|
56
|
+
if (!match) continue;
|
|
57
|
+
const name = match[1], expected = release.packages[name];
|
|
58
|
+
if (!expected || entry?.link || entry.version !== expected.version || entry.integrity !== expected.integrity) fail(`version/integrity mismatch: ${path}:${location}`);
|
|
59
|
+
const installed = resolve(root, location), rel = relative(root, installed);
|
|
60
|
+
if (!rel || rel.startsWith('..') || isAbsolute(rel)) fail(`escaping lock location: ${location}`);
|
|
61
|
+
if (typeof entry.resolved !== 'string') fail(`missing artifact location: ${location}`);
|
|
62
|
+
if (entry.resolved.startsWith('file:')) {
|
|
63
|
+
const archive = resolve(root, entry.resolved.slice(5));
|
|
64
|
+
const archiveRel = relative(root, archive);
|
|
65
|
+
if (archiveRel.startsWith('..') || isAbsolute(archiveRel) || !archive.endsWith('.tgz')) fail(`unbound local artifact: ${location}`);
|
|
66
|
+
const integrity = 'sha512-' + createHash('sha512').update(file(archive)).digest('base64');
|
|
67
|
+
if (integrity !== expected.integrity) fail(`vendor bytes differ from release: ${location}`);
|
|
68
|
+
} else if (!entry.resolved.startsWith(`https://registry.npmjs.org/${name}/-/`)) fail(`non-official artifact: ${location}`);
|
|
69
|
+
entries.set(location, { name, entry, installed });
|
|
70
|
+
}
|
|
71
|
+
return entries;
|
|
72
|
+
});
|
|
73
|
+
const [declared, installed] = locks;
|
|
74
|
+
const direct = Object.entries(manifest.dependencies ?? {}).filter(([name]) => name.startsWith('@ours.network/'));
|
|
75
|
+
if (!manifest.name?.startsWith('@ours.network/') && JSON.stringify(direct.map(([name]) => name).sort()) !== JSON.stringify([...requiredPackages].sort())) fail('direct package set differs from release source selection');
|
|
76
|
+
for (const [name, spec] of direct) {
|
|
77
|
+
const expected = release.packages[name], locked = declared.get(`node_modules/${name}`)?.entry;
|
|
78
|
+
if (!expected || (spec !== expected.version && !(typeof spec === 'string' && spec.startsWith('file:') && spec === locked?.resolved))) fail(`direct package spec differs from release: ${name}`);
|
|
79
|
+
}
|
|
80
|
+
for (const name of Object.keys(manifest.dependencies ?? {}).filter(name => name.startsWith('@ours.network/'))) {
|
|
81
|
+
if (!declared.has(`node_modules/${name}`) || !installed.has(`node_modules/${name}`)) fail(`missing direct release package: ${name}`);
|
|
82
|
+
}
|
|
83
|
+
for (const [location, value] of declared) {
|
|
84
|
+
// npm's root lock retains dev-only entries omitted from a production install.
|
|
85
|
+
if (!installed.has(location) && !value.entry.dev && !value.entry.optional) fail(`installed release entry missing: ${location}`);
|
|
86
|
+
}
|
|
87
|
+
for (const [location, value] of installed) {
|
|
88
|
+
const expected = declared.get(location);
|
|
89
|
+
if (!expected || expected.entry.version !== value.entry.version || expected.entry.integrity !== value.entry.integrity || expected.entry.resolved !== value.entry.resolved) fail(`installed/root lock mismatch: ${location}`);
|
|
90
|
+
if (!lstatSync(value.installed).isDirectory() || realpathSync(value.installed) !== value.installed) fail(`linked installed package: ${location}`);
|
|
91
|
+
const actual = json(join(value.installed, 'package.json'));
|
|
92
|
+
if (actual.name !== value.name || actual.version !== value.entry.version) fail(`installed package differs from release: ${location}`);
|
|
93
|
+
}
|
|
94
|
+
return { verified: true, packages: installed.size };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function verifyRuntimeRelease(root) {
|
|
98
|
+
const path = join(resolve(root), 'sources.json');
|
|
99
|
+
// Historic installations predate sources/release bindings; their existing
|
|
100
|
+
// provenance and conversion checks remain authoritative until explicit update.
|
|
101
|
+
if (!existsSync(path)) return { verified: false, reason: 'legacy runtime without sources' };
|
|
102
|
+
return verifyReleaseGraph(root, json(path));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The client command has its own exact dependency selection, never a graph bypass. */
|
|
106
|
+
export function hostCliPolicy(policy) {
|
|
107
|
+
const release = releaseBinding(policy);
|
|
108
|
+
if (!release?.hostCli) return null;
|
|
109
|
+
const { hostCli, ...base } = release;
|
|
110
|
+
return { release: { ...base, scope: 'host-cli', packages: hostCli }, packages: Object.fromEntries(Object.entries(hostCli).map(([name, p]) => [name, { type: 'npm', version: p.version }])) };
|
|
111
|
+
}
|
|
@@ -9,6 +9,8 @@ import { createArchive, extractArchive, validateArchive, scanSource, copyPrivate
|
|
|
9
9
|
import { exchange, tryLock, setMtimeNs } from './state-native.mjs';
|
|
10
10
|
import { recordNames, readBuildRecords, equalBuildRecords, initializeBuildMarker } from './build-context.mjs';
|
|
11
11
|
|
|
12
|
+
import { daemonOwner } from './daemon-owner.mjs';
|
|
13
|
+
|
|
12
14
|
const PROVENANCE = '.ours-provenance';
|
|
13
15
|
const APPLICATIONS = ['daemon', 'telegram', 'cowork', 'messenger'];
|
|
14
16
|
const exists = path => { try { fs.lstatSync(path); return true; } catch (e) { if (e.code === 'ENOENT') return false; throw e; } };
|
|
@@ -145,9 +147,9 @@ export async function runStateOperation(argv, env = process.env, checkpoints = {
|
|
|
145
147
|
}
|
|
146
148
|
function retainAuthority(staging) {
|
|
147
149
|
if (!['daemon', 'server'].includes(domain)) return;
|
|
148
|
-
if (!env.
|
|
150
|
+
if (!env.OURS_DAEMON_CONFIG) fail('daemon maintenance requires its selected executable and configuration');
|
|
149
151
|
const daemon = commonTree ? join(staging, 'daemon') : staging;
|
|
150
|
-
owner([env
|
|
152
|
+
owner([daemonOwner(env), 'config', 'access-retain', '--config', env.OURS_DAEMON_CONFIG, '--target-state-dir', daemon, '--json']);
|
|
151
153
|
if (!commonTree) return;
|
|
152
154
|
const credentials = join(live, 'credentials'), destination = join(staging, 'credentials');
|
|
153
155
|
for (const name of ['telegram', 'cowork', 'messenger']) if (!privateStat(join(credentials, name, 'daemon-token'), false, 0o600).size) fail('current managed credential is empty');
|
|
@@ -2,7 +2,7 @@ import { readBuildRecords, initializeBuildMarker } from '../maintenance/build-co
|
|
|
2
2
|
import { spawnSync } from 'node:child_process';
|
|
3
3
|
import { randomBytes } from 'node:crypto';
|
|
4
4
|
import {
|
|
5
|
-
closeSync, constants, fstatSync, lstatSync, mkdirSync, openSync, readFileSync,
|
|
5
|
+
existsSync, closeSync, constants, fstatSync, lstatSync, mkdirSync, openSync, readFileSync,
|
|
6
6
|
realpathSync, renameSync, unlinkSync, writeFileSync, readdirSync, chmodSync, chownSync,
|
|
7
7
|
} from 'node:fs';
|
|
8
8
|
|
|
@@ -67,7 +67,7 @@ function validateCommon(idName = 'OURS_DAEMON_ID') {
|
|
|
67
67
|
|
|
68
68
|
function composeConfig(domain) {
|
|
69
69
|
if (domain === 'daemon') {
|
|
70
|
-
const value = { stateDir: '/var/lib/ours', port: 3050, apiVisibility: 'owner'
|
|
70
|
+
const value = { stateDir: '/var/lib/ours', port: 3050, apiVisibility: 'owner' };
|
|
71
71
|
if (process.env.OURS_BROKER_URL) value.brokerUrl = process.env.OURS_BROKER_URL;
|
|
72
72
|
return value;
|
|
73
73
|
}
|
|
@@ -155,7 +155,7 @@ async function telegramInput() {
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
function cliJson(args) {
|
|
158
|
-
const result = spawnSync(process.execPath, ['/opt/ours/node_modules/@ours.network/cli/dist/cli.js', ...args], {
|
|
158
|
+
const result = spawnSync(process.execPath, [existsSync('/opt/ours/node_modules/@ours.network/daemon/dist/cli.js') ? '/opt/ours/node_modules/@ours.network/daemon/dist/cli.js' : '/opt/ours/node_modules/@ours.network/cli/dist/cli.js', ...args], {
|
|
159
159
|
encoding: 'utf8', timeout: 60_000, maxBuffer: 1024 * 1024,
|
|
160
160
|
});
|
|
161
161
|
if (result.status !== 0) fail('Official OURS CLI operation failed');
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/** Import a stopped, read-only host migration stage into the managed Docker volume. */
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import { dirname, join, resolve, isAbsolute } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { randomUUID } from 'node:crypto';
|
|
6
|
+
// Repository layout and the maintenance image intentionally place this binding differently.
|
|
7
|
+
const nativeModule = new URL('../maintenance/state-native.mjs', import.meta.url);
|
|
8
|
+
const { publishNoReplace } = await import(fs.existsSync(nativeModule) ? nativeModule.href : new URL('./state-native.mjs', import.meta.url).href);
|
|
9
|
+
|
|
10
|
+
const MARKER = '.ours-legacy-import.json';
|
|
11
|
+
const COMPONENTS = ['mcp', 'daemon'];
|
|
12
|
+
const fail = message => { throw Error(`Legacy volume import: ${message}`); };
|
|
13
|
+
function canonical(path) {
|
|
14
|
+
if (typeof path !== 'string' || !isAbsolute(path) || resolve(path) !== path) fail('paths must be normalized absolute paths');
|
|
15
|
+
if (fs.realpathSync(path) !== path) fail('symlink paths are not supported');
|
|
16
|
+
}
|
|
17
|
+
function privateEntry(path, uid, gid, directory) {
|
|
18
|
+
const stat = fs.lstatSync(path);
|
|
19
|
+
if (!(directory ? stat.isDirectory() : stat.isFile()) || stat.isSymbolicLink()
|
|
20
|
+
|| stat.uid !== uid || stat.gid !== gid
|
|
21
|
+
|| (directory ? (stat.mode & 0o7777) !== 0o700 : ((stat.mode & 0o7077) !== 0 || !(stat.mode & 0o400)))
|
|
22
|
+
|| (!directory && stat.nlink !== 1)) fail('unsafe ownership, permissions, or linked state entry');
|
|
23
|
+
return stat;
|
|
24
|
+
}
|
|
25
|
+
function scan(root, uid, gid) {
|
|
26
|
+
const entries = [];
|
|
27
|
+
function visit(path, relative) {
|
|
28
|
+
const stat = fs.lstatSync(path), directory = stat.isDirectory();
|
|
29
|
+
privateEntry(path, uid, gid, directory); entries.push({ relative, directory, mode: stat.mode & 0o700 });
|
|
30
|
+
if (directory) for (const name of fs.readdirSync(path).sort()) visit(join(path, name), relative ? join(relative, name) : name);
|
|
31
|
+
}
|
|
32
|
+
canonical(root); visit(root, ''); return entries;
|
|
33
|
+
}
|
|
34
|
+
function readReceipt(root, uid, gid, targetRoot) {
|
|
35
|
+
privateEntry(join(root, MARKER), uid, gid, false);
|
|
36
|
+
const value = JSON.parse(fs.readFileSync(join(root, MARKER), 'utf8'));
|
|
37
|
+
const keys = ['sourceStateDir', 'sourceConfigPath', 'targetRoot', 'rootName'];
|
|
38
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)
|
|
39
|
+
|| Object.keys(value).sort().join('\0') !== [...keys].sort().join('\0')
|
|
40
|
+
|| keys.some(key => typeof value[key] !== 'string' || !value[key])
|
|
41
|
+
|| value.targetRoot !== targetRoot
|
|
42
|
+
|| ['sourceStateDir', 'sourceConfigPath', 'targetRoot'].some(key => !isAbsolute(value[key]) || resolve(value[key]) !== value[key])) fail('invalid or mismatched migration receipt');
|
|
43
|
+
return Object.fromEntries(keys.map(key => [key, value[key]]));
|
|
44
|
+
}
|
|
45
|
+
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
46
|
+
function existingDestination(path, expected, uid, gid, targetRoot) {
|
|
47
|
+
if (!fs.existsSync(path)) return false;
|
|
48
|
+
canonical(path); privateEntry(path, uid, gid, true);
|
|
49
|
+
if (!fs.readdirSync(path).length) return false;
|
|
50
|
+
if (!fs.existsSync(join(path, MARKER)) || !same(readReceipt(path, uid, gid, targetRoot), expected)) fail('nonmatching existing destination');
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
function makeDirectory(path, uid, gid) {
|
|
54
|
+
if (!fs.existsSync(path)) { fs.mkdirSync(path, { mode: 0o700 }); fs.chownSync(path, uid, gid); }
|
|
55
|
+
canonical(path); privateEntry(path, uid, gid, true);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Caller holds installation exclusion and has stopped all destination writers. */
|
|
59
|
+
export function importLegacyVolume({ sourceRoot = '/legacy-import', storageRoot = '/storage', targetRoot, uid, gid }) {
|
|
60
|
+
if (![uid, gid].every(value => Number.isSafeInteger(value) && value >= 0)) fail('numeric target UID/GID required');
|
|
61
|
+
if (typeof targetRoot !== 'string' || !isAbsolute(targetRoot) || resolve(targetRoot) !== targetRoot) fail('expected target root required');
|
|
62
|
+
canonical(sourceRoot); canonical(storageRoot);
|
|
63
|
+
const sourceOwner = fs.lstatSync(sourceRoot);
|
|
64
|
+
privateEntry(sourceRoot, sourceOwner.uid, sourceOwner.gid, true);
|
|
65
|
+
const sources = COMPONENTS.map(component => {
|
|
66
|
+
const path = join(sourceRoot, component);
|
|
67
|
+
const entries = scan(path, sourceOwner.uid, sourceOwner.gid);
|
|
68
|
+
return { component, path, entries, receipt: readReceipt(path, sourceOwner.uid, sourceOwner.gid, targetRoot) };
|
|
69
|
+
});
|
|
70
|
+
if (!same(sources[0].receipt, sources[1].receipt)) fail('source daemon and MCP receipts differ');
|
|
71
|
+
const rootStat = fs.lstatSync(storageRoot);
|
|
72
|
+
const freshRoot = rootStat.isDirectory() && rootStat.uid === 0 && rootStat.gid === 0 && fs.readdirSync(storageRoot).length === 0;
|
|
73
|
+
if (!freshRoot) privateEntry(storageRoot, uid, gid, true);
|
|
74
|
+
const state = join(storageRoot, 'state');
|
|
75
|
+
if (fs.existsSync(state)) { canonical(state); privateEntry(state, uid, gid, true); }
|
|
76
|
+
// Validate every existing destination before changing even an empty volume.
|
|
77
|
+
for (const source of sources) source.reused = existingDestination(join(state, source.component), source.receipt, uid, gid, targetRoot);
|
|
78
|
+
if (freshRoot) { fs.chmodSync(storageRoot, 0o700); fs.chownSync(storageRoot, uid, gid); }
|
|
79
|
+
makeDirectory(state, uid, gid);
|
|
80
|
+
for (const source of sources) {
|
|
81
|
+
if (source.reused) continue;
|
|
82
|
+
const destination = join(state, source.component), staging = join(state, `.legacy-import-${randomUUID()}`);
|
|
83
|
+
try {
|
|
84
|
+
fs.cpSync(source.path, staging, { recursive: true, errorOnExist: true, force: false });
|
|
85
|
+
for (const entry of [...source.entries].reverse()) {
|
|
86
|
+
const path = entry.relative ? join(staging, entry.relative) : staging;
|
|
87
|
+
fs.chmodSync(path, entry.mode); fs.chownSync(path, uid, gid);
|
|
88
|
+
}
|
|
89
|
+
scan(staging, uid, gid);
|
|
90
|
+
if (!same(readReceipt(staging, uid, gid, targetRoot), source.receipt)) fail('copied receipt changed');
|
|
91
|
+
if (fs.existsSync(destination)) fs.rmdirSync(destination); // empty skeleton only
|
|
92
|
+
publishNoReplace(staging, destination);
|
|
93
|
+
} finally { fs.rmSync(staging, { recursive: true, force: true }); }
|
|
94
|
+
}
|
|
95
|
+
return { imported: sources.filter(source => !source.reused).map(source => source.component), retained: sources.filter(source => source.reused).map(source => source.component) };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
99
|
+
try {
|
|
100
|
+
importLegacyVolume({ targetRoot: process.env.OURS_LEGACY_TARGET_ROOT, uid: Number(process.env.OURS_UID), gid: Number(process.env.OURS_GID) });
|
|
101
|
+
console.log('Legacy daemon and MCP state imported into the selected volume.');
|
|
102
|
+
} catch (error) { console.error(error.message); process.exitCode = 1; }
|
|
103
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readBuildRecords, initializeBuildMarker } from '../maintenance/build-context.mjs';
|
|
2
|
+
import { verifyRuntimeRelease } from '../maintenance/release-graph.mjs';
|
|
2
3
|
import { accessSync, constants, lstatSync, readFileSync } from 'node:fs';
|
|
3
4
|
|
|
4
5
|
export function privatePath(path, directory = false, writable = false) {
|
|
@@ -43,5 +44,6 @@ export function checkCredential(path) {
|
|
|
43
44
|
// Called with the startup state-directory lock held. These are build records,
|
|
44
45
|
// not a storage schema or a declaration that arbitrary upgrades are compatible.
|
|
45
46
|
export function recordBuild(state) {
|
|
47
|
+
verifyRuntimeRelease('/opt/ours');
|
|
46
48
|
initializeBuildMarker(`${state}/.ours-provenance`, readBuildRecords('/opt/ours'));
|
|
47
49
|
}
|
package/assets/sources.json
CHANGED
|
@@ -1,21 +1,101 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
"release": {
|
|
3
|
+
"schema": 1,
|
|
4
|
+
"channel": "nightly",
|
|
5
|
+
"installerVersion": "1.2.1-nightly.10",
|
|
6
|
+
"packages": {
|
|
7
|
+
"@ours.network/sdk": {
|
|
8
|
+
"version": "3.8.1-nightly.9",
|
|
9
|
+
"integrity": "sha512-9xh/7hWZ8zRV+8Vimu7OTf2qUAp4o4MJFxlCVtyi1YO0WyFFgk+dZcT8nWlXY/JWIPFdb8wnYW/OSoqGUAw4Hw=="
|
|
10
|
+
},
|
|
11
|
+
"@ours.network/cli": {
|
|
12
|
+
"version": "2.8.1-nightly.7",
|
|
13
|
+
"integrity": "sha512-Kq3iySBsOgazTxawxY8aFvaMTQ6fnkPZ2KmO/k6NhPaVpZbmBcL/9OWefKYEUlDMvuSOEVzjQLZa0Wac4oopuA=="
|
|
14
|
+
},
|
|
15
|
+
"@ours.network/tg-connector": {
|
|
16
|
+
"version": "1.0.1-nightly.3",
|
|
17
|
+
"integrity": "sha512-UDHEgM5C3mnnyfV3KTM2XFDGZ7vYs7oBE/3r8bKDtxJdlQZ4bxxI44LKrPI3gO2sdGROsDdM8/tvzwbJ8MiBYw=="
|
|
18
|
+
},
|
|
19
|
+
"@ours.network/cowork": {
|
|
20
|
+
"version": "1.3.2-nightly.20260921.0d6bb50",
|
|
21
|
+
"integrity": "sha512-gyUgY7Fw6RQVhONqexLNcS8m0WuBUQiHYIrWqd2FeAyyJOONjYGfbRtgJ67Xin1YTYf8Cxlomuf6j8cEc4ZwZg=="
|
|
22
|
+
},
|
|
23
|
+
"@ours.network/messenger-server": {
|
|
24
|
+
"version": "1.0.31-nightly.3",
|
|
25
|
+
"integrity": "sha512-j86POD2U2yh7jiIqbIixDu8TpEpaVf+dHLs3adtTRfTs6V7ucl//digdkyc1uaZrUliRduxXKBOrb/1Nu648xQ=="
|
|
26
|
+
},
|
|
27
|
+
"@ours.network/fleet": {
|
|
28
|
+
"version": "1.2.0-nightly.3",
|
|
29
|
+
"integrity": "sha512-FzizLkh1fQ9RkzmID1v42QlZs9E0QHWJ44IWIWzYd0DRaetcXg/5FUVTNc82U5yWZyveBZTJq6mvg9H73NSlkw=="
|
|
30
|
+
},
|
|
31
|
+
"@ours.network/mcp": {
|
|
32
|
+
"version": "1.2.0-nightly.5",
|
|
33
|
+
"integrity": "sha512-CsRsHlJwU5WkUeF8mMLysq9GsYLBA2wnl8v+qZhaJZhSJWoNb5TqfXd2v5/Su7VUpOaRwvubv/9Jf86eQgqhGw=="
|
|
34
|
+
},
|
|
35
|
+
"@ours.network/codex": {
|
|
36
|
+
"version": "1.2.0-nightly.5",
|
|
37
|
+
"integrity": "sha512-6E3tE0valPE0FSOsONh3BrrBo9Gxt1IgWm5Hwj0xm00mGQ+qsjGRrzCB8snrOh9OdnGuvPOF20aunmp4g4KoMg=="
|
|
38
|
+
},
|
|
39
|
+
"@ours.network/claude-code": {
|
|
40
|
+
"version": "1.2.0-nightly.5",
|
|
41
|
+
"integrity": "sha512-PxPegFvXx5bkvTprB8dcPjIYvE5bvjQoAtw31onOAZa0YdX2K7yYpesYcZ0sOnFdSQkI4+aaVyTb1Lj9EiMABQ=="
|
|
42
|
+
},
|
|
43
|
+
"@ours.network/daemon": {
|
|
44
|
+
"version": "3.8.1-nightly.1",
|
|
45
|
+
"integrity": "sha512-1+RA0UH/wJUSmKnmZuHS3v6UyH5PsrZWoqwa5rFKl6n2Cd8sWDs++DrwwV0IOFdgFcDOtb/k8mes4sI2J7J8mg=="
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"hostCli": {
|
|
49
|
+
"@ours.network/sdk": {
|
|
50
|
+
"version": "3.8.1-nightly.9",
|
|
51
|
+
"integrity": "sha512-9xh/7hWZ8zRV+8Vimu7OTf2qUAp4o4MJFxlCVtyi1YO0WyFFgk+dZcT8nWlXY/JWIPFdb8wnYW/OSoqGUAw4Hw=="
|
|
52
|
+
},
|
|
53
|
+
"@ours.network/cli": {
|
|
54
|
+
"version": "2.8.1-nightly.7",
|
|
55
|
+
"integrity": "sha512-Kq3iySBsOgazTxawxY8aFvaMTQ6fnkPZ2KmO/k6NhPaVpZbmBcL/9OWefKYEUlDMvuSOEVzjQLZa0Wac4oopuA=="
|
|
56
|
+
}
|
|
57
|
+
}
|
|
9
58
|
},
|
|
10
59
|
"packages": {
|
|
11
|
-
"@ours.network/sdk": {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"@ours.network/
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
"@ours.network/
|
|
60
|
+
"@ours.network/sdk": {
|
|
61
|
+
"type": "npm",
|
|
62
|
+
"version": "3.8.1-nightly.9"
|
|
63
|
+
},
|
|
64
|
+
"@ours.network/cli": {
|
|
65
|
+
"type": "npm",
|
|
66
|
+
"version": "2.8.1-nightly.7"
|
|
67
|
+
},
|
|
68
|
+
"@ours.network/tg-connector": {
|
|
69
|
+
"type": "npm",
|
|
70
|
+
"version": "1.0.1-nightly.3"
|
|
71
|
+
},
|
|
72
|
+
"@ours.network/cowork": {
|
|
73
|
+
"type": "npm",
|
|
74
|
+
"version": "1.3.2-nightly.20260921.0d6bb50"
|
|
75
|
+
},
|
|
76
|
+
"@ours.network/messenger-server": {
|
|
77
|
+
"type": "npm",
|
|
78
|
+
"version": "1.0.31-nightly.3"
|
|
79
|
+
},
|
|
80
|
+
"@ours.network/fleet": {
|
|
81
|
+
"type": "npm",
|
|
82
|
+
"version": "1.2.0-nightly.3"
|
|
83
|
+
},
|
|
84
|
+
"@ours.network/mcp": {
|
|
85
|
+
"type": "npm",
|
|
86
|
+
"version": "1.2.0-nightly.5"
|
|
87
|
+
},
|
|
88
|
+
"@ours.network/codex": {
|
|
89
|
+
"type": "npm",
|
|
90
|
+
"version": "1.2.0-nightly.5"
|
|
91
|
+
},
|
|
92
|
+
"@ours.network/claude-code": {
|
|
93
|
+
"type": "npm",
|
|
94
|
+
"version": "1.2.0-nightly.5"
|
|
95
|
+
},
|
|
96
|
+
"@ours.network/daemon": {
|
|
97
|
+
"type": "npm",
|
|
98
|
+
"version": "3.8.1-nightly.1"
|
|
99
|
+
}
|
|
20
100
|
}
|
|
21
101
|
}
|
package/install.mjs
CHANGED
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
import { readFileSync } from 'node:fs';
|
|
4
4
|
|
|
5
5
|
import { realEffects } from './lib/effects.mjs';
|
|
6
|
-
import {
|
|
6
|
+
import { runSetup } from './lib/setup.mjs';
|
|
7
7
|
import { closeSync, makeWriter, openTty } from './lib/ui.mjs';
|
|
8
8
|
|
|
9
9
|
const version = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8')).version;
|
|
10
10
|
const ttyFd = openTty();
|
|
11
11
|
|
|
12
12
|
try {
|
|
13
|
-
const code = await
|
|
13
|
+
const code = await runSetup(process.argv.slice(2), realEffects({
|
|
14
14
|
write: makeWriter(ttyFd),
|
|
15
15
|
ttyFd,
|
|
16
16
|
env: process.env,
|
package/install.sh
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
# pipes a script into your shell). It just gets Node.js/npm sorted, then does the same
|
|
10
10
|
# `npm i -g @ours.network/install` and runs `ours-install`. Meant to be run as:
|
|
11
11
|
#
|
|
12
|
-
# curl -fsSL https://raw.githubusercontent.com/adapt-toolkit/ours-
|
|
12
|
+
# curl -fsSL https://raw.githubusercontent.com/adapt-toolkit/ours-network/main/install.sh | bash
|
|
13
13
|
#
|
|
14
14
|
# This file is a THIN bootstrap: it checks that Node.js + npm are present (and prints friendly,
|
|
15
15
|
# per-OS guidance if not), then runs the real experience — the Node installer (install.mjs) that
|