superpowers-manager 0.7.3 → 0.9.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/README.md +36 -16
- package/dist/atomic.js +1 -3
- package/dist/cli-arguments.js +4 -1
- package/dist/cli.js +14 -43
- package/dist/commands/adapter-call.js +24 -0
- package/dist/commands/install.js +16 -63
- package/dist/commands/mutation.js +5 -5
- package/dist/commands/pin.js +10 -5
- package/dist/commands/prepare.js +42 -108
- package/dist/commands/probe.js +33 -19
- package/dist/commands/uninstall.js +18 -46
- package/dist/commands/unpin.js +2 -1
- package/dist/commands/update.js +8 -29
- package/dist/domain/refs.js +0 -3
- package/dist/effective-selection.js +11 -15
- package/dist/git-tree.js +4 -6
- package/dist/harness-command-result.js +36 -5
- package/dist/harnesses/claude-code/harness.js +39 -0
- package/dist/harnesses/claude-code/install.js +380 -0
- package/dist/harnesses/claude-code/native.js +79 -0
- package/dist/harnesses/claude-code/paths.js +35 -0
- package/dist/harnesses/claude-code/prepare.js +117 -0
- package/dist/harnesses/claude-code/presentation.js +25 -0
- package/dist/harnesses/claude-code/state.js +253 -0
- package/dist/harnesses/codex/adapter.js +27 -68
- package/dist/harnesses/codex/conflicts.js +3 -8
- package/dist/harnesses/codex/generated-plugin.js +4 -11
- package/dist/harnesses/codex/harness.js +4 -26
- package/dist/harnesses/codex/hooks.js +11 -27
- package/dist/harnesses/codex/lifecycle.js +22 -107
- package/dist/harnesses/codex/manifest-overlay.js +9 -27
- package/dist/harnesses/codex/paths.js +29 -0
- package/dist/harnesses/codex/prepare.js +9 -43
- package/dist/harnesses/codex/presentation.js +1 -1
- package/dist/harnesses/codex/publication.js +5 -40
- package/dist/harnesses/codex/state.js +1 -6
- package/dist/harnesses/codex/stored-state.js +1 -6
- package/dist/harnesses/opencode/config.js +105 -50
- package/dist/harnesses/opencode/discovery.js +11 -15
- package/dist/harnesses/opencode/install.js +57 -102
- package/dist/harnesses/opencode/native.js +28 -49
- package/dist/harnesses/opencode/package.js +45 -67
- package/dist/harnesses/opencode/prepare.js +2 -7
- package/dist/harnesses/opencode/presentation.js +3 -7
- package/dist/harnesses/opencode/state.js +1 -0
- package/dist/harnesses/pi/install.js +31 -120
- package/dist/harnesses/pi/native.js +9 -34
- package/dist/harnesses/pi/package.js +9 -50
- package/dist/harnesses/pi/prepare.js +3 -8
- package/dist/harnesses/pi/presentation.js +2 -7
- package/dist/harnesses/pi/settings.js +5 -9
- package/dist/harnesses/pi/shared-skills.js +23 -53
- package/dist/harnesses/pi/state.js +3 -10
- package/dist/provenance.js +3 -19
- package/dist/resource-lock.js +11 -16
- package/dist/safe-path.js +28 -1
- package/dist/selection.js +15 -19
- package/dist/snapshot-journal.js +101 -0
- package/dist/snapshot-package.js +62 -1
- package/dist/snapshot-prepare.js +9 -2
- package/dist/snapshot-presentation.js +9 -0
- package/dist/upstream.js +8 -12
- package/dist/validator.js +26 -23
- package/dist/workspace.js +29 -35
- package/package.json +10 -9
- package/dist/python-json.js +0 -4
- package/dist/upstream-workspace.js +0 -8
package/dist/commands/prepare.js
CHANGED
|
@@ -1,36 +1,24 @@
|
|
|
1
|
-
import { mkdir
|
|
2
|
-
import { dirname,
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import { dirname, join, sep } from "node:path";
|
|
3
3
|
import { atomicReplaceDir } from "../atomic.js";
|
|
4
4
|
import { oneLine } from "../cli-arguments.js";
|
|
5
5
|
import { computeEffectiveSelection } from "../effective-selection.js";
|
|
6
6
|
import { runGit } from "../git.js";
|
|
7
|
+
import { isDirectory } from "../safe-path.js";
|
|
7
8
|
import { SafetyError } from "../safety-error.js";
|
|
8
|
-
import { fetchExactCommit, gitSafeSource } from "../upstream.js";
|
|
9
|
-
import { upstreamCacheRoot } from "../upstream-workspace.js";
|
|
9
|
+
import { fetchExactCommit, gitSafeSource, upstreamCacheRoot, } from "../upstream.js";
|
|
10
10
|
import { BOUNDED_EXECUTABLE, launchFailureMessage, resolveValidator, runValidator, displayPath, } from "../validator.js";
|
|
11
|
-
import {
|
|
11
|
+
import { withWorkspaceReporting, } from "../workspace.js";
|
|
12
|
+
import { invoke } from "./adapter-call.js";
|
|
12
13
|
import { replayOutcome } from "./probe.js";
|
|
13
14
|
import { runWithMutation } from "./mutation.js";
|
|
14
15
|
// Every message this module writes is hand-written here. The cause is attached
|
|
15
16
|
// for debuggability and never reaches a stream: oneLine (src/cli-arguments.ts)
|
|
16
17
|
// reads .message only. Same arrangement as hookError
|
|
17
|
-
// (`src/harnesses/codex/hooks.ts:
|
|
18
|
+
// (`src/harnesses/codex/hooks.ts:45::function hookError`).
|
|
18
19
|
function prepareError(message, cause) {
|
|
19
20
|
return new SafetyError("prepare", message, { cause });
|
|
20
21
|
}
|
|
21
|
-
// `[ -d ]` — `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/prepare:50::if [ -d`. A regular file named `.git` is what a git
|
|
22
|
-
// worktree or `clone --separate-git-dir` leaves behind; `-e` would take the
|
|
23
|
-
// fetch branch and let git follow its `gitdir:` pointer, where the shell took
|
|
24
|
-
// the clone branch. `src/upstream.ts:332::if (!(await isDirectory` makes the
|
|
25
|
-
// same distinction.
|
|
26
|
-
async function directoryExists(path) {
|
|
27
|
-
try {
|
|
28
|
-
return (await stat(path)).isDirectory();
|
|
29
|
-
}
|
|
30
|
-
catch {
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
22
|
// mkdir throws raw ErrnoExceptions. Every command-owned call goes through here
|
|
35
23
|
// so the message on the stream is this module's, not Node's.
|
|
36
24
|
async function owned(message, fn) {
|
|
@@ -70,34 +58,13 @@ function disclosureLine(resolution) {
|
|
|
70
58
|
return `[superpowers-manager: running external validator ${configured}${via} -> ${displayPath(resolution.resolved)}]\n`;
|
|
71
59
|
}
|
|
72
60
|
const NO_VALIDATOR_OUTPUT = { stdout: "", stderr: "" };
|
|
73
|
-
function
|
|
74
|
-
return (value.length > 0 &&
|
|
75
|
-
value !== "." &&
|
|
76
|
-
value !== ".." &&
|
|
77
|
-
!value.includes("/") &&
|
|
78
|
-
!value.includes("\\"));
|
|
79
|
-
}
|
|
80
|
-
async function gatherPrepare(ctx) {
|
|
61
|
+
async function gatherPrepare(ctx, location) {
|
|
81
62
|
// Collect outcomes without writing so output failures cannot be classified as
|
|
82
63
|
// inspection failures. Replay collected outcomes after gathering completes.
|
|
83
64
|
const env = ctx.env;
|
|
84
|
-
const
|
|
85
|
-
const cache = upstreamCacheRoot(ctx.root, env, cwd);
|
|
65
|
+
const cache = upstreamCacheRoot(ctx.root, env);
|
|
86
66
|
const cacheParent = dirname(cache);
|
|
87
67
|
const adapterContext = { root: ctx.root, env };
|
|
88
|
-
let location;
|
|
89
|
-
try {
|
|
90
|
-
location = ctx.adapter.preparationLocation(adapterContext);
|
|
91
|
-
}
|
|
92
|
-
catch (cause) {
|
|
93
|
-
throw prepareError("cannot determine preparation location", cause);
|
|
94
|
-
}
|
|
95
|
-
if (!isAbsolute(location.destinationRoot)) {
|
|
96
|
-
throw prepareError("adapter returned a non-absolute preparation destination");
|
|
97
|
-
}
|
|
98
|
-
if (!validStagingLeaf(location.stagingLeaf)) {
|
|
99
|
-
throw prepareError("adapter returned an invalid preparation staging leaf");
|
|
100
|
-
}
|
|
101
68
|
const pluginRoot = location.destinationRoot;
|
|
102
69
|
const executableValidator = env.SUPERPOWERS_VALIDATOR_EXECUTABLE || "";
|
|
103
70
|
const tmpParent = dirname(pluginRoot);
|
|
@@ -109,30 +76,14 @@ async function gatherPrepare(ctx) {
|
|
|
109
76
|
message,
|
|
110
77
|
});
|
|
111
78
|
const selection = ctx.selection ?? (await computeEffectiveSelection(ctx.root, env));
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return {
|
|
118
|
-
outcome: failed(ctx.adapter.presentation.callFailure("prepare", adapterContext)
|
|
119
|
-
.unexpected),
|
|
120
|
-
cleanupWarning: null,
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
outcomes.push(prefetch.outcome);
|
|
124
|
-
if (!prefetch.outcome.ok)
|
|
125
|
-
return { outcome: failed(null), cleanupWarning: null };
|
|
126
|
-
if (prefetch.status !== 0) {
|
|
127
|
-
return {
|
|
128
|
-
outcome: failed(ctx.adapter.presentation.callFailure("prepare", adapterContext)
|
|
129
|
-
.invalidStatus),
|
|
130
|
-
cleanupWarning: null,
|
|
131
|
-
};
|
|
79
|
+
// callFailure only formats strings; computing it once up front is safe.
|
|
80
|
+
const failure = ctx.adapter.presentation.callFailure("prepare", adapterContext);
|
|
81
|
+
const prefetch = await invoke(() => ctx.adapter.validatePreparationBeforeFetch(adapterContext), failure, outcomes);
|
|
82
|
+
if (!prefetch.ok) {
|
|
83
|
+
return { value: failed(prefetch.message), cleanupWarning: null };
|
|
132
84
|
}
|
|
133
85
|
await owned(`cannot create directory: ${tmpParent}`, () => mkdir(tmpParent, { recursive: true }));
|
|
134
|
-
|
|
135
|
-
const outcome = await withWorkspace(tmpParent, ".superpowers.prepare.", async (workspace) => {
|
|
86
|
+
return await withWorkspaceReporting(tmpParent, ".superpowers.prepare.", async (workspace) => {
|
|
136
87
|
const candidate = join(workspace, location.stagingLeaf);
|
|
137
88
|
await owned(`cannot create directory: ${cacheParent}`, () => mkdir(cacheParent, { recursive: true }));
|
|
138
89
|
if (selection.selectionMode === "pinned") {
|
|
@@ -140,7 +91,12 @@ async function gatherPrepare(ctx) {
|
|
|
140
91
|
}
|
|
141
92
|
else {
|
|
142
93
|
const source = gitSafeSource(selection.effectiveSource);
|
|
143
|
-
if
|
|
94
|
+
// `[ -d ]` — `git show ad56569a4c161e7b122967442e2b026eeb6395f6:scripts/prepare:50::if [ -d`. A regular file named `.git` is what a git
|
|
95
|
+
// worktree or `clone --separate-git-dir` leaves behind; `-e` would take the
|
|
96
|
+
// fetch branch and let git follow its `gitdir:` pointer, where the shell took
|
|
97
|
+
// the clone branch. `src/upstream.ts:323::if (!(await isDirectory` makes the
|
|
98
|
+
// same distinction.
|
|
99
|
+
if (await isDirectory(join(cache, ".git"))) {
|
|
144
100
|
const fetched = await runGit([
|
|
145
101
|
"-C",
|
|
146
102
|
cache,
|
|
@@ -175,34 +131,23 @@ async function gatherPrepare(ctx) {
|
|
|
175
131
|
if (checkedOut.status !== 0) {
|
|
176
132
|
return failed(`cannot check out upstream commit: ${selection.desiredCommit}`);
|
|
177
133
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
return failed(ctx.adapter.presentation.callFailure("prepare", adapterContext)
|
|
189
|
-
.unexpected);
|
|
190
|
-
}
|
|
191
|
-
outcomes.push(prepared.outcome);
|
|
192
|
-
if (prepared.status !== 0 || !prepared.outcome.ok) {
|
|
193
|
-
return failed(prepared.outcome.ok
|
|
194
|
-
? ctx.adapter.presentation.callFailure("prepare", adapterContext)
|
|
195
|
-
.invalidStatus
|
|
196
|
-
: null);
|
|
197
|
-
}
|
|
198
|
-
if (prepared.outcome.result.root !== candidate) {
|
|
134
|
+
const prepared = await invoke(() => ctx.adapter.prepareCandidate({
|
|
135
|
+
upstreamRoot: cache,
|
|
136
|
+
workspaceRoot: workspace,
|
|
137
|
+
candidateRoot: candidate,
|
|
138
|
+
selection,
|
|
139
|
+
}, adapterContext), failure, outcomes);
|
|
140
|
+
if (!prepared.ok)
|
|
141
|
+
return failed(prepared.message);
|
|
142
|
+
const candidateResult = prepared.result.outcome.result;
|
|
143
|
+
if (candidateResult.root !== candidate) {
|
|
199
144
|
return failed("adapter returned an unexpected preparation root");
|
|
200
145
|
}
|
|
201
|
-
if (
|
|
146
|
+
if (candidateResult.commit !== selection.desiredCommit) {
|
|
202
147
|
return failed("adapter returned an unexpected preparation commit");
|
|
203
148
|
}
|
|
204
|
-
if (
|
|
205
|
-
return failed(
|
|
149
|
+
if (candidateResult.compatibility.kind === "unsupported") {
|
|
150
|
+
return failed(candidateResult.compatibility.reason);
|
|
206
151
|
}
|
|
207
152
|
let validator = NO_VALIDATOR_OUTPUT;
|
|
208
153
|
if (executableValidator.length > 0) {
|
|
@@ -248,7 +193,7 @@ async function gatherPrepare(ctx) {
|
|
|
248
193
|
// the workspace on return, and the candidate lives in it.
|
|
249
194
|
//
|
|
250
195
|
// atomicReplaceDir delegates to beginDirectoryPublication, whose outer
|
|
251
|
-
// catch (`src/atomic.ts:
|
|
196
|
+
// catch (`src/atomic.ts:315::if (cause`) wraps every non-SafetyError
|
|
252
197
|
// into a SafetyError, so the callee owns every failure on this path and
|
|
253
198
|
// re-emitting its own diagnostic is the sanctioned form of interpolation.
|
|
254
199
|
// The hand-written prefix carries the live root, which the callee's message
|
|
@@ -271,28 +216,17 @@ async function gatherPrepare(ctx) {
|
|
|
271
216
|
resolvedRef: selection.resolvedRef,
|
|
272
217
|
commit: selection.desiredCommit,
|
|
273
218
|
};
|
|
274
|
-
}, {
|
|
275
|
-
// Suppresses withWorkspace's throw on a POST-SUCCESS cleanup failure, so
|
|
276
|
-
// the PrepareOutcome the callback already computed still reaches
|
|
277
|
-
// runPrepare instead of being discarded. The reporter runs synchronously,
|
|
278
|
-
// as the option requires (`src/workspace.ts:103-106::Must`).
|
|
279
|
-
onCleanupFailure: (path) => {
|
|
280
|
-
cleanupWarning = workspaceRemovalFailure(path);
|
|
281
|
-
},
|
|
282
219
|
});
|
|
283
|
-
return { outcome, cleanupWarning };
|
|
284
220
|
}
|
|
285
221
|
export async function runPrepare(argv, ctx) {
|
|
286
|
-
|
|
222
|
+
// argv is ignored: scripts/prepare never read "$@". Probe, by contrast,
|
|
223
|
+
// rejects extra arguments in the CLI parser.
|
|
224
|
+
return await runWithMutation("prepare", ctx, async (scoped, location) => performPrepare(scoped, location));
|
|
287
225
|
}
|
|
288
|
-
async function performPrepare(
|
|
289
|
-
// scripts/prepare never reads "$@", so extra arguments are ignored. This is a
|
|
290
|
-
// deliberate asymmetry with probe, whose shell original rejected unknown
|
|
291
|
-
// arguments and whose arity therefore moved into parseArgs in slice 2.
|
|
292
|
-
void argv;
|
|
226
|
+
async function performPrepare(ctx, location) {
|
|
293
227
|
let run;
|
|
294
228
|
try {
|
|
295
|
-
run = await gatherPrepare(ctx);
|
|
229
|
+
run = await gatherPrepare(ctx, location);
|
|
296
230
|
}
|
|
297
231
|
catch (cause) {
|
|
298
232
|
// Reader wrappers supply their own diagnostics. The saved-selection read
|
|
@@ -302,7 +236,7 @@ async function performPrepare(argv, ctx) {
|
|
|
302
236
|
ctx.stderr.write(`error: ${oneLine(cause)}\n`);
|
|
303
237
|
return 1;
|
|
304
238
|
}
|
|
305
|
-
const { outcome, cleanupWarning } = run;
|
|
239
|
+
const { value: outcome, cleanupWarning } = run;
|
|
306
240
|
for (const each of outcome.outcomes)
|
|
307
241
|
replayOutcome(each, ctx);
|
|
308
242
|
if (outcome.validator.stdout.length > 0) {
|
|
@@ -328,7 +262,7 @@ async function performPrepare(argv, ctx) {
|
|
|
328
262
|
// completed before cleanup ran, so it is not being reported as unverified
|
|
329
263
|
// -- but something did still go wrong, and AGENTS.md's fail-closed rule
|
|
330
264
|
// extends to it. Mirrors
|
|
331
|
-
// `src/commands/install.ts:
|
|
265
|
+
// `src/commands/install.ts:372::if (cleanupWarning`.
|
|
332
266
|
ctx.stderr.write(`error: ${cleanupWarning}\n`);
|
|
333
267
|
return 1;
|
|
334
268
|
}
|
package/dist/commands/probe.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
1
2
|
import { assertFailureWritable, writeAdapterFailure, } from "../adapter-result.js";
|
|
2
3
|
import { oneLine } from "../cli-arguments.js";
|
|
3
4
|
import { computeEffectiveSelection } from "../effective-selection.js";
|
|
@@ -96,13 +97,17 @@ export async function gatherProbe(ctx) {
|
|
|
96
97
|
if (!controlAfter.ok)
|
|
97
98
|
return { status: 1, outcomes, message: controlAfter.message };
|
|
98
99
|
const after = await ctx.coordination.observeResources(resources);
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
100
|
+
if (!isDeepStrictEqual([
|
|
101
|
+
before,
|
|
102
|
+
prepared.result.outcome.result,
|
|
103
|
+
installed.result.outcome.result,
|
|
104
|
+
control.result.outcome.result,
|
|
105
|
+
], [
|
|
106
|
+
after,
|
|
107
|
+
preparedAfter.result.outcome.result,
|
|
108
|
+
installedAfter.result.outcome.result,
|
|
109
|
+
controlAfter.result.outcome.result,
|
|
110
|
+
])) {
|
|
106
111
|
return {
|
|
107
112
|
status: 1,
|
|
108
113
|
outcomes,
|
|
@@ -141,14 +146,10 @@ export async function gatherProbe(ctx) {
|
|
|
141
146
|
},
|
|
142
147
|
};
|
|
143
148
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (!porcelain && argv.length !== 0) {
|
|
149
|
-
ctx.stderr.write(PROBE_USAGE);
|
|
150
|
-
return 2;
|
|
151
|
-
}
|
|
149
|
+
// Runs the probe and reports a failed gather the way every probing command
|
|
150
|
+
// does: `error:` for a throw, replay of every collected outcome, then the
|
|
151
|
+
// command-level message. null means the caller must return 1.
|
|
152
|
+
export async function probeFacts(ctx) {
|
|
152
153
|
let outcome;
|
|
153
154
|
try {
|
|
154
155
|
outcome = await gatherProbe(ctx);
|
|
@@ -159,7 +160,7 @@ export async function runProbe(argv, ctx) {
|
|
|
159
160
|
// `src/selection-store.ts:116-121::if (cause instanceof SafetyError && cause.module === "selection") {`.
|
|
160
161
|
// oneLine() bounds any inherited git or filesystem diagnostic to one line.
|
|
161
162
|
ctx.stderr.write(`error: ${oneLine(cause)}\n`);
|
|
162
|
-
return
|
|
163
|
+
return null;
|
|
163
164
|
}
|
|
164
165
|
// Replay before reporting a command-level result on either path.
|
|
165
166
|
for (const each of outcome.outcomes)
|
|
@@ -170,9 +171,22 @@ export async function runProbe(argv, ctx) {
|
|
|
170
171
|
if (outcome.message !== null) {
|
|
171
172
|
ctx.stderr.write(`error: ${outcome.message}\n`);
|
|
172
173
|
}
|
|
173
|
-
return
|
|
174
|
+
return null;
|
|
174
175
|
}
|
|
175
|
-
|
|
176
|
+
return outcome.facts;
|
|
177
|
+
}
|
|
178
|
+
export async function runProbe(argv, ctx) {
|
|
179
|
+
// The CLI parser rejects these inputs first. Keep this guard for direct
|
|
180
|
+
// in-process callers, which do not necessarily pass through that parser.
|
|
181
|
+
const porcelain = argv.length === 1 && argv[0] === "--porcelain";
|
|
182
|
+
if (!porcelain && argv.length !== 0) {
|
|
183
|
+
ctx.stderr.write(PROBE_USAGE);
|
|
184
|
+
return 2;
|
|
185
|
+
}
|
|
186
|
+
const facts = await probeFacts(ctx);
|
|
187
|
+
if (facts === null)
|
|
188
|
+
return 1;
|
|
189
|
+
const rendered = ctx.adapter.presentation.renderProbe(facts);
|
|
176
190
|
ctx.stdout.write(porcelain ? rendered.porcelain : rendered.human);
|
|
177
|
-
return
|
|
191
|
+
return facts.resourceState === "recovery-required" ? 1 : 0;
|
|
178
192
|
}
|
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
import { tmpdir } from "node:os";
|
|
2
2
|
import { oneLine } from "../cli-arguments.js";
|
|
3
|
-
import {
|
|
4
|
-
import { GatherFailure,
|
|
3
|
+
import { withWorkspaceReporting, } from "../workspace.js";
|
|
4
|
+
import { GatherFailure, invoke, writeOutput } from "./adapter-call.js";
|
|
5
5
|
import { runWithMutation } from "./mutation.js";
|
|
6
6
|
import { replayOutcome } from "./probe.js";
|
|
7
|
-
async function invoke(call, failure, outcomes) {
|
|
8
|
-
const result = await callAdapter(call, failure);
|
|
9
|
-
if (result.result !== null)
|
|
10
|
-
outcomes.push(result.result.outcome);
|
|
11
|
-
return result;
|
|
12
|
-
}
|
|
13
7
|
// Collect outcomes without writing so output failures cannot be classified as
|
|
14
8
|
// inspection failures. Replay collected outcomes after gathering completes.
|
|
15
9
|
async function gatherUninstall(ctx) {
|
|
@@ -17,9 +11,8 @@ async function gatherUninstall(ctx) {
|
|
|
17
11
|
// Kept outside the workspace callback so a workspace failure preserves every
|
|
18
12
|
// outcome collected before it.
|
|
19
13
|
const outcomes = [];
|
|
20
|
-
let cleanupWarning = null;
|
|
21
14
|
try {
|
|
22
|
-
|
|
15
|
+
return await withWorkspaceReporting(parent, "superpowers-manager.uninstall.", async (workspace) => {
|
|
23
16
|
const env = { ...ctx.env, TMPDIR: workspace };
|
|
24
17
|
const failed = (message) => ({
|
|
25
18
|
status: 1,
|
|
@@ -59,44 +52,30 @@ async function gatherUninstall(ctx) {
|
|
|
59
52
|
outcomes,
|
|
60
53
|
output: ctx.adapter.presentation.renderRemovalCompletion(ownership, removalInput),
|
|
61
54
|
};
|
|
62
|
-
}, {
|
|
63
|
-
// Suppresses withWorkspace's throw on a POST-SUCCESS cleanup failure,
|
|
64
|
-
// so the UninstallOutcome the callback already computed still comes
|
|
65
|
-
// back as `outcome` instead of being discarded. Runs synchronously,
|
|
66
|
-
// as the option requires (src/workspace.ts).
|
|
67
|
-
onCleanupFailure: (path) => {
|
|
68
|
-
cleanupWarning = workspaceRemovalFailure(path);
|
|
69
|
-
},
|
|
70
55
|
});
|
|
71
|
-
return { outcome, cleanupWarning };
|
|
72
56
|
}
|
|
73
57
|
catch (cause) {
|
|
74
|
-
// Reachable only for mkdtemp failure, with nothing collected yet:
|
|
75
|
-
// callback never throws
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
// total over its declared contract rather than assuming the callback's
|
|
81
|
-
// purity at the throw site.
|
|
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.
|
|
82
64
|
throw new GatherFailure("uninstall gather failed", cause, outcomes);
|
|
83
65
|
}
|
|
84
66
|
}
|
|
85
67
|
export async function runUninstall(argv, ctx) {
|
|
86
|
-
|
|
68
|
+
// argv is ignored: scripts/uninstall never reads "$@".
|
|
69
|
+
return await runWithMutation("uninstall", ctx, async (scoped) => performUninstall(scoped));
|
|
87
70
|
}
|
|
88
|
-
async function performUninstall(
|
|
89
|
-
// scripts/uninstall never reads "$@", so extra arguments are silently
|
|
90
|
-
// ignored -- the same asymmetry runPrepare documents at its own
|
|
91
|
-
// `void argv;`.
|
|
92
|
-
void argv;
|
|
71
|
+
async function performUninstall(ctx) {
|
|
93
72
|
let run;
|
|
94
73
|
try {
|
|
95
74
|
run = await gatherUninstall(ctx);
|
|
96
75
|
}
|
|
97
76
|
catch (cause) {
|
|
98
|
-
// Replay collected outcomes before reporting a gather failure.
|
|
99
|
-
//
|
|
77
|
+
// Replay collected outcomes before reporting a gather failure. A
|
|
78
|
+
// post-success cleanup failure arrives as cleanupWarning, not here.
|
|
100
79
|
const outcomes = cause instanceof GatherFailure ? cause.outcomes : [];
|
|
101
80
|
for (const outcome of outcomes)
|
|
102
81
|
replayOutcome(outcome, ctx);
|
|
@@ -104,7 +83,7 @@ async function performUninstall(argv, ctx) {
|
|
|
104
83
|
ctx.stderr.write(`error: ${oneLine(inner)}\n`);
|
|
105
84
|
return 1;
|
|
106
85
|
}
|
|
107
|
-
const { outcome, cleanupWarning } = run;
|
|
86
|
+
const { value: outcome, cleanupWarning } = run;
|
|
108
87
|
// Replay before reporting a command-level result on either path.
|
|
109
88
|
for (const each of outcome.outcomes)
|
|
110
89
|
replayOutcome(each, ctx);
|
|
@@ -115,19 +94,12 @@ async function performUninstall(argv, ctx) {
|
|
|
115
94
|
if (outcome.message !== null) {
|
|
116
95
|
ctx.stderr.write(`error: ${outcome.message}\n`);
|
|
117
96
|
}
|
|
118
|
-
if (outcome.output !== null)
|
|
119
|
-
|
|
120
|
-
ctx.stdout.write(`${line}\n`);
|
|
121
|
-
for (const line of outcome.output.stderr)
|
|
122
|
-
ctx.stderr.write(`${line}\n`);
|
|
123
|
-
}
|
|
97
|
+
if (outcome.output !== null)
|
|
98
|
+
writeOutput(outcome.output, ctx);
|
|
124
99
|
status = 1;
|
|
125
100
|
}
|
|
126
101
|
else {
|
|
127
|
-
|
|
128
|
-
ctx.stdout.write(`${line}\n`);
|
|
129
|
-
for (const line of outcome.output.stderr)
|
|
130
|
-
ctx.stderr.write(`${line}\n`);
|
|
102
|
+
writeOutput(outcome.output, ctx);
|
|
131
103
|
status = 0;
|
|
132
104
|
}
|
|
133
105
|
if (cleanupWarning !== null) {
|
package/dist/commands/unpin.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { lstat, unlink } from "node:fs/promises";
|
|
2
2
|
import { oneLine } from "../cli-arguments.js";
|
|
3
3
|
import { selectionStatePath } from "../effective-selection.js";
|
|
4
|
+
import { isErrno } from "../safe-path.js";
|
|
4
5
|
import { selectionError } from "../selection.js";
|
|
5
6
|
import { readConfigRef } from "../upstream.js";
|
|
6
7
|
const NOT_REGULAR = "selection state path is not a regular file; remove it manually after inspecting";
|
|
@@ -24,7 +25,7 @@ async function attemptUnpin(ctx) {
|
|
|
24
25
|
return await lstat(path);
|
|
25
26
|
}
|
|
26
27
|
catch (cause) {
|
|
27
|
-
if (cause
|
|
28
|
+
if (isErrno(cause, "ENOENT"))
|
|
28
29
|
return null;
|
|
29
30
|
throw selectionError(`cannot inspect selection state: ${path}`);
|
|
30
31
|
}
|
package/dist/commands/update.js
CHANGED
|
@@ -1,37 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
import { runInstall, writeOutput } from "./install.js";
|
|
5
|
-
import { gatherProbe, replayOutcome } from "./probe.js";
|
|
1
|
+
import { writeOutput } from "./adapter-call.js";
|
|
2
|
+
import { runInstall } from "./install.js";
|
|
3
|
+
import { probeFacts } from "./probe.js";
|
|
6
4
|
import { runPrepare } from "./prepare.js";
|
|
7
5
|
import { runWithMutation } from "./mutation.js";
|
|
8
6
|
export async function runUpdate(argv, ctx) {
|
|
9
|
-
|
|
7
|
+
// argv is ignored: scripts/update never reads "$@".
|
|
8
|
+
return await runWithMutation("update", ctx, async (scoped) => performUpdate(scoped));
|
|
10
9
|
}
|
|
11
|
-
async function performUpdate(
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
void argv;
|
|
15
|
-
let probe;
|
|
16
|
-
try {
|
|
17
|
-
probe = await gatherProbe(ctx);
|
|
18
|
-
}
|
|
19
|
-
catch (cause) {
|
|
20
|
-
// Gathering does not write, so this catch cannot misclassify an output
|
|
21
|
-
// failure. oneLine() bounds any inherited diagnostic to one line.
|
|
22
|
-
ctx.stderr.write(`error: ${oneLine(cause)}\n`);
|
|
10
|
+
async function performUpdate(ctx) {
|
|
11
|
+
const facts = await probeFacts(ctx);
|
|
12
|
+
if (facts === null)
|
|
23
13
|
return 1;
|
|
24
|
-
}
|
|
25
|
-
// Replay first, on both paths, before any decision -- clause 1.
|
|
26
|
-
for (const outcome of probe.outcomes)
|
|
27
|
-
replayOutcome(outcome, ctx);
|
|
28
|
-
if (probe.status === 1) {
|
|
29
|
-
if (probe.message !== null) {
|
|
30
|
-
ctx.stderr.write(`error: ${probe.message}\n`);
|
|
31
|
-
}
|
|
32
|
-
return 1;
|
|
33
|
-
}
|
|
34
|
-
const facts = probe.facts;
|
|
35
14
|
if (facts.ownership.installEligibility.kind === "blocked") {
|
|
36
15
|
writeOutput(facts.ownership.installEligibility.output, ctx);
|
|
37
16
|
return 1;
|
package/dist/domain/refs.js
CHANGED
|
@@ -12,9 +12,6 @@ export const COMMIT_INPUT_RE = /^[0-9A-Fa-f]{40}$/;
|
|
|
12
12
|
export function isTagRef(value) {
|
|
13
13
|
return TAG_RE.test(value);
|
|
14
14
|
}
|
|
15
|
-
export function isCommit(value) {
|
|
16
|
-
return COMMIT_RE.test(value);
|
|
17
|
-
}
|
|
18
15
|
export function normalizeCommitInput(value) {
|
|
19
16
|
return COMMIT_INPUT_RE.test(value) ? value.toLowerCase() : null;
|
|
20
17
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// `selectionError` already exists at
|
|
2
|
-
// `src/selection.ts:
|
|
2
|
+
// `src/selection.ts:34::export function selectionError` and tags errors with
|
|
3
3
|
// module "selection". Import it rather than defining a second one.
|
|
4
4
|
import { normalizeSaved, selectionError, validateSource, } from "./selection.js";
|
|
5
5
|
import { readSelectionState } from "./selection-store.js";
|
|
@@ -67,7 +67,6 @@ export async function computeEffectiveSelection(root, env) {
|
|
|
67
67
|
let selectionOrigin;
|
|
68
68
|
let selectionMode;
|
|
69
69
|
let requestedRef;
|
|
70
|
-
let usesSavedPin = false;
|
|
71
70
|
if (env.SUPERPOWERS_REF) {
|
|
72
71
|
selectionOrigin = "environment";
|
|
73
72
|
selectionMode = "override";
|
|
@@ -77,19 +76,6 @@ export async function computeEffectiveSelection(root, env) {
|
|
|
77
76
|
selectionOrigin = "user-config";
|
|
78
77
|
selectionMode = "pinned";
|
|
79
78
|
requestedRef = saved.saved_requested_ref;
|
|
80
|
-
usesSavedPin = true;
|
|
81
|
-
}
|
|
82
|
-
else if (saved.saved_mode === "track-latest") {
|
|
83
|
-
selectionOrigin = "user-config";
|
|
84
|
-
selectionMode = "track-latest";
|
|
85
|
-
requestedRef = "latest-release";
|
|
86
|
-
}
|
|
87
|
-
else {
|
|
88
|
-
selectionOrigin = "package-default";
|
|
89
|
-
selectionMode = "default";
|
|
90
|
-
requestedRef = await readConfigRef(root, env);
|
|
91
|
-
}
|
|
92
|
-
if (usesSavedPin) {
|
|
93
79
|
return {
|
|
94
80
|
selectionOrigin,
|
|
95
81
|
selectionMode,
|
|
@@ -104,6 +90,16 @@ export async function computeEffectiveSelection(root, env) {
|
|
|
104
90
|
saved,
|
|
105
91
|
};
|
|
106
92
|
}
|
|
93
|
+
else if (saved.saved_mode === "track-latest") {
|
|
94
|
+
selectionOrigin = "user-config";
|
|
95
|
+
selectionMode = "track-latest";
|
|
96
|
+
requestedRef = "latest-release";
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
selectionOrigin = "package-default";
|
|
100
|
+
selectionMode = "default";
|
|
101
|
+
requestedRef = await readConfigRef(root, env);
|
|
102
|
+
}
|
|
107
103
|
const resolution = await resolveRef(effectiveSource, requestedRef);
|
|
108
104
|
// `Resolution` (src/upstream.ts) is { kind, ref, commit } — the
|
|
109
105
|
// resolved ref field is named `ref`, not `resolvedRef`.
|
package/dist/git-tree.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { chmod, mkdir, symlink, writeFile } from "node:fs/promises";
|
|
3
|
-
import { dirname, isAbsolute, join,
|
|
3
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
5
|
import { ARTIFACT_RECEIPT } from "./artifact-tree.js";
|
|
6
6
|
import { COMMIT_RE } from "./domain/refs.js";
|
|
7
|
-
import { assertNoFollowType, assertSymlinkTargetContained, } from "./safe-path.js";
|
|
7
|
+
import { assertNoFollowType, assertSymlinkTargetContained, isContained, } from "./safe-path.js";
|
|
8
8
|
import { SafetyError } from "./safety-error.js";
|
|
9
9
|
const exec = promisify(execFile);
|
|
10
10
|
const DECODER = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
@@ -66,13 +66,11 @@ export async function materializeGitTree(repository, commit, destination) {
|
|
|
66
66
|
}
|
|
67
67
|
for (const entry of entries.filter((entry) => entry.mode === "120000")) {
|
|
68
68
|
const target = DECODER.decode(await gitBytes(repository, ["cat-file", "blob", entry.oid]));
|
|
69
|
-
const path = join(destination, entry.path)
|
|
69
|
+
const path = join(destination, entry.path);
|
|
70
70
|
if (!target ||
|
|
71
71
|
target.includes("\0") ||
|
|
72
72
|
isAbsolute(target) ||
|
|
73
|
-
|
|
74
|
-
suffix.startsWith("../") ||
|
|
75
|
-
isAbsolute(suffix))
|
|
73
|
+
!isContained(resolve(destination), resolve(dirname(path), target)))
|
|
76
74
|
throw new Error("link escape");
|
|
77
75
|
await symlink(target, path);
|
|
78
76
|
}
|