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`);
@@ -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 { GatherFailure, invoke, writeOutput } from "./adapter-call.js";
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 cannot be classified as
10
- // inspection failures. Replay collected outcomes after gathering completes.
11
- // The callback never throws: invoke() catches every ctx.adapter failure and
12
- // every predicate here is pure, so a post-success cleanup failure is the only
13
- // case withWorkspaceReporting's cleanupWarning has to carry.
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
- const outcomes = [];
17
- try {
18
- return await withWorkspaceReporting(parent, "superpowers-manager.install.", async (workspace) => {
19
- const env = { ...ctx.env, TMPDIR: workspace };
20
- const failed = (message) => ({
21
- kind: "failed",
22
- outcomes,
23
- message,
24
- });
25
- const adapterContext = { root: ctx.root, env };
26
- // Stage 1: inspect ownership, re-checked even though gatherProbe just
27
- // reported it. Mutation authority requires CURRENT, VALIDATED
28
- // evidence -- a probe's answer is neither by the time this runs.
29
- const ownershipFailure = ctx.adapter.presentation.callFailure("install-ownership", adapterContext);
30
- const ownership = await invoke(() => ctx.adapter.inspectOwnership(adapterContext), ownershipFailure, outcomes);
31
- if (!ownership.ok)
32
- return failed(ownership.message);
33
- const ownershipDecision = ownership.result.outcome.result.installEligibility;
34
- if (ownershipDecision.kind === "blocked") {
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: "blocked",
37
- outcomes,
38
- output: ownershipDecision.output,
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
- // Stage 2: inspect update-control, re-checked for the same reason.
42
- const controlFailure = ctx.adapter.presentation.callFailure("install-control", adapterContext);
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: "blocked",
50
- outcomes,
51
- output: mutationDecision.output,
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
- // Stage 3: the mutation itself. Nothing above may have issued this --
55
- // that is the whole point of stages 1 and 2 running first.
56
- const activationFailure = activationBlock(artifact.compatibility, ctx.options.allowExperimental);
57
- if (activationFailure !== null)
58
- return failed(activationFailure);
59
- const installFailure = ctx.adapter.presentation.callFailure("install", adapterContext);
60
- const install = await invoke(() => ctx.adapter.install(artifact, adapterContext), installFailure, outcomes);
61
- if (!install.ok)
62
- return failed(install.message);
63
- let transaction;
64
- try {
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 outcomes = cause instanceof GatherFailure ? cause.outcomes : [];
232
- for (const outcome of outcomes)
233
- replayOutcome(outcome, ctx);
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 outcome.outcomes)
221
+ for (const each of outcomes)
240
222
  replayOutcome(each, ctx);
241
223
  let status;
242
224
  if (outcome.kind === "blocked") {
@@ -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:372::if (cleanupWarning`.
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 { GatherFailure, invoke, writeOutput } from "./adapter-call.js";
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 cannot be classified as
8
- // inspection failures. Replay collected outcomes after gathering completes.
9
- async function gatherUninstall(ctx) {
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
- // Kept outside the workspace callback so a workspace failure preserves every
12
- // outcome collected before it.
13
- const outcomes = [];
14
- try {
15
- return await withWorkspaceReporting(parent, "superpowers-manager.uninstall.", async (workspace) => {
16
- const env = { ...ctx.env, TMPDIR: workspace };
17
- const failed = (message) => ({
18
- status: 1,
19
- outcomes,
20
- message,
21
- output: null,
22
- });
23
- const adapterContext = { root: ctx.root, env };
24
- // Stage 1: inspect ownership, before removal.
25
- const beforeFailure = ctx.adapter.presentation.callFailure("remove-ownership", adapterContext);
26
- const before = await invoke(() => ctx.adapter.inspectOwnership(adapterContext), beforeFailure, outcomes);
27
- if (!before.ok)
28
- return failed(before.message);
29
- // Stage 2: remove, passing the private input through untouched.
30
- const removalInput = before.result.outcome.result.removalInput;
31
- const removeFailure = ctx.adapter.presentation.callFailure("remove", adapterContext, removalInput);
32
- const removed = await invoke(() => ctx.adapter.remove(removalInput, adapterContext), removeFailure, outcomes);
33
- if (!removed.ok)
34
- return failed(removed.message);
35
- // Stage 3: inspect ownership again. Everything below reads this
36
- // post-removal state, not the inspection before removal.
37
- const afterFailure = ctx.adapter.presentation.callFailure("post-remove", adapterContext);
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: 0,
52
- outcomes,
53
- output: ctx.adapter.presentation.renderRemovalCompletion(ownership, removalInput),
40
+ status: 1,
41
+ message: null,
42
+ output: ownership.removalVerification.output,
54
43
  };
55
- });
56
- }
57
- catch (cause) {
58
- // Reachable only for mkdtemp failure, with nothing collected yet: the
59
- // callback never throws (every ctx.adapter throw is caught inside
60
- // invoke()), and a post-success cleanup failure comes back as
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 outcomes = cause instanceof GatherFailure ? cause.outcomes : [];
80
- for (const outcome of outcomes)
81
- replayOutcome(outcome, ctx);
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 outcome.outcomes)
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 = trimHome(value).replace(/\$([A-Za-z0-9_]+|\{[^}]*\})/gu, (token, name) => {
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 = trimHome(env.HERMES_HOME ?? "");
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, canonicalizeProspectivePath, isContained, } from "../../safe-path.js";
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
- await assertNoFollowType(paths.configRoot, ["directory", "missing"]);
9
- await assertNoFollowType(paths.managerRoot, ["directory", "missing"]);
10
- const roots = [paths.preparedRoot, paths.installedRoot, paths.recoveryRoot];
11
- const canonical = await Promise.all(roots.map(async (root) => {
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.0",
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",