@ours.network/install 0.18.0-nightly.4 → 0.18.0-nightly.6

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/lib/target.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  // ours-install v3 — argument handling and daemon detection.
2
2
  //
3
- // Spec: installer-spec-v3 §§1-3. Everything here is PURE: the orchestrator
3
+ // Pure argument handling and daemon-target selection. The orchestrator
4
4
  // injects the probe, the file reads and the port check, so the whole decision
5
5
  // table is testable without a socket, a daemon or a filesystem.
6
6
  //
@@ -19,17 +19,15 @@ export const FREE_PORT_SPAN = 1000;
19
19
 
20
20
  // Ports that are other components' DEFAULTS, not facts about the machine: 3051
21
21
  // is the Telegram connector's, 3052 is cowork's loopback console. The free-port
22
- // search skips them; an explicit --port is still honoured as typed (spec §2).
22
+ // search skips them; an explicit --port is still honoured exactly as typed.
23
23
  //
24
- // NOTE, deliberately not silently reconciled: lib/logic.mjs's RESERVED_PORTS is
25
- // [3051] on this branch, while spec §2 states [3051, 3052] citing the
26
- // dev4/version-plumbing branch. This constant follows the spec. The two should
27
- // be merged once someone decides whether cowork's 3052 belongs in the shipped
28
- // list — flagged rather than assumed.
24
+ // Keep both component defaults out of automatic selection: 3051 belongs to the
25
+ // Telegram connector and 3052 to cowork. Explicit operator selections remain
26
+ // valid.
29
27
  export const INSTALL_RESERVED_PORTS = [3051, 3052];
30
28
 
31
29
  // The CLI-owned PID record that proves a daemon belongs to a state directory
32
- // even when nothing is recorded in its config (spec §1).
30
+ // even when nothing is recorded in its config.
33
31
  export const CLI_PID_RECORD = 'ours-cli-daemon.json';
34
32
  // The SAME record, written by a different daemon. `ours daemon start` writes
35
33
  // ours-cli-daemon.json; prerelease ours-mcp wrote daemon.pid (packages/core
@@ -77,7 +75,7 @@ export function samePath(a, b) {
77
75
  }
78
76
 
79
77
  // -----------------------------------------------------------------------------
80
- // §2 — arguments
78
+ // Arguments
81
79
  // -----------------------------------------------------------------------------
82
80
 
83
81
  const VALUE_FLAGS = new Set(['--state-dir', '--port']);
@@ -143,14 +141,14 @@ export function parseInstallArgs(argv = [], env = {}, { home = homedir() } = {})
143
141
  }
144
142
 
145
143
  // -----------------------------------------------------------------------------
146
- // §§1, 3 — is there a daemon at this state directory?
144
+ // Is there a daemon at this state directory?
147
145
  // -----------------------------------------------------------------------------
148
146
 
149
147
  /**
150
148
  * The port to probe first: the one recorded in <state-dir>/config.json, else the
151
149
  * built-in default. An explicit --port does NOT change where we look — the
152
150
  * question is which daemon owns this directory, and that is answered by the
153
- * directory's own record, not by what the operator typed (spec §2 step 2).
151
+ * directory's own record, not by what the operator typed.
154
152
  */
