@ours.network/install 0.17.0-nightly.9 → 0.17.1
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 +55 -220
- package/install.mjs +252 -503
- package/install.sh +3 -0
- package/lib/logic.mjs +25 -416
- package/lib/marketplace.mjs +104 -0
- package/package.json +1 -1
- package/uninstall.mjs +4 -36
- package/lib/components.mjs +0 -351
- package/lib/detect.mjs +0 -182
- package/lib/effects.mjs +0 -331
- package/lib/extras.mjs +0 -400
- package/lib/journal.mjs +0 -113
- package/lib/nightly-install.mjs +0 -739
- package/lib/nightly-uninstall.mjs +0 -396
- package/lib/orchestrate-uninstall.mjs +0 -293
- package/lib/orchestrate.mjs +0 -1035
- package/lib/plan.mjs +0 -238
- package/lib/profiles.mjs +0 -524
- package/lib/rerun.mjs +0 -119
- package/lib/target.mjs +0 -357
- package/lib/uninstall.mjs +0 -467
- package/lib/usage.mjs +0 -47
package/lib/extras.mjs
DELETED
|
@@ -1,400 +0,0 @@
|
|
|
1
|
-
// ours-install v3 — the four retained extras, re-pointed at the v3 arrangement.
|
|
2
|
-
//
|
|
3
|
-
// Owner ruling 2026-08-17: the v3 installer KEEPS harness plugins, ours-fleet,
|
|
4
|
-
// voice setup and the copy-paste hand-off prompt. Spec v3's silence about them
|
|
5
|
-
// was an oversight, not a decision.
|
|
6
|
-
//
|
|
7
|
-
// They are not four more screens carried across, because v2 and v3 disagree
|
|
8
|
-
// about which package IS the daemon. In v2 @ours.network/mcp is the daemon and
|
|
9
|
-
// gets the unit; in v3 the daemon is the ours-sdk CLI and ours-mcp is a
|
|
10
|
-
// per-session stdio proxy with no unit. Each of the four had assumed the v2
|
|
11
|
-
// arrangement somewhere, and this module is where the new assumption lives.
|
|
12
|
-
//
|
|
13
|
-
// Pure, like target.mjs / plan.mjs / components.mjs: no I/O, no subprocess, no
|
|
14
|
-
// terminal. Every function takes what was observed and returns a plan; the
|
|
15
|
-
// orchestrator (a later PR) is what performs it.
|
|
16
|
-
|
|
17
|
-
import { join, resolve } from 'node:path';
|
|
18
|
-
import { pkgSpec } from './logic.mjs';
|
|
19
|
-
|
|
20
|
-
const cfgPath = (stateDir) => join(resolve(stateDir), 'config.json');
|
|
21
|
-
|
|
22
|
-
// -----------------------------------------------------------------------------
|
|
23
|
-
// §5 — harness plugins
|
|
24
|
-
// -----------------------------------------------------------------------------
|
|
25
|
-
|
|
26
|
-
export const CLAUDE_MARKET = 'adapt-toolkit/ours-claude-marketplace';
|
|
27
|
-
export const CODEX_MARKET = 'adapt-toolkit/ours-codex-marketplace';
|
|
28
|
-
|
|
29
|
-
export const HARNESSES = [
|
|
30
|
-
{ name: 'claude-code', label: 'Claude Code' },
|
|
31
|
-
{ name: 'codex', label: 'Codex' },
|
|
32
|
-
{ name: 'hermes', label: 'Hermes' },
|
|
33
|
-
];
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* SPEC §5 PROMISES SOMETHING TWO OF THE THREE REGISTRATIONS CANNOT DO.
|
|
37
|
-
*
|
|
38
|
-
* §5: "For any other state directory the installer registers the harness MCP
|
|
39
|
-
* entry with OURS_CONFIG=<state-dir>/config.json in its environment, so the pair
|
|
40
|
-
* travels together." planMcpAttachment already returns exactly that harnessEnv —
|
|
41
|
-
* and the orchestrator only PRINTS it. That is not an oversight to be fixed by
|
|
42
|
-
* wiring it up harder; none of the three registrations can carry a value:
|
|
43
|
-
*
|
|
44
|
-
* Claude Code the marketplace plugin's mcpServers.ours is command+args, with
|
|
45
|
-
* no env key, and `claude plugin install` injects nothing per
|
|
46
|
-
* install.
|
|
47
|
-
* Codex .mcp.json's env_vars is an allowlist of NAMES, not a value map
|
|
48
|
-
* (pinned by packages/codex/test/plugin-package.test.mjs). The
|
|
49
|
-
* value must already be in the ambient environment.
|
|
50
|
-
* Hermes renderConfigBlock is OUR writer, and now emits an `env:` block
|
|
51
|
-
* carrying OURS_CONFIG — so for Hermes the pair is real.
|
|
52
|
-
*
|
|
53
|
-
* So §5's guarantee is ALREADY unmet today for every non-default state
|
|
54
|
-
* directory, silently: the harness attaches to ~/.ours while the operator was
|
|
55
|
-
* told the run targeted somewhere else. Owner ruling Q1 settles the shape:
|
|
56
|
-
*
|
|
57
|
-
* default state directory today's behaviour, byte for byte.
|
|
58
|
-
* Hermes, non-default real: the pair is handed to ours-hermes-install's
|
|
59
|
-
* invocation and written into ~/.hermes/config.yaml
|
|
60
|
-
* as the ours server's own env block.
|
|
61
|
-
* Claude / Codex, non-def install the plugin (it is still the right plugin)
|
|
62
|
-
* and PRINT the exact line the operator must add.
|
|
63
|
-
* Never claim §5's guarantee in the screen text.
|
|
64
|
-
*
|
|
65
|
-
* Deliberately NOT done: registering a second, user-scoped `ours` MCP server via
|
|
66
|
-
* `claude mcp add --env`. Two `ours` servers in front of one harness, and which
|
|
67
|
-
* wins is not something anyone here has verified.
|
|
68
|
-
*/
|
|
69
|
-
export const HARNESS_ENV_SUPPORT = {
|
|
70
|
-
// 'applied' — the pair is genuinely carried into the registration.
|
|
71
|
-
// 'printed' — the operator is told the exact line and nothing is claimed.
|
|
72
|
-
'claude-code': 'printed',
|
|
73
|
-
codex: 'printed',
|
|
74
|
-
hermes: 'applied',
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
const manualSteps = {
|
|
78
|
-
'claude-code': (channel) => [
|
|
79
|
-
`/plugin marketplace add ${CLAUDE_MARKET}`,
|
|
80
|
-
'/plugin install ours',
|
|
81
|
-
],
|
|
82
|
-
codex: (channel) => [
|
|
83
|
-
`codex plugin marketplace add ${CODEX_MARKET}`,
|
|
84
|
-
'codex plugin add ours@ours-codex-marketplace',
|
|
85
|
-
`npm i -g ${pkgSpec('codex', channel)}`,
|
|
86
|
-
],
|
|
87
|
-
hermes: (channel) => [
|
|
88
|
-
`npm i -g ${pkgSpec('hermes', channel)}`,
|
|
89
|
-
'ours-hermes-install',
|
|
90
|
-
],
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
const driveSteps = {
|
|
94
|
-
'claude-code': (channel) => [
|
|
95
|
-
['claude', 'plugin', 'marketplace', 'add', CLAUDE_MARKET],
|
|
96
|
-
['claude', 'plugin', 'install', 'ours@ours.network'],
|
|
97
|
-
],
|
|
98
|
-
codex: (channel) => [
|
|
99
|
-
['codex', 'plugin', 'marketplace', 'add', CODEX_MARKET],
|
|
100
|
-
['codex', 'plugin', 'add', 'ours@ours-codex-marketplace'],
|
|
101
|
-
// Owner-mandated in v2 and kept: choosing the Codex plugin also installs the
|
|
102
|
-
// ours-codex live launcher, in the same step.
|
|
103
|
-
['npm', 'i', '-g', pkgSpec('codex', channel)],
|
|
104
|
-
],
|
|
105
|
-
// Hermes has no driven CLI: nothing here ever calls a `hermes` binary. Its
|
|
106
|
-
// plugin install is npm + ours-hermes-install, which writes ~/.hermes.
|
|
107
|
-
// --skip-daemon because in v3 the daemon is emphatically not ours-mcp's.
|
|
108
|
-
hermes: (channel) => [
|
|
109
|
-
['npm', 'i', '-g', pkgSpec('hermes', channel)],
|
|
110
|
-
['ours-hermes-install', '--skip-daemon'],
|
|
111
|
-
],
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* One plan per harness the caller observed.
|
|
116
|
-
*
|
|
117
|
-
* `harnesses` is [{ name, status }] where status is classifyHarnessProbe's
|
|
118
|
-
* verdict ('ok' | 'alias' | 'unsafe' | 'absent'). Hermes is detected by its
|
|
119
|
-
* config directory rather than a CLI, which is the caller's business; this only
|
|
120
|
-
* consumes the verdict.
|
|
121
|
-
*
|
|
122
|
-
* The v2 golden rule is kept intact: 'alias' / 'unsafe' NEVER dead-end. A
|
|
123
|
-
* harness we cannot safely drive still gets its manual steps printed, so the
|
|
124
|
-
* plugin is still installable.
|
|
125
|
-
*
|
|
126
|
-
* `env` is what an invocation must carry, and it is EMPTY unless the harness can
|
|
127
|
-
* genuinely apply it. `envLine` is what the operator is told. `claimsPair` is
|
|
128
|
-
* false whenever the pair is only printed — the screen text renderer reads it so
|
|
129
|
-
* §5's guarantee cannot be claimed where it does not hold.
|
|
130
|
-
*/
|
|
131
|
-
export function planHarnessPlugins({
|
|
132
|
-
harnesses = [],
|
|
133
|
-
stateDir,
|
|
134
|
-
isDefaultStateDir,
|
|
135
|
-
channel = 'latest',
|
|
136
|
-
assumeYes = false,
|
|
137
|
-
answers = {},
|
|
138
|
-
} = {}) {
|
|
139
|
-
const config = stateDir ? cfgPath(stateDir) : null;
|
|
140
|
-
return harnesses.map((h) => {
|
|
141
|
-
const name = String(h?.name ?? '');
|
|
142
|
-
const known = HARNESSES.find((k) => k.name === name);
|
|
143
|
-
const label = known?.label ?? name;
|
|
144
|
-
const status = String(h?.status ?? 'absent');
|
|
145
|
-
const support = HARNESS_ENV_SUPPORT[name] ?? 'printed';
|
|
146
|
-
const applies = !isDefaultStateDir && support === 'applied';
|
|
147
|
-
|
|
148
|
-
const base = {
|
|
149
|
-
name,
|
|
150
|
-
label,
|
|
151
|
-
status,
|
|
152
|
-
// Default state directory → today's behaviour, byte for byte: no env
|
|
153
|
-
// anywhere, nothing extra printed, nothing claimed.
|
|
154
|
-
envSupport: isDefaultStateDir ? 'none' : support,
|
|
155
|
-
env: applies ? { OURS_CONFIG: config } : {},
|
|
156
|
-
envLine: isDefaultStateDir || applies ? null : `export OURS_CONFIG=${config}`,
|
|
157
|
-
claimsPair: isDefaultStateDir ? true : applies,
|
|
158
|
-
manual: manualSteps[name] ? manualSteps[name](channel) : [],
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
if (!known) return { ...base, action: 'skip', reason: 'unknown harness' };
|
|
162
|
-
if (status === 'absent') return { ...base, action: 'skip', reason: 'not installed' };
|
|
163
|
-
|
|
164
|
-
const wanted = assumeYes ? true : answers[name] !== false;
|
|
165
|
-
if (!wanted) return { ...base, action: 'skip', reason: 'declined', offerOnRerun: true };
|
|
166
|
-
|
|
167
|
-
if (status === 'ok') return { ...base, action: 'drive', steps: driveSteps[name](channel) };
|
|
168
|
-
return {
|
|
169
|
-
...base,
|
|
170
|
-
action: 'manual',
|
|
171
|
-
reason: status === 'alias' ? 'installed as an alias, not the real command' : 'found, but did not answer --version',
|
|
172
|
-
};
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// -----------------------------------------------------------------------------
|
|
177
|
-
// §5 — what the operator has to do BEFORE any of this works
|
|
178
|
-
// -----------------------------------------------------------------------------
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* The restart each harness needs before its new plugin is live.
|
|
182
|
-
*
|
|
183
|
-
* A CORRECTNESS PROBLEM WEARING A COSMETIC COSTUME. The ours MCP server is spawned
|
|
184
|
-
* BY the harness, once per session (`ours-mcp proxy` over stdio), so a harness that
|
|
185
|
-
* was already running when its plugin was installed has no ours tools and will not
|
|
186
|
-
* get them until it restarts. v3 said nothing at all about this: the screen read
|
|
187
|
-
* "Everything installed cleanly", the user went back to a running Claude Code,
|
|
188
|
-
* found no ours tools, and concluded the install had failed. The nightly installer
|
|
189
|
-
* prints these hints and v3 dropped them.
|
|
190
|
-
*
|
|
191
|
-
* Derived from what THIS RUN installed rather than from a registry — v3 already
|
|
192
|
-
* knows, and its own summary is a better source than a persisted file that can go
|
|
193
|
-
* stale against reality.
|
|
194
|
-
*
|
|
195
|
-
* The connectors are deliberately absent. The installer runs their
|
|
196
|
-
* `install-service` itself, so their new configuration is already applied; telling
|
|
197
|
-
* someone to restart something that was just restarted for them is noise, and noise
|
|
198
|
-
* in this list is what stops the real lines being read.
|
|
199
|
-
*/
|
|
200
|
-
export const HARNESS_RESTART = {
|
|
201
|
-
'claude-code': 'restart Claude Code',
|
|
202
|
-
codex: 'start a new Codex session (or `ours-codex`)',
|
|
203
|
-
hermes: 'run /reload-mcp in Hermes',
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
export function restartHints(summary = []) {
|
|
207
|
-
const live = (row) => row && (row.state === 'installed' || row.state === 'current');
|
|
208
|
-
const hints = [];
|
|
209
|
-
for (const [name, action] of Object.entries(HARNESS_RESTART)) {
|
|
210
|
-
const row = summary.find((r) => r.key === name);
|
|
211
|
-
if (live(row)) hints.push({ key: name, action });
|
|
212
|
-
}
|
|
213
|
-
// Nothing to restart if no harness got a plugin this run. The MCP server on its
|
|
214
|
-
// own changes nothing a running harness can see, so an "install the MCP server
|
|
215
|
-
// and restart everything" line would be advice with no reason behind it.
|
|
216
|
-
return hints;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// -----------------------------------------------------------------------------
|
|
220
|
-
// ours-fleet — the one that needs zero code
|
|
221
|
-
// -----------------------------------------------------------------------------
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* ours-fleet needs NO change in either repo, and the installer configures
|
|
225
|
-
* nothing in it.
|
|
226
|
-
*
|
|
227
|
-
* `ours-fleet init` takes no daemon argument of any kind and never reads a
|
|
228
|
-
* daemon config: it makes its directories, installs its own units and prints a
|
|
229
|
-
* next step. Fleet resolves the ours daemon the same way the MCP client does —
|
|
230
|
-
* OURS_CONFIG ?? ~/.ours/config.json, then OURS_PORT / OURS_STATE_DIR /
|
|
231
|
-
* OURS_API_TOKEN — and it does so PER ROLE, through
|
|
232
|
-
* resolveEndpoint({ ...process.env, ...role.env }). Different roles can already
|
|
233
|
-
* target different daemons. Temp supervisors inherit the same four names.
|
|
234
|
-
*
|
|
235
|
-
* So for a non-default state directory the installer's ONLY job is to say the
|
|
236
|
-
* one fleet.yaml line that points a role at the daemon this run created. Saying
|
|
237
|
-
* it is the whole feature; anything more would be configuring a tool that is
|
|
238
|
-
* already correct.
|
|
239
|
-
*/
|
|
240
|
-
export function planFleet({ stateDir, isDefaultStateDir, wanted = true, channel = 'latest' } = {}) {
|
|
241
|
-
const config = stateDir ? cfgPath(stateDir) : null;
|
|
242
|
-
const plan = {
|
|
243
|
-
key: 'fleet',
|
|
244
|
-
label: 'ours-fleet',
|
|
245
|
-
// FOLLOWS THE CHANNEL, and the correction matters more than it looks.
|
|
246
|
-
//
|
|
247
|
-
// This comment used to say the opposite — that ours-fleet lives in its own
|
|
248
|
-
// repo and publishes no nightly tag, so pkgSpec pinned it to @latest. That
|
|
249
|
-
// was true when v3 was written against `main` and it is FALSE here: fleet
|
|
250
|
-
// does publish a nightly dist-tag, and the nightly stack needs the fleet
|
|
251
|
-
// build carrying the SDK integration. A nightly installer that quietly
|
|
252
|
-
// installs stable fleet is precisely the split-brain deployment the channel
|
|
253
|
-
// exists to prevent — the same architecture boundary that made a mixed
|
|
254
|
-
// tg-connector fatal. lib/logic.mjs is the single source of that mapping and
|
|
255
|
-
// this defers to it rather than restating it.
|
|
256
|
-
install: ['npm', 'i', '-g', pkgSpec('fleet', channel)],
|
|
257
|
-
init: ['ours-fleet', 'init'],
|
|
258
|
-
// Stated as data so a test can pin it: this feature writes no fleet config.
|
|
259
|
-
writes: [],
|
|
260
|
-
roleEnv: isDefaultStateDir ? {} : { OURS_CONFIG: config },
|
|
261
|
-
instruction: isDefaultStateDir
|
|
262
|
-
? null
|
|
263
|
-
: `fleet roles that should use this daemon need one line in fleet.yaml:\n env: { OURS_CONFIG: ${config} }`,
|
|
264
|
-
};
|
|
265
|
-
return wanted ? { ...plan, action: 'install' } : { ...plan, action: 'skip', offerOnRerun: true };
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// -----------------------------------------------------------------------------
|
|
269
|
-
// voice setup — the installer takes the restart beat
|
|
270
|
-
// -----------------------------------------------------------------------------
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* WHY THE INSTALLER OWNS THE RESTART NOW (owner ruling Q2).
|
|
274
|
-
*
|
|
275
|
-
* cmdVoiceSetup computes `managed = runningPid() !== null` from ours-mcp's OWN
|
|
276
|
-
* pid record. A v3 daemon is started by `ours daemon start`, which writes
|
|
277
|
-
* <state-dir>/ours-cli-daemon.json — not that record. So runningPid() is null
|
|
278
|
-
* and EVERY v3 daemon classifies as `external`. transactVoiceConfig then returns
|
|
279
|
-
* at its `if (daemonState !== 'managed') return { ok: true, stage: 'write' }`:
|
|
280
|
-
* config written, no apply, no restart, no readiness check, exit 0.
|
|
281
|
-
*
|
|
282
|
-
* Nothing goes wrong — voice-setup will not run `ours-mcp restart` against a
|
|
283
|
-
* CLI-started daemon, which is right. But v2's entire restart protocol
|
|
284
|
-
* (restartHandled, the exit-2 branch, "voice setup performed the required
|
|
285
|
-
* restart") can only fire for a `managed` daemon, so carrying it across would
|
|
286
|
-
* ship a branch that can never be taken. The beat it owned now has no owner, and
|
|
287
|
-
* the installer is the only process that knows the daemon is CLI-managed.
|
|
288
|
-
*
|
|
289
|
-
* One phase, AFTER the component phase — voice-setup is an `ours-mcp`
|
|
290
|
-
* subcommand and v3 installs @ours.network/mcp as a COMPONENT, so it is not on
|
|
291
|
-
* PATH any earlier. The create path needs no special case: in v3 the first
|
|
292
|
-
* `ours daemon start` already happened back in the daemon phase.
|
|
293
|
-
*
|
|
294
|
-
* FILED, NOT FIXED HERE: voice-setup's own hints still say `ours-mcp start` and
|
|
295
|
-
* "restart its external launcher" (packages/core/src/cli.ts:542,547). Under v3
|
|
296
|
-
* the answer is `ours daemon restart --config <cfg>`. That is a packages/core
|
|
297
|
-
* change with its own blast radius and does not belong in an installer PR.
|
|
298
|
-
*/
|
|
299
|
-
export function planVoice({
|
|
300
|
-
mcpInstalled = false,
|
|
301
|
-
ready = false,
|
|
302
|
-
assumeYes = false,
|
|
303
|
-
accepted = null,
|
|
304
|
-
configChanged = false,
|
|
305
|
-
stateDir,
|
|
306
|
-
port,
|
|
307
|
-
} = {}) {
|
|
308
|
-
const config = stateDir ? cfgPath(stateDir) : null;
|
|
309
|
-
const skip = (reason, message, extra = {}) => ({
|
|
310
|
-
key: 'voice', action: 'skip', reason, message, offerOnRerun: true, restartOwed: false, ...extra,
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
if (!mcpInstalled) {
|
|
314
|
-
return skip('no-mcp', 'voice setup needs the MCP server, which this run did not install — re-run ours-install to add both.');
|
|
315
|
-
}
|
|
316
|
-
if (ready) {
|
|
317
|
-
return { key: 'voice', action: 'skip', reason: 'already-configured', message: 'voice transcription is already configured', offerOnRerun: false, restartOwed: false };
|
|
318
|
-
}
|
|
319
|
-
if (assumeYes) {
|
|
320
|
-
return skip('non-interactive', 'voice setup is interactive and this run is not — re-run ours-install to configure it.');
|
|
321
|
-
}
|
|
322
|
-
if (accepted === false) {
|
|
323
|
-
return skip('declined', 'skipped cleanly — re-run ours-install any time to configure voice.');
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
const env = config ? { OURS_CONFIG: config } : {};
|
|
327
|
-
return {
|
|
328
|
-
key: 'voice',
|
|
329
|
-
action: 'setup',
|
|
330
|
-
env,
|
|
331
|
-
setup: ['ours-mcp', 'voice-setup'],
|
|
332
|
-
statusCheck: ['ours-mcp', 'voice-status', '--json'],
|
|
333
|
-
// The beat. Only owed when the config actually changed: an unchanged config
|
|
334
|
-
// is not a reason to bounce a daemon somebody else may be using.
|
|
335
|
-
restartOwed: Boolean(configChanged),
|
|
336
|
-
restart: configChanged && config ? ['ours', 'daemon', 'restart', '--config', config] : null,
|
|
337
|
-
// Never rolls the daemon back: voice-setup leaves the prior config intact on
|
|
338
|
-
// its own failure path, so the recovery is a retry, not an undo.
|
|
339
|
-
retryHint: config ? `ours daemon restart --config ${config}` : null,
|
|
340
|
-
port: Number.isInteger(port) ? port : null,
|
|
341
|
-
};
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// -----------------------------------------------------------------------------
|
|
345
|
-
// the copy-paste hand-off prompt
|
|
346
|
-
// -----------------------------------------------------------------------------
|
|
347
|
-
|
|
348
|
-
/**
|
|
349
|
-
* buildHandoffPrompt is already pure, already renumbers and already drops steps
|
|
350
|
-
* for components that were not installed. Re-pointing it is one preamble.
|
|
351
|
-
*
|
|
352
|
-
* DEFAULT STATE DIRECTORY → THE TEXT IS UNCHANGED, BYTE FOR BYTE. That is the
|
|
353
|
-
* overwhelming majority case and it is pinned by a test, because the agent on
|
|
354
|
-
* the other end of this prompt configures fleet roles and harness environments,
|
|
355
|
-
* and a stray line about a state directory the user never chose is a worse
|
|
356
|
-
* outcome than no line at all.
|
|
357
|
-
*/
|
|
358
|
-
export function buildHandoffPromptV3({
|
|
359
|
-
identity = false,
|
|
360
|
-
fleet = false,
|
|
361
|
-
telegram = false,
|
|
362
|
-
stateDir = null,
|
|
363
|
-
isDefaultStateDir = true,
|
|
364
|
-
} = {}) {
|
|
365
|
-
const steps = [];
|
|
366
|
-
if (identity) {
|
|
367
|
-
steps.push(
|
|
368
|
-
'Create my Ours human identity — this is me, the human; my agents act on\n'
|
|
369
|
-
+ ' my behalf. Ask me what name others should see, then create it.',
|
|
370
|
-
);
|
|
371
|
-
}
|
|
372
|
-
if (fleet) {
|
|
373
|
-
steps.push(
|
|
374
|
-
'Set up my ours-fleet: ask me what agents I want in my fleet for\n'
|
|
375
|
-
+ ' PERMANENT use (a name + role/purpose for each), then create and\n'
|
|
376
|
-
+ ' configure those permanent fleet agents for me.',
|
|
377
|
-
);
|
|
378
|
-
}
|
|
379
|
-
if (telegram) {
|
|
380
|
-
steps.push(
|
|
381
|
-
'Set up my Telegram bot: ask me for my bot\'s name and its token from\n'
|
|
382
|
-
+ ' @BotFather, register the bot, create a chat↔agent connection, and\n'
|
|
383
|
-
+ ' give me the invite link to send.',
|
|
384
|
-
);
|
|
385
|
-
}
|
|
386
|
-
if (steps.length === 0) return { text: '', empty: true };
|
|
387
|
-
|
|
388
|
-
const preamble = isDefaultStateDir || !stateDir
|
|
389
|
-
? ''
|
|
390
|
-
: `My ours daemon uses the state directory ${resolve(stateDir)} (config\n${cfgPath(stateDir)}). When you configure anything for me — fleet roles,\nharness environments — set OURS_CONFIG to that path.\n\n`;
|
|
391
|
-
|
|
392
|
-
const numbered = steps.map((s, i) => `${i + 1}. ${s}`).join('\n');
|
|
393
|
-
const text = preamble
|
|
394
|
-
+ 'I just installed the ours.network stack. Please help me finish setup, one\n'
|
|
395
|
-
+ 'step at a time, explaining as you go:\n\n'
|
|
396
|
-
+ numbered + '\n\n'
|
|
397
|
-
+ 'Do these in order, wait for my answers, and tell me if you need anything\n'
|
|
398
|
-
+ "from me. Don't assume — ask.";
|
|
399
|
-
return { text, empty: false };
|
|
400
|
-
}
|
package/lib/journal.mjs
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
// ours-install v3 — the config journal.
|
|
2
|
-
//
|
|
3
|
-
// WHAT THIS IS FOR, IN ONE SENTENCE: v3 could write a config file describing a
|
|
4
|
-
// daemon it then failed to bring up, print one warning line, and go on to say
|
|
5
|
-
// "install complete".
|
|
6
|
-
//
|
|
7
|
-
// The nightly installer does not have that failure. It snapshots every config
|
|
8
|
-
// file before touching it and restores the bytes when the plan does not complete
|
|
9
|
-
// (lib/nightly-install.mjs `snap`/`rollbackSnapshots`), and it says precisely what
|
|
10
|
-
// it could NOT undo. This is that behaviour, carried into v3's shape rather than
|
|
11
|
-
// copied into it.
|
|
12
|
-
//
|
|
13
|
-
// THE LINE THIS DOES NOT CROSS. Package and plugin installs are NOT rolled back.
|
|
14
|
-
// Not because it is hard, but because it is wrong: npm cannot be un-run
|
|
15
|
-
// meaningfully, a newer package is not damage, and nightly draws the line in the
|
|
16
|
-
// same place and says so. What gets restored is exactly the bytes of files this
|
|
17
|
-
// installer rewrote. Everything else is REPORTED.
|
|
18
|
-
//
|
|
19
|
-
// SCOPE IS PER UNIT OF WORK, NOT PER RUN, and that is the one design decision
|
|
20
|
-
// here worth reading twice. v3's rule is that a failed extra never undoes a
|
|
21
|
-
// daemon that came up correctly (lib/orchestrate.mjs `attempt`), so a single
|
|
22
|
-
// run-wide journal restored at the end would fight the architecture: a cowork
|
|
23
|
-
// failure would roll back the daemon's own config. Instead each journal covers
|
|
24
|
-
// ONE write and the step that makes its bytes true — write the connector config,
|
|
25
|
-
// then install its service; if the service does not come up, the config goes
|
|
26
|
-
// back. The pairing is the whole idea, and it is why `snapshot` and `restoreAll`
|
|
27
|
-
// are on an object you hold for the length of one unit of work rather than
|
|
28
|
-
// functions you call anywhere.
|
|
29
|
-
|
|
30
|
-
import { ok, info, warn } from './ui.mjs';
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* A journal for one unit of work.
|
|
34
|
-
*
|
|
35
|
-
* `effects.snapshot(path)` and `effects.restore(path, snapshot)` are the seam, so
|
|
36
|
-
* this is testable without a filesystem for the same reason everything else here
|
|
37
|
-
* is. On a dry run nothing was written, so nothing is snapshotted and nothing can
|
|
38
|
-
* be restored — the journal is inert rather than special-cased at each call site.
|
|
39
|
-
*/
|
|
40
|
-
export function configJournal(effects, { dryRun = false } = {}) {
|
|
41
|
-
const entries = [];
|
|
42
|
-
return {
|
|
43
|
-
get entries() { return entries.slice(); },
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Record the current bytes of `path` BEFORE it is written. Idempotent per
|
|
47
|
-
* path: the FIRST snapshot is the one that survives, because that is the
|
|
48
|
-
* state the run started from. Snapshotting twice and keeping the second would
|
|
49
|
-
* "restore" to a value this run itself wrote.
|
|
50
|
-
*/
|
|
51
|
-
snapshot(path) {
|
|
52
|
-
if (dryRun) return null;
|
|
53
|
-
if (entries.some((e) => e.path === path)) return entries.find((e) => e.path === path).snapshot;
|
|
54
|
-
const snapshot = effects.snapshot(path);
|
|
55
|
-
entries.push({ path, snapshot });
|
|
56
|
-
return snapshot;
|
|
57
|
-
},
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Put every journalled file back, most recent first.
|
|
61
|
-
*
|
|
62
|
-
* A restore that itself fails is reported, never thrown: the caller is already
|
|
63
|
-
* on a failure path, and losing the original error to a second one would hide
|
|
64
|
-
* the thing that actually went wrong. The report says which files went back
|
|
65
|
-
* and which did not, so a partially recovered state is visible rather than
|
|
66
|
-
* implied.
|
|
67
|
-
*/
|
|
68
|
-
restoreAll() {
|
|
69
|
-
const restored = [];
|
|
70
|
-
const failed = [];
|
|
71
|
-
for (const entry of entries.slice().reverse()) {
|
|
72
|
-
try {
|
|
73
|
-
effects.restore(entry.path, entry.snapshot);
|
|
74
|
-
restored.push({ path: entry.path, existed: entry.snapshot?.exists !== false });
|
|
75
|
-
} catch (error) {
|
|
76
|
-
failed.push({ path: entry.path, reason: error instanceof Error ? error.message : String(error) });
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
entries.length = 0;
|
|
80
|
-
return { restored, failed };
|
|
81
|
-
},
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* The report, and it is half the feature.
|
|
87
|
-
*
|
|
88
|
-
* A rollback nobody is told about is indistinguishable from a run that did
|
|
89
|
-
* nothing. Three separate facts, each stated plainly:
|
|
90
|
-
*
|
|
91
|
-
* · which files were put back, and whether "back" meant deleting one this run
|
|
92
|
-
* created — because "restored" and "removed the file we made" are different
|
|
93
|
-
* things to read on a screen;
|
|
94
|
-
* · which could not be put back, if any;
|
|
95
|
-
* · that completed package installs were NOT rolled back. This last line is the
|
|
96
|
-
* honest boundary, and it is nightly's wording rather than a new one.
|
|
97
|
-
*/
|
|
98
|
-
export function reportRollback(effects, outcome, { packagesInstalled = false } = {}) {
|
|
99
|
-
const { restored = [], failed = [] } = outcome ?? {};
|
|
100
|
-
if (restored.length === 0 && failed.length === 0) return false;
|
|
101
|
-
for (const item of restored) {
|
|
102
|
-
effects.out(item.existed
|
|
103
|
-
? ok(`rolled back ${item.path} to its previous contents`)
|
|
104
|
-
: ok(`removed ${item.path} — this run created it and did not finish`));
|
|
105
|
-
}
|
|
106
|
-
for (const item of failed) {
|
|
107
|
-
effects.out(warn(`could NOT roll back ${item.path}: ${item.reason} — inspect it before re-running`));
|
|
108
|
-
}
|
|
109
|
-
if (packagesInstalled) {
|
|
110
|
-
effects.out(info('completed package installs were not rolled back — a newer package is not damage, and npm cannot be un-run'));
|
|
111
|
-
}
|
|
112
|
-
return true;
|
|
113
|
-
}
|