@ours.network/install 1.2.1-nightly.4 → 1.2.1-nightly.41
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/GATEWAY_SETUP.md +178 -0
- package/README.md +471 -9
- package/ROOTLESS_PODMAN_VALIDATION.md +210 -0
- package/assets/Dockerfile +14 -12
- package/assets/Dockerfile.gateway +5 -0
- package/assets/docker-compose.yaml +8 -1
- package/assets/fleet-settings.example.json +48 -0
- package/assets/gateway-entrypoint.sh +37 -0
- package/assets/release-lock.json +1177 -4508
- package/assets/release.json +37 -19
- package/assets/scripts/build/build-common.mjs +2 -1
- package/assets/scripts/build/build-sdk.mjs +6 -1
- 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 +7 -3
- package/assets/scripts/maintenance/recover-notifications.mjs +26 -0
- package/assets/scripts/maintenance/release-graph.mjs +18 -1
- package/assets/scripts/maintenance/state-operation.mjs +62 -5
- package/assets/scripts/runtime/README.md +7 -0
- package/assets/scripts/runtime/client-setup.mjs +80 -6
- package/assets/scripts/runtime/dependency-ready.mjs +13 -0
- package/assets/scripts/runtime/diagnostics.mjs +72 -0
- package/assets/scripts/runtime/entrypoint.sh +5 -1
- package/assets/scripts/runtime/health-messenger.mjs +7 -6
- package/assets/scripts/runtime/notifications-gateway.mjs +90 -0
- package/assets/scripts/runtime/recover.mjs +165 -0
- package/assets/scripts/runtime/start-cowork.sh +5 -0
- package/assets/scripts/runtime/start-messenger.sh +29 -4
- package/assets/scripts/runtime/start-telegram.sh +5 -0
- package/assets/scripts/runtime/supervise.mjs +35 -0
- package/assets/sources.json +54 -28
- package/install.mjs +7 -2
- package/lib/boot-readiness.mjs +43 -0
- package/lib/build-transition.mjs +16 -0
- package/lib/client-cli.mjs +127 -0
- package/lib/container-engine.mjs +127 -0
- package/lib/diagnostics.mjs +1 -0
- package/lib/docker-conversion-runtime.mjs +3 -2
- package/lib/docker-layout-installation.mjs +2 -1
- package/lib/docker-recovery.mjs +55 -0
- package/lib/docker-runtime-repair.mjs +160 -0
- package/lib/effects.mjs +357 -133
- package/lib/gateway-transition.mjs +87 -0
- package/lib/gateway.mjs +158 -0
- package/lib/legacy-migration.mjs +13 -2
- package/lib/orchestrate-uninstall.mjs +18 -6
- package/lib/orchestrate.mjs +193 -43
- package/lib/plan.mjs +27 -11
- package/lib/podman-preflight.mjs +40 -0
- package/lib/server-onboarding.mjs +164 -16
- package/lib/server-ports.mjs +17 -0
- package/lib/setup-options.mjs +99 -28
- package/lib/setup.mjs +35 -17
- package/lib/target.mjs +24 -8
- package/lib/uninstall.mjs +2 -2
- package/lib/usage.mjs +21 -7
- package/lib/workspace-setup.mjs +83 -0
- package/package.json +4 -2
package/lib/setup-options.mjs
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
|
+
import { hostname } from 'node:os';
|
|
2
|
+
import { serverBase } from './gateway.mjs';
|
|
1
3
|
import { join, resolve, isAbsolute, dirname } from 'node:path';
|
|
2
4
|
|
|
3
5
|
const scopes = ['all', 'server', 'client'];
|
|
4
6
|
const integrations = ['codex', 'claude-code', 'fleet'];
|
|
5
7
|
const valueFlags = new Map(Object.entries({
|
|
6
|
-
'--scope': 'scope', '--action': 'operation', '--mode': 'mode', '--state-dir': 'stateDir',
|
|
7
|
-
'--
|
|
8
|
+
'--container-engine': 'containerEngine', '--server-url': 'serverUrl', '--scope': 'scope', '--action': 'operation', '--mode': 'mode', '--state-dir': 'stateDir',
|
|
9
|
+
'--username': 'localUsername', '--name': 'name', '--surname': 'surname', '--hostname': 'hostname',
|
|
10
|
+
'--identity-name': 'identityName', '--integrations': 'integrations', '--fleet-settings': 'fleetSettingsPath', '--fleet-task-workflow': 'fleetTaskWorkflowAgents',
|
|
8
11
|
'--config': 'config', '--sources': 'sources', '--migrate-from': 'migrateFrom', '--port': 'port', '--cowork-port': 'coworkPort', '--messenger-port': 'messengerPort',
|
|
9
12
|
}));
|
|
10
|
-
const boolFlags = new Map([['--compatible', 'compatible'], ['--dry-run', 'dryRun'], ['--migrate', 'migrate']]);
|
|
13
|
+
const boolFlags = new Map([['--compatible', 'compatible'], ['--dry-run', 'dryRun'], ['--migrate', 'migrate'], ['--disable-fleet-agents-setup', 'disableFleetAgentsSetup']]);
|
|
11
14
|
const allowed = new Set([...valueFlags.values(), ...boolFlags.values(), 'interactive', 'explicitPorts']);
|
|
12
15
|
const defaults = { port: 3050, coworkPort: 3052, messengerPort: 8420 };
|
|
13
16
|
const paths = ['stateDir', 'config', 'sources', 'fleetSettingsPath', 'migrateFrom'];
|
|
@@ -15,7 +18,7 @@ const nonempty = value => typeof value === 'string' && value.trim().length > 0 &
|
|
|
15
18
|
|
|
16
19
|
export function recommendedMode({ platform, arch, release = '' } = {}) {
|
|
17
20
|
if (platform === 'linux' && /microsoft|wsl/i.test(release)) return { mode: 'docker', reason: 'Docker is recommended on Windows and WSL.' };
|
|
18
|
-
if (platform === 'linux' && arch === 'x64') return { mode: '
|
|
21
|
+
if (platform === 'linux' && arch === 'x64') return { mode: 'docker', reason: 'Docker provides the integrated gateway for full-stack setup; native mode is server-only.' };
|
|
19
22
|
if (platform === 'darwin') return { mode: 'docker', reason: 'Docker is recommended on macOS.' };
|
|
20
23
|
if (platform === 'win32') return { mode: 'docker', reason: 'Docker is recommended on Windows; run the installer inside WSL with Docker Desktop.' };
|
|
21
24
|
return { mode: 'docker', reason: 'Docker is recommended for this platform.' };
|
|
@@ -31,17 +34,51 @@ export function validateSetupOptions(input, { interactive = input?.interactive =
|
|
|
31
34
|
const client = options.scope !== 'server';
|
|
32
35
|
if (options.migrateFrom !== undefined && (!server || options.operation !== 'install')) throw new Error('--migrate-from requires server or all scope and operation install');
|
|
33
36
|
if (options.migrateFrom !== undefined && (!nonempty(options.migrateFrom) || !isAbsolute(options.migrateFrom))) throw new Error('--migrate-from requires an absolute daemon config path');
|
|
37
|
+
const suppliedHuman = ['localUsername', 'name', 'surname'].some(key => options[key] !== undefined);
|
|
38
|
+
if (suppliedHuman) {
|
|
39
|
+
for (const key of ['localUsername', 'name', 'surname']) if (!nonempty(options[key])) throw new Error(`Missing human profile option: ${key}`);
|
|
40
|
+
if (!/^[a-z0-9][a-z0-9-]{0,29}$/.test(options.localUsername)) throw new Error('Local username must contain 1–30 lowercase letters, digits or hyphens');
|
|
41
|
+
for (const key of ['name', 'surname']) {
|
|
42
|
+
if (options[key].trim().length > 100) throw new Error(`${key} must contain at most 100 characters`);
|
|
43
|
+
options[key] = options[key].trim();
|
|
44
|
+
}
|
|
45
|
+
// Name and Surname together name the person's own identity, which Messenger runs as.
|
|
46
|
+
const person = `${options.name} ${options.surname}`;
|
|
47
|
+
if ([...person].length > 64 || person !== person.normalize('NFC') || /[\\/\p{Cc}\p{Cf}\p{Cs}\p{Zl}\p{Zp}]/u.test(person))
|
|
48
|
+
throw new Error('Name and Surname together must contain at most 64 NFC characters, including the space between them, without slashes or control characters');
|
|
49
|
+
options.hostname = options.hostname?.trim() || defaultHostname();
|
|
50
|
+
if (!/^[a-z0-9][a-z0-9-]{0,29}$/.test(options.hostname)) throw new Error('Hostname must contain 1–30 lowercase letters, digits or hyphens');
|
|
51
|
+
const identity = `${options.localUsername}@${options.hostname}`;
|
|
52
|
+
if (options.migrateFrom === undefined && options.identityName !== undefined && options.identityName !== identity) throw new Error('--identity-name conflicts with username and hostname');
|
|
53
|
+
if (options.migrateFrom === undefined) options.identityName = identity;
|
|
54
|
+
} else if (options.hostname !== undefined) throw new Error('--hostname requires username, name and surname');
|
|
55
|
+
if (options.disableFleetAgentsSetup !== undefined && typeof options.disableFleetAgentsSetup !== 'boolean') throw new Error('disableFleetAgentsSetup must be a boolean');
|
|
56
|
+
if (options.disableFleetAgentsSetup && options.fleetSettingsPath) throw new Error('--disable-fleet-agents-setup conflicts with --fleet-settings');
|
|
57
|
+
if (options.fleetTaskWorkflowAgents !== undefined) {
|
|
58
|
+
// Fleet owns what the workflow is; the installer only carries which agents the operator named.
|
|
59
|
+
const agents = options.fleetTaskWorkflowAgents;
|
|
60
|
+
if (!Array.isArray(agents) || !agents.length || agents.some(name => typeof name !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(name)) || new Set(agents).size !== agents.length)
|
|
61
|
+
throw new Error('--fleet-task-workflow requires unique Fleet agent names separated by commas');
|
|
62
|
+
if (!options.integrations?.includes?.('fleet')) throw new Error('--fleet-task-workflow requires the fleet integration');
|
|
63
|
+
if (options.disableFleetAgentsSetup) throw new Error('--fleet-task-workflow conflicts with --disable-fleet-agents-setup: no agents are configured by this run');
|
|
64
|
+
options.fleetTaskWorkflowAgents = [...agents];
|
|
65
|
+
}
|
|
34
66
|
const missing = [];
|
|
35
67
|
if (server) for (const [key, flag] of [['mode', '--mode'], ['stateDir', '--state-dir'], ...(options.migrateFrom === undefined ? [['identityName', '--identity-name']] : [])]) if (!nonempty(options[key])) missing.push(flag);
|
|
36
68
|
if (client && options.integrations === undefined) missing.push('--integrations (use none to skip)');
|
|
37
69
|
if (!server && !nonempty(options.config)) missing.push('--config');
|
|
38
70
|
if (server && (options.operation === 'update' || options.migrateFrom !== undefined) && options.compatible !== true) missing.push('--compatible');
|
|
39
|
-
if (Array.isArray(options.integrations) && options.integrations.includes('fleet') && !interactive && !nonempty(options.fleetSettingsPath)) missing.push('--fleet-settings');
|
|
71
|
+
if (Array.isArray(options.integrations) && options.integrations.includes('fleet') && !interactive && !options.disableFleetAgentsSetup && !nonempty(options.fleetSettingsPath)) missing.push('--fleet-settings');
|
|
40
72
|
if (missing.length) throw new Error(`Missing required setup options: ${missing.join(', ')}`);
|
|
41
73
|
if (options.mode === 'native') options.mode = 'packages';
|
|
42
74
|
if (server && !['packages', 'docker'].includes(options.mode)) throw new Error('Mode must be packages (or native) or docker');
|
|
75
|
+
if (options.containerEngine !== undefined && (!server || options.mode !== 'docker' || !['docker', 'podman'].includes(options.containerEngine))) throw new Error('--container-engine docker|podman requires server container mode');
|
|
76
|
+
if (options.serverUrl !== undefined) {
|
|
77
|
+
if (!server || options.mode !== 'docker' || options.operation !== 'install') throw new Error('--server-url requires Docker server installation');
|
|
78
|
+
options.serverUrl = serverBase(options.serverUrl);
|
|
79
|
+
}
|
|
43
80
|
if (server && options.config !== undefined) throw new Error('--config is only valid for client scope');
|
|
44
|
-
if (!server) for (const key of ['mode', 'stateDir', 'identityName', ...Object.keys(defaults), 'compatible', 'migrate']) {
|
|
81
|
+
if (!server) for (const key of ['mode', 'stateDir', 'identityName', 'localUsername', 'name', 'surname', 'hostname', ...Object.keys(defaults), 'compatible', 'migrate']) {
|
|
45
82
|
if (options[key] !== undefined) throw new Error(`${key} is only valid for server or all scope`);
|
|
46
83
|
}
|
|
47
84
|
if (!client && (options.integrations !== undefined || options.fleetSettingsPath !== undefined)) throw new Error('--integrations and --fleet-settings require all or client scope');
|
|
@@ -84,7 +121,7 @@ function expandPaths(options, home) {
|
|
|
84
121
|
return output;
|
|
85
122
|
}
|
|
86
123
|
|
|
87
|
-
export function parseSetupArgs(argv, { home } = {}) {
|
|
124
|
+
export function parseSetupArgs(argv, { home, validate = true } = {}) {
|
|
88
125
|
if (!Array.isArray(argv) || !argv.every(arg => typeof arg === 'string')) throw new Error('Arguments must be strings');
|
|
89
126
|
const options = { interactive: false, explicitPorts: [] };
|
|
90
127
|
const seen = new Set();
|
|
@@ -107,6 +144,7 @@ export function parseSetupArgs(argv, { home } = {}) {
|
|
|
107
144
|
if (!nonempty(value) || value.startsWith('--')) throw new Error(`${flag} requires a value`);
|
|
108
145
|
const key = valueFlags.get(flag);
|
|
109
146
|
if (key === 'integrations') put(key, value === 'none' ? [] : value.split(',').map(item => item.trim()));
|
|
147
|
+
else if (key === 'fleetTaskWorkflowAgents') put(key, value.split(',').map(item => item.trim()));
|
|
110
148
|
else if (Object.hasOwn(defaults, key)) {
|
|
111
149
|
if (!/^[1-9]\d*$/.test(value)) throw new Error(`${flag} requires an integer port`);
|
|
112
150
|
put(key, Number(value)); options.explicitPorts.push(key);
|
|
@@ -115,30 +153,34 @@ export function parseSetupArgs(argv, { home } = {}) {
|
|
|
115
153
|
if (positional.length && scopes.includes(positional[0])) put('scope', positional.shift());
|
|
116
154
|
if (positional.length && ['install', 'update'].includes(positional[0])) put('operation', positional.shift());
|
|
117
155
|
if (positional.length) throw new Error(`Unexpected setup argument: ${positional.join(' ')}`);
|
|
118
|
-
return validateSetupOptions(expandPaths(options, home), { interactive: false });
|
|
156
|
+
return validate ? validateSetupOptions(expandPaths(options, home), { interactive: false }) : expandPaths(options, home);
|
|
119
157
|
}
|
|
120
158
|
|
|
121
|
-
export
|
|
159
|
+
export function defaultHostname() {
|
|
160
|
+
return hostname().toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/^[^a-z0-9]+/, '').slice(0, 30) || 'workspace';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function collectSetupOptions(effects, presets = {}) {
|
|
122
164
|
if (effects.interactive !== true) throw new Error('Interactive setup requires a TTY; provide the complete CLI options instead');
|
|
123
|
-
const options = { interactive: true, explicitPorts: [] };
|
|
165
|
+
const options = { ...presets, interactive: true, explicitPorts: presets.explicitPorts ?? [] };
|
|
124
166
|
const scopeChoices = [
|
|
125
167
|
{ value: 'all', label: 'Everything on this computer' },
|
|
126
168
|
{ value: 'server', label: 'Server only' },
|
|
127
169
|
{ value: 'client', label: 'Connect this computer to an existing server' },
|
|
128
170
|
];
|
|
129
171
|
effects.out('Choose what this computer should do. Everything runs the server here and connects your selected apps; client-only uses a server you already have.');
|
|
130
|
-
options.scope
|
|
172
|
+
options.scope ??= await effects.select('What would you like to set up?', scopeChoices, 'all');
|
|
131
173
|
if (!scopes.includes(options.scope)) throw new Error('Scope must be all, server, or client');
|
|
132
174
|
let existing;
|
|
133
175
|
let pendingMigration;
|
|
134
176
|
if (options.scope !== 'client') {
|
|
135
177
|
const recommendedRoot = join(effects.home, '.ours-install');
|
|
136
178
|
effects.out(`Stores the server programs and data, including identities and messages. The recommended folder is ${recommendedRoot}; choose another only if you want to manage its location yourself.`);
|
|
137
|
-
const location = await effects.select('Where should server programs and data be stored?', [
|
|
179
|
+
const location = options.stateDir ? 'preset' : await effects.select('Where should server programs and data be stored?', [
|
|
138
180
|
{ value: 'recommended', label: `Use the recommended folder (${recommendedRoot})` },
|
|
139
181
|
{ value: 'custom', label: 'Choose another folder' },
|
|
140
182
|
], 'recommended');
|
|
141
|
-
options.stateDir
|
|
183
|
+
options.stateDir ??= location === 'recommended' ? recommendedRoot : await effects.askLine('Folder for server programs and data: ', recommendedRoot);
|
|
142
184
|
options.stateDir = expandPaths({ stateDir: options.stateDir }, effects.home).stateDir;
|
|
143
185
|
existing = effects.readJson(join(options.stateDir, 'installation.json'));
|
|
144
186
|
const migrationJournal = existing?.legacyMigrationSource ? effects.readJson(join(options.stateDir, 'legacy-migration.json')) : null;
|
|
@@ -150,7 +192,7 @@ export async function collectSetupOptions(effects) {
|
|
|
150
192
|
? 'This folder already contains an installation. Update selects this installer’s release while retaining state; repair reinstalls the retained package selection.'
|
|
151
193
|
: options.scope === 'client' ? 'Install connects your selected apps. Update refreshes their packages while keeping the server connection.'
|
|
152
194
|
: 'Install creates a managed installation in the selected folder. Update requires an installation already recorded there.');
|
|
153
|
-
options.operation
|
|
195
|
+
options.operation ??= !existing && options.scope !== 'client' ? 'install' : await effects.select('What should happen?', [
|
|
154
196
|
{ value: 'install', label: pendingMigration ? 'Resume the unfinished migration' : existing ? 'Repair this installation' : 'Install and configure' },
|
|
155
197
|
{ value: 'update', label: 'Update an existing installation' },
|
|
156
198
|
], existing && !pendingMigration ? 'update' : 'install');
|
|
@@ -160,7 +202,8 @@ export async function collectSetupOptions(effects) {
|
|
|
160
202
|
const legacyState = nonempty(legacy?.stateDir) ? legacy.stateDir
|
|
161
203
|
: legacy && legacy.stateDir === undefined && nonempty(effects.readJson(join(effects.home, '.ours', 'root.json'))?.name)
|
|
162
204
|
? join(effects.home, '.ours') : null;
|
|
163
|
-
if (
|
|
205
|
+
if (options.migrateFrom) { /* Explicit selection is validated below. */ }
|
|
206
|
+
else if (pendingMigration) options.migrateFrom = pendingMigration;
|
|
164
207
|
else if (legacyState) {
|
|
165
208
|
effects.out(`Found an existing ours installation at ${legacyState}. Upgrade it to keep its identities, messages and settings. The old server will stop; its original state is kept as a recovery copy.`);
|
|
166
209
|
if (await effects.ask('Upgrade this existing ours installation and keep its data?', true)) options.migrateFrom = defaultConfig;
|
|
@@ -184,41 +227,69 @@ export async function collectSetupOptions(effects) {
|
|
|
184
227
|
if (!nonempty(root?.name)) throw new Error('Cannot read the existing Human identity name; choose the configuration file for a complete existing installation');
|
|
185
228
|
options.identityName = root.name;
|
|
186
229
|
effects.out(`Your existing Human identity, ${root.name}, will be retained; no replacement identity will be created. Migration keeps the original state, but compatibility with an older release cannot be guaranteed automatically.`);
|
|
187
|
-
options.compatible
|
|
230
|
+
options.compatible ??= await effects.ask('Proceed with this release and keep the original state as a recovery copy?', false);
|
|
188
231
|
if (!options.compatible) throw new Error('Migration requires explicit compatibility confirmation (--compatible)');
|
|
189
232
|
}
|
|
190
233
|
}
|
|
191
234
|
if (options.scope !== 'client') {
|
|
192
235
|
const recommendation = recommendedMode({ ...effects.platform, arch: effects.platform?.arch ?? process.arch });
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
236
|
+
// Native runs the server only: local clients need the container gateway. A retained
|
|
237
|
+
// native installation keeps its mode, so a full setup of it cannot finish.
|
|
238
|
+
// (An unfinished migration is resumed in its recorded mode and is validated with the plan.)
|
|
239
|
+
if (options.scope === 'all' && existing?.mode === 'packages' && !options.migrateFrom)
|
|
240
|
+
throw new Error('This installation runs natively, which supports the server only. Choose the server-only setup to maintain it (ours-install server); local clients need an installation that runs in Docker or Podman. Nothing was changed.');
|
|
241
|
+
const nativeOffered = options.scope === 'server' || existing?.mode === 'packages';
|
|
242
|
+
effects.out(nativeOffered
|
|
243
|
+
? `${recommendation.reason} Native runs directly on this computer; Docker runs in containers and needs Docker installed and running. An existing installation must keep its current mode.`
|
|
244
|
+
: 'The server runs in containers. Docker and Podman both work; the one you choose must be installed and running. An existing installation must keep its current mode.');
|
|
245
|
+
const runtime = options.mode ?? await effects.select('How should the server run?', [
|
|
246
|
+
...(nativeOffered ? [{ value: 'packages', label: 'Native packages' }] : []), { value: 'docker', label: 'Docker' }, { value: 'podman', label: 'Podman' },
|
|
247
|
+
], existing?.mode === 'docker' && existing?.containerEngine === 'podman' ? 'podman' : existing?.mode ?? recommendation.mode);
|
|
248
|
+
options.mode = runtime === 'podman' ? 'docker' : runtime;
|
|
249
|
+
if (runtime === 'podman') options.containerEngine = 'podman';
|
|
197
250
|
if (!options.migrateFrom) {
|
|
198
|
-
|
|
199
|
-
|
|
251
|
+
if (existing?.messengerIdentity) {
|
|
252
|
+
effects.out(`Existing Human identity ${existing.messengerIdentity} and Messenger profile will be retained.`);
|
|
253
|
+
// Supplied profile inputs never rename a retained root.
|
|
254
|
+
if (!['localUsername', 'name', 'surname'].some(key => options[key] !== undefined)) options.identityName ??= existing.messengerIdentity;
|
|
255
|
+
} else if (options.identityName === undefined || options.localUsername !== undefined) {
|
|
256
|
+
effects.out('Choose a local username (no global uniqueness check). Name and Surname appear in Messenger; the optional host name identifies this workspace.');
|
|
257
|
+
options.localUsername ??= await effects.askLine('Local username: ', (effects.username?.() ?? 'me').toLowerCase().replace(/[^a-z0-9-]/g, '-').slice(0, 30));
|
|
258
|
+
options.name ??= await effects.askLine('Name: ', '');
|
|
259
|
+
options.surname ??= await effects.askLine('Surname: ', '');
|
|
260
|
+
options.hostname ??= await effects.askLine('Host / machine name (optional): ', '');
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (['localUsername', 'name', 'surname'].some(key => options[key] !== undefined)) {
|
|
264
|
+
// Partial profile presets still collect missing fields on retained and
|
|
265
|
+
// migration paths; these inputs never replace their authoritative root.
|
|
266
|
+
effects.out('Name and Surname initialize a missing Messenger profile; an existing profile is retained.');
|
|
267
|
+
options.localUsername ??= await effects.askLine('Local username: ', (effects.username?.() ?? 'me').toLowerCase().replace(/[^a-z0-9-]/g, '-').slice(0, 30));
|
|
268
|
+
options.name ??= await effects.askLine('Name: ', '');
|
|
269
|
+
options.surname ??= await effects.askLine('Surname: ', '');
|
|
270
|
+
options.hostname ??= await effects.askLine('Host / machine name (optional): ', '');
|
|
200
271
|
}
|
|
201
|
-
for (const [key, fallback] of Object.entries(defaults)) options[key]
|
|
272
|
+
for (const [key, fallback] of Object.entries(defaults)) options[key] ??= existing?.[key] ?? fallback;
|
|
202
273
|
if (options.operation === 'update') {
|
|
203
274
|
effects.out('The update retains identities and messages and creates a recovery backup before changing stored state. Proceed only if you accept this release for your existing data; compatibility is not automatically guaranteed.');
|
|
204
|
-
options.compatible
|
|
275
|
+
options.compatible ??= await effects.ask('Proceed with this update and keep a recovery backup?', false);
|
|
205
276
|
}
|
|
206
277
|
} else {
|
|
207
278
|
const savedProfile = join(effects.home, '.ours-client', 'profile.json');
|
|
208
279
|
effects.out('A connection profile identifies the server and its private access credential. Reuse your saved connection or select a profile supplied by the server owner.');
|
|
209
|
-
const connection = await effects.select('Which server connection should this computer use?', [
|
|
280
|
+
const connection = options.config ? 'preset' : await effects.select('Which server connection should this computer use?', [
|
|
210
281
|
{ value: 'saved', label: 'Use the saved server connection' }, { value: 'file', label: 'Choose a connection profile file' },
|
|
211
282
|
], effects.readJson(savedProfile) ? 'saved' : 'file');
|
|
212
|
-
options.config
|
|
283
|
+
options.config ??= connection === 'saved' ? savedProfile : await effects.askLine('Connection profile file: ', savedProfile);
|
|
213
284
|
}
|
|
214
285
|
if (options.scope !== 'server') {
|
|
215
286
|
const detected = typeof effects.detectHarnesses === 'function' ? await effects.detectHarnesses() : [];
|
|
216
287
|
const selected = integrations.filter(name => name === 'fleet' || detected.some(item => item.name === name && item.status === 'ok'));
|
|
217
288
|
effects.out('Choose the apps to connect. Detected agent apps are selected by default; Fleet configures persistent agents but leaves them stopped. Use Space to toggle choices, then Enter to continue. You can select none.');
|
|
218
|
-
options.integrations
|
|
289
|
+
options.integrations ??= await effects.multiselect('Which integrations should be configured?', [
|
|
219
290
|
{ value: 'codex', label: 'Codex' }, { value: 'claude-code', label: 'Claude Code' }, { value: 'fleet', label: 'Fleet — persistent agents' },
|
|
220
291
|
], selected);
|
|
221
|
-
if (options.integrations.includes('fleet')) {
|
|
292
|
+
if (options.integrations.includes('fleet') && !options.disableFleetAgentsSetup && !options.fleetSettingsPath) {
|
|
222
293
|
effects.out('Fleet needs model and agent settings. Its guided setup asks for these later; a prepared settings file applies your existing choices. Fleet roles will not start automatically.');
|
|
223
294
|
const fleetMode = await effects.select('How should Fleet be configured?', [
|
|
224
295
|
{ value: 'wizard', label: 'Configure interactively with Fleet' }, { value: 'file', label: 'Use a prepared settings file' },
|
package/lib/setup.mjs
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
|
+
import { validateGatewayClientProfile } from './target.mjs';
|
|
2
|
+
import { gatewayAddress } from './gateway.mjs';
|
|
1
3
|
import { inspectLegacyMigration } from './legacy-migration.mjs';
|
|
2
4
|
import { join } from 'node:path';
|
|
3
5
|
import { parseSetupArgs, collectSetupOptions, validateSetupOptions } from './setup-options.mjs';
|
|
4
6
|
import { parseNetworkArgs, validateHostProfile, InstallUsageError } from './target.mjs';
|
|
5
|
-
import { validateInstallation } from './plan.mjs';
|
|
7
|
+
import { clientPackageNames, validateInstallation } from './plan.mjs';
|
|
6
8
|
import { runServerCommand, runClientCommand } from './orchestrate.mjs';
|
|
7
9
|
import { banner, heading, info, ok, warn, progress } from './ui.mjs';
|
|
8
10
|
import { isCancel } from './prompt.mjs';
|
|
9
11
|
import { USAGE } from './usage.mjs';
|
|
10
12
|
import { validateIdentityName } from './server-onboarding.mjs';
|
|
11
13
|
import { validateFleetSettings } from './fleet-settings.mjs';
|
|
14
|
+
import { installerFailure } from './diagnostics.mjs';
|
|
12
15
|
|
|
13
|
-
const maintenance = new Set(['status', 'start', 'stop', 'restart', 'rebuild', 'access-issue', 'access-replace', 'backup', 'restore', 'reset']);
|
|
14
|
-
const clientPackages = integrations => [...new Set(['sdk', ...(integrations.includes('fleet') ? ['cli'] : []), ...integrations])];
|
|
16
|
+
const maintenance = new Set(['status', 'start', 'stop', 'restart', 'uninstall', 'rebuild', 'gateway-enable', 'access-issue', 'access-replace', 'backup', 'restore', 'reset']);
|
|
15
17
|
|
|
16
18
|
export function completeReleasePolicy(retained, supplied) {
|
|
17
19
|
if (retained?.release) {
|
|
@@ -32,7 +34,9 @@ export async function prepareSetupPlan(options, effects) {
|
|
|
32
34
|
options = validateSetupOptions(options, { interactive: options.interactive });
|
|
33
35
|
if (effects.platform?.platform === 'win32') throw new InstallUsageError('Run ours-install inside WSL with Docker Desktop integration on Windows. Direct Windows Node installations are not supported.');
|
|
34
36
|
const plan = { ...options };
|
|
35
|
-
if (options.scope
|
|
37
|
+
if (options.scope === 'all' && options.mode === 'packages' && options.integrations?.length)
|
|
38
|
+
throw new InstallUsageError('Gateway-only clients require a gateway. Use Docker full-stack setup, or install the native server separately and provision an HTTP gateway before client setup; nothing was changed.');
|
|
39
|
+
if (options.scope !== 'server') {
|
|
36
40
|
for (const name of ['OURS_API_TOKEN', 'OURS_PORT', 'OURS_STATE_DIR', 'OURS_DAEMON_ID']) {
|
|
37
41
|
if (effects.env?.[name]?.trim()) throw new InstallUsageError(`${name} conflicts with the selected client profile. Clear this override before full-stack/client setup; nothing was changed.`);
|
|
38
42
|
}
|
|
@@ -54,6 +58,7 @@ export async function prepareSetupPlan(options, effects) {
|
|
|
54
58
|
const value = effects.readJson(join(options.stateDir, 'installation.json'));
|
|
55
59
|
if (value) {
|
|
56
60
|
plan.existing = validateInstallation(value, options.stateDir);
|
|
61
|
+
if (options.containerEngine && options.containerEngine !== (plan.existing.containerEngine ?? 'docker')) throw new InstallUsageError('Container engine conflicts with retained installation');
|
|
57
62
|
if (options.mode !== plan.existing.mode) throw new InstallUsageError('The selected mode conflicts with the retained installation; choose its existing mode or a separate empty directory');
|
|
58
63
|
for (const key of ['port', 'coworkPort', 'messengerPort']) {
|
|
59
64
|
if (options.explicitPorts?.includes(key) && options[key] !== plan.existing[key]) throw new InstallUsageError(`${key} conflicts with the retained installation`);
|
|
@@ -61,33 +66,33 @@ export async function prepareSetupPlan(options, effects) {
|
|
|
61
66
|
}
|
|
62
67
|
if (options.operation === 'install' && !options.migrateFrom) {
|
|
63
68
|
const retained = readObject(effects, plan.existing.sourcesPath, 'Retained source policy');
|
|
64
|
-
plan.sourcePolicy = options.scope === 'server'
|
|
69
|
+
plan.sourcePolicy = options.scope === 'server' ? retained : completeReleasePolicy(retained, options.sources ? policy : null);
|
|
65
70
|
}
|
|
66
71
|
} else if (options.operation === 'update') {
|
|
67
72
|
throw new InstallUsageError('Update requires an existing installation.json; choose install for a new installation');
|
|
68
73
|
}
|
|
69
74
|
// Reject a local client already attached to another server before changing the server.
|
|
70
|
-
const saved = options.scope === 'all'
|
|
71
|
-
if (saved && (!plan.existing || saved.expectedInstanceId !== plan.existing.instanceId || saved.endpoint !== `http://127.0.0.1:${plan.port}`)) {
|
|
75
|
+
const saved = options.scope === 'all' ? effects.readManagedClientProfile() : null;
|
|
76
|
+
if (saved && (!plan.existing || saved.expectedInstanceId !== plan.existing.instanceId || saved.endpoint !== (plan.existing?.gateway ? gatewayAddress(plan.existing).base + '/daemon' : `http://127.0.0.1:${plan.port}`))) {
|
|
72
77
|
throw new InstallUsageError('This user already has clients attached to a different server; their saved connection was not changed');
|
|
73
78
|
}
|
|
74
79
|
} else {
|
|
75
|
-
const profile =
|
|
80
|
+
const profile = validateGatewayClientProfile(readObject(effects, options.config, 'Client profile'));
|
|
76
81
|
if (!profile) throw new InstallUsageError('Client profile must contain endpoint, expectedInstanceId and credentialPath');
|
|
77
82
|
if (!effects.readText(profile.credentialPath)?.trim()) throw new InstallUsageError('Client credential file is missing or empty');
|
|
78
83
|
plan.profile = profile;
|
|
79
84
|
const saved = effects.readManagedClientProfile();
|
|
80
|
-
if (saved &&
|
|
85
|
+
if (saved && saved.expectedInstanceId !== profile.expectedInstanceId) throw new InstallUsageError('Managed clients already select another server; existing connection was not changed');
|
|
81
86
|
if (saved && options.operation === 'install' && !options.sources) plan.sourcePolicy = readObject(effects, saved.installer.sourcesPath, 'Retained client source policy');
|
|
82
87
|
}
|
|
83
88
|
// Dry-run never spawns a resolver or acquires an installation lock.
|
|
84
89
|
if (!options.dryRun) {
|
|
85
|
-
if (options.scope === 'all' && options.operation === 'update'
|
|
90
|
+
if (options.scope === 'all' && options.operation === 'update') {
|
|
86
91
|
const running = await effects.serverLifecycle(plan.existing, 'status', ['daemon']);
|
|
87
92
|
if (!running.includes('daemon')) throw new InstallUsageError('Full-stack update requires the selected daemon to be running for client verification. Start it first, or use server update to preserve its stopped state.');
|
|
88
93
|
}
|
|
89
94
|
if (options.scope !== 'client') await effects.resolveSourcePolicy(plan.sourcePolicy, 'server');
|
|
90
|
-
if (options.scope !== 'server'
|
|
95
|
+
if (options.scope !== 'server') await effects.resolveSourcePolicy(plan.sourcePolicy, 'client', clientPackageNames(options.integrations));
|
|
91
96
|
}
|
|
92
97
|
return plan;
|
|
93
98
|
}
|
|
@@ -100,7 +105,7 @@ export async function executeSetupPlan(plan, effects, { server = runServerComman
|
|
|
100
105
|
if (plan.dryRun) {
|
|
101
106
|
effects.out(info('Preview only. No packages, identities, credentials or services will be changed.'));
|
|
102
107
|
if (plan.scope !== 'client') effects.out(info('Server: prerequisites → runtime preparation/update → retained identity/state restoration → readiness.'));
|
|
103
|
-
if (plan.scope !== 'server'
|
|
108
|
+
if (plan.scope !== 'server') effects.out(info('Clients: private connection → exact packages → selected integrations → Fleet settings when selected.'));
|
|
104
109
|
return 0;
|
|
105
110
|
}
|
|
106
111
|
let clientConfig = plan.config;
|
|
@@ -110,6 +115,9 @@ export async function executeSetupPlan(plan, effects, { server = runServerComman
|
|
|
110
115
|
const result = await server({ ...plan, role: 'server', operation: plan.operation, sourcePolicy: plan.sourcePolicy }, effects);
|
|
111
116
|
if (result !== 0) return result;
|
|
112
117
|
const record = validateInstallation(effects.readJson(join(plan.stateDir, 'installation.json')), plan.stateDir);
|
|
118
|
+
if (plan.name && plan.surname && plan.operation === 'install') {
|
|
119
|
+
await effects.serverEnsureHumanProfile(record, { name: plan.name, surname: plan.surname });
|
|
120
|
+
}
|
|
113
121
|
if (plan.operation === 'update') {
|
|
114
122
|
effects.out(progress(0, 1, 'Retained identities', 'Verify the Human identity after state restoration; existing names and keys are retained.'));
|
|
115
123
|
const running = await effects.serverLifecycle(record, 'status', ['daemon']);
|
|
@@ -118,21 +126,23 @@ export async function executeSetupPlan(plan, effects, { server = runServerComman
|
|
|
118
126
|
effects.out(ok('Retained identities verified.'));
|
|
119
127
|
} else effects.out(info('Daemon remains stopped; stored identities are retained and will restore on the next start.'));
|
|
120
128
|
}
|
|
121
|
-
if (plan.scope === 'all'
|
|
129
|
+
if (plan.scope === 'all') {
|
|
122
130
|
clientPolicy = completeReleasePolicy(readObject(effects, record.sourcesPath, 'Server source policy'), plan.sourcePolicy);
|
|
123
131
|
effects.out(heading('Connect local clients'));
|
|
124
132
|
const handoff = await effects.prepareLocalClient(record, plan.integrations, plan.fleetSettingsPath);
|
|
125
133
|
clientConfig = handoff.configPath;
|
|
126
134
|
}
|
|
127
135
|
}
|
|
128
|
-
if (plan.scope !== 'server'
|
|
136
|
+
if (plan.scope !== 'server') {
|
|
129
137
|
const result = await client({ role: 'client', operation: 'install', config: clientConfig,
|
|
130
138
|
integrations: plan.integrations, fleetSettingsPath: plan.fleetSettingsPath, sourcePolicy: clientPolicy,
|
|
139
|
+
disableFleetAgentsSetup: plan.disableFleetAgentsSetup, fleetTaskWorkflowAgents: plan.fleetTaskWorkflowAgents,
|
|
131
140
|
preset: true, nonInteractive: !plan.interactive }, effects);
|
|
132
141
|
if (result !== 0) return result;
|
|
133
142
|
}
|
|
143
|
+
if (plan.disableFleetAgentsSetup && plan.integrations?.includes('fleet')) effects.out(info('Fleet is installed. Run ours-fleet setup-tunnel with the expiring one-use payload from the App command; a compatible Fleet prints the QR and connection code automatically. Configure agents and models in the App after linking.'));
|
|
134
144
|
effects.out(ok(`Requested ${plan.operation} completed. Existing identities were retained.`));
|
|
135
|
-
if (plan.integrations?.includes('fleet')) effects.out(info('Fleet is configured but stopped. Review its settings, then run ours-fleet doctor, ours-fleet config and ours-fleet up.'));
|
|
145
|
+
if (plan.integrations?.includes('fleet') && !plan.disableFleetAgentsSetup) effects.out(info('Fleet is configured but stopped. Review its settings, then run ours-fleet doctor, ours-fleet config and ours-fleet up.'));
|
|
136
146
|
return 0;
|
|
137
147
|
}
|
|
138
148
|
|
|
@@ -144,12 +154,20 @@ export async function runSetup(argv, effects) {
|
|
|
144
154
|
if (argv[0] === 'server' && maintenance.has(argv[1])) return await runServerCommand(parseNetworkArgs(argv), effects);
|
|
145
155
|
if (!argv.length && effects.env?.OURS_ASSUME_YES) throw new InstallUsageError('OURS_ASSUME_YES cannot fill an interactive setup plan. Supply complete CLI presets for unattended installation.');
|
|
146
156
|
if (!argv.length) { effects.out(banner()); effects.out(heading('Interactive setup')); }
|
|
147
|
-
|
|
157
|
+
let options;
|
|
158
|
+
if (argv.length) {
|
|
159
|
+
const presets = parseSetupArgs(argv, { home: effects.home, validate: false });
|
|
160
|
+
try { options = validateSetupOptions(presets, { interactive: false }); }
|
|
161
|
+
catch (error) {
|
|
162
|
+
if (!effects.interactive || effects.env?.OURS_ASSUME_YES || !/^Missing (required setup options|human profile option):/.test(error.message)) throw error;
|
|
163
|
+
options = await collectSetupOptions(effects, presets);
|
|
164
|
+
}
|
|
165
|
+
} else options = await collectSetupOptions(effects);
|
|
148
166
|
const plan = await prepareSetupPlan(options, effects);
|
|
149
167
|
return await executeSetupPlan(plan, effects);
|
|
150
168
|
} catch (error) {
|
|
151
169
|
if (isCancel(error)) { effects.out(warn('Installation cancelled.')); return 130; }
|
|
152
|
-
effects.out(warn(`ours-install: ${error.
|
|
170
|
+
effects.out(warn(`ours-install: ${installerFailure(error, effects.env)}`));
|
|
153
171
|
return 2;
|
|
154
172
|
}
|
|
155
173
|
}
|
package/lib/target.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { serverBase } from './gateway.mjs';
|
|
1
2
|
// ours-install v3 — argument handling and daemon detection.
|
|
2
3
|
//
|
|
3
4
|
// Pure argument handling and daemon-target selection. The orchestrator
|
|
@@ -85,17 +86,30 @@ export function validateHostProfile(value) {
|
|
|
85
86
|
const mixed = LEGACY_PROFILE_KEYS.filter((key) => Object.hasOwn(value, key));
|
|
86
87
|
if (mixed.length) throw profileError(`legacy selection keys cannot be mixed with a host profile (${mixed.join(', ')}).`);
|
|
87
88
|
const { endpoint, expectedInstanceId, credentialPath } = value;
|
|
88
|
-
if (typeof endpoint !== 'string' || endpoint.trim() !== endpoint || endpoint === '') throw profileError('endpoint must be a non-empty HTTP
|
|
89
|
+
if (typeof endpoint !== 'string' || endpoint.trim() !== endpoint || endpoint === '') throw profileError('endpoint must be a non-empty HTTP or HTTPS base URL.');
|
|
89
90
|
if (typeof expectedInstanceId !== 'string' || !PROFILE_UUID.test(expectedInstanceId)) throw profileError('expectedInstanceId must be a lowercase UUID.');
|
|
90
91
|
if (typeof credentialPath !== 'string' || credentialPath === '' || !credentialPath.startsWith('/') || resolve(credentialPath) !== credentialPath) {
|
|
91
92
|
throw profileError('credentialPath must be a normalized absolute path.');
|
|
92
93
|
}
|
|
93
94
|
let url;
|
|
94
|
-
try { url = new URL(endpoint); } catch { throw profileError('endpoint must be an HTTP
|
|
95
|
-
if (url.protocol !== 'http:'
|
|
96
|
-
throw profileError('endpoint must be an HTTP
|
|
95
|
+
try { url = new URL(endpoint); } catch { throw profileError('endpoint must be an HTTP or HTTPS base URL.'); }
|
|
96
|
+
if ((url.protocol !== 'http:' && url.protocol !== 'https:') || url.username || url.password || url.search || url.hash) {
|
|
97
|
+
throw profileError('endpoint must be an HTTP or HTTPS base URL without credentials, query, or fragment.');
|
|
97
98
|
}
|
|
98
|
-
|
|
99
|
+
const endpointBase = serverBase(endpoint);
|
|
100
|
+
if (value.serverUrl !== undefined) {
|
|
101
|
+
const serverUrl = serverBase(value.serverUrl);
|
|
102
|
+
if (endpointBase !== serverUrl + '/daemon') throw profileError('endpoint must select serverUrl/daemon.');
|
|
103
|
+
return { serverUrl, endpoint: endpointBase, expectedInstanceId, credentialPath };
|
|
104
|
+
}
|
|
105
|
+
return { endpoint: endpointBase, expectedInstanceId, credentialPath };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Client installs require the single gateway even when a legacy server profile exists. */
|
|
109
|
+
export function validateGatewayClientProfile(value) {
|
|
110
|
+
if (!value || typeof value.serverUrl !== 'string') throw profileError('serverUrl is required; enable the server gateway and import its client profile. Direct daemon endpoints are not client routes.');
|
|
111
|
+
const serverUrl = serverBase(value.serverUrl);
|
|
112
|
+
return validateHostProfile({ ...value, endpoint: value.endpoint ?? serverUrl + '/daemon' });
|
|
99
113
|
}
|
|
100
114
|
|
|
101
115
|
export function resolveProfileSelection({ args, env = {}, home = homedir(), exists, readProfile }) {
|
|
@@ -454,10 +468,11 @@ export function searchFreePort(isTaken, { floor = FREE_PORT_FLOOR, reserved = IN
|
|
|
454
468
|
export function parseNetworkArgs(argv) {
|
|
455
469
|
if (!['server', 'client'].includes(argv[0])) return null;
|
|
456
470
|
const [role, operation] = argv;
|
|
457
|
-
const operations = role === 'client' ? ['install'] : ['install', 'status', 'start', 'stop', 'restart', 'access-issue', 'access-replace', 'backup', 'restore', 'reset', 'update', 'rebuild'];
|
|
471
|
+
const operations = role === 'client' ? ['install'] : ['install', 'status', 'start', 'stop', 'restart', 'uninstall', 'access-issue', 'access-replace', 'backup', 'restore', 'reset', 'update', 'rebuild', 'gateway-enable'];
|
|
458
472
|
if (!operations.includes(operation)) throw new InstallUsageError(`Unsupported ${role} operation: ${operation ?? '(missing)'}`);
|
|
459
473
|
const allowed = role === 'client' ? ['config'] : ['state-dir'];
|
|
460
|
-
if (role === 'server' && operation === 'install') allowed.push('mode', 'sources', 'migrate');
|
|
474
|
+
if (role === 'server' && operation === 'install') allowed.push('mode', 'sources', 'migrate', 'container-engine');
|
|
475
|
+
if (role === 'server' && ['install', 'gateway-enable'].includes(operation)) allowed.push('server-url');
|
|
461
476
|
if (operation === 'access-issue') allowed.push('output');
|
|
462
477
|
if (['access-replace', 'reset'].includes(operation)) allowed.push('confirm');
|
|
463
478
|
if (['restore', 'update'].includes(operation)) allowed.push('compatible');
|
|
@@ -486,13 +501,14 @@ export function parseNetworkArgs(argv) {
|
|
|
486
501
|
} else {
|
|
487
502
|
const value = inline ?? argv[++i];
|
|
488
503
|
if (!value || value.startsWith('--')) throw new InstallUsageError(`--${name} requires a value`);
|
|
489
|
-
result[key] = name === '
|
|
504
|
+
result[key] = ['mode', 'container-engine'].includes(name) ? value : name === 'server-url' ? serverBase(value) : resolve(value);
|
|
490
505
|
}
|
|
491
506
|
}
|
|
492
507
|
const required = role === 'client' ? [] : ['stateDir'];
|
|
493
508
|
if (operation === 'access-issue') required.push('output');
|
|
494
509
|
if (['access-replace', 'reset'].includes(operation)) required.push('confirm');
|
|
495
510
|
for (const key of required) if (!result[key]) throw new InstallUsageError(`${operation} requires --${key.replace(/[A-Z]/g, c => '-' + c.toLowerCase())}`);
|
|
511
|
+
if (result.containerEngine !== undefined && (result.mode !== 'docker' || !['docker', 'podman'].includes(result.containerEngine))) throw new InstallUsageError('--container-engine docker|podman requires --mode docker');
|
|
496
512
|
if (result.mode && !['packages', 'docker'].includes(result.mode)) throw new InstallUsageError('--mode must be packages or docker');
|
|
497
513
|
return result;
|
|
498
514
|
}
|
package/lib/uninstall.mjs
CHANGED
|
@@ -174,13 +174,13 @@ export function planDaemonRemoval({ stateDir, cliStartedIt }) {
|
|
|
174
174
|
const service = {
|
|
175
175
|
id: 'service',
|
|
176
176
|
unit: unit.ok ? unit.unit : null,
|
|
177
|
-
command: ['ours
|
|
177
|
+
command: ['ours-daemon', 'uninstall-service', '--yes', '--state-dir', dir, '--config', join(dir, 'config.json')],
|
|
178
178
|
note: 'removes the unit managed by the ours CLI',
|
|
179
179
|
};
|
|
180
180
|
return [
|
|
181
181
|
service,
|
|
182
182
|
cliStartedIt
|
|
183
|
-
? { id: 'stop', command: ['ours
|
|
183
|
+
? { id: 'stop', command: ['ours-daemon', 'stop', '--state-dir', dir, '--config', join(dir, 'config.json')] }
|
|
184
184
|
: { id: 'stop-external', command: null, continues: true, note: 'this daemon was not started by the CLI; naming its launcher and continuing' },
|
|
185
185
|
];
|
|
186
186
|
}
|
package/lib/usage.mjs
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
// Public setup and maintenance commands.
|
|
2
|
-
export const USAGE = `ours-install — interactive setup
|
|
2
|
+
export const USAGE = `ours-install — interactive setup with optional CLI presets.
|
|
3
3
|
|
|
4
4
|
ours-install
|
|
5
5
|
Opens the console form. Choose all (server + clients), server, or client;
|
|
6
6
|
runtime, installation directory, identity, integrations and Fleet settings.
|
|
7
|
-
|
|
7
|
+
Full setup runs the server in Docker or Podman, which provide the gateway local clients need.
|
|
8
|
+
Native mode is offered for server-only setup. Windows uses WSL.
|
|
9
|
+
|
|
10
|
+
Install now, configure Fleet agents in the App after linking:
|
|
11
|
+
ours-install --username alex --name Alex --surname Taylor --disable-fleet-agents-setup
|
|
12
|
+
Remaining installation choices are prompted on a terminal. No enrollment credentials or expiry.
|
|
8
13
|
|
|
9
14
|
Full stack, with every required answer preset (no prompts):
|
|
10
15
|
ours-install --mode docker --state-dir /private/ours --identity-name "Your Name" --integrations codex,fleet --fleet-settings /private/fleet.json
|
|
@@ -22,13 +27,21 @@ export const USAGE = `ours-install — interactive setup or complete CLI presets
|
|
|
22
27
|
--scope all|server|client preset which parts to configure (default all)
|
|
23
28
|
--action install|update equivalent to the positional operation
|
|
24
29
|
--mode docker|native packages is also accepted for native mode
|
|
30
|
+
--container-engine docker|podman server container backend (default Docker); retained for maintenance
|
|
25
31
|
--state-dir PATH installation root; required for all/server
|
|
32
|
+
--username USERNAME local username; no global uniqueness check
|
|
33
|
+
--name NAME Messenger given name
|
|
34
|
+
--surname SURNAME Messenger family name
|
|
35
|
+
--hostname NAME optional host/workspace name; defaults to this machine
|
|
36
|
+
--disable-fleet-agents-setup install Fleet CLI without its roles/model wizard
|
|
26
37
|
--identity-name NAME desired Human name for a fresh server; existing root is retained
|
|
27
38
|
--integrations LIST codex,claude-code,fleet; use none to skip clients explicitly
|
|
28
|
-
--fleet-settings PATH JSON settings for Fleet;
|
|
39
|
+
--fleet-settings PATH JSON settings for Fleet; required unless Fleet agents setup is disabled
|
|
40
|
+
--fleet-task-workflow A[,B] let these Fleet agents run Fleet task commands outside their command sandbox
|
|
29
41
|
--config PATH complete connection profile for client-only setup
|
|
30
42
|
--sources PATH explicit full development source policy override
|
|
31
|
-
--port N
|
|
43
|
+
--port N public gateway port for Docker (default 3050; e.g. 4050)
|
|
44
|
+
--server-url URL external gateway URL (Docker install; configure authenticated entry separately)
|
|
32
45
|
--cowork-port N cowork port (default 3052)
|
|
33
46
|
--messenger-port N messenger port (default 8420)
|
|
34
47
|
--compatible required for server updates and legacy state migration
|
|
@@ -39,13 +52,14 @@ export const USAGE = `ours-install — interactive setup or complete CLI presets
|
|
|
39
52
|
--help, -h show help
|
|
40
53
|
--version, -V print installer version
|
|
41
54
|
|
|
42
|
-
CLI presets
|
|
43
|
-
|
|
55
|
+
Partial CLI presets on a terminal ask for the remaining choices. Without a terminal,
|
|
56
|
+
or with OURS_ASSUME_YES, supply complete CLI presets; missing answers fail before installation. Both input modes run the same
|
|
44
57
|
installer with preparation, identity restoration, update and readiness progress.
|
|
45
58
|
Fleet is configured but left stopped for operator review.
|
|
46
59
|
|
|
47
60
|
Scoped maintenance:
|
|
48
|
-
ours-install server status|start|stop|restart|rebuild --state-dir PATH
|
|
61
|
+
ours-install server status|start|stop|restart|rebuild|uninstall --state-dir PATH
|
|
62
|
+
ours-install server gateway-enable --state-dir PATH [--server-url URL]
|
|
49
63
|
ours-install server access-issue --state-dir PATH --output PATH
|
|
50
64
|
ours-install server access-replace --state-dir PATH --confirm
|
|
51
65
|
ours-install server backup|restore server|daemon|telegram|cowork|messenger LABEL --state-dir PATH
|