impel-cli 0.15.3 → 0.16.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 +13 -9
- package/package.json +1 -1
- package/src/apps.js +56 -5
- package/src/commands/setup.js +74 -25
- package/src/commands/update.js +27 -4
- package/src/installRecovery/checkpoint.js +5 -3
- package/src/installRecovery/engine.js +437 -0
- package/src/installRecovery/inference.js +167 -0
- package/src/installRecovery/redact.js +14 -3
- package/src/installRecovery/tools.js +648 -0
- package/src/installRecovery/actions.js +0 -440
- package/src/installRecovery/client.js +0 -84
- package/src/installRecovery/loop.js +0 -258
|
@@ -1,258 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import { fileURLToPath } from "node:url";
|
|
3
|
-
|
|
4
|
-
import { promptText } from "../prompt.js";
|
|
5
|
-
import {
|
|
6
|
-
deterministicInstallRecoveryAction,
|
|
7
|
-
executeInstallRecoveryAction,
|
|
8
|
-
installRecoveryActionMutates,
|
|
9
|
-
validateInstallRecoveryAction,
|
|
10
|
-
} from "./actions.js";
|
|
11
|
-
import {
|
|
12
|
-
clearInstallRecoveryCheckpoint,
|
|
13
|
-
loadInstallRecoveryCheckpoint,
|
|
14
|
-
saveInstallRecoveryCheckpoint,
|
|
15
|
-
} from "./checkpoint.js";
|
|
16
|
-
import { createInstallRecoveryClient } from "./client.js";
|
|
17
|
-
import {
|
|
18
|
-
installRecoveryFingerprint,
|
|
19
|
-
installRecoveryStateFingerprint,
|
|
20
|
-
sanitizeInstallFailureEnvelope,
|
|
21
|
-
} from "./redact.js";
|
|
22
|
-
|
|
23
|
-
const CLI_VERSION = JSON.parse(
|
|
24
|
-
fs.readFileSync(
|
|
25
|
-
fileURLToPath(new URL("../../package.json", import.meta.url)),
|
|
26
|
-
"utf8"
|
|
27
|
-
)
|
|
28
|
-
).version;
|
|
29
|
-
const TERMINAL_STATUSES = new Set(["fixed", "blocked", "aborted"]);
|
|
30
|
-
const POLL_INTERVAL_MS = 1_000;
|
|
31
|
-
|
|
32
|
-
function recoveryDisabled(environment) {
|
|
33
|
-
return ["1", "true", "yes"].includes(
|
|
34
|
-
String(environment.IMPEL_DISABLE_INSTALL_RECOVERY || "").toLowerCase()
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function yes(answer) {
|
|
39
|
-
return /^(?:y|yes)$/iu.test(String(answer || "").trim());
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
async function confirmMutation(action, io) {
|
|
43
|
-
if (!installRecoveryActionMutates(action.id)) return true;
|
|
44
|
-
if (!io.isTTY) return false;
|
|
45
|
-
const answer = await io.promptText(
|
|
46
|
-
`Install recovery proposes ${action.id}: ${action.reason}\nRun this allowlisted local action? [y/N] `
|
|
47
|
-
);
|
|
48
|
-
return yes(answer);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function checkpointFrom(state, fingerprint) {
|
|
52
|
-
return {
|
|
53
|
-
protocolVersion: 1,
|
|
54
|
-
sessionId: state.sessionId,
|
|
55
|
-
recoveryToken: state.recoveryToken,
|
|
56
|
-
fingerprint,
|
|
57
|
-
status: state.status,
|
|
58
|
-
turn: state.turn,
|
|
59
|
-
expiresAt: state.expiresAt,
|
|
60
|
-
updatedAt: Date.now(),
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function terminalOutcome(status) {
|
|
65
|
-
if (status === "fixed") return "fixed";
|
|
66
|
-
if (status === "aborted") return "aborted";
|
|
67
|
-
return "blocked";
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async function uploadConsent(io, explicit, failure) {
|
|
71
|
-
io.log(
|
|
72
|
-
"Install recovery can send a sanitized failure envelope to Impel. Credentials, home paths, email addresses, ANSI controls, and raw tokens are removed."
|
|
73
|
-
);
|
|
74
|
-
io.log(
|
|
75
|
-
"The hosted agent selects typed action IDs; the Impel CLI runs the corresponding reviewed commands on this machine after local policy and confirmation checks."
|
|
76
|
-
);
|
|
77
|
-
io.log("Sanitized payload preview:");
|
|
78
|
-
io.log(JSON.stringify(failure, null, 2));
|
|
79
|
-
if (explicit) return true;
|
|
80
|
-
if (!io.isTTY) return false;
|
|
81
|
-
return yes(await io.promptText("Send the sanitized failure and start assisted recovery? [y/N] "));
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export async function runInstallRecovery(options, overrides = {}) {
|
|
85
|
-
const environment = options.environment || process.env;
|
|
86
|
-
const io = {
|
|
87
|
-
promptText,
|
|
88
|
-
isTTY: process.stdin.isTTY,
|
|
89
|
-
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
90
|
-
loadCheckpoint: loadInstallRecoveryCheckpoint,
|
|
91
|
-
saveCheckpoint: saveInstallRecoveryCheckpoint,
|
|
92
|
-
clearCheckpoint: clearInstallRecoveryCheckpoint,
|
|
93
|
-
client:
|
|
94
|
-
options.client ||
|
|
95
|
-
createInstallRecoveryClient({
|
|
96
|
-
appUrl: options.config.appUrl,
|
|
97
|
-
fetchImpl: options.fetchImpl || fetch,
|
|
98
|
-
}),
|
|
99
|
-
executeAction: executeInstallRecoveryAction,
|
|
100
|
-
now: () => Date.now(),
|
|
101
|
-
log: console.log,
|
|
102
|
-
warn: console.warn,
|
|
103
|
-
...overrides,
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
if (options.noRecovery || recoveryDisabled(environment)) {
|
|
107
|
-
return { status: "disabled", fixed: false, uploaded: false };
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const failure = sanitizeInstallFailureEnvelope(options.failure);
|
|
111
|
-
const fingerprint = installRecoveryFingerprint(failure);
|
|
112
|
-
const encounteredErrors = [failure.message];
|
|
113
|
-
const actionContext = {
|
|
114
|
-
...(options.actionContext || {}),
|
|
115
|
-
platform: failure.platform,
|
|
116
|
-
environment,
|
|
117
|
-
isTTY: io.isTTY,
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
// Always attempt one deterministic, allowlisted local diagnostic first. A
|
|
121
|
-
// mutation still needs the same explicit confirmation as a hosted proposal.
|
|
122
|
-
const deterministicAction = deterministicInstallRecoveryAction(failure);
|
|
123
|
-
const deterministicConfirmed = await confirmMutation(deterministicAction, io);
|
|
124
|
-
const deterministicResult = await io.executeAction(deterministicAction, {
|
|
125
|
-
...actionContext,
|
|
126
|
-
confirmed: deterministicConfirmed,
|
|
127
|
-
});
|
|
128
|
-
io.log(`Install recovery local check: ${deterministicResult.summary}`);
|
|
129
|
-
if (deterministicResult.outcome === "failed") {
|
|
130
|
-
encounteredErrors.push(deterministicResult.summary);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const consented = await uploadConsent(io, options.explicit === true, failure);
|
|
134
|
-
if (!consented) {
|
|
135
|
-
io.log("Install recovery stayed local; no diagnostic data was uploaded.");
|
|
136
|
-
return {
|
|
137
|
-
status: "local_only",
|
|
138
|
-
fixed: false,
|
|
139
|
-
uploaded: false,
|
|
140
|
-
localResult: deterministicResult,
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
let state;
|
|
145
|
-
const existing = io.loadCheckpoint();
|
|
146
|
-
if (existing?.fingerprint === fingerprint) {
|
|
147
|
-
state = existing;
|
|
148
|
-
io.log(`Resuming install recovery session ${existing.sessionId}.`);
|
|
149
|
-
} else {
|
|
150
|
-
state = await io.client.create(options.config.pat, {
|
|
151
|
-
protocolVersion: 1,
|
|
152
|
-
orgId: options.config.tenantId,
|
|
153
|
-
cliVersion: CLI_VERSION,
|
|
154
|
-
consent: true,
|
|
155
|
-
fingerprint,
|
|
156
|
-
stateFingerprint: installRecoveryStateFingerprint({
|
|
157
|
-
failure,
|
|
158
|
-
deterministicResult,
|
|
159
|
-
}),
|
|
160
|
-
failure: {
|
|
161
|
-
...failure,
|
|
162
|
-
diagnostics: {
|
|
163
|
-
...(failure.diagnostics || {}),
|
|
164
|
-
localCheck: deterministicResult.summary,
|
|
165
|
-
localCheckOutcome: deterministicResult.outcome,
|
|
166
|
-
},
|
|
167
|
-
},
|
|
168
|
-
});
|
|
169
|
-
io.saveCheckpoint(checkpointFrom(state, fingerprint));
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const deadline = Math.min(
|
|
173
|
-
state.expiresAt || io.now() + 15 * 60 * 1_000,
|
|
174
|
-
io.now() + 15 * 60 * 1_000
|
|
175
|
-
);
|
|
176
|
-
let terminal = false;
|
|
177
|
-
try {
|
|
178
|
-
while (io.now() < deadline) {
|
|
179
|
-
if (!TERMINAL_STATUSES.has(state.status)) {
|
|
180
|
-
state = await io.client.read(state.sessionId, state.recoveryToken);
|
|
181
|
-
io.saveCheckpoint(checkpointFrom(state, fingerprint));
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (state.status === "awaiting_action") {
|
|
185
|
-
const action = validateInstallRecoveryAction(state.proposal?.action);
|
|
186
|
-
if (!action || !state.actionNonce) {
|
|
187
|
-
encounteredErrors.push("Hosted recovery returned an invalid typed action.");
|
|
188
|
-
state.status = "blocked";
|
|
189
|
-
state.terminalSummary = "Hosted recovery returned an invalid typed action.";
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
io.log(`Install recovery: ${state.proposal.userMessage || state.proposal.summary}`);
|
|
193
|
-
const confirmed = await confirmMutation(action, io);
|
|
194
|
-
const result = await io.executeAction(action, {
|
|
195
|
-
...actionContext,
|
|
196
|
-
confirmed,
|
|
197
|
-
});
|
|
198
|
-
if (result.outcome === "failed") encounteredErrors.push(result.summary);
|
|
199
|
-
state = await io.client.submitResult(
|
|
200
|
-
state.sessionId,
|
|
201
|
-
state.recoveryToken,
|
|
202
|
-
{
|
|
203
|
-
actionNonce: state.actionNonce,
|
|
204
|
-
actionId: action.id,
|
|
205
|
-
outcome: result.outcome,
|
|
206
|
-
stateFingerprint: installRecoveryStateFingerprint({
|
|
207
|
-
failure,
|
|
208
|
-
actionId: action.id,
|
|
209
|
-
result,
|
|
210
|
-
}),
|
|
211
|
-
summary: result.summary,
|
|
212
|
-
exitCode: result.exitCode,
|
|
213
|
-
...(result.stdout ? { stdout: result.stdout } : {}),
|
|
214
|
-
...(result.stderr ? { stderr: result.stderr } : {}),
|
|
215
|
-
}
|
|
216
|
-
);
|
|
217
|
-
io.saveCheckpoint(checkpointFrom(state, fingerprint));
|
|
218
|
-
continue;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
if (TERMINAL_STATUSES.has(state.status)) {
|
|
222
|
-
terminal = true;
|
|
223
|
-
const summary =
|
|
224
|
-
state.terminalSummary || state.proposal?.summary || "Install recovery finished.";
|
|
225
|
-
await io.client.complete(state.sessionId, state.recoveryToken, {
|
|
226
|
-
outcome: terminalOutcome(state.status),
|
|
227
|
-
summary,
|
|
228
|
-
encounteredErrors: [...new Set(encounteredErrors)].slice(0, 20),
|
|
229
|
-
});
|
|
230
|
-
io.log(`Install recovery ${state.status}: ${summary}`);
|
|
231
|
-
return {
|
|
232
|
-
status: state.status,
|
|
233
|
-
fixed: state.status === "fixed",
|
|
234
|
-
uploaded: true,
|
|
235
|
-
summary,
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
await io.sleep(POLL_INTERVAL_MS);
|
|
240
|
-
}
|
|
241
|
-
const summary = terminal
|
|
242
|
-
? state.terminalSummary
|
|
243
|
-
: "Install recovery reached its 15-minute local safety limit.";
|
|
244
|
-
io.warn(summary);
|
|
245
|
-
return { status: "blocked", fixed: false, uploaded: true, summary };
|
|
246
|
-
} catch (error) {
|
|
247
|
-
io.warn(`Install recovery paused: ${error?.message || error}`);
|
|
248
|
-
io.warn("Re-run `impel setup --repair` to resume the short-lived session.");
|
|
249
|
-
return {
|
|
250
|
-
status: "paused",
|
|
251
|
-
fixed: false,
|
|
252
|
-
uploaded: true,
|
|
253
|
-
summary: String(error?.message || error),
|
|
254
|
-
};
|
|
255
|
-
} finally {
|
|
256
|
-
if (terminal) io.clearCheckpoint();
|
|
257
|
-
}
|
|
258
|
-
}
|