@ours.network/install 1.2.1-nightly.23 → 1.2.1-nightly.24

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 CHANGED
@@ -1,3 +1,17 @@
1
+ ## Install first, connect the App afterwards
2
+
3
+ ```sh
4
+ ours-install --username alex --name Alex --surname Taylor --disable-fleet-agents-setup
5
+ ```
6
+
7
+ This permanent command contains no tunnel credentials or enrollment expiry. On a terminal it asks for the remaining installation choices, including Docker, Podman or native packages. The username is local and has no global uniqueness check. A fresh root is named `<username>@<hostname>`; the optional `--hostname` defaults to the machine name. Messenger stores Name and Surname through its supported profile API. Retried setup retains root identities and existing Messenger profiles.
8
+
9
+ `--disable-fleet-agents-setup` installs the Fleet CLI and saves the choice without opening its agent/model wizard or rewriting existing Fleet configuration. Configure agents in the App after device linking. Complete presets still work unattended; partial presets require a terminal and cannot use `OURS_ASSUME_YES`.
10
+
11
+ The separate `ours-fleet setup-tunnel --file /private/setup-payload` step requires the matching Fleet follow-up release. Its private App payload remains scoped, signed-root-bound and expiring. Then run `ours-fleet link-device` and paste its private single-use code into the App. Do not put the expiring tunnel payload in the permanent installer command.
12
+
13
+ Native packages currently provide server-only installation; full Fleet/App onboarding requires the supported gateway (Docker or Podman). The installer refuses unsupported native full-stack setup before changing anything. This PR does not add a native gateway or release unpublished Fleet commands.
14
+
1
15
  See [one-connection setup, localhost:4050 and systemd migration](GATEWAY_SETUP.md) for the gateway-only client workflow.
2
16
 
3
17
  # @ours.network/install — one installer for the whole stack
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema": 1,
3
3
  "channel": "nightly",
