superpowers-manager 0.10.0 → 0.10.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.
|
@@ -15,17 +15,6 @@ export async function callAdapter(call, failure) {
|
|
|
15
15
|
};
|
|
16
16
|
return { ok: true, result: { status: result.status, outcome } };
|
|
17
17
|
}
|
|
18
|
-
// Preserve outcomes collected before gathering throws so command catches can
|
|
19
|
-
// replay them before reporting the underlying failure.
|
|
20
|
-
export class GatherFailure extends Error {
|
|
21
|
-
inner;
|
|
22
|
-
outcomes;
|
|
23
|
-
constructor(message, inner, outcomes) {
|
|
24
|
-
super(message);
|
|
25
|
-
this.inner = inner;
|
|
26
|
-
this.outcomes = outcomes;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
18
|
export function writeOutput(output, ctx) {
|
|
30
19
|
for (const line of output.stdout)
|
|
31
20
|
ctx.stdout.write(`${line}\n`);
|
package/dist/commands/install.js
CHANGED
|
@@ -1,181 +1,162 @@
|
|
|
1
1
|
import { tmpdir } from "node:os";
|
|
2
2
|
import { oneLine } from "../cli-arguments.js";
|
|
3
3
|
import { withWorkspaceReporting, } from "../workspace.js";
|
|
4
|
-
import {
|
|
4
|
+
import { invoke, writeOutput } from "./adapter-call.js";
|
|
5
5
|
import { probeFacts, replayOutcome } from "./probe.js";
|
|
6
6
|
import { runPrepare } from "./prepare.js";
|
|
7
7
|
import { runWithMutation } from "./mutation.js";
|
|
8
8
|
import { activationBlock } from "../harness-compatibility.js";
|
|
9
|
-
// Collect outcomes without writing so output failures
|
|
10
|
-
// inspection failures.
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
async function gatherInstallStages(ctx, selection, artifact) {
|
|
9
|
+
// Collect outcomes into the caller's array without writing, so output failures
|
|
10
|
+
// cannot be classified as inspection failures. The caller replays them after
|
|
11
|
+
// gathering, including when gathering throws; a post-success cleanup failure
|
|
12
|
+
// comes back as withWorkspaceReporting's cleanupWarning instead.
|
|
13
|
+
async function gatherInstallStages(ctx, selection, artifact, outcomes) {
|
|
15
14
|
const parent = ctx.env.TMPDIR ?? tmpdir();
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
15
|
+
return await withWorkspaceReporting(parent, "superpowers-manager.install.", async (workspace) => {
|
|
16
|
+
const env = { ...ctx.env, TMPDIR: workspace };
|
|
17
|
+
const failed = (message) => ({
|
|
18
|
+
kind: "failed",
|
|
19
|
+
message,
|
|
20
|
+
});
|
|
21
|
+
const adapterContext = { root: ctx.root, env };
|
|
22
|
+
// Stage 1: inspect ownership, re-checked even though gatherProbe just
|
|
23
|
+
// reported it. Mutation authority requires CURRENT, VALIDATED
|
|
24
|
+
// evidence -- a probe's answer is neither by the time this runs.
|
|
25
|
+
const ownershipFailure = ctx.adapter.presentation.callFailure("install-ownership", adapterContext);
|
|
26
|
+
const ownership = await invoke(() => ctx.adapter.inspectOwnership(adapterContext), ownershipFailure, outcomes);
|
|
27
|
+
if (!ownership.ok)
|
|
28
|
+
return failed(ownership.message);
|
|
29
|
+
const ownershipDecision = ownership.result.outcome.result.installEligibility;
|
|
30
|
+
if (ownershipDecision.kind === "blocked") {
|
|
31
|
+
return {
|
|
32
|
+
kind: "blocked",
|
|
33
|
+
output: ownershipDecision.output,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
// Stage 2: inspect update-control, re-checked for the same reason.
|
|
37
|
+
const controlFailure = ctx.adapter.presentation.callFailure("install-control", adapterContext);
|
|
38
|
+
const control = await invoke(() => ctx.adapter.inspectUpdateControl(adapterContext), controlFailure, outcomes);
|
|
39
|
+
if (!control.ok)
|
|
40
|
+
return failed(control.message);
|
|
41
|
+
const mutationDecision = control.result.outcome.result.mutationEligibility;
|
|
42
|
+
if (mutationDecision.kind === "blocked") {
|
|
43
|
+
return {
|
|
44
|
+
kind: "blocked",
|
|
45
|
+
output: mutationDecision.output,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
// Stage 3: the mutation itself. Nothing above may have issued this --
|
|
49
|
+
// that is the whole point of stages 1 and 2 running first.
|
|
50
|
+
const activationFailure = activationBlock(artifact.compatibility, ctx.options.allowExperimental);
|
|
51
|
+
if (activationFailure !== null)
|
|
52
|
+
return failed(activationFailure);
|
|
53
|
+
const installFailure = ctx.adapter.presentation.callFailure("install", adapterContext);
|
|
54
|
+
const install = await invoke(() => ctx.adapter.install(artifact, adapterContext), installFailure, outcomes);
|
|
55
|
+
if (!install.ok)
|
|
56
|
+
return failed(install.message);
|
|
57
|
+
let transaction;
|
|
58
|
+
try {
|
|
59
|
+
const candidate = install.result.outcome.result.transaction;
|
|
60
|
+
if (candidate !== undefined) {
|
|
61
|
+
if (candidate === null || typeof candidate !== "object")
|
|
62
|
+
throw new Error("invalid transaction");
|
|
63
|
+
const finalize = candidate.finalize.bind(candidate);
|
|
64
|
+
const rollback = candidate.rollback.bind(candidate);
|
|
65
|
+
if (typeof finalize !== "function" || typeof rollback !== "function")
|
|
66
|
+
throw new Error("invalid transaction");
|
|
67
|
+
transaction = { finalize, rollback };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return failed("invalid installation transaction receipt; preserve recovery material for manual resolution");
|
|
72
|
+
}
|
|
73
|
+
// Stage 4: inspect fingerprint, to verify the mutation actually took.
|
|
74
|
+
//
|
|
75
|
+
// Deliberately NOT short-circuited on `!inspected.ok`, unlike stages
|
|
76
|
+
// 1-3. `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/install:57::spw_verify_installed_fingerprint` handed the inspect result to
|
|
77
|
+
// spw_verify_installed_fingerprint whatever it contained, and that
|
|
78
|
+
// function's first guard, from
|
|
79
|
+
// `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/lifecycle.sh:91-94::spw_inspect_fingerprint`,
|
|
80
|
+
// is what turns a failed inspection into "error: installed manager
|
|
81
|
+
// fingerprint inspection failed after install."
|
|
82
|
+
// renderInstallVerification's failed-inspection arm
|
|
83
|
+
// (`src/harnesses/codex/presentation.ts:280::if (inspection.status !== 0 || !inspection.outcome.ok) {`)
|
|
84
|
+
// exists for this result-bearing path. Returning
|
|
85
|
+
// failed() instead reported the adapter's own generic diagnostic and
|
|
86
|
+
// dropped the post-install verification claim -- a mutation had
|
|
87
|
+
// already been issued at stage 3,
|
|
88
|
+
// so "the install could not be verified" is the contract, not "an
|
|
89
|
+
// adapter call failed". A ctx.adapter THROW has no result to render.
|
|
90
|
+
// A pending transaction still rolls back before reporting that failure.
|
|
91
|
+
const inspectionFailure = ctx.adapter.presentation.callFailure("post-install", adapterContext);
|
|
92
|
+
const inspected = await invoke(() => ctx.adapter.inspectInstalled(selection, adapterContext), inspectionFailure, outcomes);
|
|
93
|
+
const inspection = inspected.result;
|
|
94
|
+
const verified = inspection !== null &&
|
|
95
|
+
inspection.status === 0 &&
|
|
96
|
+
inspection.outcome.ok &&
|
|
97
|
+
inspection.outcome.result.kind === "current";
|
|
98
|
+
if (transaction !== undefined) {
|
|
99
|
+
const operation = verified ? "finalize" : "rollback";
|
|
100
|
+
const settlement = await invoke(() => (verified ? transaction.finalize() : transaction.rollback()), {
|
|
101
|
+
unexpected: `installation ${operation} did not complete; preserve recovery material for manual resolution`,
|
|
102
|
+
invalidStatus: `installation ${operation} returned an invalid result; preserve recovery material for manual resolution`,
|
|
103
|
+
}, outcomes, (value) => value === null);
|
|
104
|
+
if (!verified) {
|
|
105
|
+
const restored = await invoke(() => ctx.adapter.inspectInstalled(selection, adapterContext), inspectionFailure, outcomes);
|
|
106
|
+
const restoration = restored.ok
|
|
107
|
+
? `installed state after rollback: ${restored.result.outcome.result.kind}; identity=${restored.result.outcome.result.observedIdentity}`
|
|
108
|
+
: "installed state after rollback could not be inspected";
|
|
109
|
+
const verificationOutput = inspection === null
|
|
110
|
+
? null
|
|
111
|
+
: ctx.adapter.presentation.renderInstallVerification(selection.desiredCommit, install.result, inspection);
|
|
112
|
+
const inspectionMessage = verificationOutput === null && !inspected.ok
|
|
113
|
+
? inspected.message
|
|
114
|
+
: null;
|
|
115
|
+
const settlementMessage = !settlement.ok ? settlement.message : null;
|
|
35
116
|
return {
|
|
36
|
-
kind: "
|
|
37
|
-
|
|
38
|
-
|
|
117
|
+
kind: "verified",
|
|
118
|
+
status: 1,
|
|
119
|
+
stdout: verificationOutput?.stdout ?? [],
|
|
120
|
+
stderr: [
|
|
121
|
+
...(verificationOutput?.stderr ?? []),
|
|
122
|
+
...(inspectionMessage === null
|
|
123
|
+
? []
|
|
124
|
+
: [`error: ${inspectionMessage}`]),
|
|
125
|
+
...(settlementMessage === null
|
|
126
|
+
? []
|
|
127
|
+
: [`error: ${settlementMessage}`]),
|
|
128
|
+
restoration,
|
|
129
|
+
],
|
|
39
130
|
};
|
|
40
131
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const control = await invoke(() => ctx.adapter.inspectUpdateControl(adapterContext), controlFailure, outcomes);
|
|
44
|
-
if (!control.ok)
|
|
45
|
-
return failed(control.message);
|
|
46
|
-
const mutationDecision = control.result.outcome.result.mutationEligibility;
|
|
47
|
-
if (mutationDecision.kind === "blocked") {
|
|
132
|
+
if (!settlement.ok) {
|
|
133
|
+
const actual = await invoke(() => ctx.adapter.inspectInstalled(selection, adapterContext), inspectionFailure, outcomes);
|
|
48
134
|
return {
|
|
49
|
-
kind: "
|
|
50
|
-
|
|
51
|
-
|
|
135
|
+
kind: "verified",
|
|
136
|
+
status: 1,
|
|
137
|
+
stdout: [],
|
|
138
|
+
stderr: [
|
|
139
|
+
"error: installation was verified, but transaction cleanup did not complete; preserve recovery material for manual resolution",
|
|
140
|
+
actual.ok
|
|
141
|
+
? `installed state after failed finalization: ${actual.result.outcome.result.kind}; identity=${actual.result.outcome.result.observedIdentity}`
|
|
142
|
+
: "installed state after failed finalization could not be inspected",
|
|
143
|
+
...(settlement.message === null
|
|
144
|
+
? []
|
|
145
|
+
: [`error: ${settlement.message}`]),
|
|
146
|
+
],
|
|
52
147
|
};
|
|
53
148
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const candidate = install.result.outcome.result.transaction;
|
|
66
|
-
if (candidate !== undefined) {
|
|
67
|
-
if (candidate === null || typeof candidate !== "object")
|
|
68
|
-
throw new Error("invalid transaction");
|
|
69
|
-
const finalize = candidate.finalize.bind(candidate);
|
|
70
|
-
const rollback = candidate.rollback.bind(candidate);
|
|
71
|
-
if (typeof finalize !== "function" ||
|
|
72
|
-
typeof rollback !== "function")
|
|
73
|
-
throw new Error("invalid transaction");
|
|
74
|
-
transaction = { finalize, rollback };
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
78
|
-
return failed("invalid installation transaction receipt; preserve recovery material for manual resolution");
|
|
79
|
-
}
|
|
80
|
-
// Stage 4: inspect fingerprint, to verify the mutation actually took.
|
|
81
|
-
//
|
|
82
|
-
// Deliberately NOT short-circuited on `!inspected.ok`, unlike stages
|
|
83
|
-
// 1-3. `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/install:57::spw_verify_installed_fingerprint` handed the inspect result to
|
|
84
|
-
// spw_verify_installed_fingerprint whatever it contained, and that
|
|
85
|
-
// function's first guard, from
|
|
86
|
-
// `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/core/lifecycle.sh:91-94::spw_inspect_fingerprint`,
|
|
87
|
-
// is what turns a failed inspection into "error: installed manager
|
|
88
|
-
// fingerprint inspection failed after install."
|
|
89
|
-
// renderInstallVerification's failed-inspection arm
|
|
90
|
-
// (`src/harnesses/codex/presentation.ts:280::if (inspection.status !== 0 || !inspection.outcome.ok) {`)
|
|
91
|
-
// exists for this result-bearing path. Returning
|
|
92
|
-
// failed() instead reported the adapter's own generic diagnostic and
|
|
93
|
-
// dropped the post-install verification claim -- a mutation had
|
|
94
|
-
// already been issued at stage 3,
|
|
95
|
-
// so "the install could not be verified" is the contract, not "an
|
|
96
|
-
// adapter call failed". A ctx.adapter THROW has no result to render.
|
|
97
|
-
// A pending transaction still rolls back before reporting that failure.
|
|
98
|
-
const inspectionFailure = ctx.adapter.presentation.callFailure("post-install", adapterContext);
|
|
99
|
-
const inspected = await invoke(() => ctx.adapter.inspectInstalled(selection, adapterContext), inspectionFailure, outcomes);
|
|
100
|
-
const inspection = inspected.result;
|
|
101
|
-
const verified = inspection !== null &&
|
|
102
|
-
inspection.status === 0 &&
|
|
103
|
-
inspection.outcome.ok &&
|
|
104
|
-
inspection.outcome.result.kind === "current";
|
|
105
|
-
if (transaction !== undefined) {
|
|
106
|
-
const operation = verified ? "finalize" : "rollback";
|
|
107
|
-
const settlement = await invoke(() => (verified ? transaction.finalize() : transaction.rollback()), {
|
|
108
|
-
unexpected: `installation ${operation} did not complete; preserve recovery material for manual resolution`,
|
|
109
|
-
invalidStatus: `installation ${operation} returned an invalid result; preserve recovery material for manual resolution`,
|
|
110
|
-
}, outcomes, (value) => value === null);
|
|
111
|
-
if (!verified) {
|
|
112
|
-
const restored = await invoke(() => ctx.adapter.inspectInstalled(selection, adapterContext), inspectionFailure, outcomes);
|
|
113
|
-
const restoration = restored.ok
|
|
114
|
-
? `installed state after rollback: ${restored.result.outcome.result.kind}; identity=${restored.result.outcome.result.observedIdentity}`
|
|
115
|
-
: "installed state after rollback could not be inspected";
|
|
116
|
-
const verificationOutput = inspection === null
|
|
117
|
-
? null
|
|
118
|
-
: ctx.adapter.presentation.renderInstallVerification(selection.desiredCommit, install.result, inspection);
|
|
119
|
-
const inspectionMessage = verificationOutput === null && !inspected.ok
|
|
120
|
-
? inspected.message
|
|
121
|
-
: null;
|
|
122
|
-
const settlementMessage = !settlement.ok
|
|
123
|
-
? settlement.message
|
|
124
|
-
: null;
|
|
125
|
-
return {
|
|
126
|
-
kind: "verified",
|
|
127
|
-
outcomes,
|
|
128
|
-
status: 1,
|
|
129
|
-
stdout: verificationOutput?.stdout ?? [],
|
|
130
|
-
stderr: [
|
|
131
|
-
...(verificationOutput?.stderr ?? []),
|
|
132
|
-
...(inspectionMessage === null
|
|
133
|
-
? []
|
|
134
|
-
: [`error: ${inspectionMessage}`]),
|
|
135
|
-
...(settlementMessage === null
|
|
136
|
-
? []
|
|
137
|
-
: [`error: ${settlementMessage}`]),
|
|
138
|
-
restoration,
|
|
139
|
-
],
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
if (!settlement.ok) {
|
|
143
|
-
const actual = await invoke(() => ctx.adapter.inspectInstalled(selection, adapterContext), inspectionFailure, outcomes);
|
|
144
|
-
return {
|
|
145
|
-
kind: "verified",
|
|
146
|
-
outcomes,
|
|
147
|
-
status: 1,
|
|
148
|
-
stdout: [],
|
|
149
|
-
stderr: [
|
|
150
|
-
"error: installation was verified, but transaction cleanup did not complete; preserve recovery material for manual resolution",
|
|
151
|
-
actual.ok
|
|
152
|
-
? `installed state after failed finalization: ${actual.result.outcome.result.kind}; identity=${actual.result.outcome.result.observedIdentity}`
|
|
153
|
-
: "installed state after failed finalization could not be inspected",
|
|
154
|
-
...(settlement.message === null
|
|
155
|
-
? []
|
|
156
|
-
: [`error: ${settlement.message}`]),
|
|
157
|
-
],
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
if (inspection === null)
|
|
162
|
-
return failed(inspected.ok ? null : inspected.message);
|
|
163
|
-
const output = ctx.adapter.presentation.renderInstallVerification(selection.desiredCommit, install.result, inspection);
|
|
164
|
-
return {
|
|
165
|
-
kind: "verified",
|
|
166
|
-
outcomes,
|
|
167
|
-
status: verified ? 0 : 1,
|
|
168
|
-
stdout: output.stdout,
|
|
169
|
-
stderr: output.stderr,
|
|
170
|
-
};
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
catch (cause) {
|
|
174
|
-
// Reachable only for mkdtemp failure (nothing collected yet): the
|
|
175
|
-
// callback never throws, and a post-success cleanup failure comes back
|
|
176
|
-
// as cleanupWarning.
|
|
177
|
-
throw new GatherFailure("install gather failed", cause, outcomes);
|
|
178
|
-
}
|
|
149
|
+
}
|
|
150
|
+
if (inspection === null)
|
|
151
|
+
return failed(inspected.ok ? null : inspected.message);
|
|
152
|
+
const output = ctx.adapter.presentation.renderInstallVerification(selection.desiredCommit, install.result, inspection);
|
|
153
|
+
return {
|
|
154
|
+
kind: "verified",
|
|
155
|
+
status: verified ? 0 : 1,
|
|
156
|
+
stdout: output.stdout,
|
|
157
|
+
stderr: output.stderr,
|
|
158
|
+
};
|
|
159
|
+
});
|
|
179
160
|
}
|
|
180
161
|
export async function runInstall(argv, ctx) {
|
|
181
162
|
if (ctx.adapter.presentation.installNotice !== "")
|
|
@@ -223,20 +204,21 @@ async function performInstall(ctx) {
|
|
|
223
204
|
ctx.stderr.write("error: adapter reported a failure status for prepared harness inspection\n");
|
|
224
205
|
return 1;
|
|
225
206
|
}
|
|
207
|
+
// Owned here, not in gatherInstallStages, so a gather throw still replays
|
|
208
|
+
// every outcome collected before it.
|
|
209
|
+
const outcomes = [];
|
|
226
210
|
let stage;
|
|
227
211
|
try {
|
|
228
|
-
stage = await gatherInstallStages(ctx, facts.selection, prepared.outcome.result);
|
|
212
|
+
stage = await gatherInstallStages(ctx, facts.selection, prepared.outcome.result, outcomes);
|
|
229
213
|
}
|
|
230
214
|
catch (cause) {
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
const inner = cause instanceof GatherFailure ? cause.inner : cause;
|
|
235
|
-
ctx.stderr.write(`error: ${oneLine(inner)}\n`);
|
|
215
|
+
for (const each of outcomes)
|
|
216
|
+
replayOutcome(each, ctx);
|
|
217
|
+
ctx.stderr.write(`error: ${oneLine(cause)}\n`);
|
|
236
218
|
return 1;
|
|
237
219
|
}
|
|
238
220
|
const { value: outcome, cleanupWarning } = stage;
|
|
239
|
-
for (const each of
|
|
221
|
+
for (const each of outcomes)
|
|
240
222
|
replayOutcome(each, ctx);
|
|
241
223
|
let status;
|
|
242
224
|
if (outcome.kind === "blocked") {
|
package/dist/commands/prepare.js
CHANGED
|
@@ -262,7 +262,7 @@ async function performPrepare(ctx, location) {
|
|
|
262
262
|
// completed before cleanup ran, so it is not being reported as unverified
|
|
263
263
|
// -- but something did still go wrong, and AGENTS.md's fail-closed rule
|
|
264
264
|
// extends to it. Mirrors
|
|
265
|
-
// `src/commands/install.ts:
|
|
265
|
+
// `src/commands/install.ts:351::if (cleanupWarning`.
|
|
266
266
|
ctx.stderr.write(`error: ${cleanupWarning}\n`);
|
|
267
267
|
return 1;
|
|
268
268
|
}
|
|
@@ -1,91 +1,76 @@
|
|
|
1
1
|
import { tmpdir } from "node:os";
|
|
2
2
|
import { oneLine } from "../cli-arguments.js";
|
|
3
3
|
import { withWorkspaceReporting, } from "../workspace.js";
|
|
4
|
-
import {
|
|
4
|
+
import { invoke, writeOutput } from "./adapter-call.js";
|
|
5
5
|
import { runWithMutation } from "./mutation.js";
|
|
6
6
|
import { replayOutcome } from "./probe.js";
|
|
7
|
-
// Collect outcomes without writing so output failures
|
|
8
|
-
// inspection failures.
|
|
9
|
-
|
|
7
|
+
// Collect outcomes into the caller's array without writing, so output failures
|
|
8
|
+
// cannot be classified as inspection failures. The caller replays them after
|
|
9
|
+
// gathering, including when gathering throws.
|
|
10
|
+
async function gatherUninstall(ctx, outcomes) {
|
|
10
11
|
const parent = ctx.env.TMPDIR ?? tmpdir();
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const after = await invoke(() => ctx.adapter.inspectOwnership(adapterContext), afterFailure, outcomes);
|
|
39
|
-
if (!after.ok)
|
|
40
|
-
return failed(after.message);
|
|
41
|
-
const ownership = after.result.outcome.result;
|
|
42
|
-
if (ownership.removalVerification.kind === "blocked") {
|
|
43
|
-
return {
|
|
44
|
-
status: 1,
|
|
45
|
-
outcomes,
|
|
46
|
-
message: null,
|
|
47
|
-
output: ownership.removalVerification.output,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
12
|
+
return await withWorkspaceReporting(parent, "superpowers-manager.uninstall.", async (workspace) => {
|
|
13
|
+
const env = { ...ctx.env, TMPDIR: workspace };
|
|
14
|
+
const failed = (message) => ({
|
|
15
|
+
status: 1,
|
|
16
|
+
message,
|
|
17
|
+
output: null,
|
|
18
|
+
});
|
|
19
|
+
const adapterContext = { root: ctx.root, env };
|
|
20
|
+
// Stage 1: inspect ownership, before removal.
|
|
21
|
+
const beforeFailure = ctx.adapter.presentation.callFailure("remove-ownership", adapterContext);
|
|
22
|
+
const before = await invoke(() => ctx.adapter.inspectOwnership(adapterContext), beforeFailure, outcomes);
|
|
23
|
+
if (!before.ok)
|
|
24
|
+
return failed(before.message);
|
|
25
|
+
// Stage 2: remove, passing the private input through untouched.
|
|
26
|
+
const removalInput = before.result.outcome.result.removalInput;
|
|
27
|
+
const removeFailure = ctx.adapter.presentation.callFailure("remove", adapterContext, removalInput);
|
|
28
|
+
const removed = await invoke(() => ctx.adapter.remove(removalInput, adapterContext), removeFailure, outcomes);
|
|
29
|
+
if (!removed.ok)
|
|
30
|
+
return failed(removed.message);
|
|
31
|
+
// Stage 3: inspect ownership again. Everything below reads this
|
|
32
|
+
// post-removal state, not the inspection before removal.
|
|
33
|
+
const afterFailure = ctx.adapter.presentation.callFailure("post-remove", adapterContext);
|
|
34
|
+
const after = await invoke(() => ctx.adapter.inspectOwnership(adapterContext), afterFailure, outcomes);
|
|
35
|
+
if (!after.ok)
|
|
36
|
+
return failed(after.message);
|
|
37
|
+
const ownership = after.result.outcome.result;
|
|
38
|
+
if (ownership.removalVerification.kind === "blocked") {
|
|
50
39
|
return {
|
|
51
|
-
status:
|
|
52
|
-
|
|
53
|
-
output:
|
|
40
|
+
status: 1,
|
|
41
|
+
message: null,
|
|
42
|
+
output: ownership.removalVerification.output,
|
|
54
43
|
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
// cleanupWarning. Wrapping with `outcomes` anyway keeps the class total
|
|
62
|
-
// over its declared contract rather than assuming the callback's purity at
|
|
63
|
-
// the throw site.
|
|
64
|
-
throw new GatherFailure("uninstall gather failed", cause, outcomes);
|
|
65
|
-
}
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
status: 0,
|
|
47
|
+
output: ctx.adapter.presentation.renderRemovalCompletion(ownership, removalInput),
|
|
48
|
+
};
|
|
49
|
+
});
|
|
66
50
|
}
|
|
67
51
|
export async function runUninstall(argv, ctx) {
|
|
68
52
|
// argv is ignored: scripts/uninstall never reads "$@".
|
|
69
53
|
return await runWithMutation("uninstall", ctx, async (scoped) => performUninstall(scoped));
|
|
70
54
|
}
|
|
71
55
|
async function performUninstall(ctx) {
|
|
56
|
+
// Owned here, not in gatherUninstall, so a gather throw still replays every
|
|
57
|
+
// outcome collected before it.
|
|
58
|
+
const outcomes = [];
|
|
72
59
|
let run;
|
|
73
60
|
try {
|
|
74
|
-
run = await gatherUninstall(ctx);
|
|
61
|
+
run = await gatherUninstall(ctx, outcomes);
|
|
75
62
|
}
|
|
76
63
|
catch (cause) {
|
|
77
64
|
// Replay collected outcomes before reporting a gather failure. A
|
|
78
65
|
// post-success cleanup failure arrives as cleanupWarning, not here.
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const inner = cause instanceof GatherFailure ? cause.inner : cause;
|
|
83
|
-
ctx.stderr.write(`error: ${oneLine(inner)}\n`);
|
|
66
|
+
for (const each of outcomes)
|
|
67
|
+
replayOutcome(each, ctx);
|
|
68
|
+
ctx.stderr.write(`error: ${oneLine(cause)}\n`);
|
|
84
69
|
return 1;
|
|
85
70
|
}
|
|
86
71
|
const { value: outcome, cleanupWarning } = run;
|
|
87
72
|
// Replay before reporting a command-level result on either path.
|
|
88
|
-
for (const each of
|
|
73
|
+
for (const each of outcomes)
|
|
89
74
|
replayOutcome(each, ctx);
|
|
90
75
|
let status;
|
|
91
76
|
if (outcome.status === 1) {
|
|
@@ -1,22 +1,9 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
|
+
import { pythonStrip } from "../../python-text.js";
|
|
3
4
|
import { assertNoFollowType } from "../../safe-path.js";
|
|
4
|
-
function trimHome(value) {
|
|
5
|
-
const whitespace = (index) => {
|
|
6
|
-
const code = value.charCodeAt(index);
|
|
7
|
-
return ((code >= 0x1c && code <= 0x1f) ||
|
|
8
|
-
/\p{White_Space}/u.test(value.charAt(index)));
|
|
9
|
-
};
|
|
10
|
-
let start = 0;
|
|
11
|
-
let end = value.length;
|
|
12
|
-
while (start < end && whitespace(start))
|
|
13
|
-
start++;
|
|
14
|
-
while (end > start && whitespace(end - 1))
|
|
15
|
-
end--;
|
|
16
|
-
return value.slice(start, end);
|
|
17
|
-
}
|
|
18
5
|
function expandHome(value, env, home) {
|
|
19
|
-
const expanded =
|
|
6
|
+
const expanded = pythonStrip(value).replace(/\$([A-Za-z0-9_]+|\{[^}]*\})/gu, (token, name) => {
|
|
20
7
|
const variable = name.startsWith("{") ? name.slice(1, -1) : name;
|
|
21
8
|
return Object.hasOwn(env, variable) ? (env[variable] ?? token) : token;
|
|
22
9
|
});
|
|
@@ -29,7 +16,7 @@ function expandHome(value, env, home) {
|
|
|
29
16
|
return expanded;
|
|
30
17
|
}
|
|
31
18
|
export function hermesPaths(env, cwd) {
|
|
32
|
-
const configured =
|
|
19
|
+
const configured = pythonStrip(env.HERMES_HOME ?? "");
|
|
33
20
|
const home = env.HOME && env.HOME.length > 0 ? env.HOME : homedir();
|
|
34
21
|
const hermesHome = configured.length > 0
|
|
35
22
|
? resolve(cwd, expandHome(configured, env, home))
|
|
@@ -1,21 +1,15 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { isAbsolute, join, resolve } from "node:path";
|
|
3
|
-
import { assertNoFollowType
|
|
4
|
-
function overlaps(left, right) {
|
|
5
|
-
return isContained(left, right) || isContained(right, left);
|
|
6
|
-
}
|
|
3
|
+
import { assertNoFollowType } from "../../safe-path.js";
|
|
7
4
|
export async function assertOpenCodePreparationSeparate(paths) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
for (const root of [
|
|
6
|
+
paths.configRoot,
|
|
7
|
+
paths.managerRoot,
|
|
8
|
+
paths.preparedRoot,
|
|
9
|
+
paths.installedRoot,
|
|
10
|
+
paths.recoveryRoot,
|
|
11
|
+
])
|
|
12
12
|
await assertNoFollowType(root, ["directory", "missing"]);
|
|
13
|
-
return await canonicalizeProspectivePath(root);
|
|
14
|
-
}));
|
|
15
|
-
if (overlaps(canonical[0], canonical[1]) ||
|
|
16
|
-
overlaps(canonical[0], canonical[2]) ||
|
|
17
|
-
overlaps(canonical[1], canonical[2]))
|
|
18
|
-
throw new Error("storage roots overlap");
|
|
19
13
|
}
|
|
20
14
|
export function openCodePaths(env, cwd) {
|
|
21
15
|
const home = env.HOME && env.HOME.length > 0 ? env.HOME : homedir();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "superpowers-manager",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "Install obra/superpowers directly from upstream for Codex, Pi, OpenCode, Claude Code, and Hermes Agent. Stay on a known-good release or commit, try a branch, and upgrade when you choose.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"superpowers",
|