155
153
  export function candidatePort(config) {
156
154
  const recorded = config && typeof config.port === 'number' && Number.isFinite(config.port) ? config.port : null;
@@ -158,7 +156,7 @@ export function candidatePort(config) {
158
156
  }
159
157
 
160
158
  /**
161
- * Classify one probe result against the target state directory (spec §3).
159
+ * Classify one probe result against the target state directory.
162
160
  *
163
161
  * present — an ours daemon answered and reports THIS state directory
164
162
  * foreign — something answered, but it is not an ours daemon, or it is one
@@ -176,7 +174,11 @@ export function classifyProbe(probe, targetStateDir) {
176
174
  if (!samePath(probe.stateDir, targetStateDir)) {
177
175
  return { kind: 'foreign', reason: 'daemon owns a different state directory', stateDir: resolve(probe.stateDir) };
178
176
  }
179
- return { kind: 'present', stateDir: resolve(probe.stateDir) };
177
+ return {
178
+ kind: 'present',
179
+ stateDir: resolve(probe.stateDir),
180
+ daemonVersion: typeof probe.version === 'string' ? probe.version : null,
181
+ };
180
182
  }
181
183
 
182
184
  /**
@@ -242,12 +244,12 @@ export async function findDaemon({ stateDir, probe, readJson, readText }) {
242
244
  }
243
245
  // A FOREIGN DAEMON ON A PORT WE GUESSED IS NOT A REASON TO REFUSE.
244
246
  //
245
- // AMENDS #56. As first written, any foreign answer on the candidate port
247
+ // Regression guard. As first written, any foreign answer on the candidate port
246
248
  // refused the run. But a state directory with no recorded port has told us
247
249
  // nothing, so the candidate is the built-in default — which, on any machine
248
250
  // that already runs a daemon, is where the FIRST daemon answers. The result
249
251
  // was that a second daemon could never be created while the first was up:
250
- // §7 coexistence was unreachable, and the refusal's own advice ("re-run with
252
+ // coexistence was unreachable, and the refusal's own advice ("re-run with
251
253
  // --port for a free port") could not work either, because an explicit --port
252
254
  // deliberately does not change where we look.
253
255
  //
@@ -276,7 +278,7 @@ export async function findDaemon({ stateDir, probe, readJson, readText }) {
276
278
  }
277
279
 
278
280
  // -----------------------------------------------------------------------------
279
- // §2 — the derived-port rule
281
+ // Derived-port rule
280
282
  // -----------------------------------------------------------------------------
281
283
 
282
284
  /**
@@ -327,7 +329,13 @@ export async function resolveTarget({ stateDir, port = null, portExplicit = fals
327
329
  message: `--port ${port} disagrees with port ${found.port}, where the daemon for ${target} is actually running`,
328
330
  };
329
331
  }
330
- return { action: 'update', port: found.port, stateDir: target, config: found.config };
332
+ return {
333
+ action: 'update',
334
+ port: found.port,
335
+ stateDir: target,
336
+ config: found.config,
337
+ daemonVersion: found.daemonVersion ?? null,
338
+ };
331
339
  }
332
340
 
333
341
  // Creating. Only now is a port chosen.
package/lib/uninstall.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  // ours-uninstall v3 — removing ONE daemon, and the non-interactive contract.
2
2
  //
3
- // Spec: installer-spec-v3 §§8-9. Pure, like every other stage: the caller
3
+ // Pure uninstall planning, like every other stage: the caller
4
4
  // injects the file reads and this returns a plan.
5
5
  //
6
6
  // ours-uninstall [--state-dir PATH] [--purge] [--dry-run]
@@ -11,7 +11,7 @@
11
11
  // THE BIAS OF THIS WHOLE FILE IS TOWARD KEEPING THINGS. State is kept by default,
12
12
  // a component's config file is kept even when its daemon keys are removed, and
13
13
  // global packages are kept while any other daemon still needs them. The one
14
- // destructive operation, --purge, is separately gated (§8 step 5) because
14
+ // destructive operation, --purge, is separately gated because
15
15
  // it deletes identity keys, and no other step here is irreversible.
16
16
 
17
17
  import { join, resolve } from 'node:path';
@@ -22,9 +22,9 @@ import { canonHarnesses } from './logic.mjs';
22
22
  /**
23
23
  * Does this directory even look like an ours state directory?
24
24
  *
25
- * WHY THIS EXISTS. The owner removed the "created by an installer run" gate —
26
- * purge means purge, on any state directory. That was a deliberate ruling and
27
- * this does not reintroduce it: this asks "is this a state directory at all",
25
+ * Purge applies to any explicitly selected ours state directory, not only one
26
+ * created by this installer. This does not add a provenance gate: it asks
27
+ * "is this a state directory at all",
28
28
  * not "is it ours". With provenance gone, the typed path would otherwise be the
29
29
  * only thing between `ours-uninstall --state-dir ~ --purge` and a deleted home
30
30
  * directory, and a typed path is no protection against a path typed exactly as
@@ -85,7 +85,7 @@ export function inspectComponentConfig(path, { readText } = {}) {
85
85
  }
86
86
 
87
87
  /**
88
- * §8 step 1 — refuse if a component still points at this daemon.
88
+ * Refuse if a component still points at this daemon.
89
89
  *
90
90
  * Read the connector's and cowork's config files; if either names this daemon's
91
91
  * endpoint or its state directory, list them and stop. Exit 2, nothing removed.
@@ -160,7 +160,7 @@ export function planComponentDetach(key, existing) {
160
160
  }
161
161
 
162
162
  /**
163
- * §8 steps 3-4 — the boot service, then the daemon itself.
163
+ * Stop the boot service, then the daemon itself.
164
164
  *
165
165
  * Both delegate their refusals rather than reimplementing them: `ours daemon
166
166
  * uninstall-service` refuses to remove a unit not marked as CLI-managed, and
@@ -186,14 +186,13 @@ export function planDaemonRemoval({ stateDir, cliStartedIt }) {
186
186
  }
187
187
 
188
188
  /**
189
- * §8 step 5 — state. Kept unless every gate opens.
189
+ * State is kept unless every purge gate opens.
190
190
  *
191
191
  * --purge given — never the default; deleting identity keys is opt-in.
192
- * interactive — an unattended run never deletes state (§9).
192
+ * interactive — an unattended run never deletes state.
193
193
  * looks like a state directory — see looksLikeStateDir.
194
- * typed confirmation — the full path, not a y/N. The owner removed the
195
- * provenance condition, not the deliberateness, and with
196
- * provenance gone this is the last thing standing between
194
+ * typed confirmation — the full path, not a y/N. Without a provenance
195
+ * condition, this is the last thing standing between
197
196
  * a mistyped command and someone's identity keys.
198
197
  *
199
198
  * Returns the exact directory, never a glob or a parent, and only when every gate
@@ -219,7 +218,7 @@ export function planStatePurge({ stateDir, purge = false, assumeYes = false, exi
219
218
  }
220
219
 
221
220
  /**
222
- * §8 step 6 — global packages are shared. Remove them only when no OTHER state
221
+ * Global packages are shared. Remove them only when no other state
223
222
  * directory on this machine still has a daemon config; otherwise keep them and
224
223
  * say which daemon still needs them.
225
224
  */
@@ -257,7 +256,7 @@ export function planGlobalPackages({ stateDir, otherStateDirsWithConfig = [], pl
257
256
  }
258
257
 
259
258
  // -----------------------------------------------------------------------------
260
- // §8 — the harness plugins the installer wrote
259
+ // Harness plugins written by the installer
261
260
  // -----------------------------------------------------------------------------
262
261
 
263
262
  /**
@@ -397,7 +396,7 @@ export function planPluginRemoval({ home, env = {}, exists = () => false, lastDa
397
396
  }
398
397
 
399
398
  /**
400
- * §8 — WHICH harnesses to detach (inventory item 9.5).
399
+ * Decide which harnesses to detach using the documented environment contract.
401
400
  *
402
401
  * The nightly uninstaller let the operator choose: a `checkboxSelect` picker on a
403
402
  * terminal, `OURS_UNINSTALL` without one, and NOTHING removed when it had
@@ -472,7 +471,7 @@ export function selectHarnesses(plugins, chosen) {
472
471
  }
473
472
 
474
473
  /**
475
- * The whole §8 order, refusing at step 1 rather than starting and stopping
474
+ * Preserve the full uninstall order, refusing before any mutation rather than stopping
476
475
  * half-way.
477
476
  */
478
477
  export function planUninstall({ home, env = {}, endpoint, stateDir, purge = false, assumeYes = false, confirmedComponents = [], readJson, readText, exists = () => true, cliStartedIt = true, otherStateDirsWithConfig = [], typedConfirmation = null, explicitHarnessSelection = false, platform = 'linux' }) {
@@ -528,7 +527,7 @@ export function planUninstall({ home, env = {}, endpoint, stateDir, purge = fals
528
527
  }
529
528
 
530
529
  // -----------------------------------------------------------------------------
531
- // §9 — the non-interactive contract
530
+ // Non-interactive behavior
532
531
  // -----------------------------------------------------------------------------
533
532
 
534
533
  /**
@@ -565,7 +564,7 @@ export function refusalSurvivesAssumeYes(refusal) {
565
564
  }
566
565
 
567
566
  // -----------------------------------------------------------------------------
568
- // §9 — the OURS_UNINSTALL_* contract (inventory item 10.9)
567
+ // Documented OURS_UNINSTALL_* environment contract
569
568
  // -----------------------------------------------------------------------------
570
569
 
571
570
  /**
@@ -592,13 +591,13 @@ export function refusalSurvivesAssumeYes(refusal) {
592
591
  * that matters most.
593
592
  * OURS_UNINSTALL_TELEGRAM
594
593
  * OURS_UNINSTALL_ROOMS at the value `detach`, which is exactly the
595
- * confirmation §8 step 1 asks a human for and which an
594
+ * confirmation that the interactive flow asks a human for and which an
596
595
  * unattended run otherwise cannot give, so today the
597
596
  * run refuses instead of detaching.
598
597
  *
599
598
  * REFUSED
600
599
  * OURS_UNINSTALL_DATA=yes v3 never deletes state without a human
601
- * present (§9), and that rule protects private
600
+ * present; that rule protects private
602
601
  * keys that exist nowhere else.
603
602
  * OURS_UNINSTALL_PROFILE names an entry in the daemon registry. v3
604
603
  * has no registry; the selector is --state-dir.
package/lib/usage.mjs CHANGED
@@ -13,8 +13,9 @@ export const USAGE = `ours-install — the unified ours.network stack installer.
13
13
 
14
14
  Progress-driven setup for the whole stack: one shared daemon, MCP, Telegram,
15
15
  cowork, detected harness plugins (Claude Code / Codex / Hermes), a Human
16
- identity, and ours-fleet. The daemon and cowork start; Telegram and Fleet are
17
- staged but stopped. The installer asks only for information it cannot infer and
16
+ identity, and ours-fleet. The daemon, Telegram connector, and cowork shim start
17
+ as durable services; only Fleet is staged but stopped. The installer asks only
18
+ for information it cannot infer and
18
19
  ends with exact next commands plus a copy-paste agent hand-off prompt.
19
20
 
20
21
  --state-dir the daemon's STATE DIRECTORY, which is what identifies a daemon
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/install",
3
- "version": "0.18.0-nightly.4",
3
+ "version": "0.18.0-nightly.6",
4
4
  "private": false,
5
5
  "description": "The all-in-one ours.network installer: one shared daemon, MCP, cowork, Telegram, Fleet, harness plugins, Human identity, stopped Fleet starter, progress UI, and guided next steps.",
6
6
  "type": "module",