4
- "installerVersion": "1.2.1-nightly.23",
4
+ "installerVersion": "1.2.1-nightly.24",
5
5
  "packages": {
6
6
  "@ours.network/sdk": {
7
7
  "version": "3.8.1-nightly.15",
@@ -2,7 +2,7 @@
2
2
  "release": {
3
3
  "schema": 1,
4
4
  "channel": "nightly",
5
- "installerVersion": "1.2.1-nightly.23",
5
+ "installerVersion": "1.2.1-nightly.24",
6
6
  "packages": {
7
7
  "@ours.network/sdk": {
8
8
  "version": "3.8.1-nightly.15",
package/lib/effects.mjs CHANGED
@@ -1288,7 +1288,7 @@ export function networkEffects(effects) {
1288
1288
  if (discovery.ok) return validateGatewayDiscovery(base, await discovery.json(), resolve(credentialPath));
1289
1289
  throw new Error(`Gateway discovery answered HTTP ${discovery.status}; enable the gateway. Direct daemon fallback is not supported.`);
1290
1290
  },
1291
- importClientProfile({ profile, sourcesPath, sources: resolvedSources, integrations, fleetSettingsPath, refresh = false }) {
1291
+ importClientProfile({ profile, sourcesPath, sources: resolvedSources, integrations, fleetSettingsPath, disableFleetAgentsSetup = false, refresh = false }) {
1292
1292
  profile = validateGatewayClientProfile(profile);
1293
1293
  const root = join(home, '.ours-client');
1294
1294
  const configPath = join(root, 'profile.json');
@@ -1314,7 +1314,7 @@ export function networkEffects(effects) {
1314
1314
  atomicWriteConfig(configPath, JSON.stringify(saved, null, 2) + '\n');
1315
1315
  return { configPath, profile: validateGatewayClientProfile(saved), settings: current.installer };
1316
1316
  }
1317
- const settings = { sourcesPath: join(root, 'sources.json'), integrations };
1317
+ const settings = { sourcesPath: join(root, 'sources.json'), integrations, ...(disableFleetAgentsSetup ? { disableFleetAgentsSetup: true } : {}) };
1318
1318
  if (fleetSettings) settings.fleetSettingsPath = join(root, 'fleet-settings.json');
1319
1319
  // Publish the profile last: clients cannot select incomplete imported inputs.
1320
1320
  atomicWriteConfig(settings.sourcesPath, sources);
@@ -99,11 +99,12 @@ function clientDiagnostic(error) {
99
99
 
100
100
  function clientRetry(effects, imported, integrations) {
101
101
  effects.out(info(`Saved server connection: ${imported.configPath}.`));
102
- if (integrations.includes('fleet') && !imported.settings.fleetSettingsPath) {
102
+ if (integrations.includes('fleet') && !imported.settings.disableFleetAgentsSetup && !imported.settings.fleetSettingsPath) {
103
103
  effects.out(info('Saved profile and settings retained. Run ours-install, choose Connect to an existing server, and reuse the saved profile to finish the interactive Fleet configuration.'));
104
104
  return;
105
105
  }
106
106
  const args = ['client', 'install', '--config', imported.configPath, '--integrations', integrations.join(',') || 'none', '--sources', imported.settings.sourcesPath];
107
+ if (imported.settings.disableFleetAgentsSetup) args.push('--disable-fleet-agents-setup');
107
108
  if (imported.settings.fleetSettingsPath) args.push('--fleet-settings', imported.settings.fleetSettingsPath);
108
109
  const command = `ours-install client install ${args.slice(2).map((value, i) => i % 2 === 0 ? value : shellQuote(value)).join(' ')}`;
109
110
  effects.out(info(`Saved profile and settings retained; re-run ${command}`));
@@ -1011,6 +1012,11 @@ export async function runFleetPhase(args, effects, { target, isDefaultStateDir }
1011
1012
  return { key: 'fleet', label: plan.label, state: 'skipped' };
1012
1013
  }
1013
1014
  const install = args.acquiredFleet ? { ok: true } : await attempt(effects, args.dryRun, plan.install.join(' '), () => effects.run(plan.install[0], plan.install.slice(1)));
1015
+ if (args.disableFleetAgentsSetup) {
1016
+ if (!install.ok) return { key: 'fleet', label: plan.label, state: 'failed', note: 'Fleet installation failed' };
1017
+ effects.out(ok('Fleet CLI installed; agent and model setup deferred to the App. Existing Fleet configuration was retained.'));
1018
+ return { key: 'fleet', label: plan.label, state: 'installed', note: 'agents setup deferred' };
1019
+ }
1014
1020
  // Fleet owns its v2 manifest, roles, models and permissions. With no prepared
1015
1021
  // settings, its wizard keeps the user's terminal while this invocation carries
1016
1022
  // the selected daemon; --settings makes the same call strictly noninteractive.
@@ -1474,7 +1480,8 @@ export async function runClientCommand(command, effects) {
1474
1480
  }
1475
1481
  if (!Array.isArray(integrations) || integrations.some(name => !['codex', 'claude-code', 'fleet'].includes(name)) || new Set(integrations).size !== integrations.length) throw new InstallUsageError('installer.integrations must be an array of codex, claude-code and/or fleet; use an empty array for CLI only');
1476
1482
  let fleetSettingsPath = command.preset ? command.fleetSettingsPath : settings?.fleetSettingsPath;
1477
- if (command.nonInteractive && integrations.includes('fleet') && !fleetSettingsPath) throw new InstallUsageError('Fleet in CLI mode requires --fleet-settings; no interactive wizard will be opened');
1483
+ const disableFleetAgentsSetup = command.disableFleetAgentsSetup ?? settings?.disableFleetAgentsSetup ?? false;
1484
+ if (command.nonInteractive && !disableFleetAgentsSetup && integrations.includes('fleet') && !fleetSettingsPath) throw new InstallUsageError('Fleet in CLI mode requires --fleet-settings; no interactive wizard will be opened');
1478
1485
  if (fleetSettingsPath !== undefined && (typeof fleetSettingsPath !== 'string' || !fleetSettingsPath))
1479
1486
  throw new InstallUsageError('installer.fleetSettingsPath must be a non-empty path when supplied');
1480
1487
  if (fleetSettingsPath) fleetSettingsPath = resolve(settingsBase, fleetSettingsPath);
@@ -1492,7 +1499,7 @@ export async function runClientCommand(command, effects) {
1492
1499
  }
1493
1500
  effects.out(progress(0, 4, 'Client configuration', 'Prepare the selected integrations and private connection profile.'));
1494
1501
  if (profile.serverUrl && integrations.includes('fleet')) await effects.qualifyGatewayClient({ profile, sourcesPath, sources: resolvedSources, integrations, refresh: !!command.preset });
1495
- const imported = effects.importClientProfile({ profile, sourcesPath, sources: resolvedSources, integrations, fleetSettingsPath, refresh: !!command.preset });
1502
+ const imported = effects.importClientProfile({ profile, sourcesPath, sources: resolvedSources, integrations, fleetSettingsPath, disableFleetAgentsSetup, refresh: !!command.preset });
1496
1503
  let phase = 'Validate saved server connection';
1497
1504
  try {
1498
1505
  await effects.verifyHostProfile(imported.configPath);
@@ -1500,7 +1507,7 @@ export async function runClientCommand(command, effects) {
1500
1507
  phase = 'Acquire client packages';
1501
1508
  const exactSuite = await effects.acquireClientPackages(imported.configPath, imported.settings.sourcesPath, integrations, { refresh: !!command.preset });
1502
1509
  const args = { assumeYes: true, dryRun: false, channel: 'latest', clientIntegrations: integrations,
1503
- acquiredFleet: exactSuite.fleetBin, fleetSettingsPath: imported.settings.fleetSettingsPath };
1510
+ acquiredFleet: exactSuite.fleetBin, fleetSettingsPath: imported.settings.fleetSettingsPath, disableFleetAgentsSetup: imported.settings.disableFleetAgentsSetup };
1504
1511
  const target = { mode: 'host-profile', managed: true, configPath: imported.configPath, profile: imported.profile, endpoint: imported.profile.endpoint };
1505
1512
  effects.out(progress(2, 4, 'Client integrations', 'Register the selected agent integrations.'));
1506
1513
  phase = 'Register client integrations';
@@ -115,6 +115,36 @@ try {
115
115
  effects.out?.(`Human identity ${after.root.name} is ready.`);
116
116
  return { name: after.root.name, cid: after.root.cid, created: true };
117
117
  },
118
+ async serverEnsureHumanProfile(record, human) {
119
+ validateRecord(record);
120
+ for (const key of ['name', 'surname']) if (typeof human[key] !== 'string' || !human[key].trim() || human[key].length > 100 || /[\x00-\x1f\x7f]/.test(human[key])) throw new Error('Invalid Messenger human profile');
121
+ // Messenger owns the profile lifecycle. Never rename the daemon root or
122
+ // overwrite a profile already present on a retained installation.
123
+ const script = `
124
+ import { readFileSync } from 'node:fs';
125
+ const selection = JSON.parse(process.argv[1]);
126
+ const token = readFileSync(selection.credentialPath, 'utf8').trim();
127
+ const base = selection.base;
128
+ async function request(path, value) {
129
+ const response = await fetch(base + '/api/' + path, { method: value === undefined ? 'GET' : 'POST', redirect: 'error', signal: AbortSignal.timeout(15000), headers: { 'X-Ours-Api-Token': token, ...(value === undefined ? {} : { 'Content-Type': 'application/json', Origin: selection.origin, 'X-Ours-Messenger-CSRF': '1' }) }, ...(value === undefined ? {} : { body: JSON.stringify(value) }) });
130
+ if (!response.ok) { await response.body?.cancel(); throw new Error('Messenger human profile operation failed (HTTP ' + response.status + ')'); }
131
+ return response.json();
132
+ }
133
+ const identity = await request('identity');
134
+ if (identity.humanProfile) process.stdout.write('Retained Messenger Name and Surname.');
135
+ else { await request('identity/profile', selection.human); process.stdout.write('Messenger Name and Surname initialized.'); }
136
+ `;
137
+ const docker = record.mode === 'docker';
138
+ const selection = { base: `http://127.0.0.1:${docker ? 8420 : record.messengerPort}`,
139
+ origin: `http://127.0.0.1:${record.messengerPort}`,
140
+ credentialPath: docker ? '/credentials/messenger/daemon-token' : installationPaths(record).credentials.messenger,
141
+ human: { name: human.name, surname: human.surname } };
142
+ const args = ['--input-type=module', '-e', script, JSON.stringify(selection)];
143
+ const result = docker ? await compose(record, ['exec', '-T', 'messenger', 'node', ...args])
144
+ : await effects.run(process.execPath, args, { env: localEnv(record), sensitive: true });
145
+ if (result.code !== undefined && result.code !== 0) throw new Error('Messenger human profile initialization failed');
146
+ effects.out?.(result.stdout);
147
+ },
118
148
  async prepareLocalClient(record, integrations, fleetSettingsPath) {
119
149
  validateRecord(record);
120
150
  if (!Array.isArray(integrations) || new Set(integrations).size !== integrations.length
@@ -1,3 +1,4 @@
1
+ import { hostname } from 'node:os';
1
2
  import { serverBase } from './gateway.mjs';
2
3
  import { join, resolve, isAbsolute, dirname } from 'node:path';
3
4
 
@@ -5,10 +6,11 @@ const scopes = ['all', 'server', 'client'];
5
6
  const integrations = ['codex', 'claude-code', 'fleet'];
6
7
  const valueFlags = new Map(Object.entries({
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',
8
10
  '--identity-name': 'identityName', '--integrations': 'integrations', '--fleet-settings': 'fleetSettingsPath',
9
11
  '--config': 'config', '--sources': 'sources', '--migrate-from': 'migrateFrom', '--port': 'port', '--cowork-port': 'coworkPort', '--messenger-port': 'messengerPort',
10
12
  }));
11
- 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']]);
12
14
  const allowed = new Set([...valueFlags.values(), ...boolFlags.values(), 'interactive', 'explicitPorts']);
13
15
  const defaults = { port: 3050, coworkPort: 3052, messengerPort: 8420 };
14
16
  const paths = ['stateDir', 'config', 'sources', 'fleetSettingsPath', 'migrateFrom'];
@@ -32,12 +34,28 @@ export function validateSetupOptions(input, { interactive = input?.interactive =
32
34
  const client = options.scope !== 'server';
33
35
  if (options.migrateFrom !== undefined && (!server || options.operation !== 'install')) throw new Error('--migrate-from requires server or all scope and operation install');
34
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
+ options.hostname = options.hostname?.trim() || defaultHostname();
46
+ 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');
47
+ const identity = `${options.localUsername}@${options.hostname}`;
48
+ if (options.migrateFrom === undefined && options.identityName !== undefined && options.identityName !== identity) throw new Error('--identity-name conflicts with username and hostname');
49
+ if (options.migrateFrom === undefined) options.identityName = identity;
50
+ } else if (options.hostname !== undefined) throw new Error('--hostname requires username, name and surname');
51
+ if (options.disableFleetAgentsSetup !== undefined && typeof options.disableFleetAgentsSetup !== 'boolean') throw new Error('disableFleetAgentsSetup must be a boolean');
52
+ if (options.disableFleetAgentsSetup && options.fleetSettingsPath) throw new Error('--disable-fleet-agents-setup conflicts with --fleet-settings');
35
53
  const missing = [];
36
54
  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);
37
55
  if (client && options.integrations === undefined) missing.push('--integrations (use none to skip)');
38
56
  if (!server && !nonempty(options.config)) missing.push('--config');
39
57
  if (server && (options.operation === 'update' || options.migrateFrom !== undefined) && options.compatible !== true) missing.push('--compatible');
40
- if (Array.isArray(options.integrations) && options.integrations.includes('fleet') && !interactive && !nonempty(options.fleetSettingsPath)) missing.push('--fleet-settings');
58
+ if (Array.isArray(options.integrations) && options.integrations.includes('fleet') && !interactive && !options.disableFleetAgentsSetup && !nonempty(options.fleetSettingsPath)) missing.push('--fleet-settings');
41
59
  if (missing.length) throw new Error(`Missing required setup options: ${missing.join(', ')}`);
42
60
  if (options.mode === 'native') options.mode = 'packages';
43
61
  if (server && !['packages', 'docker'].includes(options.mode)) throw new Error('Mode must be packages (or native) or docker');
@@ -47,7 +65,7 @@ export function validateSetupOptions(input, { interactive = input?.interactive =
47
65
  options.serverUrl = serverBase(options.serverUrl);
48
66
  }
49
67
  if (server && options.config !== undefined) throw new Error('--config is only valid for client scope');
50
- if (!server) for (const key of ['mode', 'stateDir', 'identityName', ...Object.keys(defaults), 'compatible', 'migrate']) {
68
+ if (!server) for (const key of ['mode', 'stateDir', 'identityName', 'localUsername', 'name', 'surname', 'hostname', ...Object.keys(defaults), 'compatible', 'migrate']) {
51
69
  if (options[key] !== undefined) throw new Error(`${key} is only valid for server or all scope`);
52
70
  }
53
71
  if (!client && (options.integrations !== undefined || options.fleetSettingsPath !== undefined)) throw new Error('--integrations and --fleet-settings require all or client scope');
@@ -124,27 +142,31 @@ export function parseSetupArgs(argv, { home, validate = true } = {}) {
124
142
  return validate ? validateSetupOptions(expandPaths(options, home), { interactive: false }) : expandPaths(options, home);
125
143
  }
126
144
 
127
- export async function collectSetupOptions(effects) {
145
+ export function defaultHostname() {
146
+ return hostname().toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/^[^a-z0-9]+/, '').slice(0, 30) || 'workspace';
147
+ }
148
+
149
+ export async function collectSetupOptions(effects, presets = {}) {
128
150
  if (effects.interactive !== true) throw new Error('Interactive setup requires a TTY; provide the complete CLI options instead');
129
- const options = { interactive: true, explicitPorts: [] };
151
+ const options = { ...presets, interactive: true, explicitPorts: presets.explicitPorts ?? [] };
130
152
  const scopeChoices = [
131
153
  { value: 'all', label: 'Everything on this computer' },
132
154
  { value: 'server', label: 'Server only' },
133
155
  { value: 'client', label: 'Connect this computer to an existing server' },
134
156
  ];
135
157
  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.');
136
- options.scope = await effects.select('What would you like to set up?', scopeChoices, 'all');
158
+ options.scope ??= await effects.select('What would you like to set up?', scopeChoices, 'all');
137
159
  if (!scopes.includes(options.scope)) throw new Error('Scope must be all, server, or client');
138
160
  let existing;
139
161
  let pendingMigration;
140
162
  if (options.scope !== 'client') {
141
163
  const recommendedRoot = join(effects.home, '.ours-install');
142
164
  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.`);
143
- const location = await effects.select('Where should server programs and data be stored?', [
165
+ const location = options.stateDir ? 'preset' : await effects.select('Where should server programs and data be stored?', [
144
166
  { value: 'recommended', label: `Use the recommended folder (${recommendedRoot})` },
145
167
  { value: 'custom', label: 'Choose another folder' },
146
168
  ], 'recommended');
147
- options.stateDir = location === 'recommended' ? recommendedRoot : await effects.askLine('Folder for server programs and data: ', recommendedRoot);
169
+ options.stateDir ??= location === 'recommended' ? recommendedRoot : await effects.askLine('Folder for server programs and data: ', recommendedRoot);
148
170
  options.stateDir = expandPaths({ stateDir: options.stateDir }, effects.home).stateDir;
149
171
  existing = effects.readJson(join(options.stateDir, 'installation.json'));
150
172
  const migrationJournal = existing?.legacyMigrationSource ? effects.readJson(join(options.stateDir, 'legacy-migration.json')) : null;
@@ -156,7 +178,7 @@ export async function collectSetupOptions(effects) {
156
178
  ? 'This folder already contains an installation. Update selects this installer’s release while retaining state; repair reinstalls the retained package selection.'
157
179
  : options.scope === 'client' ? 'Install connects your selected apps. Update refreshes their packages while keeping the server connection.'
158
180
  : 'Install creates a managed installation in the selected folder. Update requires an installation already recorded there.');
159
- options.operation = !existing && options.scope !== 'client' ? 'install' : await effects.select('What should happen?', [
181
+ options.operation ??= !existing && options.scope !== 'client' ? 'install' : await effects.select('What should happen?', [
160
182
  { value: 'install', label: pendingMigration ? 'Resume the unfinished migration' : existing ? 'Repair this installation' : 'Install and configure' },
161
183
  { value: 'update', label: 'Update an existing installation' },
162
184
  ], existing && !pendingMigration ? 'update' : 'install');
@@ -166,7 +188,8 @@ export async function collectSetupOptions(effects) {
166
188
  const legacyState = nonempty(legacy?.stateDir) ? legacy.stateDir
167
189
  : legacy && legacy.stateDir === undefined && nonempty(effects.readJson(join(effects.home, '.ours', 'root.json'))?.name)
168
190
  ? join(effects.home, '.ours') : null;
169
- if (pendingMigration) options.migrateFrom = pendingMigration;
191
+ if (options.migrateFrom) { /* Explicit selection is validated below. */ }
192
+ else if (pendingMigration) options.migrateFrom = pendingMigration;
170
193
  else if (legacyState) {
171
194
  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.`);
172
195
  if (await effects.ask('Upgrade this existing ours installation and keep its data?', true)) options.migrateFrom = defaultConfig;
@@ -190,41 +213,61 @@ export async function collectSetupOptions(effects) {
190
213
  if (!nonempty(root?.name)) throw new Error('Cannot read the existing Human identity name; choose the configuration file for a complete existing installation');
191
214
  options.identityName = root.name;
192
215
  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.`);
193
- options.compatible = await effects.ask('Proceed with this release and keep the original state as a recovery copy?', false);
216
+ options.compatible ??= await effects.ask('Proceed with this release and keep the original state as a recovery copy?', false);
194
217
  if (!options.compatible) throw new Error('Migration requires explicit compatibility confirmation (--compatible)');
195
218
  }
196
219
  }
197
220
  if (options.scope !== 'client') {
198
221
  const recommendation = recommendedMode({ ...effects.platform, arch: effects.platform?.arch ?? process.arch });
199
222
  effects.out(`${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.`);
200
- options.mode = await effects.select('How should the server run?', [
201
- { value: 'packages', label: 'Native packages' }, { value: 'docker', label: 'Docker' },
202
- ], existing?.mode ?? recommendation.mode);
223
+ const runtime = options.mode ?? await effects.select('How should the server run?', [
224
+ { value: 'packages', label: 'Native packages' }, { value: 'docker', label: 'Docker' }, { value: 'podman', label: 'Podman' },
225
+ ], existing?.mode === 'docker' && existing?.containerEngine === 'podman' ? 'podman' : existing?.mode ?? recommendation.mode);
226
+ options.mode = runtime === 'podman' ? 'docker' : runtime;
227
+ if (runtime === 'podman') options.containerEngine = 'podman';
203
228
  if (!options.migrateFrom) {
204
- effects.out(existing ? 'Existing identities and names are retained. This name is used only if a Human identity needs to be created.' : 'Choose the name other people and agents should see for your Human identity.');
205
- options.identityName = await effects.askLine('What name should others see? ', existing?.messengerIdentity ?? effects.username?.() ?? 'me');
229
+ if (existing?.messengerIdentity) {
230
+ effects.out(`Existing Human identity ${existing.messengerIdentity} and Messenger profile will be retained.`);
231
+ // Supplied profile inputs never rename a retained root.
232
+ if (!['localUsername', 'name', 'surname'].some(key => options[key] !== undefined)) options.identityName ??= existing.messengerIdentity;
233
+ } else if (options.identityName === undefined || options.localUsername !== undefined) {
234
+ effects.out('Choose a local username (no global uniqueness check). Name and Surname appear in Messenger; the optional host name identifies this workspace.');
235
+ options.localUsername ??= await effects.askLine('Local username: ', (effects.username?.() ?? 'me').toLowerCase().replace(/[^a-z0-9-]/g, '-').slice(0, 30));
236
+ options.name ??= await effects.askLine('Name: ', '');
237
+ options.surname ??= await effects.askLine('Surname: ', '');
238
+ options.hostname ??= await effects.askLine('Host / machine name (optional): ', '');
239
+ }
240
+ }
241
+ if (['localUsername', 'name', 'surname'].some(key => options[key] !== undefined)) {
242
+ // Partial profile presets still collect missing fields on retained and
243
+ // migration paths; these inputs never replace their authoritative root.
244
+ effects.out('Name and Surname initialize a missing Messenger profile; an existing profile is retained.');
245
+ options.localUsername ??= await effects.askLine('Local username: ', (effects.username?.() ?? 'me').toLowerCase().replace(/[^a-z0-9-]/g, '-').slice(0, 30));
246
+ options.name ??= await effects.askLine('Name: ', '');
247
+ options.surname ??= await effects.askLine('Surname: ', '');
248
+ options.hostname ??= await effects.askLine('Host / machine name (optional): ', '');
206
249
  }
207
- for (const [key, fallback] of Object.entries(defaults)) options[key] = existing?.[key] ?? fallback;
250
+ for (const [key, fallback] of Object.entries(defaults)) options[key] ??= existing?.[key] ?? fallback;
208
251
  if (options.operation === 'update') {
209
252
  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.');
210
- options.compatible = await effects.ask('Proceed with this update and keep a recovery backup?', false);
253
+ options.compatible ??= await effects.ask('Proceed with this update and keep a recovery backup?', false);
211
254
  }
212
255
  } else {
213
256
  const savedProfile = join(effects.home, '.ours-client', 'profile.json');
214
257
  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.');
215
- const connection = await effects.select('Which server connection should this computer use?', [
258
+ const connection = options.config ? 'preset' : await effects.select('Which server connection should this computer use?', [
216
259
  { value: 'saved', label: 'Use the saved server connection' }, { value: 'file', label: 'Choose a connection profile file' },
217
260
  ], effects.readJson(savedProfile) ? 'saved' : 'file');
218
- options.config = connection === 'saved' ? savedProfile : await effects.askLine('Connection profile file: ', savedProfile);
261
+ options.config ??= connection === 'saved' ? savedProfile : await effects.askLine('Connection profile file: ', savedProfile);
219
262
  }
220
263
  if (options.scope !== 'server') {
221
264
  const detected = typeof effects.detectHarnesses === 'function' ? await effects.detectHarnesses() : [];
222
265
  const selected = integrations.filter(name => name === 'fleet' || detected.some(item => item.name === name && item.status === 'ok'));
223
266
  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.');
224
- options.integrations = await effects.multiselect('Which integrations should be configured?', [
267
+ options.integrations ??= await effects.multiselect('Which integrations should be configured?', [
225
268
  { value: 'codex', label: 'Codex' }, { value: 'claude-code', label: 'Claude Code' }, { value: 'fleet', label: 'Fleet — persistent agents' },
226
269
  ], selected);
227
- if (options.integrations.includes('fleet')) {
270
+ if (options.integrations.includes('fleet') && !options.disableFleetAgentsSetup && !options.fleetSettingsPath) {
228
271
  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.');
229
272
  const fleetMode = await effects.select('How should Fleet be configured?', [
230
273
  { value: 'wizard', label: 'Configure interactively with Fleet' }, { value: 'file', label: 'Use a prepared settings file' },
package/lib/setup.mjs CHANGED
@@ -114,6 +114,9 @@ export async function executeSetupPlan(plan, effects, { server = runServerComman
114
114
  const result = await server({ ...plan, role: 'server', operation: plan.operation, sourcePolicy: plan.sourcePolicy }, effects);
115
115
  if (result !== 0) return result;
116
116
  const record = validateInstallation(effects.readJson(join(plan.stateDir, 'installation.json')), plan.stateDir);
117
+ if (plan.name && plan.surname && plan.operation === 'install') {
118
+ await effects.serverEnsureHumanProfile(record, { name: plan.name, surname: plan.surname });
119
+ }
117
120
  if (plan.operation === 'update') {
118
121
  effects.out(progress(0, 1, 'Retained identities', 'Verify the Human identity after state restoration; existing names and keys are retained.'));
119
122
  const running = await effects.serverLifecycle(record, 'status', ['daemon']);
@@ -132,11 +135,12 @@ export async function executeSetupPlan(plan, effects, { server = runServerComman
132
135
  if (plan.scope !== 'server') {
133
136
  const result = await client({ role: 'client', operation: 'install', config: clientConfig,
134
137
  integrations: plan.integrations, fleetSettingsPath: plan.fleetSettingsPath, sourcePolicy: clientPolicy,
135
- preset: true, nonInteractive: !plan.interactive }, effects);
138
+ disableFleetAgentsSetup: plan.disableFleetAgentsSetup, preset: true, nonInteractive: !plan.interactive }, effects);
136
139
  if (result !== 0) return result;
137
140
  }
141
+ if (plan.disableFleetAgentsSetup && plan.integrations?.includes('fleet')) effects.out(info('Fleet is installed. Run ours-fleet setup-tunnel with the private expiring App setup file, then ours-fleet link-device. Configure agents and models in the App after linking.'));
138
142
  effects.out(ok(`Requested ${plan.operation} completed. Existing identities were retained.`));
139
- 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.'));
143
+ 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.'));
140
144
  return 0;
141
145
  }
142
146
 
@@ -148,7 +152,15 @@ export async function runSetup(argv, effects) {
148
152
  if (argv[0] === 'server' && maintenance.has(argv[1])) return await runServerCommand(parseNetworkArgs(argv), effects);
149
153
  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.');
150
154
  if (!argv.length) { effects.out(banner()); effects.out(heading('Interactive setup')); }
151
- const options = argv.length ? parseSetupArgs(argv, { home: effects.home }) : await collectSetupOptions(effects);
155
+ let options;
156
+ if (argv.length) {
157
+ const presets = parseSetupArgs(argv, { home: effects.home, validate: false });
158
+ try { options = validateSetupOptions(presets, { interactive: false }); }
159
+ catch (error) {
160
+ if (!effects.interactive || effects.env?.OURS_ASSUME_YES || !/^Missing (required setup options|human profile option):/.test(error.message)) throw error;
161
+ options = await collectSetupOptions(effects, presets);
162
+ }
163
+ } else options = await collectSetupOptions(effects);
152
164
  const plan = await prepareSetupPlan(options, effects);
153
165
  return await executeSetupPlan(plan, effects);
154
166
  } catch (error) {
package/lib/usage.mjs CHANGED
@@ -1,11 +1,15 @@
1
1
  // Public setup and maintenance commands.
2
- export const USAGE = `ours-install — interactive setup or complete CLI presets.
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
  Docker provides the full-stack gateway; native mode is server-only. Windows uses WSL.
8
8
 
9
+ Install now, configure Fleet agents in the App after linking:
10
+ ours-install --username alex --name Alex --surname Taylor --disable-fleet-agents-setup
11
+ Remaining installation choices are prompted on a terminal. No enrollment credentials or expiry.
12
+
9
13
  Full stack, with every required answer preset (no prompts):
10
14
  ours-install --mode docker --state-dir /private/ours --identity-name "Your Name" --integrations codex,fleet --fleet-settings /private/fleet.json
11
15
 
@@ -24,9 +28,14 @@ export const USAGE = `ours-install — interactive setup or complete CLI presets
24
28
  --mode docker|native packages is also accepted for native mode
25
29
  --container-engine docker|podman server container backend (default Docker); retained for maintenance
26
30
  --state-dir PATH installation root; required for all/server
31
+ --username USERNAME local username; no global uniqueness check
32
+ --name NAME Messenger given name
33
+ --surname SURNAME Messenger family name
34
+ --hostname NAME optional host/workspace name; defaults to this machine
35
+ --disable-fleet-agents-setup install Fleet CLI without its roles/model wizard
27
36
  --identity-name NAME desired Human name for a fresh server; existing root is retained
28
37
  --integrations LIST codex,claude-code,fleet; use none to skip clients explicitly
29
- --fleet-settings PATH JSON settings for Fleet; mandatory for CLI presets selecting Fleet
38
+ --fleet-settings PATH JSON settings for Fleet; required unless Fleet agents setup is disabled
30
39
  --config PATH complete connection profile for client-only setup
31
40
  --sources PATH explicit full development source policy override
32
41
  --port N public gateway port for Docker (default 3050; e.g. 4050)
@@ -41,8 +50,8 @@ export const USAGE = `ours-install — interactive setup or complete CLI presets
41
50
  --help, -h show help
42
51
  --version, -V print installer version
43
52
 
44
- CLI presets must be complete and never open the interactive form or a Fleet wizard.
45
- Missing answers are reported before installation. Both input modes run the same
53
+ Partial CLI presets on a terminal ask for the remaining choices. Without a terminal,
54
+ or with OURS_ASSUME_YES, supply complete CLI presets; missing answers fail before installation. Both input modes run the same
46
55
  installer with preparation, identity restoration, update and readiness progress.
47
56
  Fleet is configured but left stopped for operator review.
48
57
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/install",
3
- "version": "1.2.1-nightly.23",
3
+ "version": "1.2.1-nightly.24",
4
4
  "private": false,
5
5
  "description": "The all-in-one ours.network installer: one shared daemon, MCP, cowork, Telegram, Fleet initialization, harness plugins, Human identity, progress UI, and guided next steps.",
6
6
  "type": "module",