@inventarch/cli 0.0.0-stage → 1.0.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/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +19 -2
- package/SPEC.md +30 -0
- package/assets/base/71f0665cb1ffc3f3964fc7710528451f635d664bdf4b8c95005ee97c21db5928.ia.tgz +0 -0
- package/assets/base.json +6 -0
- package/assets/host/43a1f8c471141b1f2f46cef4411a9a61c00da1c1aa19d277189ae2423282bfd6.tgz +0 -0
- package/assets/host.json +6 -0
- package/assets/vocabulary.json +3268 -0
- package/dist/args.d.ts +62 -0
- package/dist/args.d.ts.map +1 -0
- package/dist/args.js +117 -0
- package/dist/args.js.map +1 -0
- package/dist/briefing.d.ts +76 -0
- package/dist/briefing.d.ts.map +1 -0
- package/dist/briefing.js +105 -0
- package/dist/briefing.js.map +1 -0
- package/dist/channel.d.ts +22 -0
- package/dist/channel.d.ts.map +1 -0
- package/dist/channel.js +74 -0
- package/dist/channel.js.map +1 -0
- package/dist/claude-cli.d.ts +35 -0
- package/dist/claude-cli.d.ts.map +1 -0
- package/dist/claude-cli.js +101 -0
- package/dist/claude-cli.js.map +1 -0
- package/dist/commands.d.ts +39 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +436 -0
- package/dist/commands.js.map +1 -0
- package/dist/compile.d.ts +39 -0
- package/dist/compile.d.ts.map +1 -0
- package/dist/compile.js +136 -0
- package/dist/compile.js.map +1 -0
- package/dist/consumer.d.ts +137 -0
- package/dist/consumer.d.ts.map +1 -0
- package/dist/consumer.js +366 -0
- package/dist/consumer.js.map +1 -0
- package/dist/decline.d.ts +7 -0
- package/dist/decline.d.ts.map +1 -0
- package/dist/decline.js +78 -0
- package/dist/decline.js.map +1 -0
- package/dist/distribute.d.ts +163 -0
- package/dist/distribute.d.ts.map +1 -0
- package/dist/distribute.js +664 -0
- package/dist/distribute.js.map +1 -0
- package/dist/doctor-user.d.ts +45 -0
- package/dist/doctor-user.d.ts.map +1 -0
- package/dist/doctor-user.js +193 -0
- package/dist/doctor-user.js.map +1 -0
- package/dist/doctor.d.ts +57 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +754 -0
- package/dist/doctor.js.map +1 -0
- package/dist/format.d.ts +25 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +168 -0
- package/dist/format.js.map +1 -0
- package/dist/home-remedy.d.ts +10 -0
- package/dist/home-remedy.d.ts.map +1 -0
- package/dist/home-remedy.js +26 -0
- package/dist/home-remedy.js.map +1 -0
- package/dist/host-projection.d.ts +11 -0
- package/dist/host-projection.d.ts.map +1 -0
- package/dist/host-projection.js +73 -0
- package/dist/host-projection.js.map +1 -0
- package/dist/host.d.ts +162 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +599 -0
- package/dist/host.js.map +1 -0
- package/dist/init.d.ts +217 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +840 -0
- package/dist/init.js.map +1 -0
- package/dist/inspect.d.ts +32 -0
- package/dist/inspect.d.ts.map +1 -0
- package/dist/inspect.js +226 -0
- package/dist/inspect.js.map +1 -0
- package/dist/main.d.ts +24 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +215 -0
- package/dist/main.js.map +1 -0
- package/dist/operation-help.d.ts +14 -0
- package/dist/operation-help.d.ts.map +1 -0
- package/dist/operation-help.js +75 -0
- package/dist/operation-help.js.map +1 -0
- package/dist/pack.d.ts +17 -0
- package/dist/pack.d.ts.map +1 -0
- package/dist/pack.js +121 -0
- package/dist/pack.js.map +1 -0
- package/dist/render.d.ts +165 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +277 -0
- package/dist/render.js.map +1 -0
- package/dist/session.d.ts +13 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +25 -0
- package/dist/session.js.map +1 -0
- package/dist/user-host.d.ts +59 -0
- package/dist/user-host.d.ts.map +1 -0
- package/dist/user-host.js +228 -0
- package/dist/user-host.js.map +1 -0
- package/dist/validate.d.ts +53 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +220 -0
- package/dist/validate.js.map +1 -0
- package/dist/vocabulary.d.ts +55 -0
- package/dist/vocabulary.d.ts.map +1 -0
- package/dist/vocabulary.js +179 -0
- package/dist/vocabulary.js.map +1 -0
- package/package.json +153 -3
package/dist/host.js
ADDED
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ia host`: docs/specs/host-registration/README.md §4.
|
|
3
|
+
*
|
|
4
|
+
* One verb owns the managed host set of §5.1: the MCP entry, the steward guard hook and the workspace projection,
|
|
5
|
+
* plus the context element §5.2's gate keeps unselected. It plans by default and never writes while planning — the
|
|
6
|
+
* payload's location is computed, not created. `--apply` materializes the embedded payload once per user (§3.3),
|
|
7
|
+
* re-plans every sub-transaction against the verified payload, then applies them in order — mcp, hooks, projection —
|
|
8
|
+
* and removal runs them in reverse (§8). No cross-file atomicity is claimed: a rerun converges, and a pending journal
|
|
9
|
+
* refuses naming its recovery. Nothing here reports that a server answered or a hook ran; `applied.observed` is
|
|
10
|
+
* always false (plan-0002 M5 exit).
|
|
11
|
+
*
|
|
12
|
+
* The plan asks the mechanisms themselves. `planHostFor` and `planGuardFor` plan against the payload apply will
|
|
13
|
+
* materialize, described from the pin without reading it (`expectedHostCache`), so a conflict in the preview is the
|
|
14
|
+
* service's own refusal — its code and message unchanged (contract §4.1) — and is the refusal apply would raise.
|
|
15
|
+
*/
|
|
16
|
+
import { createHash } from 'node:crypto';
|
|
17
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
18
|
+
import { resolve } from 'node:path';
|
|
19
|
+
import { lifecycleProfile } from '@inventarch/agent-composition-system/lifecycle-profile';
|
|
20
|
+
import { WORKSPACE_PROJECTION_MARKER } from '@inventarch/compliance';
|
|
21
|
+
import { applyGuardRegistration, planGuardFor, planGuardRegistration, } from '@inventarch/distribution/guard-registration';
|
|
22
|
+
import { hostRow, workspaceRow } from '@inventarch/distribution/hosts';
|
|
23
|
+
import { applyHost, assertHostRegistrationIdle, expectedHostCache, HOST_REGISTRATION, planHost, planHostFor, } from '@inventarch/distribution/host';
|
|
24
|
+
import { assertHomeOutsideWorkspace, hostPayloadPath, materializeHostPayload, readHostPin, } from '@inventarch/distribution/host-home';
|
|
25
|
+
import { assertIaHomeUsable } from '@inventarch/distribution/ia-home';
|
|
26
|
+
import { readMaterializedPlugin } from '@inventarch/distribution/plugin-home';
|
|
27
|
+
import { applyProjection, planProjection } from '@inventarch/distribution/projection';
|
|
28
|
+
import { UsageError } from './args.js';
|
|
29
|
+
import { homeSrcRemedy, hostHome, located } from './home-remedy.js';
|
|
30
|
+
import { confirm, Interrupted, Refusal, refusalOf, requireRoot } from './consumer.js';
|
|
31
|
+
import { checkAdmitted, renderProjectionFor } from './host-projection.js';
|
|
32
|
+
import { codeOf } from './session.js';
|
|
33
|
+
import { atom, blockSymbolWidth, commandFacts, document, entry, fieldRows, headerLine, indentOf, quote, sectionLabel, truncateDigest, words, } from './render.js';
|
|
34
|
+
import { runUserHost } from './user-host.js';
|
|
35
|
+
const DAMAGED = 'The ia installation is damaged; reinstall @inventarch/cli so assets/host travels with it.';
|
|
36
|
+
/** §7.1: the user-level command a workspace plan names when the plugin is missing. It takes no --root. */
|
|
37
|
+
export const USER_PLUGIN_COMMAND = 'ia host claude --user --apply';
|
|
38
|
+
export const USER_PLUGIN_MISSING = `The user-level plugin is not installed; run "${USER_PLUGIN_COMMAND}" to add it.`;
|
|
39
|
+
/** §5.3's sentence, verbatim, per host. Every element — the projection included — is machine-local. */
|
|
40
|
+
export const MACHINE_LOCAL = {
|
|
41
|
+
claude: 'These files are machine-local: .mcp.json, .claude/settings.local.json, .claude/rules/ia-workspace.md, .claude/skills/ia-authoring/, the projected .claude/agents/*.md files and .ia/distributions/. Do not commit them.',
|
|
42
|
+
codex: 'These files are machine-local: .codex/config.toml, AGENTS.md, .agents/skills/ia-authoring/ and .ia/distributions/. Do not commit them.',
|
|
43
|
+
};
|
|
44
|
+
/** §5.4: the MCP element's one fixed addition, from §2.3's host facts. */
|
|
45
|
+
export const MCP_APPROVAL = 'Claude Code asks for approval before starting a project MCP server.';
|
|
46
|
+
/** Operator decision 2026-09-23 ("narrow + disclose"): what the registered steward guard refuses, said where it is registered. */
|
|
47
|
+
export const GUARD_SCOPE = "The steward guard denies file-tool edits to the files ia host owns and to records under .ia/src/systems/<name>/ unless the edit comes from that system's steward subagent (<name>-steward).";
|
|
48
|
+
/** §5.1's files, exported once so `ia doctor` names the same ones. */
|
|
49
|
+
export const MCP_PATH = { claude: '.mcp.json', codex: '.codex/config.toml' };
|
|
50
|
+
export const SETTINGS = '.claude/settings.local.json';
|
|
51
|
+
export const HOSTS_AREA = '.ia/distributions/hosts';
|
|
52
|
+
/** §5.1's state files. Their presence says whether this workspace owns an element; their content is the mechanisms'. */
|
|
53
|
+
export const STATE = {
|
|
54
|
+
mcp: (host) => `${HOSTS_AREA}/${host}-${HOST_REGISTRATION}.json`,
|
|
55
|
+
hooks: `${HOSTS_AREA}/claude-guard-${HOST_REGISTRATION}.json`,
|
|
56
|
+
projection: (host) => `${HOSTS_AREA}/${host}-projection.json`,
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* What one host's MCP element owns, named so a user can find and delete exactly it. Codex's is a marked block
|
|
60
|
+
* (`reconcileConfig` in `@inventarch/distribution`), so the markers themselves are part of it: a leftover marker makes the
|
|
61
|
+
* next plan refuse the block as unowned.
|
|
62
|
+
*/
|
|
63
|
+
const OWNED_MCP = {
|
|
64
|
+
claude: 'the ia-workspace entry in .mcp.json',
|
|
65
|
+
codex: `the IA block in .codex/config.toml, from "# BEGIN IA PROJECTION host-${HOST_REGISTRATION}" ` +
|
|
66
|
+
`through "# END IA PROJECTION host-${HOST_REGISTRATION}", markers included`,
|
|
67
|
+
};
|
|
68
|
+
const OWNED_GUARD = `the IA guard group in ${SETTINGS}`;
|
|
69
|
+
/**
|
|
70
|
+
* The repair for an ownership state file that cannot be read, or null when `path` is not one. The mechanisms refuse
|
|
71
|
+
* every plan until the file goes, and deleting it alone would leave what it owned looking unmanaged, so the repair
|
|
72
|
+
* deletes both. `ia host` and `ia doctor` both print it, so the two cannot word it differently.
|
|
73
|
+
*/
|
|
74
|
+
export function stateRepair(host, path, rerun) {
|
|
75
|
+
if (path === STATE.mcp(host))
|
|
76
|
+
return `Delete ${path} and ${OWNED_MCP[host]}, then run "${rerun}".`;
|
|
77
|
+
if (host === 'claude' && path === STATE.hooks)
|
|
78
|
+
return `Delete ${path} and ${OWNED_GUARD}, then run "${rerun}".`;
|
|
79
|
+
if (path === STATE.projection(host))
|
|
80
|
+
return `Delete ${path}, then run "${rerun}".`;
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The repair for an owned entry changed by hand. Apply refuses it and removal refuses it too, so it goes first; then
|
|
85
|
+
* removal clears the ownership state and registration starts over. Each step was exercised against the verb.
|
|
86
|
+
*/
|
|
87
|
+
export function modifiedRepair(host, elements) {
|
|
88
|
+
const owned = elements.map((element) => (element === 'mcp' ? OWNED_MCP[host] : OWNED_GUARD)).join(' and ');
|
|
89
|
+
return `Delete ${owned}, then run "ia host ${host} --remove --apply" and "ia host ${host} --apply".`;
|
|
90
|
+
}
|
|
91
|
+
/** §8: each pending journal names its own recovery on the distribution binary. `ia doctor` reports the same ones. */
|
|
92
|
+
export const JOURNALS = [
|
|
93
|
+
[`${HOSTS_AREA}/pending.json`, 'recover-host'],
|
|
94
|
+
[`${HOSTS_AREA}/guard-pending.json`, 'recover-guard'],
|
|
95
|
+
[`${HOSTS_AREA}/lifecycle-pending.json`, 'recover-lifecycle'],
|
|
96
|
+
];
|
|
97
|
+
/** §8: the lock every host sub-transaction holds while it writes (`acquireHostRegistrationLock`). */
|
|
98
|
+
export const HOST_LOCK = `${HOSTS_AREA}/lock.json`;
|
|
99
|
+
/** A recovery on the distribution binary, rooted and quoted as every printed root is. */
|
|
100
|
+
export const recoverCommand = (command, root) => `ia-distribution ${command} --root ${quote(root)}`;
|
|
101
|
+
/**
|
|
102
|
+
* §8 "or refuses naming a recovery": a held host lock is another run or a killed one, and nothing here can tell
|
|
103
|
+
* which, so the next action names the recovery that decides it — `recoverHost` takes the lock in recovery mode,
|
|
104
|
+
* which clears a dead holder's lock and refuses a live one. It names no `ia host` command, so `--root` needs no rewrite.
|
|
105
|
+
*/
|
|
106
|
+
export const lockNext = (root) => `Another ia host run holds the host lock, or a killed run left it: run "${recoverCommand('recover-host', root)}" (it clears a dead holder's lock and refuses a live one), then rerun.`;
|
|
107
|
+
/**
|
|
108
|
+
* The refusal for a held host lock, located at the lock and naming its recovery, or null for any other error. Every
|
|
109
|
+
* mechanism an `ia host` apply or an install's projection refresh runs raises IA-DIST-INSTALL-BUSY only from that
|
|
110
|
+
* lock, so there the code alone identifies it.
|
|
111
|
+
*/
|
|
112
|
+
export function lockRefusal(error, root) {
|
|
113
|
+
const refusal = refusalOf(error);
|
|
114
|
+
return refusal.code === 'IA-DIST-INSTALL-BUSY'
|
|
115
|
+
? new Refusal(refusal.code, refusal.message, 3, { path: HOST_LOCK }, lockNext(root))
|
|
116
|
+
: null;
|
|
117
|
+
}
|
|
118
|
+
/** lifecycle-profile.ts: Codex's hooks are unavailable, and the profile says why. */
|
|
119
|
+
const CODEX_HOOKS = lifecycleProfile('codex', '0.116.0').reason ?? 'Codex hooks are unavailable.';
|
|
120
|
+
/** §3.3 step 1, through the service: a missing or malformed pin or archive is IA-DIST-ARTIFACT-UNAVAILABLE. */
|
|
121
|
+
const bundledPin = (packageRoot) => located(null, DAMAGED, () => readHostPin(packageRoot));
|
|
122
|
+
/**
|
|
123
|
+
* §7: the release `ia doctor` and `ia inspect` observe hosts against. An installation that carries no payload is not
|
|
124
|
+
* an error there: the release comparison is skipped, and doctor says why with the code.
|
|
125
|
+
*/
|
|
126
|
+
export function pinnedRelease(packageRoot) {
|
|
127
|
+
try {
|
|
128
|
+
return { release: readHostPin(packageRoot).pin.release };
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
return { release: null, code: codeOf(error, 'IA-DIST-ARTIFACT-UNAVAILABLE') };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/** §4: the target must be initialized and admit; anything else names ia init or ia validate. */
|
|
135
|
+
function requireInitialized(root) {
|
|
136
|
+
if (!existsSync(resolve(root, '.ia/release.json')))
|
|
137
|
+
throw new Refusal('IA-CLI-CONFLICT', 'The target is not an initialized workspace: .ia/release.json is absent', 3, { path: root }, 'Run "ia init" first.');
|
|
138
|
+
}
|
|
139
|
+
/** The journal a failed transaction left behind, if any, with the command that recovers it. */
|
|
140
|
+
const pendingJournal = (root) => JOURNALS.find(([path]) => existsSync(resolve(root, path)));
|
|
141
|
+
const recoverNext = (root, command, host) => `Run "${recoverCommand(command, root)}", then rerun "ia host ${host}".`;
|
|
142
|
+
/**
|
|
143
|
+
* A next action of this verb made runnable from any cwd: every quoted `ia host <host>` command it names gains
|
|
144
|
+
* `--root <root>`. `ia init --host` uses it, because its `<directory>` may not be the cwd a rerun discovers from.
|
|
145
|
+
* Every command this file prints opens with `"ia host <host>` and none names `--root` itself, so the rewrite reaches
|
|
146
|
+
* each command and nothing else.
|
|
147
|
+
*/
|
|
148
|
+
export const rootedNext = (next, host, root) => next.replace(new RegExp(`"ia host ${host}(?! --root\\b)`, 'g'), () => `"ia host ${host} --root ${quote(root)}`);
|
|
149
|
+
/** A next action as printed: with `--root` given, each `ia host <host>` command in it carries the root. */
|
|
150
|
+
export const hostNext = (text, host, root, rooted) => rooted ? rootedNext(text, host, root) : text;
|
|
151
|
+
/** The file a mechanism located its refusal at (`locate` in @inventarch/distribution's files.ts), or null. */
|
|
152
|
+
export const refusedPath = (error) => error !== null && typeof error === 'object' && 'path' in error && typeof error.path === 'string' ? error.path : null;
|
|
153
|
+
/**
|
|
154
|
+
* §6.3: the repair for a projection refusal located at `path`. The ownership state has its own repair; any other file
|
|
155
|
+
* is never replaced or adopted, so it is moved or deleted first. `ia host`, `ia doctor` and the install refresh all
|
|
156
|
+
* name it this way.
|
|
157
|
+
*/
|
|
158
|
+
export const projectionRepair = (host, path, rerun) => stateRepair(host, path, rerun) ?? `Move or delete ${path}, then run "${rerun}".`;
|
|
159
|
+
/** §8: a pending journal refuses the whole verb and names the recovery that clears it. */
|
|
160
|
+
function requireIdle(root, host, supplied) {
|
|
161
|
+
try {
|
|
162
|
+
assertHostRegistrationIdle(root);
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
const refusal = refusalOf(error);
|
|
166
|
+
const [journal, command] = pendingJournal(root) ?? JOURNALS[0];
|
|
167
|
+
throw new Refusal(refusal.code, refusal.message, 3, { path: journal }, recoverNext(supplied, command, host));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/** A mechanism's refusal as an element conflict: the service's code and message, located at its file. */
|
|
171
|
+
function conflictOf(error, path) {
|
|
172
|
+
const refusal = refusalOf(error);
|
|
173
|
+
return { code: refusal.code, path: refusedPath(error) ?? path, reason: refusal.message };
|
|
174
|
+
}
|
|
175
|
+
const pin = (text) => text === null ? null : createHash('sha256').update(text).digest('hex');
|
|
176
|
+
/** A registration plan's action, from the bytes it would write against the bytes it read. */
|
|
177
|
+
const actionOf = (plan) => plan.after.state === null
|
|
178
|
+
? 'remove'
|
|
179
|
+
: plan.before.state === null
|
|
180
|
+
? 'create'
|
|
181
|
+
: pin(plan.after.config) === plan.before.config && pin(plan.after.state) === plan.before.state
|
|
182
|
+
? 'unchanged'
|
|
183
|
+
: 'update';
|
|
184
|
+
/** One registration element, planned by its mechanism; `none` when a removal finds nothing owned. */
|
|
185
|
+
function registrationElement(id, path, owned, remove, plan) {
|
|
186
|
+
if (remove && !owned)
|
|
187
|
+
return { id, action: 'none', paths: [path], capability: 'absent', conflict: null };
|
|
188
|
+
try {
|
|
189
|
+
return { id, action: actionOf(plan()), paths: [path], capability: 'registered', conflict: null };
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
return { id, action: 'refused', paths: [path], capability: 'refused', conflict: conflictOf(error, path) };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* §5.2: the context element. Claude reports it `not selected` until the gate lifts; Codex reports it `unsupported by
|
|
197
|
+
* host`, because it is delivered through lifecycle hooks and Codex hooks are unavailable — the same reason the
|
|
198
|
+
* hooks element gives — so both hosts carry the same four rows.
|
|
199
|
+
*/
|
|
200
|
+
const contextElement = (host) => ({
|
|
201
|
+
id: 'context',
|
|
202
|
+
action: 'none',
|
|
203
|
+
paths: [],
|
|
204
|
+
capability: host === 'claude' ? 'not selected' : 'unsupported by host',
|
|
205
|
+
conflict: null,
|
|
206
|
+
});
|
|
207
|
+
/** §5.1 `projection`, planned by the real planner: it reads only the workspace, so the plan is exactly apply's. */
|
|
208
|
+
function projectionElement(root, host, artifacts) {
|
|
209
|
+
try {
|
|
210
|
+
const files = planProjection({ root, host, artifacts, marker: WORKSPACE_PROJECTION_MARKER }).actions;
|
|
211
|
+
const touched = files.filter((file) => file.action !== 'unowned');
|
|
212
|
+
const action = artifacts.length === 0
|
|
213
|
+
? touched.some((file) => file.action === 'remove')
|
|
214
|
+
? 'remove'
|
|
215
|
+
: 'none'
|
|
216
|
+
: touched.every((file) => file.action === 'unchanged')
|
|
217
|
+
? 'unchanged'
|
|
218
|
+
: touched.every((file) => file.action === 'create')
|
|
219
|
+
? 'create'
|
|
220
|
+
: 'update';
|
|
221
|
+
return {
|
|
222
|
+
id: 'projection',
|
|
223
|
+
action,
|
|
224
|
+
paths: touched.map((file) => file.path),
|
|
225
|
+
capability: action === 'none' ? 'absent' : 'registered',
|
|
226
|
+
conflict: null,
|
|
227
|
+
files,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
return {
|
|
232
|
+
id: 'projection',
|
|
233
|
+
action: 'refused',
|
|
234
|
+
paths: [],
|
|
235
|
+
capability: 'refused',
|
|
236
|
+
conflict: conflictOf(error, STATE.projection(host)),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/** The invocation is rebuilt from the parsed arguments, so the printed command is the one that was understood. */
|
|
241
|
+
function invocationOf(context, host, remove) {
|
|
242
|
+
const root = context.args.value('root');
|
|
243
|
+
return [
|
|
244
|
+
`ia host ${host}`,
|
|
245
|
+
...(remove ? ['--remove'] : []),
|
|
246
|
+
...(root === undefined ? [] : ['--root', quote(root)]),
|
|
247
|
+
].join(' ');
|
|
248
|
+
}
|
|
249
|
+
/** §4's plan. It reads the workspace and the package's pin and writes nothing, the payload included. */
|
|
250
|
+
export function collectHost(context) {
|
|
251
|
+
const { args, host } = context;
|
|
252
|
+
// Host plugin distribution spec §4: the adapter table decides. An unknown host is usage, and so is --context with
|
|
253
|
+
// Codex; both precede every read (§3). A known row without the workspace set (Cursor, planned) refuses.
|
|
254
|
+
const given = args.positionals[0] ?? '';
|
|
255
|
+
if (hostRow(given) === undefined)
|
|
256
|
+
throw new UsageError(`ia host takes claude, codex or cursor; got ${given}`);
|
|
257
|
+
const row = workspaceRow(given);
|
|
258
|
+
if (row === undefined)
|
|
259
|
+
throw new Refusal('IA-DIST-HOST-UNSUPPORTED', `${hostRow(given).label} support is planned; nothing was written`, 3, null, 'Run "ia host claude" or "ia host codex".');
|
|
260
|
+
const name = row.id;
|
|
261
|
+
const remove = args.flag('remove'), selected = args.value('context');
|
|
262
|
+
if (selected !== undefined && name === 'codex')
|
|
263
|
+
throw new UsageError('--context applies to claude only; Codex hooks are unavailable');
|
|
264
|
+
const supplied = requireRoot(context);
|
|
265
|
+
requireInitialized(supplied);
|
|
266
|
+
// Spec §5.2's gate: until the binding's coordinate, bootstrap and policy are derived and one context run from a v2
|
|
267
|
+
// payload is observed generating, the context element refuses. The spec fixes this code for the gate.
|
|
268
|
+
if (selected !== undefined)
|
|
269
|
+
throw new Refusal('IA-DIST-HOST-UNSUPPORTED', 'The lifecycle context element is not qualified in this release', 3, { path: supplied }, `Run "ia host ${name}" without --context.`);
|
|
270
|
+
// The mechanisms record the real path (files.ts `workspace()`), so the plan works on the same one.
|
|
271
|
+
const root = realpathSync(supplied);
|
|
272
|
+
const home = hostHome(host.env);
|
|
273
|
+
// §3.3: neither may contain the other; the service raises IA-DIST-PATH-UNSAFE.
|
|
274
|
+
located(supplied, 'Move the workspace, or set IA_HOME to an absolute directory outside it.', () => assertHomeOutsideWorkspace(home, root));
|
|
275
|
+
// §3: refuse a home that itself looks like a workspace at plan time, before any payload write is attempted.
|
|
276
|
+
located(null, homeSrcRemedy(home), () => assertIaHomeUsable(home));
|
|
277
|
+
const { pin: bundled } = bundledPin(host.packageRoot);
|
|
278
|
+
requireIdle(root, name, supplied);
|
|
279
|
+
// §4: zero error findings. A removal renders nothing, so it checks admission alone.
|
|
280
|
+
let artifacts = [];
|
|
281
|
+
if (remove)
|
|
282
|
+
checkAdmitted(root);
|
|
283
|
+
else
|
|
284
|
+
artifacts = renderProjectionFor(root, name);
|
|
285
|
+
// A host home reached through a symbolic link or junction is refused by the mechanism (IA-DIST-PATH-UNSAFE), because
|
|
286
|
+
// the payload it verifies must be the one the registration names; the remedy is a home with no link in its path.
|
|
287
|
+
const expected = located(null, 'Set IA_HOME to a directory path with no links, or unset it.', () => expectedHostCache(hostPayloadPath(home, bundled.release), bundled.release));
|
|
288
|
+
const owned = new Set();
|
|
289
|
+
if (existsSync(resolve(root, STATE.mcp(name))))
|
|
290
|
+
owned.add('mcp');
|
|
291
|
+
if (name === 'claude' && existsSync(resolve(root, STATE.hooks)))
|
|
292
|
+
owned.add('hooks');
|
|
293
|
+
const mcp = registrationElement('mcp', MCP_PATH[name], owned.has('mcp'), remove, () => remove ? planHostFor(root, name, null, HOST_REGISTRATION) : planHostFor(root, name, expected));
|
|
294
|
+
const hooks = name === 'codex'
|
|
295
|
+
? { id: 'hooks', action: 'none', paths: [], capability: 'unsupported by host', conflict: null }
|
|
296
|
+
: registrationElement('hooks', SETTINGS, owned.has('hooks'), remove, () => remove ? planGuardRegistration(root, { remove: HOST_REGISTRATION }) : planGuardFor(root, expected));
|
|
297
|
+
return {
|
|
298
|
+
root: supplied,
|
|
299
|
+
host: name,
|
|
300
|
+
remove,
|
|
301
|
+
release: bundled.release,
|
|
302
|
+
home,
|
|
303
|
+
elements: [mcp, hooks, contextElement(name), projectionElement(root, name, artifacts)],
|
|
304
|
+
undo: `ia host ${name} --remove --apply`,
|
|
305
|
+
invocation: invocationOf(context, name, remove),
|
|
306
|
+
owned,
|
|
307
|
+
rooted: context.args.value('root') !== undefined,
|
|
308
|
+
userPlugin: row.user === 'plugin' ? { installed: readMaterializedPlugin(home) !== null, command: USER_PLUGIN_COMMAND } : null,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
/** A next action of this verb as printed: with `--root` given, each `ia host` command in it carries the root. */
|
|
312
|
+
const say = (view, text) => hostNext(text, view.host, view.root, view.rooted);
|
|
313
|
+
/** The next action a conflict renders, and the one its refusal carries, rooted when `--root` was given. */
|
|
314
|
+
const nextFor = (view, id, conflict) => say(view, conflictNext(view, id, conflict));
|
|
315
|
+
/** The unrooted next action for a conflict. §5.3 fixes the unowned MCP entry's wording. */
|
|
316
|
+
function conflictNext(view, id, conflict) {
|
|
317
|
+
const rerun = `ia host ${view.host}${view.remove ? ' --remove' : ''} --apply`;
|
|
318
|
+
// An unreadable ownership state: the mechanisms locate the refusal at the state file itself.
|
|
319
|
+
const unreadable = stateRepair(view.host, conflict.path, rerun);
|
|
320
|
+
if (unreadable !== null)
|
|
321
|
+
return unreadable;
|
|
322
|
+
if (conflict.code === 'IA-DIST-LOCAL-MODIFICATION' && id === 'hooks' && view.owned.has('hooks'))
|
|
323
|
+
return modifiedRepair(view.host, ['hooks']);
|
|
324
|
+
if (conflict.code === 'IA-DIST-LOCAL-MODIFICATION' && id === 'mcp') {
|
|
325
|
+
// Owned: removal tolerates a deleted entry, so deleting it and re-registering converges. Unowned: §5.3's wording,
|
|
326
|
+
// made exact for Codex, where the collision can be any ia-workspace table or a file that no longer parses.
|
|
327
|
+
if (view.owned.has('mcp'))
|
|
328
|
+
return modifiedRepair(view.host, ['mcp']);
|
|
329
|
+
return view.host === 'codex'
|
|
330
|
+
? 'Remove any ia-workspace server outside the IA block from .codex/config.toml and make sure the file parses as TOML, then run "ia host codex --apply".'
|
|
331
|
+
: `Remove that entry, then run "ia host ${view.host} --apply".`;
|
|
332
|
+
}
|
|
333
|
+
if (conflict.code === 'IA-DIST-HOST-UNSUPPORTED' && id === 'hooks')
|
|
334
|
+
return `Remove disableAllHooks from the Claude settings, then run "${rerun}".`;
|
|
335
|
+
if (id === 'projection')
|
|
336
|
+
return projectionRepair(view.host, conflict.path, rerun);
|
|
337
|
+
return `Fix ${conflict.path}, then run "${rerun}".`;
|
|
338
|
+
}
|
|
339
|
+
const refusalFor = (view, id, conflict) => new Refusal(conflict.code, conflict.reason, 3, { path: conflict.path }, nextFor(view, id, conflict));
|
|
340
|
+
/** Apply's re-plan against the verified payload. A refusal found here is raised before any workspace write. */
|
|
341
|
+
function replan(view, id, path, run) {
|
|
342
|
+
try {
|
|
343
|
+
return run();
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
throw refusalFor(view, id, conflictOf(error, path));
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* §3.3 steps 3-4. A payload directory that fails verification is named by the mechanism and the remedy is to delete
|
|
351
|
+
* it; nothing overwrites it. Any other failure is the bundled payload's own, which only a reinstall repairs.
|
|
352
|
+
*/
|
|
353
|
+
function materialize(view, packageRoot) {
|
|
354
|
+
const { pin: bundled, archive, archivePath } = bundledPin(packageRoot);
|
|
355
|
+
const payload = hostPayloadPath(view.home, bundled.release);
|
|
356
|
+
// Re-checked explicitly rather than inferred from a caught refusal's message: verifyNaming's own PATH-UNSAFE
|
|
357
|
+
// wrapping ("... at <payload>; delete that directory") also names a path under view.home, so pattern-matching the
|
|
358
|
+
// message would misroute a real corrupt-payload refusal into this remedy.
|
|
359
|
+
located(null, homeSrcRemedy(view.home), () => assertIaHomeUsable(view.home));
|
|
360
|
+
try {
|
|
361
|
+
return materializeHostPayload({ home: view.home, archive: archive(), pin: bundled }).directory;
|
|
362
|
+
}
|
|
363
|
+
catch (error) {
|
|
364
|
+
const refusal = refusalOf(error), corrupt = refusal.message.includes(payload);
|
|
365
|
+
throw new Refusal(refusal.code, refusal.message, 3, { path: corrupt ? payload : archivePath }, corrupt ? say(view, `Delete ${payload}, then run "ia host ${view.host} --apply".`) : DAMAGED);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* §8: mcp, hooks, projection in order; removal in reverse. The payload is materialized, then every sub-transaction is
|
|
370
|
+
* re-planned with the verifying planners before any is applied, so a refusal any planner can find is raised before
|
|
371
|
+
* the workspace changes; each apply then re-plans under the host lock and refuses a plan that went stale.
|
|
372
|
+
* Cancellation is observed between sub-transactions. A failure after an element was written names the rerun that
|
|
373
|
+
* finishes the set, or the recovery a pending journal needs. `checkpoint` runs after each element completes, which
|
|
374
|
+
* is where an interruption test cuts.
|
|
375
|
+
*/
|
|
376
|
+
export function applyHostSet(view, packageRoot, signal, checkpoint = () => { }) {
|
|
377
|
+
const refused = view.elements.find((element) => element.conflict !== null);
|
|
378
|
+
if (refused !== undefined)
|
|
379
|
+
throw refusalFor(view, refused.id, refused.conflict);
|
|
380
|
+
const root = realpathSync(view.root), host = view.host;
|
|
381
|
+
const rerun = `ia host ${host}${view.remove ? ' --remove' : ''} --apply`;
|
|
382
|
+
const done = [];
|
|
383
|
+
const skipped = (id) => ({
|
|
384
|
+
id,
|
|
385
|
+
status: view.elements.find((element) => element.id === id).capability,
|
|
386
|
+
});
|
|
387
|
+
const step = (id, run) => {
|
|
388
|
+
if (signal?.aborted)
|
|
389
|
+
throw new Interrupted(say(view, `Run "${rerun}" to finish.`));
|
|
390
|
+
let status;
|
|
391
|
+
try {
|
|
392
|
+
status = run();
|
|
393
|
+
}
|
|
394
|
+
catch (error) {
|
|
395
|
+
const refusal = refusalOf(error), journal = pendingJournal(root);
|
|
396
|
+
if (journal !== undefined)
|
|
397
|
+
throw new Refusal(refusal.code, refusal.message, 3, { path: journal[0] }, say(view, recoverNext(view.root, journal[1], host)));
|
|
398
|
+
const busy = lockRefusal(error, view.root);
|
|
399
|
+
if (busy !== null)
|
|
400
|
+
throw busy;
|
|
401
|
+
if (done.length > 0)
|
|
402
|
+
throw new Refusal(refusal.code, refusal.message, 3, refusal.where, say(view, `Run "${rerun}" to finish.`));
|
|
403
|
+
throw error;
|
|
404
|
+
}
|
|
405
|
+
done.push({ id, status });
|
|
406
|
+
checkpoint(id);
|
|
407
|
+
};
|
|
408
|
+
if (!view.remove) {
|
|
409
|
+
const cache = materialize(view, packageRoot);
|
|
410
|
+
const mcp = replan(view, 'mcp', MCP_PATH[host], () => planHost(root, host, cache));
|
|
411
|
+
const hooks = host === 'claude' ? replan(view, 'hooks', SETTINGS, () => planGuardRegistration(root, { cache })) : null;
|
|
412
|
+
const artifacts = renderProjectionFor(root, host);
|
|
413
|
+
const projection = replan(view, 'projection', STATE.projection(host), () => planProjection({ root, host, artifacts, marker: WORKSPACE_PROJECTION_MARKER }));
|
|
414
|
+
step('mcp', () => applyHost(mcp).status);
|
|
415
|
+
if (hooks === null)
|
|
416
|
+
done.push(skipped('hooks'));
|
|
417
|
+
else
|
|
418
|
+
step('hooks', () => applyGuardRegistration(hooks).status);
|
|
419
|
+
done.push(skipped('context'));
|
|
420
|
+
step('projection', () => applyProjection(projection).status);
|
|
421
|
+
return { status: 'host-registered', elements: done, observed: false };
|
|
422
|
+
}
|
|
423
|
+
// A removal plans only what the view found owned; `none` means there is nothing of that element to remove.
|
|
424
|
+
const absent = (id) => view.elements.find((element) => element.id === id).action === 'none';
|
|
425
|
+
const projection = replan(view, 'projection', STATE.projection(host), () => planProjection({ root, host, artifacts: [], marker: WORKSPACE_PROJECTION_MARKER }));
|
|
426
|
+
const hooks = host === 'claude' && !absent('hooks')
|
|
427
|
+
? replan(view, 'hooks', SETTINGS, () => planGuardRegistration(root, { remove: HOST_REGISTRATION }))
|
|
428
|
+
: null;
|
|
429
|
+
const mcp = absent('mcp')
|
|
430
|
+
? null
|
|
431
|
+
: replan(view, 'mcp', MCP_PATH[host], () => planHost(root, host, null, HOST_REGISTRATION));
|
|
432
|
+
step('projection', () => (applyProjection(projection).removed > 0 ? 'projection-removed' : 'absent'));
|
|
433
|
+
done.push(skipped('context'));
|
|
434
|
+
if (host === 'codex')
|
|
435
|
+
done.push(skipped('hooks'));
|
|
436
|
+
else
|
|
437
|
+
step('hooks', () => (hooks === null ? 'absent' : applyGuardRegistration(hooks).status));
|
|
438
|
+
step('mcp', () => (mcp === null ? 'absent' : applyHost(mcp).status));
|
|
439
|
+
return { status: 'host-removed', elements: done, observed: false };
|
|
440
|
+
}
|
|
441
|
+
/** §4 `--json`: the plan always, and `applied` only after an apply. */
|
|
442
|
+
const hostEnvelope = (view, applied) => ({
|
|
443
|
+
version: 1,
|
|
444
|
+
command: 'host',
|
|
445
|
+
root: view.root,
|
|
446
|
+
host: view.host,
|
|
447
|
+
apply: applied !== null,
|
|
448
|
+
plan: {
|
|
449
|
+
release: view.release,
|
|
450
|
+
home: view.home,
|
|
451
|
+
elements: view.elements,
|
|
452
|
+
undo: view.undo,
|
|
453
|
+
...(view.userPlugin === null ? {} : { userPlugin: view.userPlugin }),
|
|
454
|
+
},
|
|
455
|
+
...(applied === null ? {} : { applied }),
|
|
456
|
+
});
|
|
457
|
+
const FILE_SYMBOL = {
|
|
458
|
+
create: 'added',
|
|
459
|
+
update: 'updated',
|
|
460
|
+
unchanged: 'info',
|
|
461
|
+
remove: 'removed',
|
|
462
|
+
unowned: 'warning',
|
|
463
|
+
};
|
|
464
|
+
const elementSymbol = (element) => element.conflict !== null
|
|
465
|
+
? 'error'
|
|
466
|
+
: element.action === 'none' || element.action === 'unchanged'
|
|
467
|
+
? 'info'
|
|
468
|
+
: 'success';
|
|
469
|
+
function elementFields(view) {
|
|
470
|
+
return view.elements.map((element) => {
|
|
471
|
+
const facts = [element.action, element.capability, ...(element.id === 'projection' ? [] : element.paths)];
|
|
472
|
+
if (element.id === 'hooks' && element.capability === 'unsupported by host')
|
|
473
|
+
facts.push(CODEX_HOOKS.replace(/\.$/, ''));
|
|
474
|
+
if (element.conflict !== null)
|
|
475
|
+
facts.push(`${element.conflict.code} ${element.conflict.reason}`);
|
|
476
|
+
return {
|
|
477
|
+
symbol: elementSymbol(element),
|
|
478
|
+
label: element.id,
|
|
479
|
+
value: words(facts.join('; ')),
|
|
480
|
+
action: element.conflict === null ? null : words(nextFor(view, element.id, element.conflict)),
|
|
481
|
+
};
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
/** Each projected file with its action; an unowned steward file says why it is left alone. */
|
|
485
|
+
const fileBlock = (view, caps) => (view.elements.find((element) => element.id === 'projection')?.files ?? []).flatMap((file) => entry([
|
|
486
|
+
[
|
|
487
|
+
atom(file.path, 'cyan', 0),
|
|
488
|
+
...words(file.action === 'unowned'
|
|
489
|
+
? "unowned; marked but not listed in this workspace's projection state, left untouched"
|
|
490
|
+
: file.action, null, 2),
|
|
491
|
+
],
|
|
492
|
+
],
|
|
493
|
+
// Nested at the elements block's content column (§6.4 rule 3), so each file sits under the element it belongs to.
|
|
494
|
+
{
|
|
495
|
+
column: indentOf(1) + blockSymbolWidth(view.elements.map(elementSymbol), caps.ascii) + 2,
|
|
496
|
+
symbol: FILE_SYMBOL[file.action],
|
|
497
|
+
}, caps));
|
|
498
|
+
const headerBlock = (view, caps) => [
|
|
499
|
+
...headerLine('Plan', `host ${view.host}${view.remove ? ' --remove' : ''}`, [{ text: `release ${truncateDigest(view.release, caps.ascii)}`, column: 35 }], caps),
|
|
500
|
+
...headerLine('Root', view.root, [], caps),
|
|
501
|
+
...headerLine('Home', view.home, [], caps),
|
|
502
|
+
];
|
|
503
|
+
const elementsBlock = (view, caps) => [
|
|
504
|
+
sectionLabel('Elements', caps),
|
|
505
|
+
...fieldRows(elementFields(view), { depth: 1 }, caps),
|
|
506
|
+
...fileBlock(view, caps),
|
|
507
|
+
];
|
|
508
|
+
/**
|
|
509
|
+
* §5.3's machine-local sentence and, for a Claude registration, §5.4's approval note and the guard's scope (the
|
|
510
|
+
* hooks element is registered only for Claude). `ia init --host` prints the same blocks after its host step, so the
|
|
511
|
+
* two verbs cannot word them differently.
|
|
512
|
+
*/
|
|
513
|
+
export const hostNotes = (host, caps, remove = false) => [
|
|
514
|
+
entry([words(MACHINE_LOCAL[host])], { depth: 1, symbol: 'warning' }, caps),
|
|
515
|
+
...(host === 'claude' && !remove
|
|
516
|
+
? [
|
|
517
|
+
entry([words(MCP_APPROVAL)], { depth: 1, symbol: 'info' }, caps),
|
|
518
|
+
entry([words(GUARD_SCOPE)], { depth: 1, symbol: 'info' }, caps),
|
|
519
|
+
]
|
|
520
|
+
: []),
|
|
521
|
+
];
|
|
522
|
+
/**
|
|
523
|
+
* §7.1: a registration plan also names the missing user-level plugin's command. It is printed as is, never through
|
|
524
|
+
* `say`: that command belongs to no workspace, so it never carries --root.
|
|
525
|
+
*/
|
|
526
|
+
const notesBlocks = (view, caps) => [
|
|
527
|
+
...hostNotes(view.host, caps, view.remove),
|
|
528
|
+
...(view.userPlugin !== null && !view.userPlugin.installed && !view.remove
|
|
529
|
+
? [entry([words(USER_PLUGIN_MISSING)], { depth: 1, symbol: 'info' }, caps)]
|
|
530
|
+
: []),
|
|
531
|
+
];
|
|
532
|
+
export function renderHostPlan(view, applied, caps) {
|
|
533
|
+
const conflicted = view.elements.some((element) => element.conflict !== null);
|
|
534
|
+
return document([
|
|
535
|
+
headerBlock(view, caps),
|
|
536
|
+
applied === null
|
|
537
|
+
? entry([words('This is a preview. Nothing has been written.')], { depth: 1 }, caps)
|
|
538
|
+
: entry([
|
|
539
|
+
words(applied.status === 'host-registered'
|
|
540
|
+
? 'Registered: written; not observed answering.'
|
|
541
|
+
: 'Removed the owned host set.'),
|
|
542
|
+
], { depth: 1, symbol: 'success' }, caps),
|
|
543
|
+
elementsBlock(view, caps),
|
|
544
|
+
...notesBlocks(view, caps),
|
|
545
|
+
entry([
|
|
546
|
+
...(applied === null && !conflicted
|
|
547
|
+
? commandFacts('Apply with "', `${view.invocation} --apply --yes`, '".', 3, caps)
|
|
548
|
+
: []),
|
|
549
|
+
// `plan.undo` stays unrooted (§4 `--json`); the printed command carries --root when it was given.
|
|
550
|
+
...(view.remove ? [] : commandFacts('Undo with "', say(view, `"${view.undo}"`).slice(1, -1), '".', 3, caps)),
|
|
551
|
+
...(applied !== null && !view.remove ? [words('Run "ia doctor" for the observed host state.')] : []),
|
|
552
|
+
], { depth: 0, symbol: 'step' }, caps),
|
|
553
|
+
], { leadingBlank: true });
|
|
554
|
+
}
|
|
555
|
+
/** §2.8 rule 3's summary: what the preview shows, without its note or footer, because the question is the action. */
|
|
556
|
+
const renderHostSummary = (view, caps) => document([headerBlock(view, caps), elementsBlock(view, caps), ...notesBlocks(view, caps)], { leadingBlank: true });
|
|
557
|
+
export const CONFIRMATION = 'Apply this host registration? [y/N] ';
|
|
558
|
+
const REMOVAL_CONFIRMATION = 'Remove this host registration? [y/N] ';
|
|
559
|
+
export async function runHost(context) {
|
|
560
|
+
const { args, caps, host, json } = context;
|
|
561
|
+
// Host plugin distribution spec §7.1: `--user` is the user-level entry point, which needs no workspace.
|
|
562
|
+
if (args.flag('user'))
|
|
563
|
+
return runUserHost(context);
|
|
564
|
+
let view;
|
|
565
|
+
try {
|
|
566
|
+
view = collectHost(context);
|
|
567
|
+
}
|
|
568
|
+
catch (error) {
|
|
569
|
+
// Refusals raised before a view exists — the --context gate, a pending journal — name `ia host` commands too.
|
|
570
|
+
const supplied = args.value('root'), name = args.positionals[0];
|
|
571
|
+
if (!(error instanceof Refusal) ||
|
|
572
|
+
error.next === null ||
|
|
573
|
+
supplied === undefined ||
|
|
574
|
+
(name !== 'claude' && name !== 'codex'))
|
|
575
|
+
throw error;
|
|
576
|
+
// The real path requireRoot resolved, which succeeded before any of these refusals could be raised.
|
|
577
|
+
throw new Refusal(error.code, error.message, error.exit, error.where, rootedNext(error.next, name, realpathSync(resolve(host.cwd, supplied))), error.at);
|
|
578
|
+
}
|
|
579
|
+
const rendered = (applied) => json
|
|
580
|
+
? { exitCode: 0, stdout: JSON.stringify(hostEnvelope(view, applied)) + '\n', stderr: '' }
|
|
581
|
+
: { exitCode: 0, stdout: renderHostPlan(view, applied, caps), stderr: '' };
|
|
582
|
+
// §4: a plan with a conflict is a successful report; only --apply refuses it.
|
|
583
|
+
if (!args.flag('apply'))
|
|
584
|
+
return rendered(null);
|
|
585
|
+
// §2.8 rule 3. Parsing has already refused every --apply whose question cannot be asked or answered.
|
|
586
|
+
if (!args.flag('yes') &&
|
|
587
|
+
!(await confirm(host.interaction, renderHostSummary(view, caps), view.remove ? REMOVAL_CONFIRMATION : CONFIRMATION)))
|
|
588
|
+
return {
|
|
589
|
+
exitCode: 0,
|
|
590
|
+
stdout: document([
|
|
591
|
+
entry([words('Nothing was applied.')], { depth: 0, symbol: 'info' }, caps),
|
|
592
|
+
entry(commandFacts('Apply with "', `${view.invocation} --apply --yes`, '".', 3, caps), { depth: 0, symbol: 'step' }, caps),
|
|
593
|
+
], { leadingBlank: true }),
|
|
594
|
+
stderr: '',
|
|
595
|
+
};
|
|
596
|
+
host.signal?.throwIfAborted();
|
|
597
|
+
return rendered(applyHostSet(view, host.packageRoot, host.signal));
|
|
598
|
+
}
|
|
599
|
+
//# sourceMappingURL=host.js.map
|