@ours.network/install 0.17.0-nightly.8 → 0.17.0
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 +24 -217
- package/install.mjs +80 -459
- package/lib/logic.mjs +21 -419
- package/package.json +1 -1
- package/uninstall.mjs +2 -15
- package/lib/components.mjs +0 -300
- package/lib/effects.mjs +0 -331
- package/lib/extras.mjs +0 -357
- package/lib/journal.mjs +0 -113
- package/lib/nightly-install.mjs +0 -716
- package/lib/nightly-uninstall.mjs +0 -373
- package/lib/orchestrate-uninstall.mjs +0 -293
- package/lib/orchestrate.mjs +0 -884
- package/lib/plan.mjs +0 -238
- package/lib/profiles.mjs +0 -501
- package/lib/rerun.mjs +0 -119
- package/lib/target.mjs +0 -353
- package/lib/uninstall.mjs +0 -439
- package/lib/usage.mjs +0 -47
package/lib/uninstall.mjs
DELETED
|
@@ -1,439 +0,0 @@
|
|
|
1
|
-
// ours-uninstall v3 — removing ONE daemon, and the non-interactive contract.
|
|
2
|
-
//
|
|
3
|
-
// Spec: installer-spec-v3 §§8-9. Pure, like every other stage: the caller
|
|
4
|
-
// injects the file reads and this returns a plan.
|
|
5
|
-
//
|
|
6
|
-
// ours-uninstall [--state-dir PATH] [--purge] [--dry-run]
|
|
7
|
-
//
|
|
8
|
-
// `--state-dir` selects WHICH daemon to remove and defaults to ~/.ours. The
|
|
9
|
-
// uninstaller only ever touches that daemon and the components pointing at it.
|
|
10
|
-
//
|
|
11
|
-
// THE BIAS OF THIS WHOLE FILE IS TOWARD KEEPING THINGS. State is kept by default,
|
|
12
|
-
// a component's config file is kept even when its daemon keys are removed, and
|
|
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
|
|
15
|
-
// it deletes identity keys, and no other step here is irreversible.
|
|
16
|
-
|
|
17
|
-
import { join, resolve } from 'node:path';
|
|
18
|
-
import { unitNameForStateDir } from './plan.mjs';
|
|
19
|
-
import { tgConfigPath, coworkConfigPath } from './components.mjs';
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Does this directory even look like an ours state directory?
|
|
23
|
-
*
|
|
24
|
-
* WHY THIS EXISTS. The owner removed the "created by an installer run" gate —
|
|
25
|
-
* purge means purge, on any state directory. That was a deliberate ruling and
|
|
26
|
-
* this does not reintroduce it: this asks "is this a state directory at all",
|
|
27
|
-
* not "is it ours". With provenance gone, the typed path would otherwise be the
|
|
28
|
-
* only thing between `ours-uninstall --state-dir ~ --purge` and a deleted home
|
|
29
|
-
* directory, and a typed path is no protection against a path typed exactly as
|
|
30
|
-
* intended but meant differently.
|
|
31
|
-
*
|
|
32
|
-
* A directory qualifies if it carries any of the artefacts only a daemon writes.
|
|
33
|
-
* Absent all of them, purge refuses and says why — the operator can still delete
|
|
34
|
-
* the directory themselves, which is the right place for that decision.
|
|
35
|
-
*/
|
|
36
|
-
export const STATE_DIR_EVIDENCE = ['config.json', 'daemon-token', 'ours-cli-daemon.json', 'root.json'];
|
|
37
|
-
export function looksLikeStateDir(stateDir, exists) {
|
|
38
|
-
return STATE_DIR_EVIDENCE.some((name) => exists(join(resolve(stateDir), name)));
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* A component config file, read so that ABSENT and CORRUPT are different answers.
|
|
43
|
-
*
|
|
44
|
-
* WHY THIS IS NOT `readJson`. `effects.readJson` swallows every failure into
|
|
45
|
-
* `null` (lib/effects.mjs), which is the right shape for the installer — an
|
|
46
|
-
* unreadable daemon config there means "nothing recorded", and the run proceeds
|
|
47
|
-
* to write one. On the UNINSTALL side that same `null` is a fail-open:
|
|
48
|
-
* `componentsPointingHere` reads it as "no connector points at this daemon",
|
|
49
|
-
* step 1's refusal never fires, and the daemon is removed out from under a
|
|
50
|
-
* connector that may still be using it. The nightly uninstaller refuses here
|
|
51
|
-
* (`lib/nightly-uninstall.mjs:22,244`) and has a test pinning that it does.
|
|
52
|
-
*
|
|
53
|
-
* So this takes `readText` — already on the effects contract — and does the parse
|
|
54
|
-
* itself, which keeps the whole decision pure and testable:
|
|
55
|
-
*
|
|
56
|
-
* absent — no file (readText returned null). Nothing to point anywhere.
|
|
57
|
-
* ok — a JSON object.
|
|
58
|
-
* corrupt — the file exists and is not a readable JSON object. An EXISTING file
|
|
59
|
-
* we cannot parse is the case that must stop the run: we cannot prove
|
|
60
|
-
* it does not name this daemon, and "cannot prove" is not "does not".
|
|
61
|
-
*
|
|
62
|
-
* An empty existing file is CORRUPT, not absent — same as the nightly reader,
|
|
63
|
-
* which parses whatever `existsSync` says is there.
|
|
64
|
-
*
|
|
65
|
-
* THIS DECIDES NOTHING ABOUT CONTENTS. `componentsPointingHere` still reads its
|
|
66
|
-
* values through `readJson`, unchanged — deliberately, so this addition cannot
|
|
67
|
-
* alter which components are found. The only new outcome is `corrupt`, and the
|
|
68
|
-
* only thing that consumes it is a refusal.
|
|
69
|
-
*/
|
|
70
|
-
export function inspectComponentConfig(path, { readText } = {}) {
|
|
71
|
-
if (typeof readText !== 'function') return { state: 'unknown', reason: 'no text reader injected' };
|
|
72
|
-
const text = readText(path);
|
|
73
|
-
if (text === null || text === undefined) return { state: 'absent' };
|
|
74
|
-
let parsed;
|
|
75
|
-
try {
|
|
76
|
-
parsed = JSON.parse(String(text));
|
|
77
|
-
} catch (error) {
|
|
78
|
-
return { state: 'corrupt', reason: error instanceof Error ? error.message : String(error) };
|
|
79
|
-
}
|
|
80
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
81
|
-
return { state: 'corrupt', reason: 'expected a JSON object' };
|
|
82
|
-
}
|
|
83
|
-
return { state: 'ok' };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* §8 step 1 — refuse if a component still points at this daemon.
|
|
88
|
-
*
|
|
89
|
-
* Read the connector's and cowork's config files; if either names this daemon's
|
|
90
|
-
* endpoint or its state directory, list them and stop. Exit 2, nothing removed.
|
|
91
|
-
* The operator either repoints them or confirms their removal in the same run.
|
|
92
|
-
*
|
|
93
|
-
* Matching on EITHER the endpoint or the state directory is deliberate: a
|
|
94
|
-
* half-written pair should still be caught, and the whole point of the pair is
|
|
95
|
-
* that neither half alone is trustworthy.
|
|
96
|
-
*/
|
|
97
|
-
export function componentsPointingHere({ home, env = {}, endpoint, stateDir, readJson }) {
|
|
98
|
-
const dir = resolve(stateDir);
|
|
99
|
-
const found = [];
|
|
100
|
-
const tg = readJson(tgConfigPath(home, env));
|
|
101
|
-
if (tg && (tg.daemonUrl === endpoint || (typeof tg.daemonStateDir === 'string' && resolve(tg.daemonStateDir) === dir))) {
|
|
102
|
-
found.push({ key: 'tg', config: tgConfigPath(home, env), endpoint: tg.daemonUrl ?? null, stateDir: tg.daemonStateDir ?? null });
|
|
103
|
-
}
|
|
104
|
-
const cowork = readJson(coworkConfigPath(home, env));
|
|
105
|
-
const block = cowork && typeof cowork.daemon === 'object' && cowork.daemon !== null ? cowork.daemon : null;
|
|
106
|
-
if (block && (block.endpoint === endpoint || (typeof block.stateDir === 'string' && resolve(block.stateDir) === dir))) {
|
|
107
|
-
found.push({ key: 'cowork', config: coworkConfigPath(home, env), endpoint: block.endpoint ?? null, stateDir: block.stateDir ?? null });
|
|
108
|
-
}
|
|
109
|
-
return found;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* The other half of step 1, and the reason it is a SEPARATE question.
|
|
114
|
-
*
|
|
115
|
-
* `componentsPointingHere` answers "does a component point here?". This answers
|
|
116
|
-
* "is there a component config I cannot read at all?" — and the two must not be
|
|
117
|
-
* collapsed, because they have opposite resolutions. A component that points here
|
|
118
|
-
* can be confirmed for removal in the same run; a config that will not parse
|
|
119
|
-
* cannot be confirmed away by anybody, because nothing about its contents is
|
|
120
|
-
* known. The operator has to fix or move the file first.
|
|
121
|
-
*
|
|
122
|
-
* Returns [] when no reader capable of telling absent from corrupt was injected.
|
|
123
|
-
* That is not a silent pass: planUninstall reports the limit in the plan itself.
|
|
124
|
-
*/
|
|
125
|
-
export function unreadableComponentConfigs({ home, env = {}, readText }) {
|
|
126
|
-
if (typeof readText !== 'function') return [];
|
|
127
|
-
const out = [];
|
|
128
|
-
for (const [key, path] of [['tg', tgConfigPath(home, env)], ['cowork', coworkConfigPath(home, env)]]) {
|
|
129
|
-
const read = inspectComponentConfig(path, { readText });
|
|
130
|
-
if (read.state === 'corrupt') out.push({ key, config: path, reason: read.reason });
|
|
131
|
-
}
|
|
132
|
-
return out;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Strip a component's daemon keys while KEEPING the file. It also holds the
|
|
137
|
-
* operator's bot token and settings, and those are not ours to delete.
|
|
138
|
-
*
|
|
139
|
-
* For cowork, removing the `daemon` block returns it to embedded mode. That is a
|
|
140
|
-
* real behaviour change, not a cleanup, so the plan carries `behaviourChange` for
|
|
141
|
-
* the screen to state BEFORE it happens.
|
|
142
|
-
*/
|
|
143
|
-
export function planComponentDetach(key, existing) {
|
|
144
|
-
const base = existing && typeof existing === 'object' && !Array.isArray(existing) ? { ...existing } : {};
|
|
145
|
-
if (key === 'tg') {
|
|
146
|
-
const removed = ['daemonUrl', 'daemonStateDir'].filter((k) => k in base);
|
|
147
|
-
for (const k of removed) delete base[k];
|
|
148
|
-
return { key, removed, config: base, keepsFile: true, behaviourChange: null };
|
|
149
|
-
}
|
|
150
|
-
const had = 'daemon' in base;
|
|
151
|
-
delete base.daemon;
|
|
152
|
-
return {
|
|
153
|
-
key,
|
|
154
|
-
removed: had ? ['daemon'] : [],
|
|
155
|
-
config: base,
|
|
156
|
-
keepsFile: true,
|
|
157
|
-
behaviourChange: had ? 'cowork returns to embedded mode' : null,
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* §8 steps 3-4 — the boot service, then the daemon itself.
|
|
163
|
-
*
|
|
164
|
-
* Both delegate their refusals rather than reimplementing them: `ours daemon
|
|
165
|
-
* uninstall-service` refuses to remove a unit not marked as CLI-managed, and
|
|
166
|
-
* `ours daemon stop` refuses to signal a daemon it did not start. In the second
|
|
167
|
-
* case the screen names the external launcher and the run CONTINUES — a daemon
|
|
168
|
-
* someone else supervises is not a failure of this uninstall.
|
|
169
|
-
*/
|
|
170
|
-
export function planDaemonRemoval({ stateDir, cliStartedIt }) {
|
|
171
|
-
const dir = resolve(stateDir);
|
|
172
|
-
const unit = unitNameForStateDir(dir);
|
|
173
|
-
return [
|
|
174
|
-
{
|
|
175
|
-
id: 'service',
|
|
176
|
-
unit: unit.ok ? unit.unit : null,
|
|
177
|
-
command: ['ours', 'daemon', 'uninstall-service', '--yes', '--state-dir', dir],
|
|
178
|
-
note: 'refuses a unit not marked as CLI-managed',
|
|
179
|
-
},
|
|
180
|
-
cliStartedIt
|
|
181
|
-
? { id: 'stop', command: ['ours', 'daemon', 'stop', '--config', join(dir, 'config.json')] }
|
|
182
|
-
: { id: 'stop-external', command: null, continues: true, note: 'this daemon was not started by the CLI; naming its launcher and continuing' },
|
|
183
|
-
];
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* §8 step 5 — state. Kept unless every gate opens.
|
|
188
|
-
*
|
|
189
|
-
* --purge given — never the default; deleting identity keys is opt-in.
|
|
190
|
-
* interactive — an unattended run never deletes state (§9).
|
|
191
|
-
* looks like a state directory — see looksLikeStateDir.
|
|
192
|
-
* typed confirmation — the full path, not a y/N. The owner removed the
|
|
193
|
-
* provenance condition, not the deliberateness, and with
|
|
194
|
-
* provenance gone this is the last thing standing between
|
|
195
|
-
* a mistyped command and someone's identity keys.
|
|
196
|
-
*
|
|
197
|
-
* Returns the exact directory, never a glob or a parent, and only when every gate
|
|
198
|
-
* is satisfied.
|
|
199
|
-
*/
|
|
200
|
-
export function planStatePurge({ stateDir, purge = false, assumeYes = false, exists = () => true, typedConfirmation = null }) {
|
|
201
|
-
const dir = resolve(stateDir);
|
|
202
|
-
const keep = (reason) => ({ action: 'keep', stateDir: dir, reason, hint: 're-run with --purge to delete identities and history' });
|
|
203
|
-
if (!purge) return keep('state is kept by default');
|
|
204
|
-
if (assumeYes) return keep('state is never deleted non-interactively');
|
|
205
|
-
if (!looksLikeStateDir(dir, exists)) {
|
|
206
|
-
return keep(`${dir} does not look like an ours state directory (none of ${STATE_DIR_EVIDENCE.join(', ')}); refusing to delete it`);
|
|
207
|
-
}
|
|
208
|
-
if (typedConfirmation !== dir) {
|
|
209
|
-
return {
|
|
210
|
-
action: 'confirm-typed',
|
|
211
|
-
stateDir: dir,
|
|
212
|
-
expected: dir,
|
|
213
|
-
prompt: `This permanently deletes ${dir} and everything in it, including the identity keys and message history of every identity there. Those keys exist nowhere else and no peer can give them back.\nType the full path to confirm:`,
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
return { action: 'purge', stateDir: dir, paths: [dir] };
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* §8 step 6 — global packages are shared. Remove them only when no OTHER state
|
|
221
|
-
* directory on this machine still has a daemon config; otherwise keep them and
|
|
222
|
-
* say which daemon still needs them.
|
|
223
|
-
*/
|
|
224
|
-
export function planGlobalPackages({ stateDir, otherStateDirsWithConfig = [], pluginPackages = [] }) {
|
|
225
|
-
const dir = resolve(stateDir);
|
|
226
|
-
const others = otherStateDirsWithConfig.map((d) => resolve(d)).filter((d) => d !== dir);
|
|
227
|
-
if (others.length > 0) {
|
|
228
|
-
return { action: 'keep', packages: [], stillNeededBy: others, reason: `still used by the daemon at ${others[0]}` };
|
|
229
|
-
}
|
|
230
|
-
// The harness-plugin launchers follow the SAME rule, not a second one: they
|
|
231
|
-
// speak to a daemon through ours-mcp, so a second daemon still on the machine
|
|
232
|
-
// still needs them, and the one condition above already decides that.
|
|
233
|
-
return { action: 'remove', packages: ['@ours.network/cli', '@ours.network/mcp', ...pluginPackages], stillNeededBy: [] };
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// -----------------------------------------------------------------------------
|
|
237
|
-
// §8 — the harness plugins the installer wrote
|
|
238
|
-
// -----------------------------------------------------------------------------
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* The sentinels the plugin installers stamp around everything they write. They
|
|
242
|
-
* are the ONLY thing that makes a block removable: without them we would be
|
|
243
|
-
* editing a config file on a guess.
|
|
244
|
-
*/
|
|
245
|
-
export const YAML_BLOCK = { start: '# >>> ours.network plugin (managed block)', end: '# <<< ours.network plugin' };
|
|
246
|
-
export const MD_BLOCK = { start: '<!-- >>> ours.network plugin (managed block) -->', end: '<!-- <<< ours.network plugin -->' };
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Remove OUR block from a config file, or refuse.
|
|
250
|
-
*
|
|
251
|
-
* SAME IDENTIFICATION DISCIPLINE AS THE SYSTEMD UNIT, and for the same reason.
|
|
252
|
-
* The installer refuses to overwrite a unit it cannot positively identify rather
|
|
253
|
-
* than guessing; this refuses to edit a config it cannot positively identify.
|
|
254
|
-
* Three outcomes, and the third is the one that matters:
|
|
255
|
-
*
|
|
256
|
-
* absent — no start sentinel. The file is not ours to edit; left untouched.
|
|
257
|
-
* strip — both sentinels present. Exactly the delimited span is removed.
|
|
258
|
-
* refuse — a start sentinel with NO end. v2 deleted to end-of-file here,
|
|
259
|
-
* which would take everything the user added after our block with
|
|
260
|
-
* it. An unterminated block is damage we cannot bound, so it is
|
|
261
|
-
* reported and left alone.
|
|
262
|
-
*/
|
|
263
|
-
export function stripManagedBlock(text, markers) {
|
|
264
|
-
if (typeof text !== 'string' || !text.includes(markers.start)) {
|
|
265
|
-
return { action: 'absent', reason: 'no ours managed block in this file' };
|
|
266
|
-
}
|
|
267
|
-
const lines = text.split('\n');
|
|
268
|
-
const out = [];
|
|
269
|
-
let inside = false;
|
|
270
|
-
let closed = false;
|
|
271
|
-
for (const line of lines) {
|
|
272
|
-
if (!inside && line.includes(markers.start)) { inside = true; continue; }
|
|
273
|
-
if (inside) {
|
|
274
|
-
if (line.includes(markers.end)) { inside = false; closed = true; }
|
|
275
|
-
continue;
|
|
276
|
-
}
|
|
277
|
-
out.push(line);
|
|
278
|
-
}
|
|
279
|
-
if (!closed) {
|
|
280
|
-
return {
|
|
281
|
-
action: 'refuse',
|
|
282
|
-
reason: 'the ours managed block has no closing marker; refusing to guess where it ends',
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
return { action: 'strip', text: out.join('\n') };
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* What the harness-plugin half of an uninstall removes.
|
|
290
|
-
*
|
|
291
|
-
* `lastDaemon` is the same condition planGlobalPackages decides on, passed in
|
|
292
|
-
* rather than recomputed: while another daemon is still on this machine its
|
|
293
|
-
* harnesses still need their plugins, so nothing here is touched at all.
|
|
294
|
-
*
|
|
295
|
-
* Claude Code is deliberately manual-only. Its plugin lives in the in-app
|
|
296
|
-
* marketplace, and there is no file on disk we own — so this prints the two
|
|
297
|
-
* commands and claims nothing, which is the same never-dead-end contract the
|
|
298
|
-
* installer applies to a harness it cannot drive.
|
|
299
|
-
*
|
|
300
|
-
* Every path is EXACT — the precise file or directory the plugin installer
|
|
301
|
-
* writes. Nothing here is a glob, and nothing walks a tree looking for matches.
|
|
302
|
-
*/
|
|
303
|
-
export function planPluginRemoval({ home, env = {}, exists = () => false, lastDaemon = true } = {}) {
|
|
304
|
-
const hermesDir = env.HERMES_DIR || join(home, '.hermes');
|
|
305
|
-
const codexDir = env.CODEX_DIR || join(home, '.codex');
|
|
306
|
-
const skillsDir = env.SKILLS_DIR || join(home, '.agents', 'skills');
|
|
307
|
-
|
|
308
|
-
const manual = {
|
|
309
|
-
key: 'claude-code',
|
|
310
|
-
label: 'Claude Code plugin',
|
|
311
|
-
action: 'manual',
|
|
312
|
-
reason: "its plugin lives in Claude Code's in-app marketplace, so nothing on disk is ours to remove",
|
|
313
|
-
steps: ['/plugin uninstall ours', '/plugin marketplace remove adapt-toolkit/ours-claude-marketplace'],
|
|
314
|
-
};
|
|
315
|
-
if (!lastDaemon) {
|
|
316
|
-
return {
|
|
317
|
-
action: 'keep',
|
|
318
|
-
reason: 'another daemon on this machine still uses these plugins',
|
|
319
|
-
harnesses: [],
|
|
320
|
-
manual: [manual],
|
|
321
|
-
packages: [],
|
|
322
|
-
};
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
const harnesses = [];
|
|
326
|
-
const hermesConfig = join(hermesDir, 'config.yaml');
|
|
327
|
-
if (exists(hermesConfig) || exists(hermesDir)) {
|
|
328
|
-
harnesses.push({
|
|
329
|
-
key: 'hermes',
|
|
330
|
-
label: 'Hermes plugin',
|
|
331
|
-
blocks: [{ path: hermesConfig, markers: YAML_BLOCK }],
|
|
332
|
-
dirs: [
|
|
333
|
-
join(hermesDir, 'skills', 'communication', 'ours'),
|
|
334
|
-
join(hermesDir, 'skills', 'communication', 'writing-agent-bios'),
|
|
335
|
-
],
|
|
336
|
-
files: [join(hermesDir, 'ours-connector.env'), join(hermesDir, 'ours-connector.log')],
|
|
337
|
-
pkg: '@ours.network/hermes',
|
|
338
|
-
});
|
|
339
|
-
}
|
|
340
|
-
if (exists(join(codexDir, 'config.toml')) || exists(codexDir)) {
|
|
341
|
-
harnesses.push({
|
|
342
|
-
key: 'codex',
|
|
343
|
-
label: 'Codex plugin',
|
|
344
|
-
blocks: [
|
|
345
|
-
{ path: join(codexDir, 'config.toml'), markers: YAML_BLOCK },
|
|
346
|
-
{ path: join(codexDir, 'AGENTS.md'), markers: MD_BLOCK },
|
|
347
|
-
],
|
|
348
|
-
dirs: [join(skillsDir, 'ours'), join(skillsDir, 'writing-agent-bios')],
|
|
349
|
-
files: [],
|
|
350
|
-
pkg: '@ours.network/codex',
|
|
351
|
-
});
|
|
352
|
-
}
|
|
353
|
-
return {
|
|
354
|
-
action: 'remove',
|
|
355
|
-
harnesses,
|
|
356
|
-
manual: [manual],
|
|
357
|
-
packages: harnesses.map((h) => h.pkg),
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
/**
|
|
362
|
-
* The whole §8 order, refusing at step 1 rather than starting and stopping
|
|
363
|
-
* half-way.
|
|
364
|
-
*/
|
|
365
|
-
export function planUninstall({ home, env = {}, endpoint, stateDir, purge = false, assumeYes = false, confirmedComponents = [], readJson, readText, exists = () => true, cliStartedIt = true, otherStateDirsWithConfig = [], typedConfirmation = null }) {
|
|
366
|
-
const dir = resolve(stateDir);
|
|
367
|
-
const lastDaemon = otherStateDirsWithConfig.map((d) => resolve(d)).filter((d) => d !== dir).length === 0;
|
|
368
|
-
const plugins = planPluginRemoval({ home, env, exists, lastDaemon });
|
|
369
|
-
|
|
370
|
-
// BEFORE the pointing question, and not resolvable by confirming anything: a
|
|
371
|
-
// component config that will not parse cannot be proven not to name this
|
|
372
|
-
// daemon. Fail closed, name the file, remove nothing. This mirrors the nightly
|
|
373
|
-
// uninstaller's refusal (lib/nightly-uninstall.mjs:244) rather than inventing a
|
|
374
|
-
// second wording for the same event.
|
|
375
|
-
const unreadable = unreadableComponentConfigs({ home, env, readText });
|
|
376
|
-
if (unreadable.length > 0) {
|
|
377
|
-
return {
|
|
378
|
-
action: 'refuse',
|
|
379
|
-
exitCode: 2,
|
|
380
|
-
reason: 'component-config-unreadable',
|
|
381
|
-
components: unreadable,
|
|
382
|
-
removed: [],
|
|
383
|
-
message: `${unreadable.map((c) => `${c.config} is corrupt or unsafe to inspect${c.reason ? `: ${c.reason}` : ''}`).join('; ')}. Refusing to uninstall: a component config that cannot be read cannot be shown not to point at this daemon. Repair or move it, then re-run. Nothing was removed.`,
|
|
384
|
-
};
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const pointing = componentsPointingHere({ home, env, endpoint, stateDir: dir, readJson });
|
|
388
|
-
const unconfirmed = pointing.filter((p) => !confirmedComponents.includes(p.key));
|
|
389
|
-
if (unconfirmed.length > 0) {
|
|
390
|
-
return {
|
|
391
|
-
action: 'refuse',
|
|
392
|
-
exitCode: 2,
|
|
393
|
-
reason: 'component-still-points-here',
|
|
394
|
-
components: unconfirmed,
|
|
395
|
-
removed: [],
|
|
396
|
-
message: `${unconfirmed.map((p) => p.key).join(' and ')} still point at this daemon. Repoint them, or confirm their removal in the same run. Nothing was removed.`,
|
|
397
|
-
};
|
|
398
|
-
}
|
|
399
|
-
return {
|
|
400
|
-
action: 'uninstall',
|
|
401
|
-
stateDir: dir,
|
|
402
|
-
detach: pointing.map((p) => ({ key: p.key, service: [`ours-${p.key === 'tg' ? 'tg-connector' : 'cowork'}`, 'uninstall-service'] })),
|
|
403
|
-
daemon: planDaemonRemoval({ stateDir: dir, cliStartedIt }),
|
|
404
|
-
state: planStatePurge({ stateDir: dir, purge, assumeYes, exists, typedConfirmation }),
|
|
405
|
-
plugins,
|
|
406
|
-
packages: planGlobalPackages({ stateDir: dir, otherStateDirsWithConfig, pluginPackages: plugins.packages }),
|
|
407
|
-
};
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
// -----------------------------------------------------------------------------
|
|
411
|
-
// §9 — the non-interactive contract
|
|
412
|
-
// -----------------------------------------------------------------------------
|
|
413
|
-
|
|
414
|
-
/**
|
|
415
|
-
* What each question answers to under OURS_ASSUME_YES.
|
|
416
|
-
*
|
|
417
|
-
* The two `false` entries are the point of the table: assume-yes never turns a
|
|
418
|
-
* component on that was off, never MOVES one that already exists, and never
|
|
419
|
-
* deletes state. It suppresses questions; it does not consent on the operator's
|
|
420
|
-
* behalf to anything irreversible or to anything that changes where an existing
|
|
421
|
-
* component is pointing.
|
|
422
|
-
*/
|
|
423
|
-
export const NON_INTERACTIVE_ANSWERS = {
|
|
424
|
-
daemon: true,
|
|
425
|
-
mcp: true,
|
|
426
|
-
tg: false,
|
|
427
|
-
cowork: false,
|
|
428
|
-
repointExistingConnector: false,
|
|
429
|
-
purge: false,
|
|
430
|
-
};
|
|
431
|
-
|
|
432
|
-
/**
|
|
433
|
-
* OURS_ASSUME_YES suppresses questions; it NEVER suppresses a refusal. Every
|
|
434
|
-
* refusal in this specification applies unchanged in non-interactive mode and
|
|
435
|
-
* exits 2 without writing anything.
|
|
436
|
-
*/
|
|
437
|
-
export function refusalSurvivesAssumeYes(refusal) {
|
|
438
|
-
return refusal && refusal.action === 'refuse' ? { ...refusal, exitCode: 2 } : refusal;
|
|
439
|
-
}
|
package/lib/usage.mjs
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
// ours-install v3 — the help text.
|
|
2
|
-
//
|
|
3
|
-
// It lives here rather than in the bin because `--help` is a behaviour with a
|
|
4
|
-
// contract (the flags it names must be the flags target.mjs accepts), and a bin
|
|
5
|
-
// that is three lines long cannot be the place a contract is asserted.
|
|
6
|
-
|
|
7
|
-
export const USAGE = `ours-install — the unified ours.network stack installer.
|
|
8
|
-
|
|
9
|
-
Install: npm i -g @ours.network/install && ours-install (recommended)
|
|
10
|
-
npx @ours.network/install (one-off)
|
|
11
|
-
|
|
12
|
-
ours-install [--state-dir PATH] [--port N] [--dry-run] [--help] [--version]
|
|
13
|
-
|
|
14
|
-
Guided setup for the whole stack: the ours daemon, its components (the MCP
|
|
15
|
-
server, the Telegram connector, cowork), your harness plugins (Claude Code /
|
|
16
|
-
Codex / Hermes), ours-fleet and voice transcription — then one copy-paste
|
|
17
|
-
hand-off prompt. You approve each step; re-run any time to add a piece or update.
|
|
18
|
-
|
|
19
|
-
--state-dir the daemon's STATE DIRECTORY, which is what identifies a daemon
|
|
20
|
-
(default ~/.ours). A second state directory is a second daemon.
|
|
21
|
-
--port used only when CREATING a daemon. For a daemon that already owns
|
|
22
|
-
the state directory the port comes from its own record, and a
|
|
23
|
-
--port that disagrees with it is refused rather than corrected.
|
|
24
|
-
--dry-run walk the whole flow and print what it WOULD do — change nothing
|
|
25
|
-
--help show this help and exit
|
|
26
|
-
--version print the installer version and exit
|
|
27
|
-
|
|
28
|
-
Env: OURS_ASSUME_YES=1 (accept defaults, no prompts) · OURS_INSTALL_DRY_RUN=1 ·
|
|
29
|
-
OURS_CHANNEL=nightly · OURS_BROKER_URL · OURS_NPM. Docs: https://ours.network`;
|
|
30
|
-
|
|
31
|
-
export const UNINSTALL_USAGE = `ours-uninstall — remove one ours daemon and what attaches to it.
|
|
32
|
-
|
|
33
|
-
ours-uninstall [--state-dir PATH] [--purge] [--dry-run] [--help] [--version]
|
|
34
|
-
|
|
35
|
-
Removes the boot service, stops the daemon, and removes the global packages —
|
|
36
|
-
but ONLY when no other daemon on this machine still needs them. A component
|
|
37
|
-
still pointing at this daemon stops the run before anything is removed, so a
|
|
38
|
-
run that refuses leaves the daemon whole rather than half-dismantled.
|
|
39
|
-
|
|
40
|
-
--state-dir which daemon to remove (default ~/.ours). A daemon IS its state
|
|
41
|
-
directory, so this is the only thing that names one.
|
|
42
|
-
--purge also delete the state directory itself. Never the default, never
|
|
43
|
-
done non-interactively, and it asks you to type the full path —
|
|
44
|
-
identity keys exist nowhere else and no peer can give them back.
|
|
45
|
-
--dry-run print what it WOULD remove and remove nothing
|
|
46
|
-
--help show this help and exit
|
|
47
|
-
--version print the version and exit`;
|