@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/README.md +34 -13
- package/install.sh +3 -0
- package/lib/components.mjs +12 -13
- package/lib/detect.mjs +3 -3
- package/lib/effects.mjs +72 -10
- package/lib/extras.mjs +14 -17
- package/lib/logic.mjs +8 -17
- package/lib/marketplace.mjs +104 -0
- package/lib/orchestrate-uninstall.mjs +2 -2
- package/lib/orchestrate.mjs +226 -38
- package/lib/plan.mjs +9 -9
- package/lib/rerun.mjs +7 -7
- package/lib/target.mjs +25 -17
- package/lib/uninstall.mjs +19 -20
- package/lib/usage.mjs +3 -2
- package/package.json +1 -1
package/lib/target.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// ours-install v3 — argument handling and daemon detection.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
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
|
|
22
|
+
// search skips them; an explicit --port is still honoured exactly as typed.
|
|
23
23
|
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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 {
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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 {
|
|
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
|
-
//
|
|
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
|
|
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
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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.
|
|
195
|
-
*
|
|
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
|
-
*
|
|
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
|
-
//
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
|
17
|
-
staged but stopped. The installer asks only
|
|
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.
|
|
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",
|