@solaqua/skul 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/README.md +282 -0
- package/bin/skul.js +13 -0
- package/dist/bundle-discovery.d.ts +30 -0
- package/dist/bundle-discovery.js +284 -0
- package/dist/bundle-discovery.js.map +1 -0
- package/dist/bundle-fetch.d.ts +67 -0
- package/dist/bundle-fetch.js +378 -0
- package/dist/bundle-fetch.js.map +1 -0
- package/dist/bundle-items.d.ts +34 -0
- package/dist/bundle-items.js +149 -0
- package/dist/bundle-items.js.map +1 -0
- package/dist/bundle-manifest.d.ts +26 -0
- package/dist/bundle-manifest.js +196 -0
- package/dist/bundle-manifest.js.map +1 -0
- package/dist/bundle-materialization.d.ts +52 -0
- package/dist/bundle-materialization.js +587 -0
- package/dist/bundle-materialization.js.map +1 -0
- package/dist/bundle-translation.d.ts +38 -0
- package/dist/bundle-translation.js +502 -0
- package/dist/bundle-translation.js.map +1 -0
- package/dist/cli.d.ts +126 -0
- package/dist/cli.js +648 -0
- package/dist/cli.js.map +1 -0
- package/dist/fs-utils.d.ts +7 -0
- package/dist/fs-utils.js +27 -0
- package/dist/fs-utils.js.map +1 -0
- package/dist/git-context.d.ts +14 -0
- package/dist/git-context.js +53 -0
- package/dist/git-context.js.map +1 -0
- package/dist/git-exclude.d.ts +13 -0
- package/dist/git-exclude.js +76 -0
- package/dist/git-exclude.js.map +1 -0
- package/dist/git-index.d.ts +106 -0
- package/dist/git-index.js +224 -0
- package/dist/git-index.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +4190 -0
- package/dist/index.js.map +1 -0
- package/dist/registry.d.ts +91 -0
- package/dist/registry.js +551 -0
- package/dist/registry.js.map +1 -0
- package/dist/root-instruction-content.d.ts +11 -0
- package/dist/root-instruction-content.js +61 -0
- package/dist/root-instruction-content.js.map +1 -0
- package/dist/root-instruction-render.d.ts +31 -0
- package/dist/root-instruction-render.js +121 -0
- package/dist/root-instruction-render.js.map +1 -0
- package/dist/root-instruction-state.d.ts +41 -0
- package/dist/root-instruction-state.js +273 -0
- package/dist/root-instruction-state.js.map +1 -0
- package/dist/state-layout.d.ts +11 -0
- package/dist/state-layout.js +26 -0
- package/dist/state-layout.js.map +1 -0
- package/dist/tool-mapping.d.ts +35 -0
- package/dist/tool-mapping.js +227 -0
- package/dist/tool-mapping.js.map +1 -0
- package/package.json +72 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4190 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.run = run;
|
|
8
|
+
exports.assertTrackedRootInstructionShadowSafety = assertTrackedRootInstructionShadowSafety;
|
|
9
|
+
const node_child_process_1 = require("node:child_process");
|
|
10
|
+
const node_crypto_1 = require("node:crypto");
|
|
11
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
12
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
13
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
14
|
+
const picocolors_1 = require("picocolors");
|
|
15
|
+
const bundle_discovery_1 = require("./bundle-discovery");
|
|
16
|
+
const bundle_fetch_1 = require("./bundle-fetch");
|
|
17
|
+
const bundle_items_1 = require("./bundle-items");
|
|
18
|
+
const bundle_materialization_1 = require("./bundle-materialization");
|
|
19
|
+
const cli_1 = require("./cli");
|
|
20
|
+
const git_context_1 = require("./git-context");
|
|
21
|
+
const git_exclude_1 = require("./git-exclude");
|
|
22
|
+
const git_index_1 = require("./git-index");
|
|
23
|
+
const registry_1 = require("./registry");
|
|
24
|
+
const root_instruction_content_1 = require("./root-instruction-content");
|
|
25
|
+
const root_instruction_render_1 = require("./root-instruction-render");
|
|
26
|
+
const root_instruction_state_1 = require("./root-instruction-state");
|
|
27
|
+
const state_layout_1 = require("./state-layout");
|
|
28
|
+
const tool_mapping_1 = require("./tool-mapping");
|
|
29
|
+
// Lazily evaluated so that SKUL_NO_TUI set after module load (e.g. in tests) is respected.
|
|
30
|
+
const pc = new Proxy({}, {
|
|
31
|
+
get(_t, prop) {
|
|
32
|
+
return (0, picocolors_1.createColors)(picocolors_1.isColorSupported && !(0, cli_1.isHeadlessMode)())[prop];
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
/** Parses CLI arguments and executes the selected Skul command. */
|
|
36
|
+
async function run(argv, options = {}) {
|
|
37
|
+
const stateLayout = (0, state_layout_1.resolveGlobalStateLayout)({
|
|
38
|
+
homeDir: options.homeDir ?? node_os_1.default.homedir(),
|
|
39
|
+
});
|
|
40
|
+
const prompts = options.prompts ?? createDefaultPromptClient(stateLayout.libraryDir);
|
|
41
|
+
const parsed = await (0, cli_1.parseCliArgs)(argv, prompts);
|
|
42
|
+
const cwd = options.cwd ?? process.cwd();
|
|
43
|
+
if (parsed.kind === "help") {
|
|
44
|
+
return (0, cli_1.createHelpText)(parsed.command);
|
|
45
|
+
}
|
|
46
|
+
switch (parsed.command) {
|
|
47
|
+
case "add":
|
|
48
|
+
if (parsed.options.global) {
|
|
49
|
+
return applyBundleGlobal({
|
|
50
|
+
homeDir: options.homeDir ?? node_os_1.default.homedir(),
|
|
51
|
+
prompts,
|
|
52
|
+
registryFile: stateLayout.registryFile,
|
|
53
|
+
libraryDir: stateLayout.libraryDir,
|
|
54
|
+
bundle: parsed.options.bundle,
|
|
55
|
+
source: parsed.options.source,
|
|
56
|
+
protocol: parsed.options.protocol,
|
|
57
|
+
agents: parsed.options.agents,
|
|
58
|
+
includeItems: parsed.options.includeItems ?? [],
|
|
59
|
+
selectItems: parsed.options.selectItems ?? false,
|
|
60
|
+
dryRun: parsed.options.dryRun,
|
|
61
|
+
ref: parsed.options.ref,
|
|
62
|
+
inferredBundleFromSource: parsed.options.inferredBundleFromSource,
|
|
63
|
+
disableModelInvocation: parsed.options.disableModelInvocation,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return applyBundle({
|
|
67
|
+
cwd,
|
|
68
|
+
prompts,
|
|
69
|
+
registryFile: stateLayout.registryFile,
|
|
70
|
+
libraryDir: stateLayout.libraryDir,
|
|
71
|
+
bundle: parsed.options.bundle,
|
|
72
|
+
source: parsed.options.source,
|
|
73
|
+
protocol: parsed.options.protocol,
|
|
74
|
+
agents: parsed.options.agents,
|
|
75
|
+
includeItems: parsed.options.includeItems ?? [],
|
|
76
|
+
selectItems: parsed.options.selectItems ?? false,
|
|
77
|
+
dryRun: parsed.options.dryRun,
|
|
78
|
+
ref: parsed.options.ref,
|
|
79
|
+
inferredBundleFromSource: parsed.options.inferredBundleFromSource,
|
|
80
|
+
disableModelInvocation: parsed.options.disableModelInvocation,
|
|
81
|
+
});
|
|
82
|
+
case "list":
|
|
83
|
+
return renderBundleList({
|
|
84
|
+
libraryDir: stateLayout.libraryDir,
|
|
85
|
+
json: parsed.options.json,
|
|
86
|
+
source: parsed.options.source,
|
|
87
|
+
});
|
|
88
|
+
case "status":
|
|
89
|
+
if (parsed.options.global) {
|
|
90
|
+
return renderGlobalStatus({
|
|
91
|
+
registryFile: stateLayout.registryFile,
|
|
92
|
+
json: parsed.options.json,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return renderStatus({
|
|
96
|
+
cwd,
|
|
97
|
+
registryFile: stateLayout.registryFile,
|
|
98
|
+
json: parsed.options.json,
|
|
99
|
+
});
|
|
100
|
+
case "check":
|
|
101
|
+
return renderUpdateCheck({
|
|
102
|
+
cwd,
|
|
103
|
+
registryFile: stateLayout.registryFile,
|
|
104
|
+
libraryDir: stateLayout.libraryDir,
|
|
105
|
+
bundle: parsed.options.bundle,
|
|
106
|
+
json: parsed.options.json,
|
|
107
|
+
});
|
|
108
|
+
case "update":
|
|
109
|
+
return updateBundles({
|
|
110
|
+
cwd,
|
|
111
|
+
prompts,
|
|
112
|
+
registryFile: stateLayout.registryFile,
|
|
113
|
+
libraryDir: stateLayout.libraryDir,
|
|
114
|
+
bundle: parsed.options.bundle,
|
|
115
|
+
dryRun: parsed.options.dryRun,
|
|
116
|
+
});
|
|
117
|
+
case "shadow":
|
|
118
|
+
return shadowWorktree({
|
|
119
|
+
cwd,
|
|
120
|
+
registryFile: stateLayout.registryFile,
|
|
121
|
+
action: parsed.options.action,
|
|
122
|
+
});
|
|
123
|
+
case "sync":
|
|
124
|
+
return syncWorktree({
|
|
125
|
+
cwd,
|
|
126
|
+
registryFile: stateLayout.registryFile,
|
|
127
|
+
});
|
|
128
|
+
case "reset":
|
|
129
|
+
if (parsed.options.global) {
|
|
130
|
+
return resetGlobal({
|
|
131
|
+
homeDir: options.homeDir ?? node_os_1.default.homedir(),
|
|
132
|
+
prompts,
|
|
133
|
+
registryFile: stateLayout.registryFile,
|
|
134
|
+
dryRun: parsed.options.dryRun,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return resetWorktree({
|
|
138
|
+
cwd,
|
|
139
|
+
prompts,
|
|
140
|
+
registryFile: stateLayout.registryFile,
|
|
141
|
+
dryRun: parsed.options.dryRun,
|
|
142
|
+
});
|
|
143
|
+
case "remove":
|
|
144
|
+
if (parsed.options.global) {
|
|
145
|
+
return removeGlobalBundle({
|
|
146
|
+
homeDir: options.homeDir ?? node_os_1.default.homedir(),
|
|
147
|
+
prompts,
|
|
148
|
+
registryFile: stateLayout.registryFile,
|
|
149
|
+
libraryDir: stateLayout.libraryDir,
|
|
150
|
+
bundle: parsed.options.bundle,
|
|
151
|
+
source: parsed.options.source,
|
|
152
|
+
includeItems: parsed.options.includeItems ?? [],
|
|
153
|
+
selectItems: parsed.options.selectItems ?? false,
|
|
154
|
+
dryRun: parsed.options.dryRun,
|
|
155
|
+
inferredBundleFromSource: parsed.options.inferredBundleFromSource,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
return removeBundle({
|
|
159
|
+
cwd,
|
|
160
|
+
prompts,
|
|
161
|
+
registryFile: stateLayout.registryFile,
|
|
162
|
+
libraryDir: stateLayout.libraryDir,
|
|
163
|
+
bundle: parsed.options.bundle,
|
|
164
|
+
source: parsed.options.source,
|
|
165
|
+
includeItems: parsed.options.includeItems ?? [],
|
|
166
|
+
selectItems: parsed.options.selectItems ?? false,
|
|
167
|
+
dryRun: parsed.options.dryRun,
|
|
168
|
+
inferredBundleFromSource: parsed.options.inferredBundleFromSource,
|
|
169
|
+
});
|
|
170
|
+
case "apply":
|
|
171
|
+
if (parsed.options.global) {
|
|
172
|
+
return applyGlobal({
|
|
173
|
+
homeDir: options.homeDir ?? node_os_1.default.homedir(),
|
|
174
|
+
prompts,
|
|
175
|
+
registryFile: stateLayout.registryFile,
|
|
176
|
+
libraryDir: stateLayout.libraryDir,
|
|
177
|
+
dryRun: parsed.options.dryRun,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return applyWorktree({
|
|
181
|
+
cwd,
|
|
182
|
+
prompts,
|
|
183
|
+
registryFile: stateLayout.registryFile,
|
|
184
|
+
libraryDir: stateLayout.libraryDir,
|
|
185
|
+
dryRun: parsed.options.dryRun,
|
|
186
|
+
});
|
|
187
|
+
default:
|
|
188
|
+
return assertUnreachable(parsed);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function shadowWorktree(options) {
|
|
192
|
+
const gitContext = requireGitContext(options.cwd, "shadow");
|
|
193
|
+
let registry = readRegistryWithGuidance(options.registryFile);
|
|
194
|
+
const worktreeState = registry.worktrees[gitContext.worktreeId];
|
|
195
|
+
const shadowedFiles = worktreeState?.shadowed_files ?? {};
|
|
196
|
+
const shadowedFilePaths = Object.keys(shadowedFiles);
|
|
197
|
+
if (shadowedFilePaths.length === 0) {
|
|
198
|
+
return "No tracked root-instruction shadows found in the current worktree";
|
|
199
|
+
}
|
|
200
|
+
const nextShadowedFiles = options.action === "suspend"
|
|
201
|
+
? suspendTrackedRootInstructionShadows({
|
|
202
|
+
repoRoot: gitContext.worktreeRoot,
|
|
203
|
+
shadowedFiles,
|
|
204
|
+
})
|
|
205
|
+
: refreshTrackedRootInstructionShadows({
|
|
206
|
+
repoRoot: gitContext.worktreeRoot,
|
|
207
|
+
shadowedFiles,
|
|
208
|
+
});
|
|
209
|
+
registry = (0, registry_1.upsertWorktreeState)(registry, gitContext.worktreeId, {
|
|
210
|
+
repo_fingerprint: worktreeState.repo_fingerprint,
|
|
211
|
+
path: gitContext.worktreeRoot,
|
|
212
|
+
materialized_state: worktreeState.materialized_state,
|
|
213
|
+
shadowed_files: nextShadowedFiles,
|
|
214
|
+
});
|
|
215
|
+
(0, registry_1.writeRegistryFile)(options.registryFile, registry);
|
|
216
|
+
const actionLabel = options.action === "suspend" ? "Suspended" : "Refreshed";
|
|
217
|
+
return `${actionLabel} tracked root-instruction shadows for ${shadowedFilePaths.sort().join(", ")}`;
|
|
218
|
+
}
|
|
219
|
+
function syncWorktree(options) {
|
|
220
|
+
const gitContext = requireGitContext(options.cwd, "sync");
|
|
221
|
+
let registry = readRegistryWithGuidance(options.registryFile);
|
|
222
|
+
const worktreeState = registry.worktrees[gitContext.worktreeId];
|
|
223
|
+
const shadowedFilePaths = Object.keys(worktreeState?.shadowed_files ?? {}).sort();
|
|
224
|
+
let currentShadowedFiles = { ...(worktreeState?.shadowed_files ?? {}) };
|
|
225
|
+
if (worktreeState && shadowedFilePaths.length > 0) {
|
|
226
|
+
currentShadowedFiles = suspendTrackedRootInstructionShadows({
|
|
227
|
+
repoRoot: gitContext.worktreeRoot,
|
|
228
|
+
shadowedFiles: currentShadowedFiles,
|
|
229
|
+
});
|
|
230
|
+
registry = writeShadowedFilesForWorktree({
|
|
231
|
+
registry,
|
|
232
|
+
registryFile: options.registryFile,
|
|
233
|
+
worktreeId: gitContext.worktreeId,
|
|
234
|
+
worktreeState,
|
|
235
|
+
shadowedFiles: currentShadowedFiles,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
let syncResult = null;
|
|
239
|
+
let syncError = null;
|
|
240
|
+
try {
|
|
241
|
+
syncResult = runGitPullWithFastForward(gitContext.worktreeRoot);
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
syncError = normalizeGitCommandError("sync the current branch with git pull --ff-only", error);
|
|
245
|
+
}
|
|
246
|
+
if (worktreeState && shadowedFilePaths.length > 0) {
|
|
247
|
+
try {
|
|
248
|
+
currentShadowedFiles = refreshTrackedRootInstructionShadowsAfterSync({
|
|
249
|
+
repoRoot: gitContext.worktreeRoot,
|
|
250
|
+
shadowedFiles: currentShadowedFiles,
|
|
251
|
+
});
|
|
252
|
+
writeShadowedFilesForWorktree({
|
|
253
|
+
registry,
|
|
254
|
+
registryFile: options.registryFile,
|
|
255
|
+
worktreeId: gitContext.worktreeId,
|
|
256
|
+
worktreeState,
|
|
257
|
+
shadowedFiles: currentShadowedFiles,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
if (syncError) {
|
|
262
|
+
throw new Error(`${syncError.message}\nSkul also failed to restore tracked root-instruction shadows: ${describeError(error)}`);
|
|
263
|
+
}
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (syncError) {
|
|
268
|
+
throw syncError;
|
|
269
|
+
}
|
|
270
|
+
return renderSyncWorktreeResult({
|
|
271
|
+
syncResult: syncResult,
|
|
272
|
+
shadowRefreshResult: buildShadowRefreshResult({
|
|
273
|
+
initialShadowedFilePaths: shadowedFilePaths,
|
|
274
|
+
currentShadowedFiles,
|
|
275
|
+
}),
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
function refreshTrackedRootInstructionShadowsAfterSync(options) {
|
|
279
|
+
const refreshableShadowedFiles = {};
|
|
280
|
+
for (const [filePath, shadowedFile] of Object.entries(options.shadowedFiles)) {
|
|
281
|
+
const inspection = (0, git_index_1.inspectRootInstructionShadowTarget)({
|
|
282
|
+
repoRoot: options.repoRoot,
|
|
283
|
+
filePath,
|
|
284
|
+
});
|
|
285
|
+
if (!inspection.headBlob) {
|
|
286
|
+
node_fs_1.default.rmSync(node_path_1.default.join(options.repoRoot, filePath), { force: true });
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
refreshableShadowedFiles[filePath] = shadowedFile;
|
|
290
|
+
}
|
|
291
|
+
if (Object.keys(refreshableShadowedFiles).length === 0) {
|
|
292
|
+
return {};
|
|
293
|
+
}
|
|
294
|
+
return refreshTrackedRootInstructionShadows({
|
|
295
|
+
repoRoot: options.repoRoot,
|
|
296
|
+
shadowedFiles: refreshableShadowedFiles,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
function writeShadowedFilesForWorktree(options) {
|
|
300
|
+
const nextRegistry = (0, registry_1.upsertWorktreeState)(options.registry, options.worktreeId, {
|
|
301
|
+
repo_fingerprint: options.worktreeState.repo_fingerprint,
|
|
302
|
+
path: options.worktreeState.path,
|
|
303
|
+
materialized_state: options.worktreeState.materialized_state,
|
|
304
|
+
shadowed_files: options.shadowedFiles,
|
|
305
|
+
});
|
|
306
|
+
(0, registry_1.writeRegistryFile)(options.registryFile, nextRegistry);
|
|
307
|
+
return nextRegistry;
|
|
308
|
+
}
|
|
309
|
+
function buildShadowRefreshResult(options) {
|
|
310
|
+
const refreshedFilePaths = Object.keys(options.currentShadowedFiles).sort();
|
|
311
|
+
return {
|
|
312
|
+
refreshedFilePaths,
|
|
313
|
+
retiredFilePaths: options.initialShadowedFilePaths
|
|
314
|
+
.filter((filePath) => !refreshedFilePaths.includes(filePath))
|
|
315
|
+
.sort(),
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
function runGitPullWithFastForward(repoRoot) {
|
|
319
|
+
const previousHead = runGitForOutput(repoRoot, ["rev-parse", "HEAD"]).trim();
|
|
320
|
+
runGitForOutput(repoRoot, ["pull", "--ff-only"]);
|
|
321
|
+
const currentHead = runGitForOutput(repoRoot, ["rev-parse", "HEAD"]).trim();
|
|
322
|
+
return {
|
|
323
|
+
previousHead,
|
|
324
|
+
currentHead,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
function runGitForOutput(repoRoot, args) {
|
|
328
|
+
return (0, node_child_process_1.execFileSync)("git", args, {
|
|
329
|
+
cwd: repoRoot,
|
|
330
|
+
encoding: "utf8",
|
|
331
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
function normalizeGitCommandError(action, error) {
|
|
335
|
+
return new Error(`Failed to ${action}: ${describeGitCommandFailure(error)}`);
|
|
336
|
+
}
|
|
337
|
+
function describeGitCommandFailure(error) {
|
|
338
|
+
if (error instanceof Error &&
|
|
339
|
+
"stderr" in error &&
|
|
340
|
+
typeof error.stderr === "string") {
|
|
341
|
+
const stderr = error.stderr.trim();
|
|
342
|
+
if (stderr.length > 0) {
|
|
343
|
+
return stderr;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return describeError(error);
|
|
347
|
+
}
|
|
348
|
+
function describeError(error) {
|
|
349
|
+
return error instanceof Error ? error.message : String(error);
|
|
350
|
+
}
|
|
351
|
+
function renderSyncWorktreeResult(options) {
|
|
352
|
+
const syncMessage = options.syncResult.previousHead === options.syncResult.currentHead
|
|
353
|
+
? "Git already up to date"
|
|
354
|
+
: `Synced git worktree ${options.syncResult.previousHead.slice(0, 7)} -> ${options.syncResult.currentHead.slice(0, 7)}`;
|
|
355
|
+
const detailMessages = [];
|
|
356
|
+
if (options.shadowRefreshResult.refreshedFilePaths.length > 0) {
|
|
357
|
+
detailMessages.push(`refreshed tracked root-instruction shadows for ${options.shadowRefreshResult.refreshedFilePaths.join(", ")}`);
|
|
358
|
+
}
|
|
359
|
+
if (options.shadowRefreshResult.retiredFilePaths.length > 0) {
|
|
360
|
+
detailMessages.push(`retired tracked root-instruction shadows for ${options.shadowRefreshResult.retiredFilePaths.join(", ")} because upstream no longer tracks them`);
|
|
361
|
+
}
|
|
362
|
+
if (detailMessages.length === 0) {
|
|
363
|
+
return syncMessage;
|
|
364
|
+
}
|
|
365
|
+
return `${syncMessage}; ${detailMessages.join("; ")}`;
|
|
366
|
+
}
|
|
367
|
+
function suspendTrackedRootInstructionShadows(options) {
|
|
368
|
+
const plans = Object.entries(options.shadowedFiles)
|
|
369
|
+
.sort(([leftPath], [rightPath]) => leftPath.localeCompare(rightPath))
|
|
370
|
+
.map(([filePath, shadowedFile]) => {
|
|
371
|
+
assertTrackedRootInstructionShadowSafetyForAction({
|
|
372
|
+
repoRoot: options.repoRoot,
|
|
373
|
+
filePath,
|
|
374
|
+
action: "suspend",
|
|
375
|
+
});
|
|
376
|
+
if (shadowedFile.skip_worktree) {
|
|
377
|
+
assertTrackedRootInstructionShadowPristine({
|
|
378
|
+
repoRoot: options.repoRoot,
|
|
379
|
+
filePath,
|
|
380
|
+
shadowedFile,
|
|
381
|
+
action: "suspend",
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
return { filePath, shadowedFile };
|
|
385
|
+
});
|
|
386
|
+
for (const plan of plans) {
|
|
387
|
+
restoreTrackedRootInstructionShadowTarget({
|
|
388
|
+
repoRoot: options.repoRoot,
|
|
389
|
+
filePath: plan.filePath,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
return Object.fromEntries(plans.map(({ filePath, shadowedFile }) => [
|
|
393
|
+
filePath,
|
|
394
|
+
{
|
|
395
|
+
...shadowedFile,
|
|
396
|
+
skip_worktree: false,
|
|
397
|
+
},
|
|
398
|
+
]));
|
|
399
|
+
}
|
|
400
|
+
function refreshTrackedRootInstructionShadows(options) {
|
|
401
|
+
const plans = Object.entries(options.shadowedFiles)
|
|
402
|
+
.sort(([leftPath], [rightPath]) => leftPath.localeCompare(rightPath))
|
|
403
|
+
.map(([filePath, shadowedFile]) => {
|
|
404
|
+
assertTrackedRootInstructionShadowSafetyForAction({
|
|
405
|
+
repoRoot: options.repoRoot,
|
|
406
|
+
filePath,
|
|
407
|
+
action: "refresh",
|
|
408
|
+
});
|
|
409
|
+
if (shadowedFile.skip_worktree) {
|
|
410
|
+
assertTrackedRootInstructionShadowPristine({
|
|
411
|
+
repoRoot: options.repoRoot,
|
|
412
|
+
filePath,
|
|
413
|
+
shadowedFile,
|
|
414
|
+
action: "refresh",
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
const headBlob = requireTrackedRootInstructionHeadBlob({
|
|
418
|
+
repoRoot: options.repoRoot,
|
|
419
|
+
filePath,
|
|
420
|
+
action: "refresh",
|
|
421
|
+
});
|
|
422
|
+
const render = (0, root_instruction_render_1.renderTrackedRootInstructionShadow)({
|
|
423
|
+
baseContent: headBlob.content,
|
|
424
|
+
overlayContent: shadowedFile.overlay,
|
|
425
|
+
bundleName: shadowedFile.bundle,
|
|
426
|
+
toolName: shadowedFile.tool,
|
|
427
|
+
strategy: shadowedFile.strategy,
|
|
428
|
+
allowReplace: true,
|
|
429
|
+
});
|
|
430
|
+
return { filePath, shadowedFile, headBlob, render };
|
|
431
|
+
});
|
|
432
|
+
for (const plan of plans) {
|
|
433
|
+
const targetPath = node_path_1.default.join(options.repoRoot, plan.filePath);
|
|
434
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(targetPath), { recursive: true });
|
|
435
|
+
node_fs_1.default.writeFileSync(targetPath, plan.render.rendered);
|
|
436
|
+
(0, git_index_1.setGitSkipWorktree)({ repoRoot: options.repoRoot, filePath: plan.filePath });
|
|
437
|
+
}
|
|
438
|
+
return Object.fromEntries(plans.map(({ filePath, shadowedFile, headBlob, render }) => [
|
|
439
|
+
filePath,
|
|
440
|
+
{
|
|
441
|
+
...shadowedFile,
|
|
442
|
+
base_blob: headBlob.objectId,
|
|
443
|
+
overlay: shadowedFile.overlay,
|
|
444
|
+
overlay_fingerprint: render.overlayFingerprint,
|
|
445
|
+
rendered_fingerprint: render.renderedFingerprint,
|
|
446
|
+
skip_worktree: true,
|
|
447
|
+
},
|
|
448
|
+
]));
|
|
449
|
+
}
|
|
450
|
+
function requireTrackedRootInstructionHeadBlob(options) {
|
|
451
|
+
const inspection = (0, git_index_1.inspectRootInstructionShadowTarget)({
|
|
452
|
+
repoRoot: options.repoRoot,
|
|
453
|
+
filePath: options.filePath,
|
|
454
|
+
});
|
|
455
|
+
if (inspection.headBlob) {
|
|
456
|
+
return inspection.headBlob;
|
|
457
|
+
}
|
|
458
|
+
throw new Error(`Cannot ${options.action} tracked root-instruction shadow for ${options.filePath} because the target does not have HEAD content`);
|
|
459
|
+
}
|
|
460
|
+
function assertTrackedRootInstructionShadowPristine(options) {
|
|
461
|
+
const targetPath = node_path_1.default.join(options.repoRoot, options.filePath);
|
|
462
|
+
if (!node_fs_1.default.existsSync(targetPath) || !node_fs_1.default.statSync(targetPath).isFile()) {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
if (fingerprintFile(targetPath) === options.shadowedFile.rendered_fingerprint) {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
throw new Error(`Cannot ${options.action} tracked root-instruction shadow for ${options.filePath} because the shadow file has local manual edits`);
|
|
469
|
+
}
|
|
470
|
+
function createDefaultPromptClient(libraryDir) {
|
|
471
|
+
if ((0, cli_1.isHeadlessMode)()) {
|
|
472
|
+
return (0, cli_1.createHeadlessPromptClient)();
|
|
473
|
+
}
|
|
474
|
+
const promptClient = (0, cli_1.createPromptClientForSelections)([]);
|
|
475
|
+
return {
|
|
476
|
+
async selectBundle(source, requestedTools) {
|
|
477
|
+
const availableBundles = (0, bundle_discovery_1.listCachedBundles)({ libraryDir })
|
|
478
|
+
.filter((bundle) => isBundleSelectionCandidate({ bundle, source, requestedTools }))
|
|
479
|
+
.map((bundle) => buildBundleSelection(bundle.source, bundle.bundle, libraryDir))
|
|
480
|
+
.sort(compareBundleSelections);
|
|
481
|
+
if (availableBundles.length === 0 && requestedTools?.length) {
|
|
482
|
+
throw new Error(`No bundles cached${source ? ` for ${source}` : ""} support selected agent(s): ${requestedTools.join(", ")}`);
|
|
483
|
+
}
|
|
484
|
+
return (0, cli_1.createPromptClientForSelections)(availableBundles).selectBundle(source);
|
|
485
|
+
},
|
|
486
|
+
selectBundleItems: promptClient.selectBundleItems,
|
|
487
|
+
selectBundleItemChoices: promptClient.selectBundleItemChoices,
|
|
488
|
+
selectBundleFromSelections: promptClient.selectBundleFromSelections,
|
|
489
|
+
selectAgents: promptClient.selectAgents,
|
|
490
|
+
resolveFileConflict: promptClient.resolveFileConflict,
|
|
491
|
+
confirmManagedFileRemoval: promptClient.confirmManagedFileRemoval,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
function isBundleSelectionCandidate(options) {
|
|
495
|
+
if (options.source !== undefined &&
|
|
496
|
+
options.bundle.source !== options.source) {
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
499
|
+
if (!options.requestedTools?.length) {
|
|
500
|
+
return true;
|
|
501
|
+
}
|
|
502
|
+
const availableTools = Object.keys(options.bundle.manifest.tools);
|
|
503
|
+
return options.requestedTools.every((toolName) => availableTools.includes(toolName));
|
|
504
|
+
}
|
|
505
|
+
function buildBundleSelection(source, bundle, libraryDir) {
|
|
506
|
+
const revision = (0, bundle_fetch_1.readCachedSourceRevision)({ source, libraryDir });
|
|
507
|
+
const protocol = revision.remoteUrl
|
|
508
|
+
? (0, bundle_discovery_1.detectSourceProtocol)(revision.remoteUrl)
|
|
509
|
+
: "https";
|
|
510
|
+
return {
|
|
511
|
+
bundle,
|
|
512
|
+
source,
|
|
513
|
+
protocol,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
function compareBundleSelections(left, right) {
|
|
517
|
+
const bundleNameComparison = left.bundle.localeCompare(right.bundle);
|
|
518
|
+
if (bundleNameComparison !== 0) {
|
|
519
|
+
return bundleNameComparison;
|
|
520
|
+
}
|
|
521
|
+
return (left.source ?? "").localeCompare(right.source ?? "");
|
|
522
|
+
}
|
|
523
|
+
function renderBundleList(options) {
|
|
524
|
+
const bundles = (0, bundle_discovery_1.listCachedBundles)({ libraryDir: options.libraryDir }).filter((bundle) => options.source === undefined || bundle.source === options.source);
|
|
525
|
+
if (options.json) {
|
|
526
|
+
return JSON.stringify({
|
|
527
|
+
bundles: bundles.map((bundle) => ({
|
|
528
|
+
name: bundle.bundle,
|
|
529
|
+
source: bundle.source,
|
|
530
|
+
tools: Object.keys(bundle.manifest.tools),
|
|
531
|
+
})),
|
|
532
|
+
}, null, 2);
|
|
533
|
+
}
|
|
534
|
+
if (bundles.length === 0) {
|
|
535
|
+
return [
|
|
536
|
+
pc.bold("Available Bundles"),
|
|
537
|
+
"",
|
|
538
|
+
options.source !== undefined
|
|
539
|
+
? `No cached bundles found for ${options.source}.`
|
|
540
|
+
: "No cached bundles found.",
|
|
541
|
+
"",
|
|
542
|
+
options.source === undefined
|
|
543
|
+
? pc.dim("Add one with: skul add github.com/<owner>/<repo> <bundle-name>")
|
|
544
|
+
: pc.dim(`Cache one with: skul add ${options.source} <bundle-name>`),
|
|
545
|
+
].join("\n");
|
|
546
|
+
}
|
|
547
|
+
return [
|
|
548
|
+
pc.bold("Available Bundles"),
|
|
549
|
+
"",
|
|
550
|
+
...bundles.map((bundle) => {
|
|
551
|
+
const tools = Object.keys(bundle.manifest.tools).join(", ");
|
|
552
|
+
return `${pc.cyan(bundle.bundle)} [${bundle.source}] ${pc.dim(`(${tools})`)}`;
|
|
553
|
+
}),
|
|
554
|
+
].join("\n");
|
|
555
|
+
}
|
|
556
|
+
function renderStatus(options) {
|
|
557
|
+
const gitContext = requireGitContext(options.cwd, "status");
|
|
558
|
+
const registry = readRegistryWithGuidance(options.registryFile);
|
|
559
|
+
const repoState = registry.repos[gitContext.repoFingerprint];
|
|
560
|
+
const worktreeState = registry.worktrees[gitContext.worktreeId];
|
|
561
|
+
const hasMaterializedBundles = worktreeState
|
|
562
|
+
? worktreeHasMaterializedBundles(worktreeState.materialized_state)
|
|
563
|
+
: false;
|
|
564
|
+
const shadowedInstructionStatuses = collectShadowedInstructionStatuses({
|
|
565
|
+
repoRoot: gitContext.worktreeRoot,
|
|
566
|
+
shadowedFiles: worktreeState?.shadowed_files ?? {},
|
|
567
|
+
});
|
|
568
|
+
if (options.json) {
|
|
569
|
+
const desiredState = repoState?.desired_state ?? [];
|
|
570
|
+
const worktreeData = worktreeState
|
|
571
|
+
? {
|
|
572
|
+
path: gitContext.worktreeRoot,
|
|
573
|
+
materialized: hasMaterializedBundles,
|
|
574
|
+
bundles: Object.fromEntries(Object.entries(worktreeState.materialized_state.bundles).map(([bundleName, bundleState]) => [
|
|
575
|
+
bundleName,
|
|
576
|
+
{
|
|
577
|
+
tools: Object.fromEntries(Object.entries(bundleState.tools).map(([toolName, toolState]) => [
|
|
578
|
+
toolName,
|
|
579
|
+
{ files: toolState.files },
|
|
580
|
+
])),
|
|
581
|
+
},
|
|
582
|
+
])),
|
|
583
|
+
shadowed_files: buildShadowedFilesJson(shadowedInstructionStatuses),
|
|
584
|
+
git_exclude_configured: (0, git_exclude_1.hasSkulExcludeBlock)({
|
|
585
|
+
gitDir: gitContext.gitDir,
|
|
586
|
+
}),
|
|
587
|
+
}
|
|
588
|
+
: {
|
|
589
|
+
path: gitContext.worktreeRoot,
|
|
590
|
+
materialized: false,
|
|
591
|
+
bundles: {},
|
|
592
|
+
shadowed_files: buildShadowedFilesJson(shadowedInstructionStatuses),
|
|
593
|
+
git_exclude_configured: (0, git_exclude_1.hasSkulExcludeBlock)({
|
|
594
|
+
gitDir: gitContext.gitDir,
|
|
595
|
+
}),
|
|
596
|
+
};
|
|
597
|
+
const suggestedAction = !hasMaterializedBundles && repoState && repoState.desired_state.length > 0
|
|
598
|
+
? "skul apply"
|
|
599
|
+
: null;
|
|
600
|
+
return JSON.stringify({
|
|
601
|
+
repo: { desired_state: desiredState },
|
|
602
|
+
worktree: worktreeData,
|
|
603
|
+
...(suggestedAction !== null
|
|
604
|
+
? { suggested_action: suggestedAction }
|
|
605
|
+
: {}),
|
|
606
|
+
}, null, 2);
|
|
607
|
+
}
|
|
608
|
+
const lines = [pc.bold("Repository Desired State")];
|
|
609
|
+
if (repoState && repoState.desired_state.length > 0) {
|
|
610
|
+
for (const entry of repoState.desired_state) {
|
|
611
|
+
const toolSuffix = entry.tools ? ` (${entry.tools.join(", ")})` : "";
|
|
612
|
+
lines.push(`Bundle: ${pc.cyan(entry.bundle)}${toolSuffix}`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
else {
|
|
616
|
+
lines.push(pc.dim("Configured: no"));
|
|
617
|
+
lines.push(pc.dim('Run "skul add <bundle>" to get started'));
|
|
618
|
+
}
|
|
619
|
+
lines.push("", pc.bold("Current Worktree"), `Path: ${gitContext.worktreeRoot}`);
|
|
620
|
+
if (!hasMaterializedBundles) {
|
|
621
|
+
lines.push(pc.dim("Materialized: no"));
|
|
622
|
+
appendShadowedInstructionLines(lines, shadowedInstructionStatuses);
|
|
623
|
+
if (repoState && repoState.desired_state.length > 0) {
|
|
624
|
+
lines.push(pc.yellow('Suggested Action: run "skul apply"'));
|
|
625
|
+
}
|
|
626
|
+
return lines.join("\n");
|
|
627
|
+
}
|
|
628
|
+
lines.push(pc.green("Materialized: yes"), "", "Files:");
|
|
629
|
+
for (const [bundleName, bundleState] of Object.entries(worktreeState.materialized_state.bundles)) {
|
|
630
|
+
lines.push(` Bundle: ${pc.cyan(bundleName)}`);
|
|
631
|
+
for (const [toolName, toolState] of Object.entries(bundleState.tools)) {
|
|
632
|
+
lines.push(` Tool: ${toolName}`);
|
|
633
|
+
for (const file of toolState.files) {
|
|
634
|
+
lines.push(` ${file}`);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
appendShadowedInstructionLines(lines, shadowedInstructionStatuses);
|
|
639
|
+
lines.push("", pc.bold("Git Exclude:"));
|
|
640
|
+
lines.push(` ${(0, git_exclude_1.hasSkulExcludeBlock)({ gitDir: gitContext.gitDir }) ? pc.green("configured") : pc.yellow("missing")}`);
|
|
641
|
+
return lines.join("\n");
|
|
642
|
+
}
|
|
643
|
+
function collectShadowedInstructionStatuses(options) {
|
|
644
|
+
return Object.entries(options.shadowedFiles)
|
|
645
|
+
.sort(([leftPath], [rightPath]) => leftPath.localeCompare(rightPath))
|
|
646
|
+
.map(([filePath, shadowedFile]) => collectShadowedInstructionStatus({
|
|
647
|
+
repoRoot: options.repoRoot,
|
|
648
|
+
filePath,
|
|
649
|
+
shadowedFile,
|
|
650
|
+
}));
|
|
651
|
+
}
|
|
652
|
+
function collectShadowedInstructionStatus(options) {
|
|
653
|
+
const inspection = (0, git_index_1.inspectRootInstructionShadowTarget)({
|
|
654
|
+
repoRoot: options.repoRoot,
|
|
655
|
+
filePath: options.filePath,
|
|
656
|
+
});
|
|
657
|
+
const targetPath = node_path_1.default.join(options.repoRoot, options.filePath);
|
|
658
|
+
const currentContent = readStatusTargetFile(targetPath);
|
|
659
|
+
const overlay = extractTrackedRootInstructionOverlay({
|
|
660
|
+
content: currentContent,
|
|
661
|
+
bundleName: options.shadowedFile.bundle,
|
|
662
|
+
strategy: options.shadowedFile.strategy,
|
|
663
|
+
});
|
|
664
|
+
return {
|
|
665
|
+
path: options.filePath,
|
|
666
|
+
...options.shadowedFile,
|
|
667
|
+
active: overlay !== null,
|
|
668
|
+
base_fresh: inspection.headBlob?.objectId === options.shadowedFile.base_blob,
|
|
669
|
+
overlay_fresh: overlay !== null &&
|
|
670
|
+
fingerprintTextContent(overlay) ===
|
|
671
|
+
options.shadowedFile.overlay_fingerprint,
|
|
672
|
+
skip_worktree_active: inspection.indexFlags.includes("S"),
|
|
673
|
+
manual_edit_suspected: currentContent === null ||
|
|
674
|
+
fingerprintTextContent(currentContent) !==
|
|
675
|
+
options.shadowedFile.rendered_fingerprint,
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
function buildShadowedFilesJson(shadowedInstructionStatuses) {
|
|
679
|
+
return Object.fromEntries(shadowedInstructionStatuses.map((status) => [
|
|
680
|
+
status.path,
|
|
681
|
+
{
|
|
682
|
+
tool: status.tool,
|
|
683
|
+
bundle: status.bundle,
|
|
684
|
+
strategy: status.strategy,
|
|
685
|
+
base_blob: status.base_blob,
|
|
686
|
+
overlay_fingerprint: status.overlay_fingerprint,
|
|
687
|
+
rendered_fingerprint: status.rendered_fingerprint,
|
|
688
|
+
skip_worktree: status.skip_worktree,
|
|
689
|
+
active: status.active,
|
|
690
|
+
base_fresh: status.base_fresh,
|
|
691
|
+
overlay_fresh: status.overlay_fresh,
|
|
692
|
+
skip_worktree_active: status.skip_worktree_active,
|
|
693
|
+
manual_edit_suspected: status.manual_edit_suspected,
|
|
694
|
+
},
|
|
695
|
+
]));
|
|
696
|
+
}
|
|
697
|
+
function appendShadowedInstructionLines(lines, shadowedInstructionStatuses) {
|
|
698
|
+
if (shadowedInstructionStatuses.length === 0) {
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
lines.push("", pc.bold("Shadowed Instructions"));
|
|
702
|
+
for (const status of shadowedInstructionStatuses) {
|
|
703
|
+
lines.push(` ${status.path}`);
|
|
704
|
+
lines.push(` Bundle: ${pc.cyan(status.bundle)}`);
|
|
705
|
+
lines.push(` Tool: ${status.tool}`);
|
|
706
|
+
lines.push(` Strategy: ${status.strategy}`);
|
|
707
|
+
lines.push(` Active: ${status.active ? pc.green("yes") : pc.yellow("no")}`);
|
|
708
|
+
lines.push(` Base: ${status.base_fresh ? pc.green("current") : pc.yellow("stale")}`);
|
|
709
|
+
lines.push(` Overlay: ${status.overlay_fresh ? pc.green("current") : pc.yellow("stale")}`);
|
|
710
|
+
lines.push(` Skip-worktree: ${status.skip_worktree_active ? pc.green("set") : pc.yellow("missing")}`);
|
|
711
|
+
lines.push(` Manual edits: ${status.manual_edit_suspected ? pc.yellow("suspected") : pc.green("no")}`);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
function readStatusTargetFile(filePath) {
|
|
715
|
+
if (!node_fs_1.default.existsSync(filePath) || !node_fs_1.default.lstatSync(filePath).isFile()) {
|
|
716
|
+
return null;
|
|
717
|
+
}
|
|
718
|
+
return node_fs_1.default.readFileSync(filePath, "utf8");
|
|
719
|
+
}
|
|
720
|
+
function extractTrackedRootInstructionOverlay(options) {
|
|
721
|
+
if (options.content === null) {
|
|
722
|
+
return null;
|
|
723
|
+
}
|
|
724
|
+
if (options.strategy === "replace") {
|
|
725
|
+
return normalizeTrackedRootInstructionStatusContent(options.content);
|
|
726
|
+
}
|
|
727
|
+
const startMarker = `<!-- SKUL SHADOW START bundle=${options.bundleName} -->`;
|
|
728
|
+
const endMarker = "<!-- SKUL SHADOW END -->";
|
|
729
|
+
const startIndex = options.content.indexOf(startMarker);
|
|
730
|
+
if (startIndex < 0) {
|
|
731
|
+
return null;
|
|
732
|
+
}
|
|
733
|
+
const endIndex = options.content.indexOf(endMarker, startIndex);
|
|
734
|
+
if (endIndex < 0) {
|
|
735
|
+
return null;
|
|
736
|
+
}
|
|
737
|
+
return normalizeTrackedRootInstructionStatusContent(options.content.slice(startIndex, endIndex + endMarker.length));
|
|
738
|
+
}
|
|
739
|
+
function normalizeTrackedRootInstructionStatusContent(content) {
|
|
740
|
+
return content.replace(/\s+$/, "");
|
|
741
|
+
}
|
|
742
|
+
function fingerprintTextContent(content) {
|
|
743
|
+
return (0, node_crypto_1.createHash)("sha256").update(content).digest("hex");
|
|
744
|
+
}
|
|
745
|
+
function worktreeHasMaterializedBundles(materializedState) {
|
|
746
|
+
return Object.keys(materializedState.bundles).length > 0;
|
|
747
|
+
}
|
|
748
|
+
function renderUpdateCheck(options) {
|
|
749
|
+
const gitContext = requireGitContext(options.cwd, "check");
|
|
750
|
+
const registry = readRegistryWithGuidance(options.registryFile);
|
|
751
|
+
const repoState = registry.repos[gitContext.repoFingerprint];
|
|
752
|
+
const worktreeState = registry.worktrees[gitContext.worktreeId];
|
|
753
|
+
const entries = selectDesiredEntries(repoState?.desired_state ?? [], options.bundle, "check");
|
|
754
|
+
if (entries.length === 0) {
|
|
755
|
+
return `No bundles configured for this repository. Run "skul add <bundle>" to add one`;
|
|
756
|
+
}
|
|
757
|
+
const results = entries.map((entry) => {
|
|
758
|
+
const materializedBundle = worktreeState?.materialized_state.bundles[entry.bundle];
|
|
759
|
+
if (!entry.source) {
|
|
760
|
+
return {
|
|
761
|
+
bundle: entry.bundle,
|
|
762
|
+
status: "local-only",
|
|
763
|
+
source: null,
|
|
764
|
+
current_commit: null,
|
|
765
|
+
latest_commit: null,
|
|
766
|
+
worktree_commit: materializedBundle?.resolved_commit ?? null,
|
|
767
|
+
worktree_stale: false,
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
const remoteStatus = (0, bundle_fetch_1.inspectRemoteSource)({
|
|
771
|
+
source: entry.source,
|
|
772
|
+
libraryDir: options.libraryDir,
|
|
773
|
+
protocol: entry.protocol,
|
|
774
|
+
ref: entry.ref,
|
|
775
|
+
});
|
|
776
|
+
const desiredCommit = entry.resolved_commit ?? remoteStatus.currentCommit ?? null;
|
|
777
|
+
const worktreeCommit = materializedBundle?.resolved_commit ?? null;
|
|
778
|
+
const isPinned = remoteStatus.refKind === "commit";
|
|
779
|
+
const status = isPinned
|
|
780
|
+
? "pinned"
|
|
781
|
+
: desiredCommit !== null && desiredCommit === remoteStatus.remoteCommit
|
|
782
|
+
? "up-to-date"
|
|
783
|
+
: "update-available";
|
|
784
|
+
const worktreeStale = worktreeCommit !== null &&
|
|
785
|
+
desiredCommit !== null &&
|
|
786
|
+
worktreeCommit !== desiredCommit;
|
|
787
|
+
return {
|
|
788
|
+
bundle: entry.bundle,
|
|
789
|
+
status,
|
|
790
|
+
source: entry.source,
|
|
791
|
+
current_commit: desiredCommit,
|
|
792
|
+
latest_commit: isPinned ? desiredCommit : remoteStatus.remoteCommit,
|
|
793
|
+
worktree_commit: worktreeCommit,
|
|
794
|
+
worktree_stale: worktreeStale,
|
|
795
|
+
};
|
|
796
|
+
});
|
|
797
|
+
if (options.json) {
|
|
798
|
+
return JSON.stringify({ bundles: results }, null, 2);
|
|
799
|
+
}
|
|
800
|
+
const lines = results.map((result) => {
|
|
801
|
+
if (result.status === "local-only") {
|
|
802
|
+
return `${pc.cyan(result.bundle)}: local-only (no remote source to check)`;
|
|
803
|
+
}
|
|
804
|
+
const updateSuffix = result.status === "update-available" &&
|
|
805
|
+
result.current_commit &&
|
|
806
|
+
result.latest_commit
|
|
807
|
+
? ` ${shortCommit(result.current_commit)} -> ${shortCommit(result.latest_commit)}`
|
|
808
|
+
: "";
|
|
809
|
+
const staleSuffix = result.worktree_stale ? " (worktree stale)" : "";
|
|
810
|
+
return `${pc.cyan(result.bundle)}: ${result.status}${updateSuffix}${staleSuffix}`;
|
|
811
|
+
});
|
|
812
|
+
const hasUpdates = results.some((r) => r.status === "update-available");
|
|
813
|
+
if (hasUpdates) {
|
|
814
|
+
lines.push("", pc.dim('Run "skul update" to apply available updates'));
|
|
815
|
+
}
|
|
816
|
+
return lines.join("\n");
|
|
817
|
+
}
|
|
818
|
+
async function updateBundles(options) {
|
|
819
|
+
const gitContext = requireGitContext(options.cwd, "update");
|
|
820
|
+
let registry = readRegistryWithGuidance(options.registryFile);
|
|
821
|
+
const repoState = registry.repos[gitContext.repoFingerprint];
|
|
822
|
+
const worktreeState = registry.worktrees[gitContext.worktreeId];
|
|
823
|
+
const entries = selectDesiredEntries(repoState?.desired_state ?? [], options.bundle, "update");
|
|
824
|
+
if (entries.length === 0) {
|
|
825
|
+
return `No bundles configured for this repository. Run "skul add <bundle>" to add one`;
|
|
826
|
+
}
|
|
827
|
+
const skippedLocalOnly = [];
|
|
828
|
+
const updatePlans = entries.flatMap((entry) => {
|
|
829
|
+
if (!entry.source) {
|
|
830
|
+
skippedLocalOnly.push(entry.bundle);
|
|
831
|
+
return [];
|
|
832
|
+
}
|
|
833
|
+
const remoteStatus = (0, bundle_fetch_1.inspectRemoteSource)({
|
|
834
|
+
source: entry.source,
|
|
835
|
+
libraryDir: options.libraryDir,
|
|
836
|
+
protocol: entry.protocol,
|
|
837
|
+
ref: entry.ref,
|
|
838
|
+
});
|
|
839
|
+
const currentCommit = entry.resolved_commit ?? remoteStatus.currentCommit;
|
|
840
|
+
if ((currentCommit !== undefined &&
|
|
841
|
+
currentCommit === remoteStatus.remoteCommit) ||
|
|
842
|
+
remoteStatus.refKind === "commit") {
|
|
843
|
+
return [];
|
|
844
|
+
}
|
|
845
|
+
return [
|
|
846
|
+
{
|
|
847
|
+
entry,
|
|
848
|
+
currentCommit,
|
|
849
|
+
remoteStatus,
|
|
850
|
+
},
|
|
851
|
+
];
|
|
852
|
+
});
|
|
853
|
+
const localOnlyNote = skippedLocalOnly.length > 0
|
|
854
|
+
? `Skipped (local-only): ${skippedLocalOnly.join(", ")}`
|
|
855
|
+
: "";
|
|
856
|
+
if (updatePlans.length === 0) {
|
|
857
|
+
if (skippedLocalOnly.length === entries.length) {
|
|
858
|
+
return `No remote-backed bundles to update (${skippedLocalOnly.join(", ")} ${skippedLocalOnly.length === 1 ? "is" : "are"} local-only)`;
|
|
859
|
+
}
|
|
860
|
+
return [localOnlyNote, "All selected bundles are already up to date"]
|
|
861
|
+
.filter(Boolean)
|
|
862
|
+
.join("\n");
|
|
863
|
+
}
|
|
864
|
+
if (options.dryRun) {
|
|
865
|
+
const dryLines = updatePlans.map(({ entry, currentCommit, remoteStatus }) => `${pc.yellow("DRY RUN:")} Would update ${entry.bundle}${formatCommitTransition(currentCommit, remoteStatus.remoteCommit)}`);
|
|
866
|
+
return [localOnlyNote, ...dryLines].filter(Boolean).join("\n");
|
|
867
|
+
}
|
|
868
|
+
const existingWorktreeState = registry.worktrees[gitContext.worktreeId]?.materialized_state;
|
|
869
|
+
let currentBundles = {
|
|
870
|
+
...(existingWorktreeState?.bundles ?? {}),
|
|
871
|
+
};
|
|
872
|
+
let currentShadowedFiles = { ...(worktreeState?.shadowed_files ?? {}) };
|
|
873
|
+
const nextDesiredState = [...(repoState?.desired_state ?? [])];
|
|
874
|
+
const outputLines = [];
|
|
875
|
+
let rootInstructionBaseContents = worktreeState?.materialized_state.root_instruction_base_contents;
|
|
876
|
+
for (const { entry, currentCommit, remoteStatus } of updatePlans) {
|
|
877
|
+
const existingBundleState = currentBundles[entry.bundle];
|
|
878
|
+
const toolsToRefresh = getToolsToRefresh(entry, existingBundleState);
|
|
879
|
+
const bundleStateToReplace = existingBundleState && toolsToRefresh && toolsToRefresh.length > 0
|
|
880
|
+
? {
|
|
881
|
+
...existingBundleState,
|
|
882
|
+
tools: Object.fromEntries(Object.entries(existingBundleState.tools).filter(([toolName]) => toolsToRefresh.includes(toolName))),
|
|
883
|
+
}
|
|
884
|
+
: existingBundleState;
|
|
885
|
+
const initialRevision = (0, bundle_fetch_1.readCachedSourceRevision)({
|
|
886
|
+
source: entry.source,
|
|
887
|
+
libraryDir: options.libraryDir,
|
|
888
|
+
protocol: entry.protocol,
|
|
889
|
+
});
|
|
890
|
+
try {
|
|
891
|
+
const refreshed = (0, bundle_fetch_1.updateCachedRemoteSource)({
|
|
892
|
+
source: entry.source,
|
|
893
|
+
libraryDir: options.libraryDir,
|
|
894
|
+
protocol: entry.protocol,
|
|
895
|
+
ref: entry.ref,
|
|
896
|
+
});
|
|
897
|
+
const cachedBundle = findCachedBundleWithGuidance({
|
|
898
|
+
libraryDir: options.libraryDir,
|
|
899
|
+
bundle: entry.bundle,
|
|
900
|
+
source: entry.source,
|
|
901
|
+
});
|
|
902
|
+
const plannedWriteTargets = (0, bundle_materialization_1.previewMaterializeBundleWriteTargets)({
|
|
903
|
+
repoRoot: gitContext.worktreeRoot,
|
|
904
|
+
bundleDir: node_path_1.default.dirname(cachedBundle.manifestFile),
|
|
905
|
+
manifest: cachedBundle.manifest,
|
|
906
|
+
tools: toolsToRefresh,
|
|
907
|
+
itemSelectors: entry.items,
|
|
908
|
+
disableModelInvocation: entry.disable_model_invocation,
|
|
909
|
+
});
|
|
910
|
+
const plannedRootInstructionTargets = new Set(plannedWriteTargets.filter((filePath) => (0, root_instruction_render_1.isRootInstructionPath)(filePath)));
|
|
911
|
+
const trackedRootInstructionShadowPlan = planTrackedRootInstructionShadows({
|
|
912
|
+
repoRoot: gitContext.worktreeRoot,
|
|
913
|
+
bundleDir: node_path_1.default.dirname(cachedBundle.manifestFile),
|
|
914
|
+
manifest: cachedBundle.manifest,
|
|
915
|
+
toolNames: selectTrackedRootInstructionShadowToolNames({
|
|
916
|
+
existingBundleState,
|
|
917
|
+
nextToolNames: toolsToRefresh ??
|
|
918
|
+
Object.keys(cachedBundle.manifest.tools),
|
|
919
|
+
}),
|
|
920
|
+
itemSelectors: entry.items,
|
|
921
|
+
targetPaths: plannedRootInstructionTargets,
|
|
922
|
+
bundleName: entry.bundle,
|
|
923
|
+
bundleSource: entry.source,
|
|
924
|
+
existingShadowedFiles: currentShadowedFiles,
|
|
925
|
+
materializedBundles: currentBundles,
|
|
926
|
+
});
|
|
927
|
+
if (existingBundleState) {
|
|
928
|
+
const replacementAllowed = await confirmManagedFileRemovals(gitContext.worktreeRoot, excludeShadowedTrackedRootInstructionTargets(flattenBundleState(bundleStateToReplace), trackedRootInstructionShadowPlan.deferredMaterializationTargets), options.prompts, "replace");
|
|
929
|
+
if (!replacementAllowed) {
|
|
930
|
+
throw new Error("Replacement aborted because a modified managed file was kept");
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
assertTrackedRootInstructionShadowPlanCanApply({
|
|
934
|
+
repoRoot: gitContext.worktreeRoot,
|
|
935
|
+
bundleName: entry.bundle,
|
|
936
|
+
existingShadowedFiles: currentShadowedFiles,
|
|
937
|
+
plan: trackedRootInstructionShadowPlan,
|
|
938
|
+
});
|
|
939
|
+
rootInstructionBaseContents = (0, root_instruction_state_1.captureRootInstructionBaseContents)({
|
|
940
|
+
repoRoot: gitContext.worktreeRoot,
|
|
941
|
+
targetPaths: trackedRootInstructionShadowPlan.untrackedTargetPaths,
|
|
942
|
+
existingBaseContents: rootInstructionBaseContents,
|
|
943
|
+
managedTargetPaths: (0, root_instruction_state_1.collectManagedRootInstructionTargets)(currentBundles),
|
|
944
|
+
});
|
|
945
|
+
(0, root_instruction_state_1.assertManagedRootInstructionSyncSourcesCached)({
|
|
946
|
+
desiredState: nextDesiredState,
|
|
947
|
+
materializedBundles: currentBundles,
|
|
948
|
+
targetPaths: trackedRootInstructionShadowPlan.untrackedTargetPaths,
|
|
949
|
+
resolveCachedBundle: (entry) => resolveDesiredCachedBundle(options.libraryDir, entry),
|
|
950
|
+
});
|
|
951
|
+
if (existingBundleState) {
|
|
952
|
+
assertTrackedRootInstructionShadowSafetyForPaths({
|
|
953
|
+
repoRoot: gitContext.worktreeRoot,
|
|
954
|
+
operation: "refresh",
|
|
955
|
+
filePaths: plannedWriteTargets,
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
const desiredIndex = nextDesiredState.findIndex((candidate) => candidate.bundle === entry.bundle);
|
|
959
|
+
nextDesiredState[desiredIndex] = {
|
|
960
|
+
...nextDesiredState[desiredIndex],
|
|
961
|
+
...(refreshed.resolvedRef !== undefined
|
|
962
|
+
? { resolved_ref: refreshed.resolvedRef }
|
|
963
|
+
: {}),
|
|
964
|
+
resolved_commit: refreshed.currentCommit,
|
|
965
|
+
};
|
|
966
|
+
if (bundleStateToReplace) {
|
|
967
|
+
removeManagedPaths(gitContext.worktreeRoot, excludeShadowedTrackedRootInstructionTargets(flattenBundleState(bundleStateToReplace), trackedRootInstructionShadowPlan.deferredMaterializationTargets));
|
|
968
|
+
const materializedResult = await (0, bundle_materialization_1.materializeBundle)({
|
|
969
|
+
repoRoot: gitContext.worktreeRoot,
|
|
970
|
+
bundleDir: node_path_1.default.dirname(cachedBundle.manifestFile),
|
|
971
|
+
manifest: cachedBundle.manifest,
|
|
972
|
+
tools: toolsToRefresh,
|
|
973
|
+
itemSelectors: entry.items,
|
|
974
|
+
bundleName: entry.bundle,
|
|
975
|
+
bundleSource: entry.source,
|
|
976
|
+
assertSafeWriteTarget: createTrackedRootInstructionShadowSafetyAssertion({
|
|
977
|
+
repoRoot: gitContext.worktreeRoot,
|
|
978
|
+
operation: existingBundleState ? "refresh" : "create",
|
|
979
|
+
}),
|
|
980
|
+
allowFileOverwriteTargets: (0, root_instruction_state_1.collectManagedRootInstructionTargets)(currentBundles),
|
|
981
|
+
deferredWriteTargets: trackedRootInstructionShadowPlan.deferredMaterializationTargets,
|
|
982
|
+
rootInstructionBaseContents,
|
|
983
|
+
resolveFileConflict: options.prompts.resolveFileConflict,
|
|
984
|
+
disableModelInvocation: entry.disable_model_invocation,
|
|
985
|
+
});
|
|
986
|
+
currentBundles = {
|
|
987
|
+
...currentBundles,
|
|
988
|
+
[entry.bundle]: buildMaterializedBundleState({
|
|
989
|
+
existingBundleState,
|
|
990
|
+
materializedResult,
|
|
991
|
+
repoRoot: gitContext.worktreeRoot,
|
|
992
|
+
source: entry.source,
|
|
993
|
+
resolvedCommit: refreshed.currentCommit,
|
|
994
|
+
selectedTools: toolsToRefresh,
|
|
995
|
+
selectedItems: entry.items,
|
|
996
|
+
}),
|
|
997
|
+
};
|
|
998
|
+
currentShadowedFiles = applyTrackedRootInstructionShadowPlan({
|
|
999
|
+
repoRoot: gitContext.worktreeRoot,
|
|
1000
|
+
bundleName: entry.bundle,
|
|
1001
|
+
existingShadowedFiles: currentShadowedFiles,
|
|
1002
|
+
plan: trackedRootInstructionShadowPlan,
|
|
1003
|
+
});
|
|
1004
|
+
const syncedRootInstructionPaths = (0, root_instruction_state_1.syncManagedRootInstructionFiles)({
|
|
1005
|
+
repoRoot: gitContext.worktreeRoot,
|
|
1006
|
+
desiredState: nextDesiredState,
|
|
1007
|
+
materializedBundles: currentBundles,
|
|
1008
|
+
rootInstructionBaseContents,
|
|
1009
|
+
targetPaths: trackedRootInstructionShadowPlan.untrackedTargetPaths,
|
|
1010
|
+
resolveCachedBundle: (entry) => resolveDesiredCachedBundle(options.libraryDir, entry),
|
|
1011
|
+
});
|
|
1012
|
+
currentBundles = (0, root_instruction_state_1.refreshManagedFileFingerprintsForPaths)(gitContext.worktreeRoot, currentBundles, syncedRootInstructionPaths);
|
|
1013
|
+
}
|
|
1014
|
+
outputLines.push(pc.green(`Updated ${entry.bundle}${formatCommitTransition(currentCommit, remoteStatus.remoteCommit)}`));
|
|
1015
|
+
}
|
|
1016
|
+
catch (error) {
|
|
1017
|
+
if (!initialRevision.cached) {
|
|
1018
|
+
(0, bundle_fetch_1.removeCachedRemoteSource)({
|
|
1019
|
+
source: entry.source,
|
|
1020
|
+
libraryDir: options.libraryDir,
|
|
1021
|
+
protocol: entry.protocol,
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
else if (initialRevision.currentCommit) {
|
|
1025
|
+
(0, bundle_fetch_1.restoreCachedRemoteSourceRevision)({
|
|
1026
|
+
source: entry.source,
|
|
1027
|
+
libraryDir: options.libraryDir,
|
|
1028
|
+
protocol: entry.protocol,
|
|
1029
|
+
ref: entry.ref,
|
|
1030
|
+
commit: initialRevision.currentCommit,
|
|
1031
|
+
refName: initialRevision.currentRef,
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
throw error;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
registry = (0, registry_1.upsertRepoState)(registry, gitContext.repoFingerprint, {
|
|
1038
|
+
repo_root: gitContext.repoRoot,
|
|
1039
|
+
desired_state: nextDesiredState,
|
|
1040
|
+
});
|
|
1041
|
+
if (registry.worktrees[gitContext.worktreeId] ||
|
|
1042
|
+
Object.keys(currentBundles).length > 0) {
|
|
1043
|
+
const managedFiles = collectAllFiles({
|
|
1044
|
+
bundles: currentBundles,
|
|
1045
|
+
exclude_configured: false,
|
|
1046
|
+
...(rootInstructionBaseContents !== undefined
|
|
1047
|
+
? { root_instruction_base_contents: rootInstructionBaseContents }
|
|
1048
|
+
: {}),
|
|
1049
|
+
});
|
|
1050
|
+
const newMaterializedState = {
|
|
1051
|
+
bundles: currentBundles,
|
|
1052
|
+
exclude_configured: managedFiles.length > 0,
|
|
1053
|
+
...(rootInstructionBaseContents !== undefined
|
|
1054
|
+
? { root_instruction_base_contents: rootInstructionBaseContents }
|
|
1055
|
+
: {}),
|
|
1056
|
+
};
|
|
1057
|
+
if (Object.keys(currentBundles).length > 0) {
|
|
1058
|
+
if (managedFiles.length > 0) {
|
|
1059
|
+
(0, git_exclude_1.configureSkulExcludeBlock)({
|
|
1060
|
+
gitDir: gitContext.gitDir,
|
|
1061
|
+
files: managedFiles,
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
else {
|
|
1065
|
+
(0, git_exclude_1.removeSkulExcludeBlock)({ gitDir: gitContext.gitDir });
|
|
1066
|
+
}
|
|
1067
|
+
registry = (0, registry_1.upsertWorktreeState)(registry, gitContext.worktreeId, {
|
|
1068
|
+
repo_fingerprint: gitContext.repoFingerprint,
|
|
1069
|
+
path: gitContext.worktreeRoot,
|
|
1070
|
+
materialized_state: newMaterializedState,
|
|
1071
|
+
shadowed_files: currentShadowedFiles,
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
else {
|
|
1075
|
+
(0, git_exclude_1.removeSkulExcludeBlock)({ gitDir: gitContext.gitDir });
|
|
1076
|
+
if (Object.keys(currentShadowedFiles).length > 0) {
|
|
1077
|
+
registry = (0, registry_1.upsertWorktreeState)(registry, gitContext.worktreeId, {
|
|
1078
|
+
repo_fingerprint: gitContext.repoFingerprint,
|
|
1079
|
+
path: gitContext.worktreeRoot,
|
|
1080
|
+
materialized_state: newMaterializedState,
|
|
1081
|
+
shadowed_files: currentShadowedFiles,
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
else {
|
|
1085
|
+
registry = (0, registry_1.removeWorktreeState)(registry, gitContext.worktreeId);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
(0, registry_1.writeRegistryFile)(options.registryFile, registry);
|
|
1090
|
+
return [localOnlyNote, ...outputLines].filter(Boolean).join("\n");
|
|
1091
|
+
}
|
|
1092
|
+
async function applyBundle(options) {
|
|
1093
|
+
const gitContext = requireGitContext(options.cwd, "add");
|
|
1094
|
+
// Skip cloning in dry-run: when a remote source is specified and not yet
|
|
1095
|
+
// cached, return a preview message immediately so no network I/O occurs.
|
|
1096
|
+
// (When source is omitted, fetchBundleSourceForApply is a no-op, so the
|
|
1097
|
+
// dryRun guard at the end of this function is sufficient for that case.)
|
|
1098
|
+
if (options.dryRun && options.source) {
|
|
1099
|
+
const { cached } = (0, bundle_fetch_1.readCachedSourceRevision)({
|
|
1100
|
+
source: options.source,
|
|
1101
|
+
libraryDir: options.libraryDir,
|
|
1102
|
+
protocol: options.protocol,
|
|
1103
|
+
});
|
|
1104
|
+
if (!cached) {
|
|
1105
|
+
const toolsLabel = options.agents.length > 0
|
|
1106
|
+
? options.agents.join(", ")
|
|
1107
|
+
: "available tools";
|
|
1108
|
+
return [
|
|
1109
|
+
pc.dim(`(would clone ${options.source})`),
|
|
1110
|
+
`${pc.yellow("DRY RUN:")} Would apply ${options.bundle} for ${toolsLabel}`,
|
|
1111
|
+
].join("\n");
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
const registryBeforePrepare = readRegistryWithGuidance(options.registryFile);
|
|
1115
|
+
if (shouldApplySelectedItemsAcrossSourceBundles(options)) {
|
|
1116
|
+
return applySelectedItemsAcrossSourceBundles({
|
|
1117
|
+
cwd: options.cwd,
|
|
1118
|
+
prompts: options.prompts,
|
|
1119
|
+
registryFile: options.registryFile,
|
|
1120
|
+
libraryDir: options.libraryDir,
|
|
1121
|
+
source: options.source,
|
|
1122
|
+
protocol: options.protocol,
|
|
1123
|
+
agents: options.agents,
|
|
1124
|
+
includeItems: options.includeItems,
|
|
1125
|
+
dryRun: options.dryRun,
|
|
1126
|
+
ref: options.ref,
|
|
1127
|
+
existingDesiredState: registryBeforePrepare.repos[gitContext.repoFingerprint]
|
|
1128
|
+
?.desired_state ?? [],
|
|
1129
|
+
disableModelInvocation: options.disableModelInvocation,
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
const preparedBundle = await prepareApplyBundle({
|
|
1133
|
+
bundle: options.bundle,
|
|
1134
|
+
source: options.source,
|
|
1135
|
+
protocol: options.protocol,
|
|
1136
|
+
requestedTools: options.agents,
|
|
1137
|
+
requestedItems: options.includeItems,
|
|
1138
|
+
selectItems: options.selectItems,
|
|
1139
|
+
replaceItems: options.replaceItems,
|
|
1140
|
+
prompts: options.prompts,
|
|
1141
|
+
libraryDir: options.libraryDir,
|
|
1142
|
+
ref: options.ref,
|
|
1143
|
+
inferredBundleFromSource: options.inferredBundleFromSource,
|
|
1144
|
+
existingDesiredState: registryBeforePrepare.repos[gitContext.repoFingerprint]?.desired_state ??
|
|
1145
|
+
[],
|
|
1146
|
+
refreshedSources: options.refreshedSources,
|
|
1147
|
+
});
|
|
1148
|
+
if (options.dryRun) {
|
|
1149
|
+
return [
|
|
1150
|
+
...preparedBundle.cloneLines,
|
|
1151
|
+
`${pc.yellow("DRY RUN:")} Would ${formatApplyBundleMessage({
|
|
1152
|
+
bundle: preparedBundle.cachedBundle.bundle,
|
|
1153
|
+
toolLabel: preparedBundle.toolLabel,
|
|
1154
|
+
items: preparedBundle.replacesItemSelection
|
|
1155
|
+
? preparedBundle.selectedItems
|
|
1156
|
+
: undefined,
|
|
1157
|
+
})}`,
|
|
1158
|
+
].join("\n");
|
|
1159
|
+
}
|
|
1160
|
+
let registry = registryBeforePrepare;
|
|
1161
|
+
const existingWorktreeState = registry.worktrees[gitContext.worktreeId]?.materialized_state;
|
|
1162
|
+
let currentShadowedFiles = {
|
|
1163
|
+
...(registry.worktrees[gitContext.worktreeId]?.shadowed_files ?? {}),
|
|
1164
|
+
};
|
|
1165
|
+
let rootInstructionBaseContents = existingWorktreeState?.root_instruction_base_contents;
|
|
1166
|
+
const existingBundleState = existingWorktreeState?.bundles[preparedBundle.cachedBundle.bundle];
|
|
1167
|
+
const plannedWriteTargets = (0, bundle_materialization_1.previewMaterializeBundleWriteTargets)({
|
|
1168
|
+
repoRoot: gitContext.worktreeRoot,
|
|
1169
|
+
bundleDir: node_path_1.default.dirname(preparedBundle.cachedBundle.manifestFile),
|
|
1170
|
+
manifest: preparedBundle.cachedBundle.manifest,
|
|
1171
|
+
tools: preparedBundle.selectedTools,
|
|
1172
|
+
itemSelectors: preparedBundle.selectedItems,
|
|
1173
|
+
disableModelInvocation: options.disableModelInvocation,
|
|
1174
|
+
});
|
|
1175
|
+
const plannedRootInstructionTargets = new Set(plannedWriteTargets.filter((filePath) => (0, root_instruction_render_1.isRootInstructionPath)(filePath)));
|
|
1176
|
+
const trackedRootInstructionShadowPlan = planTrackedRootInstructionShadows({
|
|
1177
|
+
repoRoot: gitContext.worktreeRoot,
|
|
1178
|
+
bundleDir: node_path_1.default.dirname(preparedBundle.cachedBundle.manifestFile),
|
|
1179
|
+
manifest: preparedBundle.cachedBundle.manifest,
|
|
1180
|
+
toolNames: selectTrackedRootInstructionShadowToolNames({
|
|
1181
|
+
existingBundleState,
|
|
1182
|
+
nextToolNames: preparedBundle.nextToolNames,
|
|
1183
|
+
}),
|
|
1184
|
+
itemSelectors: preparedBundle.selectedItems,
|
|
1185
|
+
targetPaths: plannedRootInstructionTargets,
|
|
1186
|
+
bundleName: preparedBundle.cachedBundle.bundle,
|
|
1187
|
+
bundleSource: preparedBundle.bundleSource,
|
|
1188
|
+
existingShadowedFiles: currentShadowedFiles,
|
|
1189
|
+
materializedBundles: existingWorktreeState?.bundles ?? {},
|
|
1190
|
+
});
|
|
1191
|
+
rootInstructionBaseContents = (0, root_instruction_state_1.captureRootInstructionBaseContents)({
|
|
1192
|
+
repoRoot: gitContext.worktreeRoot,
|
|
1193
|
+
targetPaths: trackedRootInstructionShadowPlan.untrackedTargetPaths,
|
|
1194
|
+
existingBaseContents: rootInstructionBaseContents,
|
|
1195
|
+
managedTargetPaths: (0, root_instruction_state_1.collectManagedRootInstructionTargets)(existingWorktreeState?.bundles ?? {}),
|
|
1196
|
+
});
|
|
1197
|
+
const existingDesiredState = registry.repos[gitContext.repoFingerprint]?.desired_state ?? [];
|
|
1198
|
+
(0, root_instruction_state_1.assertManagedRootInstructionSyncSourcesCached)({
|
|
1199
|
+
desiredState: existingDesiredState,
|
|
1200
|
+
materializedBundles: existingWorktreeState?.bundles ?? {},
|
|
1201
|
+
targetPaths: trackedRootInstructionShadowPlan.untrackedTargetPaths,
|
|
1202
|
+
resolveCachedBundle: (entry) => resolveDesiredCachedBundle(options.libraryDir, entry),
|
|
1203
|
+
});
|
|
1204
|
+
let pathsToReplace = null;
|
|
1205
|
+
if (existingBundleState) {
|
|
1206
|
+
assertTrackedRootInstructionShadowSafetyForPaths({
|
|
1207
|
+
repoRoot: gitContext.worktreeRoot,
|
|
1208
|
+
operation: "refresh",
|
|
1209
|
+
filePaths: plannedWriteTargets,
|
|
1210
|
+
});
|
|
1211
|
+
// When a tool flag is specified, only replace the selected tool targets.
|
|
1212
|
+
const toolsToReplace = preparedBundle.hasToolSelection
|
|
1213
|
+
? options.agents.filter((t) => t in existingBundleState.tools)
|
|
1214
|
+
: Object.keys(existingBundleState.tools);
|
|
1215
|
+
pathsToReplace = excludeShadowedTrackedRootInstructionTargets(flattenBundleState({
|
|
1216
|
+
tools: Object.fromEntries(toolsToReplace.map((t) => [t, existingBundleState.tools[t]])),
|
|
1217
|
+
}), trackedRootInstructionShadowPlan.deferredMaterializationTargets);
|
|
1218
|
+
const replacementAllowed = await confirmManagedFileRemovals(gitContext.worktreeRoot, pathsToReplace, options.prompts, "replace");
|
|
1219
|
+
if (!replacementAllowed) {
|
|
1220
|
+
throw new Error("Replacement aborted because a modified managed file was kept");
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
const sharedRootInstructionState = (0, root_instruction_state_1.collectSharedRootInstructionState)(existingWorktreeState?.bundles ?? {}, plannedWriteTargets, preparedBundle.cachedBundle.bundle);
|
|
1224
|
+
if (sharedRootInstructionState.files.length > 0) {
|
|
1225
|
+
const replacementAllowed = await confirmManagedFileRemovals(gitContext.worktreeRoot, sharedRootInstructionState, options.prompts, "replace");
|
|
1226
|
+
if (!replacementAllowed) {
|
|
1227
|
+
throw new Error("Replacement aborted because a modified managed file was kept");
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
assertTrackedRootInstructionShadowPlanCanApply({
|
|
1231
|
+
repoRoot: gitContext.worktreeRoot,
|
|
1232
|
+
bundleName: preparedBundle.cachedBundle.bundle,
|
|
1233
|
+
existingShadowedFiles: currentShadowedFiles,
|
|
1234
|
+
plan: trackedRootInstructionShadowPlan,
|
|
1235
|
+
});
|
|
1236
|
+
assertTrackedRootInstructionShadowSafetyForPaths({
|
|
1237
|
+
repoRoot: gitContext.worktreeRoot,
|
|
1238
|
+
operation: existingBundleState ? "refresh" : "create",
|
|
1239
|
+
filePaths: plannedWriteTargets,
|
|
1240
|
+
});
|
|
1241
|
+
if (pathsToReplace) {
|
|
1242
|
+
removeManagedPaths(gitContext.worktreeRoot, pathsToReplace);
|
|
1243
|
+
}
|
|
1244
|
+
const materializedResult = await (0, bundle_materialization_1.materializeBundle)({
|
|
1245
|
+
repoRoot: gitContext.worktreeRoot,
|
|
1246
|
+
bundleDir: node_path_1.default.dirname(preparedBundle.cachedBundle.manifestFile),
|
|
1247
|
+
manifest: preparedBundle.cachedBundle.manifest,
|
|
1248
|
+
tools: preparedBundle.selectedTools,
|
|
1249
|
+
itemSelectors: preparedBundle.selectedItems,
|
|
1250
|
+
bundleName: preparedBundle.cachedBundle.bundle,
|
|
1251
|
+
bundleSource: preparedBundle.bundleSource,
|
|
1252
|
+
assertSafeWriteTarget: createTrackedRootInstructionShadowSafetyAssertion({
|
|
1253
|
+
repoRoot: gitContext.worktreeRoot,
|
|
1254
|
+
operation: existingBundleState ? "refresh" : "create",
|
|
1255
|
+
}),
|
|
1256
|
+
allowFileOverwriteTargets: (0, root_instruction_state_1.collectManagedRootInstructionTargets)(existingWorktreeState?.bundles ?? {}),
|
|
1257
|
+
deferredWriteTargets: trackedRootInstructionShadowPlan.deferredMaterializationTargets,
|
|
1258
|
+
rootInstructionBaseContents,
|
|
1259
|
+
resolveFileConflict: options.prompts.resolveFileConflict,
|
|
1260
|
+
disableModelInvocation: options.disableModelInvocation,
|
|
1261
|
+
});
|
|
1262
|
+
currentShadowedFiles = applyTrackedRootInstructionShadowPlan({
|
|
1263
|
+
repoRoot: gitContext.worktreeRoot,
|
|
1264
|
+
bundleName: preparedBundle.cachedBundle.bundle,
|
|
1265
|
+
existingShadowedFiles: currentShadowedFiles,
|
|
1266
|
+
plan: trackedRootInstructionShadowPlan,
|
|
1267
|
+
});
|
|
1268
|
+
const newBundleState = buildMaterializedBundleState({
|
|
1269
|
+
existingBundleState,
|
|
1270
|
+
materializedResult,
|
|
1271
|
+
repoRoot: gitContext.worktreeRoot,
|
|
1272
|
+
source: preparedBundle.bundleSource,
|
|
1273
|
+
resolvedCommit: preparedBundle.sourceRevision?.currentCommit,
|
|
1274
|
+
selectedTools: preparedBundle.selectedTools,
|
|
1275
|
+
selectedItems: preparedBundle.selectedItems,
|
|
1276
|
+
});
|
|
1277
|
+
const newDesiredEntry = buildDesiredEntryForAppliedBundle({
|
|
1278
|
+
existingDesiredState,
|
|
1279
|
+
cachedBundle: preparedBundle.cachedBundle,
|
|
1280
|
+
requestedSource: options.source,
|
|
1281
|
+
requestedProtocol: options.protocol,
|
|
1282
|
+
requestedRef: options.ref,
|
|
1283
|
+
requestedTools: preparedBundle.selectedTools,
|
|
1284
|
+
requestedItems: preparedBundle.selectedItems,
|
|
1285
|
+
replaceRequestedItems: preparedBundle.replacesItemSelection,
|
|
1286
|
+
sourceRevision: preparedBundle.sourceRevision,
|
|
1287
|
+
disableModelInvocation: options.disableModelInvocation,
|
|
1288
|
+
});
|
|
1289
|
+
const newDesiredState = [
|
|
1290
|
+
...upsertDesiredEntryPreservingOrder(existingDesiredState, newDesiredEntry),
|
|
1291
|
+
];
|
|
1292
|
+
registry = (0, registry_1.upsertRepoState)(registry, gitContext.repoFingerprint, {
|
|
1293
|
+
repo_root: gitContext.repoRoot,
|
|
1294
|
+
desired_state: newDesiredState,
|
|
1295
|
+
});
|
|
1296
|
+
// Merge into existing materialized state, preserving other bundles
|
|
1297
|
+
const newMatState = {
|
|
1298
|
+
bundles: {
|
|
1299
|
+
...(existingWorktreeState?.bundles ?? {}),
|
|
1300
|
+
[preparedBundle.cachedBundle.bundle]: newBundleState,
|
|
1301
|
+
},
|
|
1302
|
+
exclude_configured: false,
|
|
1303
|
+
...(rootInstructionBaseContents !== undefined
|
|
1304
|
+
? { root_instruction_base_contents: rootInstructionBaseContents }
|
|
1305
|
+
: {}),
|
|
1306
|
+
};
|
|
1307
|
+
const syncedRootInstructionPaths = (0, root_instruction_state_1.syncManagedRootInstructionFiles)({
|
|
1308
|
+
repoRoot: gitContext.worktreeRoot,
|
|
1309
|
+
desiredState: newDesiredState,
|
|
1310
|
+
materializedBundles: newMatState.bundles,
|
|
1311
|
+
rootInstructionBaseContents,
|
|
1312
|
+
targetPaths: trackedRootInstructionShadowPlan.untrackedTargetPaths,
|
|
1313
|
+
resolveCachedBundle: (entry) => resolveDesiredCachedBundle(options.libraryDir, entry),
|
|
1314
|
+
});
|
|
1315
|
+
newMatState.bundles = (0, root_instruction_state_1.refreshManagedFileFingerprintsForPaths)(gitContext.worktreeRoot, newMatState.bundles, syncedRootInstructionPaths);
|
|
1316
|
+
const managedFiles = collectAllFiles(newMatState);
|
|
1317
|
+
newMatState.exclude_configured = managedFiles.length > 0;
|
|
1318
|
+
if (managedFiles.length > 0) {
|
|
1319
|
+
(0, git_exclude_1.configureSkulExcludeBlock)({
|
|
1320
|
+
gitDir: gitContext.gitDir,
|
|
1321
|
+
files: managedFiles,
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
else {
|
|
1325
|
+
(0, git_exclude_1.removeSkulExcludeBlock)({ gitDir: gitContext.gitDir });
|
|
1326
|
+
}
|
|
1327
|
+
registry = (0, registry_1.upsertWorktreeState)(registry, gitContext.worktreeId, {
|
|
1328
|
+
repo_fingerprint: gitContext.repoFingerprint,
|
|
1329
|
+
path: gitContext.worktreeRoot,
|
|
1330
|
+
materialized_state: newMatState,
|
|
1331
|
+
shadowed_files: currentShadowedFiles,
|
|
1332
|
+
});
|
|
1333
|
+
(0, registry_1.writeRegistryFile)(options.registryFile, registry);
|
|
1334
|
+
return [
|
|
1335
|
+
...preparedBundle.cloneLines,
|
|
1336
|
+
pc.green(formatAppliedBundleMessage({
|
|
1337
|
+
bundle: preparedBundle.cachedBundle.bundle,
|
|
1338
|
+
toolLabel: preparedBundle.toolLabel,
|
|
1339
|
+
items: preparedBundle.replacesItemSelection
|
|
1340
|
+
? preparedBundle.selectedItems
|
|
1341
|
+
: undefined,
|
|
1342
|
+
})),
|
|
1343
|
+
].join("\n");
|
|
1344
|
+
}
|
|
1345
|
+
function formatAppliedBundleMessage(options) {
|
|
1346
|
+
return formatApplyBundleMessage({
|
|
1347
|
+
bundle: options.bundle,
|
|
1348
|
+
toolLabel: options.toolLabel,
|
|
1349
|
+
items: options.items,
|
|
1350
|
+
action: "Applied",
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
function formatApplyBundleMessage(options) {
|
|
1354
|
+
const itemLabel = options.items !== undefined && options.items.length > 0
|
|
1355
|
+
? `: ${options.items.join(", ")}`
|
|
1356
|
+
: "";
|
|
1357
|
+
return `${options.action ?? "apply"} ${options.bundle} for ${options.toolLabel}${itemLabel}`;
|
|
1358
|
+
}
|
|
1359
|
+
function shouldApplySelectedItemsAcrossSourceBundles(options) {
|
|
1360
|
+
return (options.selectItems &&
|
|
1361
|
+
options.source !== undefined &&
|
|
1362
|
+
options.inferredBundleFromSource === true);
|
|
1363
|
+
}
|
|
1364
|
+
async function applySelectedItemsAcrossSourceBundles(options) {
|
|
1365
|
+
const refreshedSources = new Set();
|
|
1366
|
+
const cloneLines = refreshBundleSourceForApply({
|
|
1367
|
+
source: options.source,
|
|
1368
|
+
libraryDir: options.libraryDir,
|
|
1369
|
+
protocol: options.protocol,
|
|
1370
|
+
ref: options.ref,
|
|
1371
|
+
}, refreshedSources);
|
|
1372
|
+
const selection = await selectSourceBundleItemApplyTargets({
|
|
1373
|
+
libraryDir: options.libraryDir,
|
|
1374
|
+
source: options.source,
|
|
1375
|
+
requestedTools: options.agents,
|
|
1376
|
+
requestedItems: options.includeItems,
|
|
1377
|
+
prompts: options.prompts,
|
|
1378
|
+
existingDesiredState: options.existingDesiredState,
|
|
1379
|
+
});
|
|
1380
|
+
const outputLines = [];
|
|
1381
|
+
for (const target of selection.removeTargets) {
|
|
1382
|
+
outputLines.push(await removeBundle({
|
|
1383
|
+
cwd: options.cwd,
|
|
1384
|
+
prompts: options.prompts,
|
|
1385
|
+
registryFile: options.registryFile,
|
|
1386
|
+
libraryDir: options.libraryDir,
|
|
1387
|
+
bundle: target.bundle,
|
|
1388
|
+
source: target.source,
|
|
1389
|
+
includeItems: [],
|
|
1390
|
+
selectItems: false,
|
|
1391
|
+
dryRun: options.dryRun,
|
|
1392
|
+
}));
|
|
1393
|
+
}
|
|
1394
|
+
for (const target of selection.applyTargets) {
|
|
1395
|
+
outputLines.push(await applyBundle({
|
|
1396
|
+
cwd: options.cwd,
|
|
1397
|
+
prompts: options.prompts,
|
|
1398
|
+
registryFile: options.registryFile,
|
|
1399
|
+
libraryDir: options.libraryDir,
|
|
1400
|
+
bundle: target.bundle,
|
|
1401
|
+
source: target.source,
|
|
1402
|
+
protocol: options.protocol,
|
|
1403
|
+
agents: target.tools,
|
|
1404
|
+
includeItems: target.items,
|
|
1405
|
+
selectItems: false,
|
|
1406
|
+
replaceItems: true,
|
|
1407
|
+
dryRun: options.dryRun,
|
|
1408
|
+
ref: options.ref,
|
|
1409
|
+
refreshedSources,
|
|
1410
|
+
disableModelInvocation: options.disableModelInvocation,
|
|
1411
|
+
}));
|
|
1412
|
+
}
|
|
1413
|
+
return [...cloneLines, ...outputLines].filter(Boolean).join("\n");
|
|
1414
|
+
}
|
|
1415
|
+
async function selectSourceBundleItemApplyTargets(options) {
|
|
1416
|
+
const selectedTools = await selectToolsForSourceBundleItems(options);
|
|
1417
|
+
const choices = listSourceBundleItemApplyChoices({
|
|
1418
|
+
libraryDir: options.libraryDir,
|
|
1419
|
+
source: options.source,
|
|
1420
|
+
tools: selectedTools,
|
|
1421
|
+
});
|
|
1422
|
+
if (choices.length === 0) {
|
|
1423
|
+
throw new Error(`No selectable bundle items found for ${options.source}`);
|
|
1424
|
+
}
|
|
1425
|
+
const requestedItems = (0, bundle_items_1.normalizeBundleItemSelectors)(options.requestedItems);
|
|
1426
|
+
const selectedValues = selectInitialSourceBundleItemApplyValues({
|
|
1427
|
+
choices,
|
|
1428
|
+
requestedItems,
|
|
1429
|
+
existingDesiredState: options.existingDesiredState,
|
|
1430
|
+
});
|
|
1431
|
+
const selections = await options.prompts.selectBundleItemChoices(choices, selectedValues, "install");
|
|
1432
|
+
const removeTargets = listDeselectedSourceBundleApplyTargets({
|
|
1433
|
+
choices,
|
|
1434
|
+
selectedValues: selections,
|
|
1435
|
+
existingDesiredState: options.existingDesiredState,
|
|
1436
|
+
source: options.source,
|
|
1437
|
+
});
|
|
1438
|
+
if (selections.length === 0 && removeTargets.length === 0) {
|
|
1439
|
+
throw new Error("No bundle items selected for install");
|
|
1440
|
+
}
|
|
1441
|
+
return {
|
|
1442
|
+
applyTargets: groupBundleItemApplyTargets({
|
|
1443
|
+
choices,
|
|
1444
|
+
selectedValues: selections,
|
|
1445
|
+
}),
|
|
1446
|
+
removeTargets,
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
async function selectToolsForSourceBundleItems(options) {
|
|
1450
|
+
const availableTools = listSelectableToolsForSource({
|
|
1451
|
+
libraryDir: options.libraryDir,
|
|
1452
|
+
source: options.source,
|
|
1453
|
+
}).filter((toolName) => options.global !== true || (0, tool_mapping_1.globalCapableToolNames)().includes(toolName));
|
|
1454
|
+
if (options.requestedTools.length > 0) {
|
|
1455
|
+
assertRequestedToolsAreSelectableForSource({
|
|
1456
|
+
requestedTools: options.requestedTools,
|
|
1457
|
+
availableTools,
|
|
1458
|
+
source: options.source,
|
|
1459
|
+
});
|
|
1460
|
+
return options.requestedTools;
|
|
1461
|
+
}
|
|
1462
|
+
if (availableTools.length === 0) {
|
|
1463
|
+
throw new Error(`No selectable agents found for ${options.source}`);
|
|
1464
|
+
}
|
|
1465
|
+
if (availableTools.length === 1) {
|
|
1466
|
+
return availableTools;
|
|
1467
|
+
}
|
|
1468
|
+
return options.prompts.selectAgents(availableTools);
|
|
1469
|
+
}
|
|
1470
|
+
function assertRequestedToolsAreSelectableForSource(options) {
|
|
1471
|
+
const unsupportedTools = options.requestedTools.filter((toolName) => !options.availableTools.includes(toolName));
|
|
1472
|
+
if (unsupportedTools.length === 0) {
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1475
|
+
throw new Error(`Source ${options.source} does not support agent(s): ${unsupportedTools.join(", ")}\nSupported agents: ${options.availableTools.join(", ")}`);
|
|
1476
|
+
}
|
|
1477
|
+
function listSourceBundleItemApplyChoices(options) {
|
|
1478
|
+
return (0, bundle_discovery_1.listCachedBundles)({ libraryDir: options.libraryDir })
|
|
1479
|
+
.filter((bundle) => bundle.source === options.source)
|
|
1480
|
+
.flatMap((bundle) => {
|
|
1481
|
+
const bundleTools = options.tools.filter((toolName) => Object.keys(bundle.manifest.tools).includes(toolName));
|
|
1482
|
+
if (bundleTools.length === 0) {
|
|
1483
|
+
return [];
|
|
1484
|
+
}
|
|
1485
|
+
const availableItems = (0, bundle_items_1.listSelectableBundleItems)({
|
|
1486
|
+
bundleDir: node_path_1.default.dirname(bundle.manifestFile),
|
|
1487
|
+
manifest: bundle.manifest,
|
|
1488
|
+
tools: bundleTools,
|
|
1489
|
+
});
|
|
1490
|
+
return availableItems.map((item) => ({
|
|
1491
|
+
value: encodeBundleItemApplyChoice({
|
|
1492
|
+
source: bundle.source,
|
|
1493
|
+
bundle: bundle.bundle,
|
|
1494
|
+
item,
|
|
1495
|
+
}),
|
|
1496
|
+
label: `${bundle.bundle}: ${item}`,
|
|
1497
|
+
source: bundle.source,
|
|
1498
|
+
bundle: bundle.bundle,
|
|
1499
|
+
item,
|
|
1500
|
+
tools: bundleTools,
|
|
1501
|
+
availableTools: Object.keys(bundle.manifest.tools),
|
|
1502
|
+
}));
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
function selectInitialSourceBundleItemApplyValues(options) {
|
|
1506
|
+
const selectedValues = new Set();
|
|
1507
|
+
const requestedItemSet = new Set(options.requestedItems);
|
|
1508
|
+
for (const choice of options.choices) {
|
|
1509
|
+
if (requestedItemSet.has(choice.item)) {
|
|
1510
|
+
selectedValues.add(choice.value);
|
|
1511
|
+
continue;
|
|
1512
|
+
}
|
|
1513
|
+
const existingEntry = options.existingDesiredState.find((entry) => entry.bundle === choice.bundle && entry.source === choice.source);
|
|
1514
|
+
if (existingEntry &&
|
|
1515
|
+
bundleItemApplyChoiceMatchesDesiredTools({
|
|
1516
|
+
choice,
|
|
1517
|
+
desiredEntry: existingEntry,
|
|
1518
|
+
}) &&
|
|
1519
|
+
(existingEntry.items === undefined ||
|
|
1520
|
+
existingEntry.items.includes(choice.item))) {
|
|
1521
|
+
selectedValues.add(choice.value);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
const missingItems = options.requestedItems.filter((item) => !options.choices.some((choice) => choice.item === item));
|
|
1525
|
+
if (missingItems.length > 0) {
|
|
1526
|
+
throw new Error(`Bundle item(s) are not available: ${missingItems.join(", ")}`);
|
|
1527
|
+
}
|
|
1528
|
+
return Array.from(selectedValues);
|
|
1529
|
+
}
|
|
1530
|
+
function listDeselectedSourceBundleApplyTargets(options) {
|
|
1531
|
+
const selectedBundleKeys = new Set(options.selectedValues.map((value) => {
|
|
1532
|
+
const choice = options.choices.find((candidate) => candidate.value === value);
|
|
1533
|
+
if (!choice) {
|
|
1534
|
+
throw new Error(`Selected bundle item is not available: ${value}`);
|
|
1535
|
+
}
|
|
1536
|
+
return encodeBundleIdentity(choice);
|
|
1537
|
+
}));
|
|
1538
|
+
const choicesByBundle = groupBundleItemApplyChoices(options.choices);
|
|
1539
|
+
const targets = [];
|
|
1540
|
+
for (const entry of options.existingDesiredState) {
|
|
1541
|
+
const key = encodeBundleIdentity(entry);
|
|
1542
|
+
if (selectedBundleKeys.has(key)) {
|
|
1543
|
+
continue;
|
|
1544
|
+
}
|
|
1545
|
+
const bundleChoices = choicesByBundle.get(key);
|
|
1546
|
+
if (!bundleChoices) {
|
|
1547
|
+
if (entry.source === options.source) {
|
|
1548
|
+
targets.push({ bundle: entry.bundle, source: entry.source });
|
|
1549
|
+
}
|
|
1550
|
+
continue;
|
|
1551
|
+
}
|
|
1552
|
+
if (!bundleItemApplyChoiceCoversDesiredTools({
|
|
1553
|
+
choice: bundleChoices[0],
|
|
1554
|
+
desiredEntry: entry,
|
|
1555
|
+
})) {
|
|
1556
|
+
continue;
|
|
1557
|
+
}
|
|
1558
|
+
const activeChoices = bundleChoices.filter((choice) => bundleItemApplyChoiceMatchesDesiredState({
|
|
1559
|
+
choice,
|
|
1560
|
+
desiredEntry: entry,
|
|
1561
|
+
}));
|
|
1562
|
+
if (activeChoices.length === 0) {
|
|
1563
|
+
continue;
|
|
1564
|
+
}
|
|
1565
|
+
targets.push({
|
|
1566
|
+
bundle: entry.bundle,
|
|
1567
|
+
source: entry.source ?? activeChoices[0].source,
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
return targets;
|
|
1571
|
+
}
|
|
1572
|
+
function groupBundleItemApplyChoices(choices) {
|
|
1573
|
+
const choicesByBundle = new Map();
|
|
1574
|
+
for (const choice of choices) {
|
|
1575
|
+
const key = encodeBundleIdentity(choice);
|
|
1576
|
+
const bundleChoices = choicesByBundle.get(key) ?? [];
|
|
1577
|
+
bundleChoices.push(choice);
|
|
1578
|
+
choicesByBundle.set(key, bundleChoices);
|
|
1579
|
+
}
|
|
1580
|
+
return choicesByBundle;
|
|
1581
|
+
}
|
|
1582
|
+
function bundleItemApplyChoiceMatchesDesiredState(options) {
|
|
1583
|
+
return (bundleItemApplyChoiceMatchesDesiredTools(options) &&
|
|
1584
|
+
(options.desiredEntry.items === undefined ||
|
|
1585
|
+
options.desiredEntry.items.includes(options.choice.item)));
|
|
1586
|
+
}
|
|
1587
|
+
function bundleItemApplyChoiceMatchesDesiredTools(options) {
|
|
1588
|
+
return (options.desiredEntry.tools === undefined ||
|
|
1589
|
+
options.choice.tools.some((toolName) => options.desiredEntry.tools?.includes(toolName)));
|
|
1590
|
+
}
|
|
1591
|
+
function bundleItemApplyChoiceCoversDesiredTools(options) {
|
|
1592
|
+
const desiredTools = options.desiredEntry.tools ?? options.choice.availableTools;
|
|
1593
|
+
return desiredTools.every((toolName) => options.choice.tools.includes(toolName));
|
|
1594
|
+
}
|
|
1595
|
+
function groupBundleItemApplyTargets(options) {
|
|
1596
|
+
const groupsByBundle = new Map();
|
|
1597
|
+
for (const value of options.selectedValues) {
|
|
1598
|
+
const choice = options.choices.find((candidate) => candidate.value === value);
|
|
1599
|
+
if (!choice) {
|
|
1600
|
+
throw new Error(`Selected bundle item is not available: ${value}`);
|
|
1601
|
+
}
|
|
1602
|
+
const key = encodeBundleIdentity(choice);
|
|
1603
|
+
const group = groupsByBundle.get(key) ?? {
|
|
1604
|
+
bundle: choice.bundle,
|
|
1605
|
+
source: choice.source,
|
|
1606
|
+
tools: choice.tools,
|
|
1607
|
+
items: [],
|
|
1608
|
+
};
|
|
1609
|
+
group.items.push(choice.item);
|
|
1610
|
+
groupsByBundle.set(key, group);
|
|
1611
|
+
}
|
|
1612
|
+
return Array.from(groupsByBundle.values());
|
|
1613
|
+
}
|
|
1614
|
+
function encodeBundleItemApplyChoice(choice) {
|
|
1615
|
+
return JSON.stringify([choice.source, choice.bundle, choice.item]);
|
|
1616
|
+
}
|
|
1617
|
+
async function prepareApplyBundle(options) {
|
|
1618
|
+
const refreshedSources = options.refreshedSources ?? new Set();
|
|
1619
|
+
const cloneLines = refreshBundleSourceForApply(options, refreshedSources);
|
|
1620
|
+
let cachedBundle;
|
|
1621
|
+
let bundleSource;
|
|
1622
|
+
let selectedToolsBeforeBundle;
|
|
1623
|
+
try {
|
|
1624
|
+
cachedBundle = findCachedBundleWithGuidance({
|
|
1625
|
+
libraryDir: options.libraryDir,
|
|
1626
|
+
bundle: options.bundle,
|
|
1627
|
+
source: options.source,
|
|
1628
|
+
});
|
|
1629
|
+
bundleSource = options.source ?? cachedBundle.source;
|
|
1630
|
+
}
|
|
1631
|
+
catch (error) {
|
|
1632
|
+
if (!shouldPromptForInferredBundle({
|
|
1633
|
+
error,
|
|
1634
|
+
libraryDir: options.libraryDir,
|
|
1635
|
+
source: options.source,
|
|
1636
|
+
inferredBundleFromSource: options.inferredBundleFromSource,
|
|
1637
|
+
})) {
|
|
1638
|
+
throw error;
|
|
1639
|
+
}
|
|
1640
|
+
selectedToolsBeforeBundle = await selectToolsBeforeBundle({
|
|
1641
|
+
libraryDir: options.libraryDir,
|
|
1642
|
+
source: options.source,
|
|
1643
|
+
requestedTools: options.requestedTools,
|
|
1644
|
+
prompts: options.preBundlePrompts ?? options.prompts,
|
|
1645
|
+
});
|
|
1646
|
+
const toolsForBundleSelection = selectedToolsBeforeBundle ??
|
|
1647
|
+
(options.requestedTools.length > 0 ? options.requestedTools : undefined);
|
|
1648
|
+
const selection = toolsForBundleSelection
|
|
1649
|
+
? await options.prompts.selectBundle(options.source, toolsForBundleSelection)
|
|
1650
|
+
: await options.prompts.selectBundle(options.source);
|
|
1651
|
+
cachedBundle = findCachedBundleWithGuidance({
|
|
1652
|
+
libraryDir: options.libraryDir,
|
|
1653
|
+
bundle: selection.bundle,
|
|
1654
|
+
source: selection.source ?? options.source,
|
|
1655
|
+
});
|
|
1656
|
+
bundleSource = selection.source ?? options.source ?? cachedBundle.source;
|
|
1657
|
+
}
|
|
1658
|
+
if (bundleSource &&
|
|
1659
|
+
(options.source !== undefined || options.ref !== undefined)) {
|
|
1660
|
+
cloneLines.push(...refreshBundleSourceForApply({
|
|
1661
|
+
source: bundleSource,
|
|
1662
|
+
libraryDir: options.libraryDir,
|
|
1663
|
+
protocol: options.protocol,
|
|
1664
|
+
ref: options.ref,
|
|
1665
|
+
}, refreshedSources));
|
|
1666
|
+
cachedBundle = findCachedBundleWithGuidance({
|
|
1667
|
+
libraryDir: options.libraryDir,
|
|
1668
|
+
bundle: cachedBundle.bundle,
|
|
1669
|
+
source: bundleSource,
|
|
1670
|
+
});
|
|
1671
|
+
}
|
|
1672
|
+
const sourceRevision = bundleSource
|
|
1673
|
+
? (0, bundle_fetch_1.readCachedSourceRevision)({
|
|
1674
|
+
source: bundleSource,
|
|
1675
|
+
libraryDir: options.libraryDir,
|
|
1676
|
+
})
|
|
1677
|
+
: undefined;
|
|
1678
|
+
const availableTools = Object.keys(cachedBundle.manifest.tools);
|
|
1679
|
+
const wasExplicitlyRequested = options.requestedTools.length > 0;
|
|
1680
|
+
const toolsToAssert = selectedToolsBeforeBundle ?? options.requestedTools;
|
|
1681
|
+
if (toolsToAssert.length > 0) {
|
|
1682
|
+
assertBundleSupportsRequestedTools(toolsToAssert, availableTools);
|
|
1683
|
+
}
|
|
1684
|
+
const selectedRequestedTools = selectedToolsBeforeBundle ??
|
|
1685
|
+
(wasExplicitlyRequested || availableTools.length <= 1
|
|
1686
|
+
? wasExplicitlyRequested
|
|
1687
|
+
? options.requestedTools
|
|
1688
|
+
: availableTools
|
|
1689
|
+
: await options.prompts.selectAgents(availableTools));
|
|
1690
|
+
const hasToolSelection = wasExplicitlyRequested ||
|
|
1691
|
+
selectedRequestedTools.length < availableTools.length;
|
|
1692
|
+
const nextToolNames = selectedRequestedTools;
|
|
1693
|
+
const existingDesiredEntry = options.existingDesiredState.find((entry) => entry.bundle === cachedBundle.bundle);
|
|
1694
|
+
const selectedItems = await resolveSelectedBundleItems({
|
|
1695
|
+
bundleDir: node_path_1.default.dirname(cachedBundle.manifestFile),
|
|
1696
|
+
manifest: cachedBundle.manifest,
|
|
1697
|
+
tools: nextToolNames,
|
|
1698
|
+
requestedItems: options.requestedItems,
|
|
1699
|
+
selectItems: options.selectItems,
|
|
1700
|
+
replaceItems: options.replaceItems,
|
|
1701
|
+
prompts: options.prompts,
|
|
1702
|
+
existingItems: existingDesiredEntry?.items,
|
|
1703
|
+
});
|
|
1704
|
+
return {
|
|
1705
|
+
cloneLines,
|
|
1706
|
+
cachedBundle,
|
|
1707
|
+
bundleSource,
|
|
1708
|
+
sourceRevision,
|
|
1709
|
+
...(hasToolSelection ? { selectedTools: selectedRequestedTools } : {}),
|
|
1710
|
+
...(selectedItems !== undefined ? { selectedItems } : {}),
|
|
1711
|
+
nextToolNames,
|
|
1712
|
+
toolLabel: nextToolNames.join(", "),
|
|
1713
|
+
hasToolSelection,
|
|
1714
|
+
replacesItemSelection: options.selectItems || options.replaceItems === true,
|
|
1715
|
+
};
|
|
1716
|
+
}
|
|
1717
|
+
async function selectToolsBeforeBundle(options) {
|
|
1718
|
+
if (!options.source || options.requestedTools.length > 0) {
|
|
1719
|
+
return undefined;
|
|
1720
|
+
}
|
|
1721
|
+
const availableTools = listSelectableToolsForSource({
|
|
1722
|
+
libraryDir: options.libraryDir,
|
|
1723
|
+
source: options.source,
|
|
1724
|
+
});
|
|
1725
|
+
if (availableTools.length <= 1) {
|
|
1726
|
+
return undefined;
|
|
1727
|
+
}
|
|
1728
|
+
return options.prompts.selectAgents(availableTools);
|
|
1729
|
+
}
|
|
1730
|
+
function listSelectableToolsForSource(options) {
|
|
1731
|
+
const toolNames = new Set();
|
|
1732
|
+
for (const bundle of (0, bundle_discovery_1.listCachedBundles)({ libraryDir: options.libraryDir })) {
|
|
1733
|
+
if (bundle.source !== options.source) {
|
|
1734
|
+
continue;
|
|
1735
|
+
}
|
|
1736
|
+
for (const toolName of Object.keys(bundle.manifest.tools)) {
|
|
1737
|
+
toolNames.add(toolName);
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
return Array.from(toolNames).sort((left, right) => left.localeCompare(right));
|
|
1741
|
+
}
|
|
1742
|
+
async function resolveSelectedBundleItems(options) {
|
|
1743
|
+
if (!options.selectItems && options.requestedItems.length === 0) {
|
|
1744
|
+
return undefined;
|
|
1745
|
+
}
|
|
1746
|
+
const availableItems = (0, bundle_items_1.listSelectableBundleItems)({
|
|
1747
|
+
bundleDir: options.bundleDir,
|
|
1748
|
+
manifest: options.manifest,
|
|
1749
|
+
tools: options.tools,
|
|
1750
|
+
});
|
|
1751
|
+
(0, bundle_items_1.assertBundleSupportsRequestedItems)({
|
|
1752
|
+
requestedItems: options.requestedItems,
|
|
1753
|
+
availableItems,
|
|
1754
|
+
});
|
|
1755
|
+
const mergedItems = (0, bundle_items_1.mergeDesiredBundleItems)({
|
|
1756
|
+
existingItems: options.existingItems,
|
|
1757
|
+
requestedItems: (0, bundle_items_1.normalizeBundleItemSelectors)(options.requestedItems),
|
|
1758
|
+
replace: options.replaceItems === true,
|
|
1759
|
+
});
|
|
1760
|
+
if (!options.selectItems) {
|
|
1761
|
+
return mergedItems;
|
|
1762
|
+
}
|
|
1763
|
+
return options.prompts.selectBundleItems(availableItems, mergedItems ?? []);
|
|
1764
|
+
}
|
|
1765
|
+
function shouldPromptForInferredBundle(options) {
|
|
1766
|
+
if (!options.inferredBundleFromSource || !options.source) {
|
|
1767
|
+
return false;
|
|
1768
|
+
}
|
|
1769
|
+
if (!(options.error instanceof Error) ||
|
|
1770
|
+
!/^Bundle not found: /.test(options.error.message)) {
|
|
1771
|
+
return false;
|
|
1772
|
+
}
|
|
1773
|
+
return (0, bundle_discovery_1.listCachedBundles)({ libraryDir: options.libraryDir }).some((bundle) => bundle.source === options.source);
|
|
1774
|
+
}
|
|
1775
|
+
function refreshBundleSourceForApply(options, refreshedSources) {
|
|
1776
|
+
if (!options.source || refreshedSources.has(options.source)) {
|
|
1777
|
+
return [];
|
|
1778
|
+
}
|
|
1779
|
+
refreshedSources.add(options.source);
|
|
1780
|
+
const initialRevision = (0, bundle_fetch_1.readCachedSourceRevision)({
|
|
1781
|
+
source: options.source,
|
|
1782
|
+
libraryDir: options.libraryDir,
|
|
1783
|
+
protocol: options.protocol,
|
|
1784
|
+
});
|
|
1785
|
+
if (initialRevision.cached && initialRevision.remoteUrl === undefined) {
|
|
1786
|
+
return [];
|
|
1787
|
+
}
|
|
1788
|
+
if (!options.ref && initialRevision.cached) {
|
|
1789
|
+
(0, bundle_fetch_1.clearAndRefetchCachedRemoteSource)({
|
|
1790
|
+
source: options.source,
|
|
1791
|
+
libraryDir: options.libraryDir,
|
|
1792
|
+
protocol: options.protocol,
|
|
1793
|
+
});
|
|
1794
|
+
}
|
|
1795
|
+
else {
|
|
1796
|
+
(0, bundle_fetch_1.updateCachedRemoteSource)({
|
|
1797
|
+
source: options.source,
|
|
1798
|
+
libraryDir: options.libraryDir,
|
|
1799
|
+
protocol: options.protocol,
|
|
1800
|
+
ref: options.ref,
|
|
1801
|
+
});
|
|
1802
|
+
}
|
|
1803
|
+
return initialRevision.cached ? [] : [pc.dim(`Cloned ${options.source}`)];
|
|
1804
|
+
}
|
|
1805
|
+
function assertBundleSupportsRequestedTools(requestedTools, availableTools) {
|
|
1806
|
+
const unsupportedTools = requestedTools.filter((toolName) => !availableTools.includes(toolName));
|
|
1807
|
+
if (unsupportedTools.length === 0) {
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
throw new Error(`Bundle does not support tool(s): ${unsupportedTools.join(", ")}\nSupported tools: ${availableTools.join(", ")}`);
|
|
1811
|
+
}
|
|
1812
|
+
function buildDesiredEntryForAppliedBundle(options) {
|
|
1813
|
+
const existingDesiredEntry = options.existingDesiredState.find((entry) => entry.bundle === options.cachedBundle.bundle);
|
|
1814
|
+
const mergedDesiredTools = mergeDesiredTools({
|
|
1815
|
+
existingEntry: existingDesiredEntry,
|
|
1816
|
+
requestedTools: options.requestedTools,
|
|
1817
|
+
replace: options.replaceRequestedTools,
|
|
1818
|
+
});
|
|
1819
|
+
const mergedDesiredItems = (0, bundle_items_1.mergeDesiredBundleItems)({
|
|
1820
|
+
existingItems: existingDesiredEntry?.items,
|
|
1821
|
+
requestedItems: options.requestedItems,
|
|
1822
|
+
replace: options.replaceRequestedItems ?? false,
|
|
1823
|
+
});
|
|
1824
|
+
const preservesExistingRef = existingDesiredEntry?.ref !== undefined &&
|
|
1825
|
+
(options.requestedSource === undefined ||
|
|
1826
|
+
options.requestedSource === existingDesiredEntry.source);
|
|
1827
|
+
const sourceProtocol = options.sourceRevision?.remoteUrl !== undefined
|
|
1828
|
+
? (0, bundle_discovery_1.detectSourceProtocol)(options.sourceRevision.remoteUrl)
|
|
1829
|
+
: undefined;
|
|
1830
|
+
const desiredProtocol = options.requestedSource !== undefined
|
|
1831
|
+
? options.requestedProtocol
|
|
1832
|
+
: existingDesiredEntry?.source !== undefined
|
|
1833
|
+
? (existingDesiredEntry.protocol ?? sourceProtocol ?? "https")
|
|
1834
|
+
: (sourceProtocol ?? existingDesiredEntry?.protocol ?? "https");
|
|
1835
|
+
return {
|
|
1836
|
+
bundle: options.cachedBundle.bundle,
|
|
1837
|
+
...(options.requestedSource !== undefined
|
|
1838
|
+
? { source: options.requestedSource }
|
|
1839
|
+
: existingDesiredEntry?.source !== undefined
|
|
1840
|
+
? { source: existingDesiredEntry.source }
|
|
1841
|
+
: options.cachedBundle.source !== undefined
|
|
1842
|
+
? { source: options.cachedBundle.source }
|
|
1843
|
+
: {}),
|
|
1844
|
+
...(mergedDesiredTools !== undefined ? { tools: mergedDesiredTools } : {}),
|
|
1845
|
+
...(mergedDesiredItems !== undefined ? { items: mergedDesiredItems } : {}),
|
|
1846
|
+
protocol: desiredProtocol,
|
|
1847
|
+
...(options.requestedRef !== undefined
|
|
1848
|
+
? { ref: options.requestedRef }
|
|
1849
|
+
: preservesExistingRef
|
|
1850
|
+
? { ref: existingDesiredEntry.ref }
|
|
1851
|
+
: {}),
|
|
1852
|
+
...(options.sourceRevision?.currentRef !== undefined
|
|
1853
|
+
? { resolved_ref: options.sourceRevision.currentRef }
|
|
1854
|
+
: existingDesiredEntry?.resolved_ref !== undefined
|
|
1855
|
+
? { resolved_ref: existingDesiredEntry.resolved_ref }
|
|
1856
|
+
: {}),
|
|
1857
|
+
...(options.sourceRevision?.currentCommit !== undefined
|
|
1858
|
+
? { resolved_commit: options.sourceRevision.currentCommit }
|
|
1859
|
+
: existingDesiredEntry?.resolved_commit !== undefined
|
|
1860
|
+
? { resolved_commit: existingDesiredEntry.resolved_commit }
|
|
1861
|
+
: {}),
|
|
1862
|
+
...((options.disableModelInvocation ??
|
|
1863
|
+
existingDesiredEntry?.disable_model_invocation)
|
|
1864
|
+
? { disable_model_invocation: true }
|
|
1865
|
+
: {}),
|
|
1866
|
+
};
|
|
1867
|
+
}
|
|
1868
|
+
async function resetWorktree(options) {
|
|
1869
|
+
const gitContext = requireGitContext(options.cwd, "reset");
|
|
1870
|
+
let registry = readRegistryWithGuidance(options.registryFile);
|
|
1871
|
+
const worktreeState = registry.worktrees[gitContext.worktreeId];
|
|
1872
|
+
const hasMaterializedBundles = worktreeState
|
|
1873
|
+
? worktreeHasMaterializedBundles(worktreeState.materialized_state)
|
|
1874
|
+
: false;
|
|
1875
|
+
const hasShadowedFiles = worktreeState
|
|
1876
|
+
? Object.keys(worktreeState.shadowed_files).length > 0
|
|
1877
|
+
: false;
|
|
1878
|
+
if (options.dryRun) {
|
|
1879
|
+
if (!hasMaterializedBundles && !hasShadowedFiles) {
|
|
1880
|
+
return `${pc.yellow("DRY RUN:")} No Skul-managed files found in the current worktree`;
|
|
1881
|
+
}
|
|
1882
|
+
const allFiles = Object.values(worktreeState.materialized_state.bundles).flatMap((bundleState) => Object.values(bundleState.tools).flatMap((toolState) => toolState.files));
|
|
1883
|
+
const lines = [
|
|
1884
|
+
`${pc.yellow("DRY RUN:")} Would restore ${Object.keys(worktreeState.shadowed_files).length} tracked shadow file(s) and remove ${allFiles.length} managed file(s) from ${gitContext.worktreeRoot}`,
|
|
1885
|
+
];
|
|
1886
|
+
for (const file of allFiles) {
|
|
1887
|
+
lines.push(` ${file}`);
|
|
1888
|
+
}
|
|
1889
|
+
for (const filePath of Object.keys(worktreeState.shadowed_files)) {
|
|
1890
|
+
lines.push(` ${filePath}`);
|
|
1891
|
+
}
|
|
1892
|
+
return lines.join("\n");
|
|
1893
|
+
}
|
|
1894
|
+
if ((hasMaterializedBundles || hasShadowedFiles) && worktreeState) {
|
|
1895
|
+
const allBundlePaths = Object.values(worktreeState.materialized_state.bundles).map(flattenBundleState);
|
|
1896
|
+
// Confirm all removals before touching any files (all-or-nothing)
|
|
1897
|
+
for (const bundlePaths of allBundlePaths) {
|
|
1898
|
+
const resetAllowed = await confirmManagedFileRemovals(gitContext.worktreeRoot, bundlePaths, options.prompts, "reset");
|
|
1899
|
+
if (!resetAllowed) {
|
|
1900
|
+
throw new Error("Reset aborted because a modified managed file was kept");
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
const remainingShadowedFiles = retireTrackedRootInstructionShadows({
|
|
1904
|
+
repoRoot: gitContext.worktreeRoot,
|
|
1905
|
+
shadowedFiles: worktreeState.shadowed_files,
|
|
1906
|
+
filePaths: Object.keys(worktreeState.shadowed_files),
|
|
1907
|
+
});
|
|
1908
|
+
for (const bundlePaths of allBundlePaths) {
|
|
1909
|
+
removeManagedPaths(gitContext.worktreeRoot, bundlePaths);
|
|
1910
|
+
}
|
|
1911
|
+
(0, root_instruction_state_1.restoreRootInstructionBaseContents)({
|
|
1912
|
+
repoRoot: gitContext.worktreeRoot,
|
|
1913
|
+
baseContents: worktreeState.materialized_state.root_instruction_base_contents,
|
|
1914
|
+
targetPaths: (0, root_instruction_state_1.collectManagedRootInstructionTargets)(worktreeState.materialized_state.bundles),
|
|
1915
|
+
});
|
|
1916
|
+
if (Object.keys(remainingShadowedFiles).length > 0) {
|
|
1917
|
+
registry = (0, registry_1.upsertWorktreeState)(registry, gitContext.worktreeId, {
|
|
1918
|
+
repo_fingerprint: gitContext.repoFingerprint,
|
|
1919
|
+
path: gitContext.worktreeRoot,
|
|
1920
|
+
materialized_state: {
|
|
1921
|
+
bundles: {},
|
|
1922
|
+
exclude_configured: false,
|
|
1923
|
+
...(worktreeState.materialized_state
|
|
1924
|
+
.root_instruction_base_contents !== undefined
|
|
1925
|
+
? {
|
|
1926
|
+
root_instruction_base_contents: worktreeState.materialized_state
|
|
1927
|
+
.root_instruction_base_contents,
|
|
1928
|
+
}
|
|
1929
|
+
: {}),
|
|
1930
|
+
},
|
|
1931
|
+
shadowed_files: remainingShadowedFiles,
|
|
1932
|
+
});
|
|
1933
|
+
}
|
|
1934
|
+
else {
|
|
1935
|
+
registry = (0, registry_1.removeWorktreeState)(registry, gitContext.worktreeId);
|
|
1936
|
+
}
|
|
1937
|
+
(0, registry_1.writeRegistryFile)(options.registryFile, registry);
|
|
1938
|
+
}
|
|
1939
|
+
const excludeRemoved = (0, git_exclude_1.removeSkulExcludeBlock)({ gitDir: gitContext.gitDir });
|
|
1940
|
+
if (!hasMaterializedBundles && !hasShadowedFiles && !excludeRemoved) {
|
|
1941
|
+
return "No Skul-managed files found in the current worktree";
|
|
1942
|
+
}
|
|
1943
|
+
return pc.green("Reset Skul-managed files from the current worktree");
|
|
1944
|
+
}
|
|
1945
|
+
async function removeBundle(options) {
|
|
1946
|
+
const gitContext = requireGitContext(options.cwd, "remove");
|
|
1947
|
+
let registry = readRegistryWithGuidance(options.registryFile);
|
|
1948
|
+
const repoState = registry.repos[gitContext.repoFingerprint];
|
|
1949
|
+
const worktreeState = registry.worktrees[gitContext.worktreeId];
|
|
1950
|
+
if (shouldRemoveItemsAcrossBundles(options)) {
|
|
1951
|
+
return removeBundleItemsAcrossActiveBundles({
|
|
1952
|
+
cwd: options.cwd,
|
|
1953
|
+
prompts: options.prompts,
|
|
1954
|
+
registryFile: options.registryFile,
|
|
1955
|
+
libraryDir: options.libraryDir,
|
|
1956
|
+
repoState,
|
|
1957
|
+
source: options.source,
|
|
1958
|
+
bundle: options.inferredBundleFromSource ? undefined : options.bundle,
|
|
1959
|
+
includeItems: options.includeItems,
|
|
1960
|
+
selectItems: options.selectItems,
|
|
1961
|
+
dryRun: options.dryRun,
|
|
1962
|
+
});
|
|
1963
|
+
}
|
|
1964
|
+
const selection = await resolveRemoveBundleSelection({
|
|
1965
|
+
requestedBundle: options.bundle,
|
|
1966
|
+
requestedSource: options.source,
|
|
1967
|
+
inferredBundleFromSource: options.inferredBundleFromSource,
|
|
1968
|
+
repoState,
|
|
1969
|
+
worktreeState,
|
|
1970
|
+
prompts: options.prompts,
|
|
1971
|
+
});
|
|
1972
|
+
const bundle = selection.bundle;
|
|
1973
|
+
const source = selection.source;
|
|
1974
|
+
const isInDesiredState = repoState?.desired_state.some((e) => e.bundle === bundle && matchesOptionalSource(e.source, source)) ?? false;
|
|
1975
|
+
const desiredEntry = repoState?.desired_state.find((e) => e.bundle === bundle && matchesOptionalSource(e.source, source));
|
|
1976
|
+
const bundleMaterializedState = findMaterializedBundleState({
|
|
1977
|
+
worktreeState,
|
|
1978
|
+
bundle,
|
|
1979
|
+
source,
|
|
1980
|
+
});
|
|
1981
|
+
const shadowedFilesForBundle = Object.entries(worktreeState?.shadowed_files ?? {}).filter(([, shadowedFile]) => shadowedFile.bundle === bundle);
|
|
1982
|
+
if (!isInDesiredState && !bundleMaterializedState) {
|
|
1983
|
+
const configured = repoState?.desired_state
|
|
1984
|
+
.filter((entry) => matchesOptionalSource(entry.source, source))
|
|
1985
|
+
.map((e) => e.bundle) ?? [];
|
|
1986
|
+
const hint = configured.length > 0
|
|
1987
|
+
? `Configured bundles: ${configured.join(", ")}`
|
|
1988
|
+
: `No bundles are configured yet. Run "skul add <bundle>" to add one`;
|
|
1989
|
+
throw new Error(`Bundle not found in active set: ${bundle}. ${hint}`);
|
|
1990
|
+
}
|
|
1991
|
+
if (options.includeItems.length > 0 || options.selectItems) {
|
|
1992
|
+
const itemRemoval = await removeBundleItems({
|
|
1993
|
+
cwd: options.cwd,
|
|
1994
|
+
prompts: options.prompts,
|
|
1995
|
+
registryFile: options.registryFile,
|
|
1996
|
+
libraryDir: options.libraryDir,
|
|
1997
|
+
gitContext,
|
|
1998
|
+
registry,
|
|
1999
|
+
repoState,
|
|
2000
|
+
worktreeState,
|
|
2001
|
+
desiredEntry,
|
|
2002
|
+
bundle,
|
|
2003
|
+
source,
|
|
2004
|
+
includeItems: options.includeItems,
|
|
2005
|
+
selectItems: options.selectItems,
|
|
2006
|
+
dryRun: options.dryRun,
|
|
2007
|
+
});
|
|
2008
|
+
if (itemRemoval.kind === "completed") {
|
|
2009
|
+
return itemRemoval.output;
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
if (options.dryRun) {
|
|
2013
|
+
if (bundleMaterializedState || shadowedFilesForBundle.length > 0) {
|
|
2014
|
+
const files = bundleMaterializedState
|
|
2015
|
+
? Object.values(bundleMaterializedState.tools).flatMap((toolState) => toolState.files)
|
|
2016
|
+
: [];
|
|
2017
|
+
const lines = [
|
|
2018
|
+
`${pc.yellow("DRY RUN:")} Would remove ${bundle} (${files.length + shadowedFilesForBundle.length} file(s))`,
|
|
2019
|
+
];
|
|
2020
|
+
for (const file of files) {
|
|
2021
|
+
lines.push(` ${file}`);
|
|
2022
|
+
}
|
|
2023
|
+
for (const [filePath] of shadowedFilesForBundle) {
|
|
2024
|
+
lines.push(` ${filePath}`);
|
|
2025
|
+
}
|
|
2026
|
+
return lines.join("\n");
|
|
2027
|
+
}
|
|
2028
|
+
return `${pc.yellow("DRY RUN:")} Would remove ${bundle} from desired state (not yet materialized in this worktree)`;
|
|
2029
|
+
}
|
|
2030
|
+
let currentShadowedFiles = { ...(worktreeState?.shadowed_files ?? {}) };
|
|
2031
|
+
if (bundleMaterializedState || shadowedFilesForBundle.length > 0) {
|
|
2032
|
+
const bundlePaths = bundleMaterializedState
|
|
2033
|
+
? flattenBundleState(bundleMaterializedState)
|
|
2034
|
+
: { files: [], file_fingerprints: {}, directories: [] };
|
|
2035
|
+
const rootInstructionBaseContents = worktreeState?.materialized_state.root_instruction_base_contents;
|
|
2036
|
+
const removedRootInstructionPaths = new Set(bundlePaths.files.filter((filePath) => (0, root_instruction_render_1.isRootInstructionPath)(filePath)));
|
|
2037
|
+
const remainingBundles = { ...worktreeState.materialized_state.bundles };
|
|
2038
|
+
delete remainingBundles[bundle];
|
|
2039
|
+
const remainingDesiredState = repoState?.desired_state.filter((entry) => !matchesBundleIdentity(entry, bundle, source)) ?? [];
|
|
2040
|
+
const rewrittenRootInstructionPaths = new Set(Array.from((0, root_instruction_state_1.collectManagedRootInstructionTargets)(remainingBundles)).filter((filePath) => removedRootInstructionPaths.has(filePath)));
|
|
2041
|
+
(0, root_instruction_state_1.assertManagedRootInstructionSyncSourcesCached)({
|
|
2042
|
+
desiredState: remainingDesiredState,
|
|
2043
|
+
materializedBundles: remainingBundles,
|
|
2044
|
+
targetPaths: rewrittenRootInstructionPaths,
|
|
2045
|
+
resolveCachedBundle: (entry) => resolveDesiredCachedBundle(options.libraryDir, entry),
|
|
2046
|
+
});
|
|
2047
|
+
const removeAllowed = await confirmManagedFileRemovals(gitContext.worktreeRoot, bundlePaths, options.prompts, "remove");
|
|
2048
|
+
if (!removeAllowed) {
|
|
2049
|
+
throw new Error("Removal aborted because a modified managed file was kept");
|
|
2050
|
+
}
|
|
2051
|
+
currentShadowedFiles = retireTrackedRootInstructionShadows({
|
|
2052
|
+
repoRoot: gitContext.worktreeRoot,
|
|
2053
|
+
shadowedFiles: currentShadowedFiles,
|
|
2054
|
+
filePaths: shadowedFilesForBundle.map(([filePath]) => filePath),
|
|
2055
|
+
});
|
|
2056
|
+
if (Object.keys(remainingBundles).length > 0) {
|
|
2057
|
+
assertTrackedRootInstructionShadowSafetyForPaths({
|
|
2058
|
+
repoRoot: gitContext.worktreeRoot,
|
|
2059
|
+
operation: "refresh",
|
|
2060
|
+
filePaths: Array.from(rewrittenRootInstructionPaths),
|
|
2061
|
+
});
|
|
2062
|
+
}
|
|
2063
|
+
removeManagedPaths(gitContext.worktreeRoot, bundlePaths);
|
|
2064
|
+
const remainingRootInstructionTargets = (0, root_instruction_state_1.collectManagedRootInstructionTargets)(remainingBundles);
|
|
2065
|
+
const restoredRootInstructionPaths = new Set(Array.from(removedRootInstructionPaths).filter((filePath) => !remainingRootInstructionTargets.has(filePath)));
|
|
2066
|
+
(0, root_instruction_state_1.restoreRootInstructionBaseContents)({
|
|
2067
|
+
repoRoot: gitContext.worktreeRoot,
|
|
2068
|
+
baseContents: rootInstructionBaseContents,
|
|
2069
|
+
targetPaths: restoredRootInstructionPaths,
|
|
2070
|
+
});
|
|
2071
|
+
const nextRootInstructionBaseContents = rootInstructionBaseContents
|
|
2072
|
+
? Object.fromEntries(Object.entries(rootInstructionBaseContents).filter(([filePath]) => !restoredRootInstructionPaths.has(filePath)))
|
|
2073
|
+
: undefined;
|
|
2074
|
+
if (Object.keys(remainingBundles).length > 0) {
|
|
2075
|
+
const syncedRootInstructionPaths = (0, root_instruction_state_1.syncManagedRootInstructionFiles)({
|
|
2076
|
+
repoRoot: gitContext.worktreeRoot,
|
|
2077
|
+
desiredState: remainingDesiredState,
|
|
2078
|
+
materializedBundles: remainingBundles,
|
|
2079
|
+
rootInstructionBaseContents: nextRootInstructionBaseContents,
|
|
2080
|
+
targetPaths: rewrittenRootInstructionPaths,
|
|
2081
|
+
resolveCachedBundle: (entry) => resolveDesiredCachedBundle(options.libraryDir, entry),
|
|
2082
|
+
});
|
|
2083
|
+
const refreshedRemainingBundles = (0, root_instruction_state_1.refreshManagedFileFingerprintsForPaths)(gitContext.worktreeRoot, remainingBundles, syncedRootInstructionPaths);
|
|
2084
|
+
const newMatState = {
|
|
2085
|
+
bundles: refreshedRemainingBundles,
|
|
2086
|
+
exclude_configured: false,
|
|
2087
|
+
...(nextRootInstructionBaseContents !== undefined &&
|
|
2088
|
+
Object.keys(nextRootInstructionBaseContents).length > 0
|
|
2089
|
+
? { root_instruction_base_contents: nextRootInstructionBaseContents }
|
|
2090
|
+
: {}),
|
|
2091
|
+
};
|
|
2092
|
+
const managedFiles = collectAllFiles(newMatState);
|
|
2093
|
+
newMatState.exclude_configured = managedFiles.length > 0;
|
|
2094
|
+
if (managedFiles.length > 0) {
|
|
2095
|
+
(0, git_exclude_1.configureSkulExcludeBlock)({
|
|
2096
|
+
gitDir: gitContext.gitDir,
|
|
2097
|
+
files: managedFiles,
|
|
2098
|
+
});
|
|
2099
|
+
}
|
|
2100
|
+
else {
|
|
2101
|
+
(0, git_exclude_1.removeSkulExcludeBlock)({ gitDir: gitContext.gitDir });
|
|
2102
|
+
}
|
|
2103
|
+
registry = (0, registry_1.upsertWorktreeState)(registry, gitContext.worktreeId, {
|
|
2104
|
+
repo_fingerprint: gitContext.repoFingerprint,
|
|
2105
|
+
path: gitContext.worktreeRoot,
|
|
2106
|
+
materialized_state: newMatState,
|
|
2107
|
+
shadowed_files: currentShadowedFiles,
|
|
2108
|
+
});
|
|
2109
|
+
}
|
|
2110
|
+
else {
|
|
2111
|
+
(0, git_exclude_1.removeSkulExcludeBlock)({ gitDir: gitContext.gitDir });
|
|
2112
|
+
if (Object.keys(currentShadowedFiles).length > 0) {
|
|
2113
|
+
registry = (0, registry_1.upsertWorktreeState)(registry, gitContext.worktreeId, {
|
|
2114
|
+
repo_fingerprint: gitContext.repoFingerprint,
|
|
2115
|
+
path: gitContext.worktreeRoot,
|
|
2116
|
+
materialized_state: {
|
|
2117
|
+
bundles: {},
|
|
2118
|
+
exclude_configured: false,
|
|
2119
|
+
},
|
|
2120
|
+
shadowed_files: currentShadowedFiles,
|
|
2121
|
+
});
|
|
2122
|
+
}
|
|
2123
|
+
else {
|
|
2124
|
+
registry = (0, registry_1.removeWorktreeState)(registry, gitContext.worktreeId);
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
if (isInDesiredState && repoState) {
|
|
2129
|
+
const newDesiredState = repoState.desired_state.filter((entry) => !matchesBundleIdentity(entry, bundle, source));
|
|
2130
|
+
registry = (0, registry_1.upsertRepoState)(registry, gitContext.repoFingerprint, {
|
|
2131
|
+
...repoState,
|
|
2132
|
+
repo_root: gitContext.repoRoot,
|
|
2133
|
+
desired_state: newDesiredState,
|
|
2134
|
+
});
|
|
2135
|
+
}
|
|
2136
|
+
(0, registry_1.writeRegistryFile)(options.registryFile, registry);
|
|
2137
|
+
return pc.green(`Removed ${bundle}`);
|
|
2138
|
+
}
|
|
2139
|
+
function shouldRemoveItemsAcrossBundles(options) {
|
|
2140
|
+
return (options.selectItems ||
|
|
2141
|
+
(options.includeItems.length > 0 &&
|
|
2142
|
+
(options.bundle === undefined ||
|
|
2143
|
+
options.inferredBundleFromSource === true)));
|
|
2144
|
+
}
|
|
2145
|
+
async function removeBundleItemsAcrossActiveBundles(options) {
|
|
2146
|
+
if (!options.repoState || options.repoState.desired_state.length === 0) {
|
|
2147
|
+
throw new Error(options.source
|
|
2148
|
+
? `No active bundles found for ${options.source}. Run "skul add ${options.source} <bundle>" to add one first`
|
|
2149
|
+
: 'No active bundles found. Run "skul add <bundle>" to add one first');
|
|
2150
|
+
}
|
|
2151
|
+
const choices = listActiveBundleItemRemovalChoices({
|
|
2152
|
+
libraryDir: options.libraryDir,
|
|
2153
|
+
desiredState: options.repoState.desired_state,
|
|
2154
|
+
source: options.source,
|
|
2155
|
+
bundle: options.bundle,
|
|
2156
|
+
});
|
|
2157
|
+
const requestedItems = (0, bundle_items_1.normalizeBundleItemSelectors)(options.includeItems);
|
|
2158
|
+
const selectedValues = options.selectItems
|
|
2159
|
+
? await promptForBundleItemRemovalChoices({
|
|
2160
|
+
prompts: options.prompts,
|
|
2161
|
+
choices,
|
|
2162
|
+
requestedItems,
|
|
2163
|
+
})
|
|
2164
|
+
: selectRequestedBundleItemRemovalChoices({
|
|
2165
|
+
choices,
|
|
2166
|
+
requestedItems,
|
|
2167
|
+
});
|
|
2168
|
+
const removalPlan = planBundleItemRemovals({
|
|
2169
|
+
desiredState: options.repoState.desired_state,
|
|
2170
|
+
choices,
|
|
2171
|
+
selectedValues,
|
|
2172
|
+
});
|
|
2173
|
+
if (options.dryRun) {
|
|
2174
|
+
return `${pc.yellow("DRY RUN:")} Would remove ${formatBundleItemRemovalSummary(removalPlan.removedItems)}`;
|
|
2175
|
+
}
|
|
2176
|
+
for (const target of groupBundleItemRemovalTargets(removalPlan.removedItems)) {
|
|
2177
|
+
await removeBundle({
|
|
2178
|
+
cwd: options.cwd,
|
|
2179
|
+
prompts: options.prompts,
|
|
2180
|
+
registryFile: options.registryFile,
|
|
2181
|
+
libraryDir: options.libraryDir,
|
|
2182
|
+
bundle: target.bundle,
|
|
2183
|
+
source: target.source,
|
|
2184
|
+
includeItems: target.items,
|
|
2185
|
+
selectItems: false,
|
|
2186
|
+
dryRun: false,
|
|
2187
|
+
});
|
|
2188
|
+
}
|
|
2189
|
+
return pc.green(`Removed ${formatBundleItemRemovalSummary(removalPlan.removedItems)}`);
|
|
2190
|
+
}
|
|
2191
|
+
function groupBundleItemRemovalTargets(removedItems) {
|
|
2192
|
+
const groupsByBundle = new Map();
|
|
2193
|
+
for (const item of removedItems) {
|
|
2194
|
+
const key = encodeBundleIdentity(item);
|
|
2195
|
+
const group = groupsByBundle.get(key) ?? {
|
|
2196
|
+
bundle: item.bundle,
|
|
2197
|
+
source: item.source,
|
|
2198
|
+
items: [],
|
|
2199
|
+
};
|
|
2200
|
+
group.items.push(item.item);
|
|
2201
|
+
groupsByBundle.set(key, group);
|
|
2202
|
+
}
|
|
2203
|
+
return Array.from(groupsByBundle.values());
|
|
2204
|
+
}
|
|
2205
|
+
function listActiveBundleItemRemovalChoices(options) {
|
|
2206
|
+
const choices = options.desiredState.flatMap((entry) => {
|
|
2207
|
+
if (!matchesOptionalSource(entry.source, options.source))
|
|
2208
|
+
return [];
|
|
2209
|
+
if (options.bundle !== undefined && entry.bundle !== options.bundle) {
|
|
2210
|
+
return [];
|
|
2211
|
+
}
|
|
2212
|
+
return listDesiredBundleItemRemovalChoices({
|
|
2213
|
+
libraryDir: options.libraryDir,
|
|
2214
|
+
desiredEntry: entry,
|
|
2215
|
+
});
|
|
2216
|
+
});
|
|
2217
|
+
if (choices.length === 0) {
|
|
2218
|
+
throw new Error(options.source
|
|
2219
|
+
? `No active bundle items found for ${options.source}`
|
|
2220
|
+
: "No active bundle items found");
|
|
2221
|
+
}
|
|
2222
|
+
return choices;
|
|
2223
|
+
}
|
|
2224
|
+
function listDesiredBundleItemRemovalChoices(options) {
|
|
2225
|
+
const cachedBundle = findCachedBundleWithGuidance({
|
|
2226
|
+
libraryDir: options.libraryDir,
|
|
2227
|
+
bundle: options.desiredEntry.bundle,
|
|
2228
|
+
source: options.desiredEntry.source,
|
|
2229
|
+
});
|
|
2230
|
+
const selectedTools = options.desiredEntry.tools ??
|
|
2231
|
+
Object.keys(cachedBundle.manifest.tools);
|
|
2232
|
+
const availableItems = (0, bundle_items_1.listSelectableBundleItems)({
|
|
2233
|
+
bundleDir: node_path_1.default.dirname(cachedBundle.manifestFile),
|
|
2234
|
+
manifest: cachedBundle.manifest,
|
|
2235
|
+
tools: selectedTools,
|
|
2236
|
+
});
|
|
2237
|
+
const activeItems = options.desiredEntry.items ?? availableItems;
|
|
2238
|
+
return activeItems.map((item) => ({
|
|
2239
|
+
value: encodeBundleItemRemovalChoice({
|
|
2240
|
+
bundle: options.desiredEntry.bundle,
|
|
2241
|
+
source: options.desiredEntry.source,
|
|
2242
|
+
item,
|
|
2243
|
+
}),
|
|
2244
|
+
label: formatBundleItemRemovalChoiceLabel({
|
|
2245
|
+
bundle: options.desiredEntry.bundle,
|
|
2246
|
+
source: options.desiredEntry.source,
|
|
2247
|
+
item,
|
|
2248
|
+
}),
|
|
2249
|
+
bundle: options.desiredEntry.bundle,
|
|
2250
|
+
source: options.desiredEntry.source,
|
|
2251
|
+
item,
|
|
2252
|
+
activeItems,
|
|
2253
|
+
}));
|
|
2254
|
+
}
|
|
2255
|
+
async function promptForBundleItemRemovalChoices(options) {
|
|
2256
|
+
const selectedValues = selectRequestedBundleItemRemovalChoices({
|
|
2257
|
+
choices: options.choices,
|
|
2258
|
+
requestedItems: options.requestedItems,
|
|
2259
|
+
allowEmptySelection: true,
|
|
2260
|
+
});
|
|
2261
|
+
const selections = await options.prompts.selectBundleItemChoices(options.choices, selectedValues, "remove");
|
|
2262
|
+
if (selections.length === 0) {
|
|
2263
|
+
throw new Error("No bundle items selected for removal");
|
|
2264
|
+
}
|
|
2265
|
+
return selections;
|
|
2266
|
+
}
|
|
2267
|
+
function selectRequestedBundleItemRemovalChoices(options) {
|
|
2268
|
+
if (options.requestedItems.length === 0 && !options.allowEmptySelection) {
|
|
2269
|
+
throw new Error("No bundle items selected for removal");
|
|
2270
|
+
}
|
|
2271
|
+
const requestedItemSet = new Set(options.requestedItems);
|
|
2272
|
+
const selectedValues = options.choices
|
|
2273
|
+
.filter((choice) => requestedItemSet.has(choice.item))
|
|
2274
|
+
.map((choice) => choice.value);
|
|
2275
|
+
const missingItems = options.requestedItems.filter((item) => !options.choices.some((choice) => choice.item === item));
|
|
2276
|
+
if (missingItems.length > 0) {
|
|
2277
|
+
throw new Error(`Bundle item(s) are not active: ${missingItems.join(", ")}`);
|
|
2278
|
+
}
|
|
2279
|
+
if (selectedValues.length === 0 && !options.allowEmptySelection) {
|
|
2280
|
+
throw new Error("No bundle items selected for removal");
|
|
2281
|
+
}
|
|
2282
|
+
return selectedValues;
|
|
2283
|
+
}
|
|
2284
|
+
function planBundleItemRemovals(options) {
|
|
2285
|
+
const selectedChoices = options.selectedValues.map((value) => {
|
|
2286
|
+
const choice = options.choices.find((candidate) => candidate.value === value);
|
|
2287
|
+
if (!choice) {
|
|
2288
|
+
throw new Error(`Selected bundle item is not active: ${value}`);
|
|
2289
|
+
}
|
|
2290
|
+
return choice;
|
|
2291
|
+
});
|
|
2292
|
+
const selectedItemsByBundle = new Map();
|
|
2293
|
+
for (const choice of selectedChoices) {
|
|
2294
|
+
const key = encodeBundleIdentity(choice);
|
|
2295
|
+
const items = selectedItemsByBundle.get(key) ?? new Set();
|
|
2296
|
+
items.add(choice.item);
|
|
2297
|
+
selectedItemsByBundle.set(key, items);
|
|
2298
|
+
}
|
|
2299
|
+
for (const entry of options.desiredState) {
|
|
2300
|
+
const selectedItems = selectedItemsByBundle.get(encodeBundleIdentity(entry));
|
|
2301
|
+
if (!selectedItems)
|
|
2302
|
+
continue;
|
|
2303
|
+
const activeItems = options.choices.find((choice) => encodeBundleIdentity(choice) === encodeBundleIdentity(entry))?.activeItems;
|
|
2304
|
+
const inactiveSelectedItems = Array.from(selectedItems).filter((item) => !(activeItems ?? entry.items ?? []).includes(item));
|
|
2305
|
+
if (inactiveSelectedItems.length > 0) {
|
|
2306
|
+
throw new Error(`Bundle item(s) are not active: ${inactiveSelectedItems.join(", ")}`);
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
return {
|
|
2310
|
+
removedItems: selectedChoices.map((choice) => ({
|
|
2311
|
+
bundle: choice.bundle,
|
|
2312
|
+
source: choice.source,
|
|
2313
|
+
item: choice.item,
|
|
2314
|
+
})),
|
|
2315
|
+
};
|
|
2316
|
+
}
|
|
2317
|
+
function encodeBundleItemRemovalChoice(choice) {
|
|
2318
|
+
return JSON.stringify([choice.source ?? null, choice.bundle, choice.item]);
|
|
2319
|
+
}
|
|
2320
|
+
function encodeBundleIdentity(choice) {
|
|
2321
|
+
return JSON.stringify([choice.source ?? null, choice.bundle]);
|
|
2322
|
+
}
|
|
2323
|
+
function formatBundleItemRemovalChoiceLabel(choice) {
|
|
2324
|
+
return choice.source
|
|
2325
|
+
? `${choice.source} / ${choice.bundle}: ${choice.item}`
|
|
2326
|
+
: `${choice.bundle}: ${choice.item}`;
|
|
2327
|
+
}
|
|
2328
|
+
function formatBundleItemRemovalSummary(removedItems) {
|
|
2329
|
+
return removedItems.map((item) => `${item.bundle}: ${item.item}`).join(", ");
|
|
2330
|
+
}
|
|
2331
|
+
async function resolveRemoveBundleSelection(options) {
|
|
2332
|
+
if (options.requestedBundle &&
|
|
2333
|
+
isRemoveBundleActive({
|
|
2334
|
+
repoState: options.repoState,
|
|
2335
|
+
worktreeState: options.worktreeState,
|
|
2336
|
+
bundle: options.requestedBundle,
|
|
2337
|
+
source: options.requestedSource,
|
|
2338
|
+
})) {
|
|
2339
|
+
return {
|
|
2340
|
+
bundle: options.requestedBundle,
|
|
2341
|
+
...(options.requestedSource !== undefined
|
|
2342
|
+
? { source: options.requestedSource }
|
|
2343
|
+
: {}),
|
|
2344
|
+
};
|
|
2345
|
+
}
|
|
2346
|
+
if (options.requestedBundle && !options.inferredBundleFromSource) {
|
|
2347
|
+
return {
|
|
2348
|
+
bundle: options.requestedBundle,
|
|
2349
|
+
...(options.requestedSource !== undefined
|
|
2350
|
+
? { source: options.requestedSource }
|
|
2351
|
+
: {}),
|
|
2352
|
+
};
|
|
2353
|
+
}
|
|
2354
|
+
if (options.requestedSource !== undefined &&
|
|
2355
|
+
options.inferredBundleFromSource) {
|
|
2356
|
+
return promptForActiveRemoveBundleSelection({
|
|
2357
|
+
repoState: options.repoState,
|
|
2358
|
+
worktreeState: options.worktreeState,
|
|
2359
|
+
prompts: options.prompts,
|
|
2360
|
+
source: options.requestedSource,
|
|
2361
|
+
});
|
|
2362
|
+
}
|
|
2363
|
+
if (options.requestedBundle !== undefined) {
|
|
2364
|
+
return { bundle: options.requestedBundle };
|
|
2365
|
+
}
|
|
2366
|
+
return promptForActiveRemoveBundleSelection({
|
|
2367
|
+
repoState: options.repoState,
|
|
2368
|
+
worktreeState: options.worktreeState,
|
|
2369
|
+
prompts: options.prompts,
|
|
2370
|
+
});
|
|
2371
|
+
}
|
|
2372
|
+
async function promptForActiveRemoveBundleSelection(options) {
|
|
2373
|
+
const activeSelections = listActiveRemoveBundleSelections({
|
|
2374
|
+
repoState: options.repoState,
|
|
2375
|
+
worktreeState: options.worktreeState,
|
|
2376
|
+
source: options.source,
|
|
2377
|
+
});
|
|
2378
|
+
if (activeSelections.length === 0) {
|
|
2379
|
+
throw new Error(options.source
|
|
2380
|
+
? `No active bundles found for ${options.source}. Run "skul add ${options.source} <bundle>" to add one first`
|
|
2381
|
+
: 'No active bundles found. Run "skul add <bundle>" to add one first');
|
|
2382
|
+
}
|
|
2383
|
+
if (activeSelections.length === 1) {
|
|
2384
|
+
return activeSelections[0];
|
|
2385
|
+
}
|
|
2386
|
+
const selection = await options.prompts.selectBundleFromSelections(activeSelections, options.source);
|
|
2387
|
+
return {
|
|
2388
|
+
bundle: selection.bundle,
|
|
2389
|
+
...(selection.source !== undefined ? { source: selection.source } : {}),
|
|
2390
|
+
};
|
|
2391
|
+
}
|
|
2392
|
+
function listActiveRemoveBundleSelections(options) {
|
|
2393
|
+
const selections = [];
|
|
2394
|
+
const seen = new Set();
|
|
2395
|
+
for (const entry of options.repoState?.desired_state ?? []) {
|
|
2396
|
+
if (!matchesOptionalSource(entry.source, options.source))
|
|
2397
|
+
continue;
|
|
2398
|
+
addActiveRemoveBundleSelection(selections, seen, {
|
|
2399
|
+
bundle: entry.bundle,
|
|
2400
|
+
...(entry.source !== undefined ? { source: entry.source } : {}),
|
|
2401
|
+
protocol: entry.protocol,
|
|
2402
|
+
});
|
|
2403
|
+
}
|
|
2404
|
+
for (const [bundle, state] of Object.entries(options.worktreeState?.materialized_state.bundles ?? {})) {
|
|
2405
|
+
if (!matchesOptionalSource(state.source, options.source))
|
|
2406
|
+
continue;
|
|
2407
|
+
addActiveRemoveBundleSelection(selections, seen, {
|
|
2408
|
+
bundle,
|
|
2409
|
+
...(state.source !== undefined ? { source: state.source } : {}),
|
|
2410
|
+
});
|
|
2411
|
+
}
|
|
2412
|
+
return selections.sort(compareBundleSelections);
|
|
2413
|
+
}
|
|
2414
|
+
function addActiveRemoveBundleSelection(selections, seen, selection) {
|
|
2415
|
+
const key = `${selection.source ?? ""}\0${selection.bundle}`;
|
|
2416
|
+
if (seen.has(key))
|
|
2417
|
+
return;
|
|
2418
|
+
seen.add(key);
|
|
2419
|
+
selections.push(selection);
|
|
2420
|
+
}
|
|
2421
|
+
function isRemoveBundleActive(options) {
|
|
2422
|
+
return ((options.repoState?.desired_state.some((entry) => entry.bundle === options.bundle &&
|
|
2423
|
+
matchesOptionalSource(entry.source, options.source)) ??
|
|
2424
|
+
false) ||
|
|
2425
|
+
findMaterializedBundleState({
|
|
2426
|
+
worktreeState: options.worktreeState,
|
|
2427
|
+
bundle: options.bundle,
|
|
2428
|
+
source: options.source,
|
|
2429
|
+
}) !== undefined);
|
|
2430
|
+
}
|
|
2431
|
+
function matchesOptionalSource(candidateSource, requestedSource) {
|
|
2432
|
+
return requestedSource === undefined || candidateSource === requestedSource;
|
|
2433
|
+
}
|
|
2434
|
+
function matchesBundleIdentity(entry, bundle, source) {
|
|
2435
|
+
return entry.bundle === bundle && matchesOptionalSource(entry.source, source);
|
|
2436
|
+
}
|
|
2437
|
+
function findMaterializedBundleState(options) {
|
|
2438
|
+
const bundleState = options.worktreeState?.materialized_state.bundles[options.bundle];
|
|
2439
|
+
if (!bundleState ||
|
|
2440
|
+
!matchesOptionalSource(bundleState.source, options.source)) {
|
|
2441
|
+
return undefined;
|
|
2442
|
+
}
|
|
2443
|
+
return bundleState;
|
|
2444
|
+
}
|
|
2445
|
+
async function removeBundleItems(options) {
|
|
2446
|
+
if (!options.repoState || !options.desiredEntry) {
|
|
2447
|
+
throw new Error(`Cannot remove selected items from ${options.bundle} because it is not in desired state`);
|
|
2448
|
+
}
|
|
2449
|
+
const cachedBundle = findCachedBundleWithGuidance({
|
|
2450
|
+
libraryDir: options.libraryDir,
|
|
2451
|
+
bundle: options.bundle,
|
|
2452
|
+
source: options.desiredEntry.source ?? options.source,
|
|
2453
|
+
});
|
|
2454
|
+
const selectedTools = options.desiredEntry.tools ??
|
|
2455
|
+
Object.keys(cachedBundle.manifest.tools);
|
|
2456
|
+
const availableItems = (0, bundle_items_1.listSelectableBundleItems)({
|
|
2457
|
+
bundleDir: node_path_1.default.dirname(cachedBundle.manifestFile),
|
|
2458
|
+
manifest: cachedBundle.manifest,
|
|
2459
|
+
tools: selectedTools,
|
|
2460
|
+
});
|
|
2461
|
+
const currentItems = options.desiredEntry.items ?? availableItems;
|
|
2462
|
+
const requestedItems = (0, bundle_items_1.normalizeBundleItemSelectors)(options.includeItems);
|
|
2463
|
+
(0, bundle_items_1.assertBundleSupportsRequestedItems)({
|
|
2464
|
+
requestedItems,
|
|
2465
|
+
availableItems,
|
|
2466
|
+
});
|
|
2467
|
+
const inactiveRequestedItems = requestedItems.filter((item) => !currentItems.includes(item));
|
|
2468
|
+
if (inactiveRequestedItems.length > 0) {
|
|
2469
|
+
throw new Error(`Bundle item(s) are not active in ${options.bundle}: ${inactiveRequestedItems.join(", ")}`);
|
|
2470
|
+
}
|
|
2471
|
+
const selectedItems = options.selectItems
|
|
2472
|
+
? await options.prompts.selectBundleItems(currentItems, requestedItems, "remove")
|
|
2473
|
+
: requestedItems;
|
|
2474
|
+
const normalizedSelectedItems = (0, bundle_items_1.normalizeBundleItemSelectors)(selectedItems);
|
|
2475
|
+
const inactiveItems = normalizedSelectedItems.filter((item) => !currentItems.includes(item));
|
|
2476
|
+
if (inactiveItems.length > 0) {
|
|
2477
|
+
throw new Error(`Bundle item(s) are not active in ${options.bundle}: ${inactiveItems.join(", ")}`);
|
|
2478
|
+
}
|
|
2479
|
+
const selectedItemSet = new Set(normalizedSelectedItems);
|
|
2480
|
+
const remainingItems = currentItems.filter((item) => !selectedItemSet.has(item));
|
|
2481
|
+
if (remainingItems.length === 0) {
|
|
2482
|
+
return { kind: "remove-bundle" };
|
|
2483
|
+
}
|
|
2484
|
+
if (options.dryRun) {
|
|
2485
|
+
return {
|
|
2486
|
+
kind: "completed",
|
|
2487
|
+
output: `${pc.yellow("DRY RUN:")} Would remove ${normalizedSelectedItems.join(", ")} from ${options.bundle}`,
|
|
2488
|
+
};
|
|
2489
|
+
}
|
|
2490
|
+
const nextRegistry = (0, registry_1.upsertRepoState)(options.registry, options.gitContext.repoFingerprint, {
|
|
2491
|
+
...options.repoState,
|
|
2492
|
+
repo_root: options.gitContext.repoRoot,
|
|
2493
|
+
desired_state: options.repoState.desired_state.map((entry) => entry.bundle === options.bundle &&
|
|
2494
|
+
matchesOptionalSource(entry.source, options.source)
|
|
2495
|
+
? { ...entry, items: remainingItems }
|
|
2496
|
+
: entry),
|
|
2497
|
+
});
|
|
2498
|
+
(0, registry_1.writeRegistryFile)(options.registryFile, nextRegistry);
|
|
2499
|
+
try {
|
|
2500
|
+
await applyWorktree({
|
|
2501
|
+
cwd: options.cwd,
|
|
2502
|
+
prompts: options.prompts,
|
|
2503
|
+
registryFile: options.registryFile,
|
|
2504
|
+
libraryDir: options.libraryDir,
|
|
2505
|
+
dryRun: false,
|
|
2506
|
+
});
|
|
2507
|
+
}
|
|
2508
|
+
catch (error) {
|
|
2509
|
+
(0, registry_1.writeRegistryFile)(options.registryFile, options.registry);
|
|
2510
|
+
throw error;
|
|
2511
|
+
}
|
|
2512
|
+
return {
|
|
2513
|
+
kind: "completed",
|
|
2514
|
+
output: pc.green(`Removed ${normalizedSelectedItems.join(", ")} from ${options.bundle}`),
|
|
2515
|
+
};
|
|
2516
|
+
}
|
|
2517
|
+
async function applyWorktree(options) {
|
|
2518
|
+
const gitContext = requireGitContext(options.cwd, "apply");
|
|
2519
|
+
let registry = readRegistryWithGuidance(options.registryFile);
|
|
2520
|
+
const repoState = registry.repos[gitContext.repoFingerprint];
|
|
2521
|
+
if (!repoState || repoState.desired_state.length === 0) {
|
|
2522
|
+
return `No bundles configured for this repository. Run "skul add <bundle>" to add one`;
|
|
2523
|
+
}
|
|
2524
|
+
const worktreeState = registry.worktrees[gitContext.worktreeId];
|
|
2525
|
+
const materializedBundles = worktreeState?.materialized_state.bundles ?? {};
|
|
2526
|
+
const cloneLines = [];
|
|
2527
|
+
const applyPlans = repoState.desired_state.flatMap((entry) => {
|
|
2528
|
+
// In dry-run mode skip actual cloning; if the source is not yet cached we
|
|
2529
|
+
// can still report intent without a manifest.
|
|
2530
|
+
const sourceRevision = entry.source
|
|
2531
|
+
? (0, bundle_fetch_1.readCachedSourceRevision)({
|
|
2532
|
+
source: entry.source,
|
|
2533
|
+
libraryDir: options.libraryDir,
|
|
2534
|
+
protocol: entry.protocol,
|
|
2535
|
+
})
|
|
2536
|
+
: undefined;
|
|
2537
|
+
if (entry.source && !sourceRevision?.cached) {
|
|
2538
|
+
if (options.dryRun) {
|
|
2539
|
+
return [{ uncached: true, entry }];
|
|
2540
|
+
}
|
|
2541
|
+
// Non-dry-run: fetch the source so the manifest is available below.
|
|
2542
|
+
const { cloned } = (0, bundle_fetch_1.fetchRemoteSource)({
|
|
2543
|
+
source: entry.source,
|
|
2544
|
+
libraryDir: options.libraryDir,
|
|
2545
|
+
protocol: entry.protocol,
|
|
2546
|
+
});
|
|
2547
|
+
if (cloned)
|
|
2548
|
+
cloneLines.push(pc.dim(`Cloned ${entry.source}`));
|
|
2549
|
+
}
|
|
2550
|
+
const cachedBundle = findCachedBundleWithGuidance({
|
|
2551
|
+
libraryDir: options.libraryDir,
|
|
2552
|
+
bundle: entry.bundle,
|
|
2553
|
+
source: entry.source,
|
|
2554
|
+
});
|
|
2555
|
+
const existingBundleState = materializedBundles[entry.bundle];
|
|
2556
|
+
if (existingBundleState &&
|
|
2557
|
+
isDesiredBundleMaterialized({
|
|
2558
|
+
desiredEntry: entry,
|
|
2559
|
+
materializedBundleState: existingBundleState,
|
|
2560
|
+
availableTools: Object.keys(cachedBundle.manifest.tools),
|
|
2561
|
+
})) {
|
|
2562
|
+
return [];
|
|
2563
|
+
}
|
|
2564
|
+
return [
|
|
2565
|
+
{
|
|
2566
|
+
uncached: false,
|
|
2567
|
+
entry,
|
|
2568
|
+
sourceRevision,
|
|
2569
|
+
cachedBundle,
|
|
2570
|
+
existingBundleState,
|
|
2571
|
+
availableTools: Object.keys(cachedBundle.manifest.tools),
|
|
2572
|
+
},
|
|
2573
|
+
];
|
|
2574
|
+
});
|
|
2575
|
+
if (applyPlans.length === 0) {
|
|
2576
|
+
return options.dryRun
|
|
2577
|
+
? "DRY RUN: All bundles are already materialized"
|
|
2578
|
+
: "All bundles are already materialized";
|
|
2579
|
+
}
|
|
2580
|
+
if (options.dryRun) {
|
|
2581
|
+
const lines = applyPlans.map((plan) => {
|
|
2582
|
+
if (plan.uncached) {
|
|
2583
|
+
return `DRY RUN: Would clone ${plan.entry.source} then apply ${plan.entry.bundle}`;
|
|
2584
|
+
}
|
|
2585
|
+
const tools = plan.entry.tools ?? Object.keys(plan.cachedBundle.manifest.tools);
|
|
2586
|
+
return `DRY RUN: Would apply ${plan.entry.bundle} for ${tools.join(", ")}`;
|
|
2587
|
+
});
|
|
2588
|
+
return lines.join("\n");
|
|
2589
|
+
}
|
|
2590
|
+
let currentBundles = { ...materializedBundles };
|
|
2591
|
+
let currentShadowedFiles = { ...(worktreeState?.shadowed_files ?? {}) };
|
|
2592
|
+
let rootInstructionBaseContents = worktreeState?.materialized_state.root_instruction_base_contents;
|
|
2593
|
+
for (const plan of applyPlans) {
|
|
2594
|
+
if (plan.uncached)
|
|
2595
|
+
continue;
|
|
2596
|
+
const { entry, sourceRevision, cachedBundle, existingBundleState, availableTools, } = plan;
|
|
2597
|
+
const refreshesExistingBundle = existingBundleState !== undefined &&
|
|
2598
|
+
entry.resolved_commit !== undefined &&
|
|
2599
|
+
existingBundleState.resolved_commit !== entry.resolved_commit;
|
|
2600
|
+
const toolsToApply = getToolsToApply({
|
|
2601
|
+
desiredEntry: entry,
|
|
2602
|
+
materializedBundleState: existingBundleState,
|
|
2603
|
+
availableTools,
|
|
2604
|
+
});
|
|
2605
|
+
const replacementState = refreshesExistingBundle
|
|
2606
|
+
? existingBundleState
|
|
2607
|
+
: existingBundleState && toolsToApply
|
|
2608
|
+
? selectExistingBundleToolState(existingBundleState, toolsToApply)
|
|
2609
|
+
: existingBundleState;
|
|
2610
|
+
const replacesExistingToolState = replacementState !== undefined &&
|
|
2611
|
+
flattenBundleState(replacementState).files.length > 0;
|
|
2612
|
+
const plannedWriteTargets = (0, bundle_materialization_1.previewMaterializeBundleWriteTargets)({
|
|
2613
|
+
repoRoot: gitContext.worktreeRoot,
|
|
2614
|
+
bundleDir: node_path_1.default.dirname(cachedBundle.manifestFile),
|
|
2615
|
+
manifest: cachedBundle.manifest,
|
|
2616
|
+
tools: toolsToApply,
|
|
2617
|
+
itemSelectors: entry.items,
|
|
2618
|
+
});
|
|
2619
|
+
const plannedRootInstructionTargets = new Set(plannedWriteTargets.filter((filePath) => (0, root_instruction_render_1.isRootInstructionPath)(filePath)));
|
|
2620
|
+
const trackedRootInstructionShadowPlan = planTrackedRootInstructionShadows({
|
|
2621
|
+
repoRoot: gitContext.worktreeRoot,
|
|
2622
|
+
bundleDir: node_path_1.default.dirname(cachedBundle.manifestFile),
|
|
2623
|
+
manifest: cachedBundle.manifest,
|
|
2624
|
+
toolNames: selectTrackedRootInstructionShadowToolNames({
|
|
2625
|
+
existingBundleState,
|
|
2626
|
+
nextToolNames: toolsToApply ?? availableTools,
|
|
2627
|
+
}),
|
|
2628
|
+
itemSelectors: entry.items,
|
|
2629
|
+
targetPaths: plannedRootInstructionTargets,
|
|
2630
|
+
bundleName: entry.bundle,
|
|
2631
|
+
bundleSource: entry.source,
|
|
2632
|
+
existingShadowedFiles: currentShadowedFiles,
|
|
2633
|
+
materializedBundles: currentBundles,
|
|
2634
|
+
});
|
|
2635
|
+
rootInstructionBaseContents = (0, root_instruction_state_1.captureRootInstructionBaseContents)({
|
|
2636
|
+
repoRoot: gitContext.worktreeRoot,
|
|
2637
|
+
targetPaths: trackedRootInstructionShadowPlan.untrackedTargetPaths,
|
|
2638
|
+
existingBaseContents: rootInstructionBaseContents,
|
|
2639
|
+
managedTargetPaths: (0, root_instruction_state_1.collectManagedRootInstructionTargets)(currentBundles),
|
|
2640
|
+
});
|
|
2641
|
+
(0, root_instruction_state_1.assertManagedRootInstructionSyncSourcesCached)({
|
|
2642
|
+
desiredState: repoState.desired_state,
|
|
2643
|
+
materializedBundles: currentBundles,
|
|
2644
|
+
targetPaths: trackedRootInstructionShadowPlan.untrackedTargetPaths,
|
|
2645
|
+
resolveCachedBundle: (entry) => resolveDesiredCachedBundle(options.libraryDir, entry),
|
|
2646
|
+
});
|
|
2647
|
+
if (replacesExistingToolState && replacementState) {
|
|
2648
|
+
assertTrackedRootInstructionShadowSafetyForPaths({
|
|
2649
|
+
repoRoot: gitContext.worktreeRoot,
|
|
2650
|
+
operation: "refresh",
|
|
2651
|
+
filePaths: plannedWriteTargets,
|
|
2652
|
+
});
|
|
2653
|
+
const replacementAllowed = await confirmManagedFileRemovals(gitContext.worktreeRoot, excludeShadowedTrackedRootInstructionTargets(flattenBundleState(replacementState), trackedRootInstructionShadowPlan.deferredMaterializationTargets), options.prompts, "replace");
|
|
2654
|
+
if (!replacementAllowed) {
|
|
2655
|
+
throw new Error("Replacement aborted because a modified managed file was kept");
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
const sharedRootInstructionState = (0, root_instruction_state_1.collectSharedRootInstructionState)(currentBundles, plannedWriteTargets, cachedBundle.bundle);
|
|
2659
|
+
if (sharedRootInstructionState.files.length > 0) {
|
|
2660
|
+
const replacementAllowed = await confirmManagedFileRemovals(gitContext.worktreeRoot, sharedRootInstructionState, options.prompts, "replace");
|
|
2661
|
+
if (!replacementAllowed) {
|
|
2662
|
+
throw new Error("Replacement aborted because a modified managed file was kept");
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
assertTrackedRootInstructionShadowPlanCanApply({
|
|
2666
|
+
repoRoot: gitContext.worktreeRoot,
|
|
2667
|
+
bundleName: entry.bundle,
|
|
2668
|
+
existingShadowedFiles: currentShadowedFiles,
|
|
2669
|
+
plan: trackedRootInstructionShadowPlan,
|
|
2670
|
+
});
|
|
2671
|
+
assertTrackedRootInstructionShadowSafetyForPaths({
|
|
2672
|
+
repoRoot: gitContext.worktreeRoot,
|
|
2673
|
+
operation: existingBundleState ? "refresh" : "create",
|
|
2674
|
+
filePaths: plannedWriteTargets,
|
|
2675
|
+
});
|
|
2676
|
+
if (replacesExistingToolState && replacementState) {
|
|
2677
|
+
const pathsToReplace = excludeShadowedTrackedRootInstructionTargets(flattenBundleState(replacementState), trackedRootInstructionShadowPlan.deferredMaterializationTargets);
|
|
2678
|
+
const replacedRootInstructionPaths = new Set(pathsToReplace.files.filter((filePath) => (0, root_instruction_render_1.isRootInstructionPath)(filePath)));
|
|
2679
|
+
removeManagedPaths(gitContext.worktreeRoot, pathsToReplace);
|
|
2680
|
+
(0, root_instruction_state_1.restoreRootInstructionBaseContents)({
|
|
2681
|
+
repoRoot: gitContext.worktreeRoot,
|
|
2682
|
+
baseContents: rootInstructionBaseContents,
|
|
2683
|
+
targetPaths: new Set(Array.from(replacedRootInstructionPaths).filter((filePath) => !plannedRootInstructionTargets.has(filePath))),
|
|
2684
|
+
});
|
|
2685
|
+
}
|
|
2686
|
+
const materializedResult = await (0, bundle_materialization_1.materializeBundle)({
|
|
2687
|
+
repoRoot: gitContext.worktreeRoot,
|
|
2688
|
+
bundleDir: node_path_1.default.dirname(cachedBundle.manifestFile),
|
|
2689
|
+
manifest: cachedBundle.manifest,
|
|
2690
|
+
tools: toolsToApply,
|
|
2691
|
+
itemSelectors: entry.items,
|
|
2692
|
+
bundleName: entry.bundle,
|
|
2693
|
+
bundleSource: entry.source,
|
|
2694
|
+
assertSafeWriteTarget: createTrackedRootInstructionShadowSafetyAssertion({
|
|
2695
|
+
repoRoot: gitContext.worktreeRoot,
|
|
2696
|
+
operation: existingBundleState ? "refresh" : "create",
|
|
2697
|
+
}),
|
|
2698
|
+
allowFileOverwriteTargets: (0, root_instruction_state_1.collectManagedRootInstructionTargets)(currentBundles),
|
|
2699
|
+
deferredWriteTargets: trackedRootInstructionShadowPlan.deferredMaterializationTargets,
|
|
2700
|
+
rootInstructionBaseContents,
|
|
2701
|
+
resolveFileConflict: options.prompts.resolveFileConflict,
|
|
2702
|
+
});
|
|
2703
|
+
currentBundles = {
|
|
2704
|
+
...currentBundles,
|
|
2705
|
+
[cachedBundle.bundle]: buildMaterializedBundleState({
|
|
2706
|
+
existingBundleState,
|
|
2707
|
+
materializedResult,
|
|
2708
|
+
repoRoot: gitContext.worktreeRoot,
|
|
2709
|
+
source: entry.source,
|
|
2710
|
+
resolvedCommit: entry.resolved_commit ?? sourceRevision?.currentCommit,
|
|
2711
|
+
selectedTools: refreshesExistingBundle ? undefined : toolsToApply,
|
|
2712
|
+
selectedItems: entry.items,
|
|
2713
|
+
}),
|
|
2714
|
+
};
|
|
2715
|
+
currentShadowedFiles = applyTrackedRootInstructionShadowPlan({
|
|
2716
|
+
repoRoot: gitContext.worktreeRoot,
|
|
2717
|
+
bundleName: entry.bundle,
|
|
2718
|
+
existingShadowedFiles: currentShadowedFiles,
|
|
2719
|
+
plan: trackedRootInstructionShadowPlan,
|
|
2720
|
+
});
|
|
2721
|
+
const syncedRootInstructionPaths = (0, root_instruction_state_1.syncManagedRootInstructionFiles)({
|
|
2722
|
+
repoRoot: gitContext.worktreeRoot,
|
|
2723
|
+
desiredState: repoState.desired_state,
|
|
2724
|
+
materializedBundles: currentBundles,
|
|
2725
|
+
rootInstructionBaseContents,
|
|
2726
|
+
targetPaths: trackedRootInstructionShadowPlan.untrackedTargetPaths,
|
|
2727
|
+
resolveCachedBundle: (entry) => resolveDesiredCachedBundle(options.libraryDir, entry),
|
|
2728
|
+
});
|
|
2729
|
+
currentBundles = (0, root_instruction_state_1.refreshManagedFileFingerprintsForPaths)(gitContext.worktreeRoot, currentBundles, syncedRootInstructionPaths);
|
|
2730
|
+
const newMatState = {
|
|
2731
|
+
bundles: currentBundles,
|
|
2732
|
+
exclude_configured: false,
|
|
2733
|
+
...(rootInstructionBaseContents !== undefined
|
|
2734
|
+
? { root_instruction_base_contents: rootInstructionBaseContents }
|
|
2735
|
+
: {}),
|
|
2736
|
+
};
|
|
2737
|
+
const managedFiles = collectAllFiles(newMatState);
|
|
2738
|
+
newMatState.exclude_configured = managedFiles.length > 0;
|
|
2739
|
+
if (managedFiles.length > 0) {
|
|
2740
|
+
(0, git_exclude_1.configureSkulExcludeBlock)({
|
|
2741
|
+
gitDir: gitContext.gitDir,
|
|
2742
|
+
files: managedFiles,
|
|
2743
|
+
});
|
|
2744
|
+
}
|
|
2745
|
+
else {
|
|
2746
|
+
(0, git_exclude_1.removeSkulExcludeBlock)({ gitDir: gitContext.gitDir });
|
|
2747
|
+
}
|
|
2748
|
+
registry = (0, registry_1.upsertWorktreeState)(registry, gitContext.worktreeId, {
|
|
2749
|
+
repo_fingerprint: gitContext.repoFingerprint,
|
|
2750
|
+
path: gitContext.worktreeRoot,
|
|
2751
|
+
materialized_state: newMatState,
|
|
2752
|
+
shadowed_files: currentShadowedFiles,
|
|
2753
|
+
});
|
|
2754
|
+
(0, registry_1.writeRegistryFile)(options.registryFile, registry);
|
|
2755
|
+
}
|
|
2756
|
+
const appliedNames = applyPlans.map(({ entry }) => entry.bundle).join(", ");
|
|
2757
|
+
return [...cloneLines, pc.green(`Applied ${appliedNames}`)].join("\n");
|
|
2758
|
+
}
|
|
2759
|
+
function selectTrackedRootInstructionShadowToolNames(options) {
|
|
2760
|
+
return Array.from(new Set([
|
|
2761
|
+
...(options.existingBundleState
|
|
2762
|
+
? Object.keys(options.existingBundleState.tools)
|
|
2763
|
+
: []),
|
|
2764
|
+
...options.nextToolNames,
|
|
2765
|
+
]));
|
|
2766
|
+
}
|
|
2767
|
+
function planTrackedRootInstructionShadows(options) {
|
|
2768
|
+
const activeOverlayContents = (0, root_instruction_content_1.collectComposedRootInstructionContents)({
|
|
2769
|
+
bundleDir: options.bundleDir,
|
|
2770
|
+
manifest: options.manifest,
|
|
2771
|
+
toolNames: options.toolNames,
|
|
2772
|
+
itemSelectors: options.itemSelectors,
|
|
2773
|
+
});
|
|
2774
|
+
const activeRootInstructionPaths = new Set(Object.keys(activeOverlayContents).filter((targetPath) => (0, root_instruction_render_1.isRootInstructionPath)(targetPath)));
|
|
2775
|
+
const trackedTargetPaths = new Set();
|
|
2776
|
+
for (const targetPath of activeRootInstructionPaths) {
|
|
2777
|
+
const inspection = (0, git_index_1.inspectRootInstructionShadowTarget)({
|
|
2778
|
+
repoRoot: options.repoRoot,
|
|
2779
|
+
filePath: targetPath,
|
|
2780
|
+
});
|
|
2781
|
+
if (inspection.tracked) {
|
|
2782
|
+
trackedTargetPaths.add(targetPath);
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
assertTrackedRootInstructionShadowConflicts({
|
|
2786
|
+
targetPaths: trackedTargetPaths,
|
|
2787
|
+
bundleName: options.bundleName,
|
|
2788
|
+
existingShadowedFiles: options.existingShadowedFiles,
|
|
2789
|
+
materializedBundles: options.materializedBundles,
|
|
2790
|
+
});
|
|
2791
|
+
if (trackedTargetPaths.size === 0) {
|
|
2792
|
+
return {
|
|
2793
|
+
writes: [],
|
|
2794
|
+
deferredMaterializationTargets: trackedTargetPaths,
|
|
2795
|
+
untrackedTargetPaths: new Set(activeRootInstructionPaths),
|
|
2796
|
+
activeShadowPaths: trackedTargetPaths,
|
|
2797
|
+
};
|
|
2798
|
+
}
|
|
2799
|
+
const writes = Array.from(options.targetPaths)
|
|
2800
|
+
.filter((targetPath) => trackedTargetPaths.has(targetPath))
|
|
2801
|
+
.map((targetPath) => renderTrackedRootInstructionShadowWrite({
|
|
2802
|
+
repoRoot: options.repoRoot,
|
|
2803
|
+
filePath: targetPath,
|
|
2804
|
+
overlayContent: activeOverlayContents[targetPath] ?? "",
|
|
2805
|
+
bundleName: options.bundleName,
|
|
2806
|
+
toolName: selectShadowToolForPath(options.toolNames, targetPath),
|
|
2807
|
+
}));
|
|
2808
|
+
const untrackedTargetPaths = new Set(Array.from(activeRootInstructionPaths).filter((targetPath) => !trackedTargetPaths.has(targetPath)));
|
|
2809
|
+
return {
|
|
2810
|
+
writes,
|
|
2811
|
+
deferredMaterializationTargets: trackedTargetPaths,
|
|
2812
|
+
untrackedTargetPaths,
|
|
2813
|
+
activeShadowPaths: trackedTargetPaths,
|
|
2814
|
+
};
|
|
2815
|
+
}
|
|
2816
|
+
function assertTrackedRootInstructionShadowConflicts(options) {
|
|
2817
|
+
for (const targetPath of options.targetPaths) {
|
|
2818
|
+
const existingShadow = options.existingShadowedFiles[targetPath];
|
|
2819
|
+
if (existingShadow && existingShadow.bundle !== options.bundleName) {
|
|
2820
|
+
throw new Error(`Cannot create tracked root-instruction shadow for ${targetPath} because it is already shadowed by ${existingShadow.bundle}`);
|
|
2821
|
+
}
|
|
2822
|
+
for (const [bundleName, bundleState] of Object.entries(options.materializedBundles)) {
|
|
2823
|
+
if (bundleName === options.bundleName) {
|
|
2824
|
+
continue;
|
|
2825
|
+
}
|
|
2826
|
+
const ownsPath = Object.values(bundleState.tools).some((toolState) => toolState.files.includes(targetPath));
|
|
2827
|
+
if (ownsPath) {
|
|
2828
|
+
throw new Error(`Cannot create tracked root-instruction shadow for ${targetPath} because it is already managed by ${bundleName}`);
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
function renderTrackedRootInstructionShadowWrite(options) {
|
|
2834
|
+
const inspection = (0, git_index_1.inspectRootInstructionShadowTarget)({
|
|
2835
|
+
repoRoot: options.repoRoot,
|
|
2836
|
+
filePath: options.filePath,
|
|
2837
|
+
});
|
|
2838
|
+
if (!inspection.headBlob) {
|
|
2839
|
+
throw new Error(`Cannot create tracked root-instruction shadow for ${options.filePath} because the target does not have HEAD content`);
|
|
2840
|
+
}
|
|
2841
|
+
const render = (0, root_instruction_render_1.renderTrackedRootInstructionShadow)({
|
|
2842
|
+
baseContent: inspection.headBlob.content,
|
|
2843
|
+
overlayContent: options.overlayContent,
|
|
2844
|
+
bundleName: options.bundleName,
|
|
2845
|
+
toolName: options.toolName,
|
|
2846
|
+
strategy: "append",
|
|
2847
|
+
});
|
|
2848
|
+
return {
|
|
2849
|
+
filePath: options.filePath,
|
|
2850
|
+
rendered: render.rendered,
|
|
2851
|
+
state: {
|
|
2852
|
+
tool: options.toolName,
|
|
2853
|
+
bundle: options.bundleName,
|
|
2854
|
+
strategy: "append",
|
|
2855
|
+
base_blob: inspection.headBlob.objectId,
|
|
2856
|
+
overlay: options.overlayContent,
|
|
2857
|
+
overlay_fingerprint: render.overlayFingerprint,
|
|
2858
|
+
rendered_fingerprint: render.renderedFingerprint,
|
|
2859
|
+
skip_worktree: true,
|
|
2860
|
+
},
|
|
2861
|
+
};
|
|
2862
|
+
}
|
|
2863
|
+
function selectShadowToolForPath(toolNames, filePath) {
|
|
2864
|
+
const matchingTool = toolNames.find((toolName) => (0, tool_mapping_1.getToolDefinition)(toolName)?.targets.root_instruction?.path === filePath);
|
|
2865
|
+
if (matchingTool) {
|
|
2866
|
+
return matchingTool;
|
|
2867
|
+
}
|
|
2868
|
+
if (filePath === "AGENTS.md") {
|
|
2869
|
+
return "codex";
|
|
2870
|
+
}
|
|
2871
|
+
return toolNames.find((toolName) => toolName !== "codex") ?? "claude-code";
|
|
2872
|
+
}
|
|
2873
|
+
function applyTrackedRootInstructionShadowPlan(options) {
|
|
2874
|
+
const nextShadowedFiles = { ...options.existingShadowedFiles };
|
|
2875
|
+
for (const [filePath, shadowedFile] of Object.entries(options.existingShadowedFiles)) {
|
|
2876
|
+
if (shadowedFile.bundle !== options.bundleName ||
|
|
2877
|
+
options.plan.activeShadowPaths.has(filePath)) {
|
|
2878
|
+
continue;
|
|
2879
|
+
}
|
|
2880
|
+
assertTrackedRootInstructionShadowRetirementSafety({
|
|
2881
|
+
repoRoot: options.repoRoot,
|
|
2882
|
+
filePath,
|
|
2883
|
+
existingShadowedFile: shadowedFile,
|
|
2884
|
+
});
|
|
2885
|
+
restoreTrackedRootInstructionShadowTarget({
|
|
2886
|
+
repoRoot: options.repoRoot,
|
|
2887
|
+
filePath,
|
|
2888
|
+
});
|
|
2889
|
+
delete nextShadowedFiles[filePath];
|
|
2890
|
+
}
|
|
2891
|
+
for (const write of options.plan.writes) {
|
|
2892
|
+
const targetPath = node_path_1.default.join(options.repoRoot, write.filePath);
|
|
2893
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(targetPath), { recursive: true });
|
|
2894
|
+
node_fs_1.default.writeFileSync(targetPath, write.rendered);
|
|
2895
|
+
(0, git_index_1.setGitSkipWorktree)({
|
|
2896
|
+
repoRoot: options.repoRoot,
|
|
2897
|
+
filePath: write.filePath,
|
|
2898
|
+
});
|
|
2899
|
+
nextShadowedFiles[write.filePath] = write.state;
|
|
2900
|
+
}
|
|
2901
|
+
return nextShadowedFiles;
|
|
2902
|
+
}
|
|
2903
|
+
function assertTrackedRootInstructionShadowPlanCanApply(options) {
|
|
2904
|
+
for (const [filePath, shadowedFile] of Object.entries(options.existingShadowedFiles)) {
|
|
2905
|
+
if (shadowedFile.bundle !== options.bundleName ||
|
|
2906
|
+
options.plan.activeShadowPaths.has(filePath)) {
|
|
2907
|
+
continue;
|
|
2908
|
+
}
|
|
2909
|
+
assertTrackedRootInstructionShadowRetirementSafety({
|
|
2910
|
+
repoRoot: options.repoRoot,
|
|
2911
|
+
filePath,
|
|
2912
|
+
existingShadowedFile: shadowedFile,
|
|
2913
|
+
});
|
|
2914
|
+
}
|
|
2915
|
+
for (const write of options.plan.writes) {
|
|
2916
|
+
assertTrackedRootInstructionShadowWriteSafety({
|
|
2917
|
+
repoRoot: options.repoRoot,
|
|
2918
|
+
filePath: write.filePath,
|
|
2919
|
+
existingShadowedFile: options.existingShadowedFiles[write.filePath],
|
|
2920
|
+
operation: options.existingShadowedFiles[write.filePath]
|
|
2921
|
+
? "refresh"
|
|
2922
|
+
: "create",
|
|
2923
|
+
});
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
function assertTrackedRootInstructionShadowWriteSafety(options) {
|
|
2927
|
+
assertTrackedRootInstructionShadowSafety({
|
|
2928
|
+
repoRoot: options.repoRoot,
|
|
2929
|
+
filePath: options.filePath,
|
|
2930
|
+
operation: options.operation,
|
|
2931
|
+
});
|
|
2932
|
+
if (!options.existingShadowedFile) {
|
|
2933
|
+
return;
|
|
2934
|
+
}
|
|
2935
|
+
const targetPath = node_path_1.default.join(options.repoRoot, options.filePath);
|
|
2936
|
+
if (!node_fs_1.default.existsSync(targetPath) || !node_fs_1.default.statSync(targetPath).isFile()) {
|
|
2937
|
+
throw new Error(`Cannot refresh tracked root-instruction shadow for ${options.filePath} because the current shadow file is missing`);
|
|
2938
|
+
}
|
|
2939
|
+
if (fingerprintFile(targetPath) !==
|
|
2940
|
+
options.existingShadowedFile.rendered_fingerprint) {
|
|
2941
|
+
throw new Error(`Cannot refresh tracked root-instruction shadow for ${options.filePath} because the current worktree content no longer matches Skul's recorded render`);
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
function assertTrackedRootInstructionShadowRetirementSafety(options) {
|
|
2945
|
+
const targetPath = node_path_1.default.join(options.repoRoot, options.filePath);
|
|
2946
|
+
if (!node_fs_1.default.existsSync(targetPath) || !node_fs_1.default.statSync(targetPath).isFile()) {
|
|
2947
|
+
throw new Error(`Cannot retire tracked root-instruction shadow for ${options.filePath} because the current shadow file is missing`);
|
|
2948
|
+
}
|
|
2949
|
+
if (fingerprintFile(targetPath) !==
|
|
2950
|
+
options.existingShadowedFile.rendered_fingerprint) {
|
|
2951
|
+
throw new Error(`Cannot retire tracked root-instruction shadow for ${options.filePath} because the current worktree content no longer matches Skul's recorded render`);
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
function retireTrackedRootInstructionShadows(options) {
|
|
2955
|
+
const nextShadowedFiles = { ...options.shadowedFiles };
|
|
2956
|
+
for (const filePath of options.filePaths) {
|
|
2957
|
+
const shadowedFile = nextShadowedFiles[filePath];
|
|
2958
|
+
if (!shadowedFile) {
|
|
2959
|
+
continue;
|
|
2960
|
+
}
|
|
2961
|
+
assertTrackedRootInstructionShadowRetirementSafety({
|
|
2962
|
+
repoRoot: options.repoRoot,
|
|
2963
|
+
filePath,
|
|
2964
|
+
existingShadowedFile: shadowedFile,
|
|
2965
|
+
});
|
|
2966
|
+
}
|
|
2967
|
+
for (const filePath of options.filePaths) {
|
|
2968
|
+
const shadowedFile = nextShadowedFiles[filePath];
|
|
2969
|
+
if (!shadowedFile) {
|
|
2970
|
+
continue;
|
|
2971
|
+
}
|
|
2972
|
+
assertTrackedRootInstructionShadowRetirementSafety({
|
|
2973
|
+
repoRoot: options.repoRoot,
|
|
2974
|
+
filePath,
|
|
2975
|
+
existingShadowedFile: shadowedFile,
|
|
2976
|
+
});
|
|
2977
|
+
restoreTrackedRootInstructionShadowTarget({
|
|
2978
|
+
repoRoot: options.repoRoot,
|
|
2979
|
+
filePath,
|
|
2980
|
+
});
|
|
2981
|
+
delete nextShadowedFiles[filePath];
|
|
2982
|
+
}
|
|
2983
|
+
return nextShadowedFiles;
|
|
2984
|
+
}
|
|
2985
|
+
function restoreTrackedRootInstructionShadowTarget(options) {
|
|
2986
|
+
const inspection = (0, git_index_1.inspectRootInstructionShadowTarget)({
|
|
2987
|
+
repoRoot: options.repoRoot,
|
|
2988
|
+
filePath: options.filePath,
|
|
2989
|
+
});
|
|
2990
|
+
if (!inspection.headBlob) {
|
|
2991
|
+
throw new Error(`Cannot restore tracked root-instruction shadow target for ${options.filePath} because the target does not have HEAD content`);
|
|
2992
|
+
}
|
|
2993
|
+
const targetPath = node_path_1.default.join(options.repoRoot, options.filePath);
|
|
2994
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(targetPath), { recursive: true });
|
|
2995
|
+
node_fs_1.default.writeFileSync(targetPath, inspection.headBlob.content);
|
|
2996
|
+
(0, git_index_1.clearGitSkipWorktree)({
|
|
2997
|
+
repoRoot: options.repoRoot,
|
|
2998
|
+
filePath: options.filePath,
|
|
2999
|
+
});
|
|
3000
|
+
}
|
|
3001
|
+
function excludeShadowedTrackedRootInstructionTargets(state, deferredMaterializationTargets) {
|
|
3002
|
+
if (deferredMaterializationTargets.size === 0) {
|
|
3003
|
+
return state;
|
|
3004
|
+
}
|
|
3005
|
+
const files = state.files.filter((filePath) => !deferredMaterializationTargets.has(filePath));
|
|
3006
|
+
const fileFingerprints = Object.fromEntries(Object.entries(state.file_fingerprints).filter(([filePath]) => !deferredMaterializationTargets.has(filePath)));
|
|
3007
|
+
return {
|
|
3008
|
+
files,
|
|
3009
|
+
file_fingerprints: fileFingerprints,
|
|
3010
|
+
...(state.directories !== undefined
|
|
3011
|
+
? { directories: state.directories }
|
|
3012
|
+
: {}),
|
|
3013
|
+
};
|
|
3014
|
+
}
|
|
3015
|
+
function isDesiredBundleMaterialized(options) {
|
|
3016
|
+
const expectedTools = options.desiredEntry.tools ?? options.availableTools;
|
|
3017
|
+
return (expectedTools.every((toolName) => toolName in options.materializedBundleState.tools &&
|
|
3018
|
+
(0, bundle_items_1.bundleItemSelectionsEqual)(options.desiredEntry.items, options.materializedBundleState.tools[toolName]?.items)) &&
|
|
3019
|
+
(options.desiredEntry.resolved_commit === undefined ||
|
|
3020
|
+
options.materializedBundleState.resolved_commit ===
|
|
3021
|
+
options.desiredEntry.resolved_commit));
|
|
3022
|
+
}
|
|
3023
|
+
function getToolsToApply(options) {
|
|
3024
|
+
const expectedTools = options.desiredEntry.tools ?? options.availableTools;
|
|
3025
|
+
if (!options.materializedBundleState) {
|
|
3026
|
+
return options.desiredEntry.tools;
|
|
3027
|
+
}
|
|
3028
|
+
if (options.desiredEntry.resolved_commit !== undefined &&
|
|
3029
|
+
options.materializedBundleState.resolved_commit !==
|
|
3030
|
+
options.desiredEntry.resolved_commit) {
|
|
3031
|
+
return options.desiredEntry.tools ?? options.availableTools;
|
|
3032
|
+
}
|
|
3033
|
+
const existingTools = options.materializedBundleState.tools;
|
|
3034
|
+
return expectedTools.filter((toolName) => !(toolName in existingTools) ||
|
|
3035
|
+
!(0, bundle_items_1.bundleItemSelectionsEqual)(options.desiredEntry.items, existingTools[toolName]?.items));
|
|
3036
|
+
}
|
|
3037
|
+
// Flatten all files and directories from every tool within a single bundle
|
|
3038
|
+
function flattenBundleState(bundleState) {
|
|
3039
|
+
const files = new Set();
|
|
3040
|
+
const file_fingerprints = {};
|
|
3041
|
+
const directories = new Set();
|
|
3042
|
+
for (const toolState of Object.values(bundleState.tools)) {
|
|
3043
|
+
for (const file of toolState.files) {
|
|
3044
|
+
files.add(file);
|
|
3045
|
+
}
|
|
3046
|
+
if (toolState.file_fingerprints)
|
|
3047
|
+
Object.assign(file_fingerprints, toolState.file_fingerprints);
|
|
3048
|
+
if (toolState.directories) {
|
|
3049
|
+
for (const directory of toolState.directories) {
|
|
3050
|
+
directories.add(directory);
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
return {
|
|
3055
|
+
files: Array.from(files),
|
|
3056
|
+
file_fingerprints,
|
|
3057
|
+
directories: Array.from(directories),
|
|
3058
|
+
};
|
|
3059
|
+
}
|
|
3060
|
+
function selectExistingBundleToolState(bundleState, toolNames) {
|
|
3061
|
+
return {
|
|
3062
|
+
...(bundleState.source !== undefined ? { source: bundleState.source } : {}),
|
|
3063
|
+
...(bundleState.resolved_commit !== undefined
|
|
3064
|
+
? { resolved_commit: bundleState.resolved_commit }
|
|
3065
|
+
: {}),
|
|
3066
|
+
tools: Object.fromEntries(toolNames.flatMap((toolName) => {
|
|
3067
|
+
const toolState = bundleState.tools[toolName];
|
|
3068
|
+
return toolState ? [[toolName, toolState]] : [];
|
|
3069
|
+
})),
|
|
3070
|
+
};
|
|
3071
|
+
}
|
|
3072
|
+
// Build per-tool registry entries from a materialization result
|
|
3073
|
+
function buildMaterializedToolStates(repoRoot, result, selectedItems) {
|
|
3074
|
+
return Object.fromEntries(Object.entries(result.byTool).map(([toolName, toolResult]) => [
|
|
3075
|
+
toolName,
|
|
3076
|
+
{
|
|
3077
|
+
files: toolResult.files,
|
|
3078
|
+
file_fingerprints: captureManagedFileFingerprints(repoRoot, toolResult.files),
|
|
3079
|
+
...(toolResult.directories.length > 0
|
|
3080
|
+
? { directories: toolResult.directories }
|
|
3081
|
+
: {}),
|
|
3082
|
+
...(selectedItems !== undefined ? { items: selectedItems } : {}),
|
|
3083
|
+
},
|
|
3084
|
+
]));
|
|
3085
|
+
}
|
|
3086
|
+
function buildMaterializedBundleState(options) {
|
|
3087
|
+
const preservedTools = options.existingBundleState && options.selectedTools
|
|
3088
|
+
? Object.fromEntries(Object.entries(options.existingBundleState.tools).filter(([toolName]) => !options.selectedTools.includes(toolName)))
|
|
3089
|
+
: {};
|
|
3090
|
+
return {
|
|
3091
|
+
...(options.source !== undefined
|
|
3092
|
+
? { source: options.source }
|
|
3093
|
+
: options.existingBundleState?.source !== undefined
|
|
3094
|
+
? { source: options.existingBundleState.source }
|
|
3095
|
+
: {}),
|
|
3096
|
+
...(options.resolvedCommit !== undefined
|
|
3097
|
+
? { resolved_commit: options.resolvedCommit }
|
|
3098
|
+
: options.existingBundleState?.resolved_commit !== undefined
|
|
3099
|
+
? { resolved_commit: options.existingBundleState.resolved_commit }
|
|
3100
|
+
: {}),
|
|
3101
|
+
tools: {
|
|
3102
|
+
...preservedTools,
|
|
3103
|
+
...buildMaterializedToolStates(options.repoRoot, options.materializedResult, options.selectedItems),
|
|
3104
|
+
},
|
|
3105
|
+
};
|
|
3106
|
+
}
|
|
3107
|
+
// Collect all files across every bundle and tool for git-exclude configuration
|
|
3108
|
+
function collectAllFiles(materializedState) {
|
|
3109
|
+
return Array.from(new Set(Object.values(materializedState.bundles).flatMap((bundleState) => Object.values(bundleState.tools).flatMap((toolState) => toolState.files))));
|
|
3110
|
+
}
|
|
3111
|
+
function removeManagedPaths(repoRoot, state) {
|
|
3112
|
+
for (const relativePath of (0, registry_1.listManagedPathsForRemoval)(state)) {
|
|
3113
|
+
const targetPath = node_path_1.default.join(repoRoot, relativePath);
|
|
3114
|
+
if (!node_fs_1.default.existsSync(targetPath)) {
|
|
3115
|
+
continue;
|
|
3116
|
+
}
|
|
3117
|
+
const stats = node_fs_1.default.lstatSync(targetPath);
|
|
3118
|
+
if (stats.isDirectory()) {
|
|
3119
|
+
try {
|
|
3120
|
+
node_fs_1.default.rmdirSync(targetPath);
|
|
3121
|
+
}
|
|
3122
|
+
catch (error) {
|
|
3123
|
+
if (!isDirectoryNotEmptyError(error)) {
|
|
3124
|
+
throw error;
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3127
|
+
continue;
|
|
3128
|
+
}
|
|
3129
|
+
node_fs_1.default.rmSync(targetPath, { force: true });
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
function isDirectoryNotEmptyError(error) {
|
|
3133
|
+
return (error instanceof Error && "code" in error && error.code === "ENOTEMPTY");
|
|
3134
|
+
}
|
|
3135
|
+
function requireGitContext(cwd, command) {
|
|
3136
|
+
const gitContext = (0, git_context_1.detectGitContext)({ cwd });
|
|
3137
|
+
if (!gitContext) {
|
|
3138
|
+
throw new Error(`skul ${command} requires a Git repository. Run "git init" to initialize one`);
|
|
3139
|
+
}
|
|
3140
|
+
return gitContext;
|
|
3141
|
+
}
|
|
3142
|
+
/**
|
|
3143
|
+
* Throws when Skul is about to create or refresh a tracked root-instruction
|
|
3144
|
+
* shadow on top of a Git path that is not in a safe state to overwrite.
|
|
3145
|
+
*
|
|
3146
|
+
* Callers typically run it before removing existing managed files and again as
|
|
3147
|
+
* a write-time backstop during materialization.
|
|
3148
|
+
*/
|
|
3149
|
+
function assertTrackedRootInstructionShadowSafety(options) {
|
|
3150
|
+
assertTrackedRootInstructionShadowSafetyForAction({
|
|
3151
|
+
repoRoot: options.repoRoot,
|
|
3152
|
+
filePath: options.filePath,
|
|
3153
|
+
action: options.operation,
|
|
3154
|
+
});
|
|
3155
|
+
}
|
|
3156
|
+
function assertTrackedRootInstructionShadowSafetyForAction(options) {
|
|
3157
|
+
const inspection = (0, git_index_1.inspectRootInstructionShadowTarget)({
|
|
3158
|
+
repoRoot: options.repoRoot,
|
|
3159
|
+
filePath: options.filePath,
|
|
3160
|
+
});
|
|
3161
|
+
if (!inspection.tracked) {
|
|
3162
|
+
return;
|
|
3163
|
+
}
|
|
3164
|
+
const actionLabel = options.action;
|
|
3165
|
+
if (inspection.issues.includes("unmerged")) {
|
|
3166
|
+
throw new Error(`Cannot ${actionLabel} tracked root-instruction shadow for ${options.filePath} because the target has unmerged index entries`);
|
|
3167
|
+
}
|
|
3168
|
+
if (inspection.issues.includes("missing-head")) {
|
|
3169
|
+
throw new Error(`Cannot ${actionLabel} tracked root-instruction shadow for ${options.filePath} because the target does not have HEAD content`);
|
|
3170
|
+
}
|
|
3171
|
+
if (inspection.issues.includes("staged-changes")) {
|
|
3172
|
+
throw new Error(`Cannot ${actionLabel} tracked root-instruction shadow for ${options.filePath} because the target has staged changes`);
|
|
3173
|
+
}
|
|
3174
|
+
if (inspection.issues.includes("unstaged-changes")) {
|
|
3175
|
+
throw new Error(`Cannot ${actionLabel} tracked root-instruction shadow for ${options.filePath} because the target has unstaged changes`);
|
|
3176
|
+
}
|
|
3177
|
+
if (inspection.issues.includes("incompatible-index-flags")) {
|
|
3178
|
+
throw new Error(`Cannot ${actionLabel} tracked root-instruction shadow for ${options.filePath} because the target has incompatible index flags: ${inspection.indexFlags.join(", ")}`);
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
3181
|
+
function assertTrackedRootInstructionShadowSafetyForPaths(options) {
|
|
3182
|
+
for (const filePath of options.filePaths) {
|
|
3183
|
+
if (!(0, root_instruction_render_1.isRootInstructionPath)(filePath)) {
|
|
3184
|
+
continue;
|
|
3185
|
+
}
|
|
3186
|
+
assertTrackedRootInstructionShadowSafety({
|
|
3187
|
+
repoRoot: options.repoRoot,
|
|
3188
|
+
filePath,
|
|
3189
|
+
operation: options.operation,
|
|
3190
|
+
});
|
|
3191
|
+
}
|
|
3192
|
+
}
|
|
3193
|
+
function createTrackedRootInstructionShadowSafetyAssertion(options) {
|
|
3194
|
+
return (repoRelativePath) => {
|
|
3195
|
+
if (!(0, root_instruction_render_1.isRootInstructionPath)(repoRelativePath)) {
|
|
3196
|
+
return;
|
|
3197
|
+
}
|
|
3198
|
+
assertTrackedRootInstructionShadowSafety({
|
|
3199
|
+
repoRoot: options.repoRoot,
|
|
3200
|
+
filePath: repoRelativePath,
|
|
3201
|
+
operation: options.operation,
|
|
3202
|
+
});
|
|
3203
|
+
};
|
|
3204
|
+
}
|
|
3205
|
+
function selectDesiredEntries(desiredState, bundle, command) {
|
|
3206
|
+
if (!bundle) {
|
|
3207
|
+
return desiredState;
|
|
3208
|
+
}
|
|
3209
|
+
const matchingEntry = desiredState.find((entry) => entry.bundle === bundle);
|
|
3210
|
+
if (!matchingEntry) {
|
|
3211
|
+
throw new Error(`Bundle not found in active set: ${bundle}. Run "skul status" to see configured bundles`);
|
|
3212
|
+
}
|
|
3213
|
+
return [matchingEntry];
|
|
3214
|
+
}
|
|
3215
|
+
function mergeDesiredTools(options) {
|
|
3216
|
+
if (options.requestedTools === undefined) {
|
|
3217
|
+
return undefined;
|
|
3218
|
+
}
|
|
3219
|
+
if (options.replace || options.existingEntry?.tools === undefined) {
|
|
3220
|
+
return [...options.requestedTools];
|
|
3221
|
+
}
|
|
3222
|
+
return Array.from(new Set([...options.existingEntry.tools, ...options.requestedTools])).sort((left, right) => left.localeCompare(right));
|
|
3223
|
+
}
|
|
3224
|
+
function upsertDesiredEntryPreservingOrder(desiredState, nextEntry) {
|
|
3225
|
+
const existingIndex = desiredState.findIndex((entry) => entry.bundle === nextEntry.bundle);
|
|
3226
|
+
if (existingIndex === -1) {
|
|
3227
|
+
return [...desiredState, nextEntry];
|
|
3228
|
+
}
|
|
3229
|
+
return desiredState.map((entry, index) => index === existingIndex ? nextEntry : entry);
|
|
3230
|
+
}
|
|
3231
|
+
function getToolsToRefresh(entry, existingBundleState) {
|
|
3232
|
+
if (entry.tools === undefined) {
|
|
3233
|
+
return undefined;
|
|
3234
|
+
}
|
|
3235
|
+
const existingTools = existingBundleState
|
|
3236
|
+
? Object.keys(existingBundleState.tools)
|
|
3237
|
+
: [];
|
|
3238
|
+
return Array.from(new Set([...entry.tools, ...existingTools])).sort((left, right) => left.localeCompare(right));
|
|
3239
|
+
}
|
|
3240
|
+
function formatCommitTransition(currentCommit, nextCommit) {
|
|
3241
|
+
return currentCommit
|
|
3242
|
+
? ` ${shortCommit(currentCommit)} -> ${shortCommit(nextCommit)}`
|
|
3243
|
+
: ` to ${shortCommit(nextCommit)}`;
|
|
3244
|
+
}
|
|
3245
|
+
function shortCommit(commit) {
|
|
3246
|
+
return commit.slice(0, 7);
|
|
3247
|
+
}
|
|
3248
|
+
function resolveDesiredCachedBundle(libraryDir, entry) {
|
|
3249
|
+
return findCachedBundleWithGuidance({
|
|
3250
|
+
libraryDir,
|
|
3251
|
+
bundle: entry.bundle,
|
|
3252
|
+
source: entry.source,
|
|
3253
|
+
});
|
|
3254
|
+
}
|
|
3255
|
+
function readRegistryWithGuidance(registryFile) {
|
|
3256
|
+
try {
|
|
3257
|
+
return (0, registry_1.readRegistryFile)(registryFile);
|
|
3258
|
+
}
|
|
3259
|
+
catch (error) {
|
|
3260
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
3261
|
+
throw new Error(`Registry is corrupted (${detail}). Please repair or remove ${registryFile} and try again.`);
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
3264
|
+
function findCachedBundleWithGuidance(options) {
|
|
3265
|
+
try {
|
|
3266
|
+
return (0, bundle_discovery_1.findCachedBundle)(options);
|
|
3267
|
+
}
|
|
3268
|
+
catch (error) {
|
|
3269
|
+
if (error instanceof Error && /^Bundle not found: /.test(error.message)) {
|
|
3270
|
+
const availableBundles = (0, bundle_discovery_1.listCachedBundles)({
|
|
3271
|
+
libraryDir: options.libraryDir,
|
|
3272
|
+
}).map((bundle) => bundle.bundle);
|
|
3273
|
+
if (availableBundles.length === 0) {
|
|
3274
|
+
throw new Error(`${error.message}\n\nNo bundles are cached yet. Add one from a Git source:\n skul add github.com/<owner>/<repo> <bundle-name>`);
|
|
3275
|
+
}
|
|
3276
|
+
throw new Error(`${error.message}\nAvailable bundles:\n${Array.from(new Set(availableBundles))
|
|
3277
|
+
.sort((left, right) => left.localeCompare(right))
|
|
3278
|
+
.join("\n")}`);
|
|
3279
|
+
}
|
|
3280
|
+
throw error;
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
async function confirmManagedFileRemovals(repoRoot, state, prompts, operation) {
|
|
3284
|
+
for (const relativePath of findModifiedManagedFiles(repoRoot, state)) {
|
|
3285
|
+
const confirmed = await prompts.confirmManagedFileRemoval(relativePath, operation);
|
|
3286
|
+
if (!confirmed) {
|
|
3287
|
+
return false;
|
|
3288
|
+
}
|
|
3289
|
+
}
|
|
3290
|
+
return true;
|
|
3291
|
+
}
|
|
3292
|
+
function findModifiedManagedFiles(repoRoot, state) {
|
|
3293
|
+
return state.files.filter((relativePath) => {
|
|
3294
|
+
const fingerprint = state.file_fingerprints?.[relativePath];
|
|
3295
|
+
if (!fingerprint) {
|
|
3296
|
+
return false;
|
|
3297
|
+
}
|
|
3298
|
+
const targetPath = node_path_1.default.join(repoRoot, relativePath);
|
|
3299
|
+
if (!node_fs_1.default.existsSync(targetPath) || !node_fs_1.default.lstatSync(targetPath).isFile()) {
|
|
3300
|
+
return false;
|
|
3301
|
+
}
|
|
3302
|
+
return fingerprint !== fingerprintFile(targetPath);
|
|
3303
|
+
});
|
|
3304
|
+
}
|
|
3305
|
+
function captureManagedFileFingerprints(repoRoot, files) {
|
|
3306
|
+
return Object.fromEntries(files.map((relativePath) => [
|
|
3307
|
+
relativePath,
|
|
3308
|
+
fingerprintFile(node_path_1.default.join(repoRoot, relativePath)),
|
|
3309
|
+
]));
|
|
3310
|
+
}
|
|
3311
|
+
function fingerprintFile(filePath) {
|
|
3312
|
+
try {
|
|
3313
|
+
return (0, node_crypto_1.createHash)("sha256").update(node_fs_1.default.readFileSync(filePath)).digest("hex");
|
|
3314
|
+
}
|
|
3315
|
+
catch {
|
|
3316
|
+
// On read failure treat the file as modified so callers prompt before deletion
|
|
3317
|
+
// rather than silently skipping a managed file that may still exist.
|
|
3318
|
+
return "";
|
|
3319
|
+
}
|
|
3320
|
+
}
|
|
3321
|
+
async function applyBundleGlobal(options) {
|
|
3322
|
+
const supportedTools = (0, tool_mapping_1.globalCapableToolNames)();
|
|
3323
|
+
if (options.agents.length > 0) {
|
|
3324
|
+
const unsupported = options.agents.filter((t) => !supportedTools.includes(t));
|
|
3325
|
+
if (unsupported.length > 0) {
|
|
3326
|
+
throw new Error(`Global mode only supports: ${supportedTools.join(", ")}. Unsupported: ${unsupported.join(", ")}`);
|
|
3327
|
+
}
|
|
3328
|
+
}
|
|
3329
|
+
const repoRelPathRemapper = tool_mapping_1.GLOBAL_TOOL_MATERIALIZATION_LAYOUT.remapRepoRelPath;
|
|
3330
|
+
let registry = readRegistryWithGuidance(options.registryFile);
|
|
3331
|
+
const existingGlobal = registry.global;
|
|
3332
|
+
if (shouldApplySelectedItemsAcrossSourceBundles(options)) {
|
|
3333
|
+
return applySelectedItemsAcrossGlobalSourceBundles({
|
|
3334
|
+
homeDir: options.homeDir,
|
|
3335
|
+
prompts: options.prompts,
|
|
3336
|
+
registryFile: options.registryFile,
|
|
3337
|
+
libraryDir: options.libraryDir,
|
|
3338
|
+
source: options.source,
|
|
3339
|
+
protocol: options.protocol,
|
|
3340
|
+
agents: options.agents.length > 0
|
|
3341
|
+
? options.agents.filter((toolName) => supportedTools.includes(toolName))
|
|
3342
|
+
: [],
|
|
3343
|
+
includeItems: options.includeItems,
|
|
3344
|
+
dryRun: options.dryRun,
|
|
3345
|
+
ref: options.ref,
|
|
3346
|
+
existingDesiredState: existingGlobal?.desired_state ?? [],
|
|
3347
|
+
disableModelInvocation: options.disableModelInvocation,
|
|
3348
|
+
});
|
|
3349
|
+
}
|
|
3350
|
+
// When no --agent is specified, auto-select all globally-capable tools that
|
|
3351
|
+
// the bundle actually supports, rather than requesting all supported tools
|
|
3352
|
+
// upfront (which would fail validation for bundles that don't cover every tool).
|
|
3353
|
+
const globalAutoSelectPrompts = {
|
|
3354
|
+
...options.prompts,
|
|
3355
|
+
selectAgents: async (availableAgents) => availableAgents.filter((t) => supportedTools.includes(t)),
|
|
3356
|
+
};
|
|
3357
|
+
// Skip cloning in dry-run: when a remote source is specified and not yet
|
|
3358
|
+
// cached, return a preview message immediately so no network I/O occurs.
|
|
3359
|
+
if (options.dryRun && options.source) {
|
|
3360
|
+
const { cached } = (0, bundle_fetch_1.readCachedSourceRevision)({
|
|
3361
|
+
source: options.source,
|
|
3362
|
+
libraryDir: options.libraryDir,
|
|
3363
|
+
protocol: options.protocol,
|
|
3364
|
+
});
|
|
3365
|
+
if (!cached) {
|
|
3366
|
+
const toolsLabel = options.agents.length > 0
|
|
3367
|
+
? options.agents.join(", ")
|
|
3368
|
+
: "globally supported tools";
|
|
3369
|
+
return [
|
|
3370
|
+
pc.dim(`(would clone ${options.source})`),
|
|
3371
|
+
`${pc.yellow("DRY RUN:")} Would apply ${options.bundle} globally for ${toolsLabel}`,
|
|
3372
|
+
].join("\n");
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
const preparedBundle = await prepareApplyBundle({
|
|
3376
|
+
bundle: options.bundle,
|
|
3377
|
+
source: options.source,
|
|
3378
|
+
protocol: options.protocol,
|
|
3379
|
+
requestedTools: options.agents.length > 0
|
|
3380
|
+
? options.agents.filter((t) => supportedTools.includes(t))
|
|
3381
|
+
: [],
|
|
3382
|
+
requestedItems: options.includeItems,
|
|
3383
|
+
selectItems: options.selectItems,
|
|
3384
|
+
replaceItems: options.replaceItems,
|
|
3385
|
+
existingDesiredState: existingGlobal?.desired_state ?? [],
|
|
3386
|
+
libraryDir: options.libraryDir,
|
|
3387
|
+
ref: options.ref,
|
|
3388
|
+
prompts: options.agents.length > 0 ? options.prompts : globalAutoSelectPrompts,
|
|
3389
|
+
preBundlePrompts: options.prompts,
|
|
3390
|
+
inferredBundleFromSource: options.inferredBundleFromSource,
|
|
3391
|
+
refreshedSources: options.refreshedSources,
|
|
3392
|
+
});
|
|
3393
|
+
const availableGlobalTools = preparedBundle.nextToolNames.filter((t) => supportedTools.includes(t));
|
|
3394
|
+
if (availableGlobalTools.length === 0) {
|
|
3395
|
+
const bundleTools = Object.keys(preparedBundle.cachedBundle.manifest.tools);
|
|
3396
|
+
throw new Error(`Bundle "${preparedBundle.cachedBundle.bundle}" has no globally installable tools (bundle provides: ${bundleTools.join(", ")}; global mode supports: ${supportedTools.join(", ")})`);
|
|
3397
|
+
}
|
|
3398
|
+
// Warn when auto-selecting (no --agent) and the bundle contains tools that
|
|
3399
|
+
// aren't globally installable — they were silently dropped by globalAutoSelectPrompts.
|
|
3400
|
+
const skippedTools = options.agents.length === 0
|
|
3401
|
+
? Object.keys(preparedBundle.cachedBundle.manifest.tools).filter((t) => !supportedTools.includes(t))
|
|
3402
|
+
: [];
|
|
3403
|
+
if (options.dryRun) {
|
|
3404
|
+
return [
|
|
3405
|
+
...preparedBundle.cloneLines,
|
|
3406
|
+
`${pc.yellow("DRY RUN:")} Would ${formatApplyGlobalBundleMessage({
|
|
3407
|
+
bundle: preparedBundle.cachedBundle.bundle,
|
|
3408
|
+
toolLabel: availableGlobalTools.join(", "),
|
|
3409
|
+
items: preparedBundle.replacesItemSelection
|
|
3410
|
+
? preparedBundle.selectedItems
|
|
3411
|
+
: undefined,
|
|
3412
|
+
})}`,
|
|
3413
|
+
].join("\n");
|
|
3414
|
+
}
|
|
3415
|
+
let rootInstructionBaseContents = existingGlobal?.materialized_state.root_instruction_base_contents;
|
|
3416
|
+
const existingBundleState = existingGlobal?.materialized_state.bundles[preparedBundle.cachedBundle.bundle];
|
|
3417
|
+
const plannedWriteTargets = (0, bundle_materialization_1.previewMaterializeBundleWriteTargets)({
|
|
3418
|
+
repoRoot: options.homeDir,
|
|
3419
|
+
bundleDir: node_path_1.default.dirname(preparedBundle.cachedBundle.manifestFile),
|
|
3420
|
+
manifest: preparedBundle.cachedBundle.manifest,
|
|
3421
|
+
tools: availableGlobalTools,
|
|
3422
|
+
pathLayout: tool_mapping_1.GLOBAL_TOOL_MATERIALIZATION_LAYOUT,
|
|
3423
|
+
itemSelectors: preparedBundle.selectedItems,
|
|
3424
|
+
disableModelInvocation: options.disableModelInvocation,
|
|
3425
|
+
});
|
|
3426
|
+
const plannedRootInstructionTargets = new Set(plannedWriteTargets.filter((p) => (0, root_instruction_render_1.isRootInstructionPath)(p)));
|
|
3427
|
+
const existingBundles = existingGlobal?.materialized_state.bundles ?? {};
|
|
3428
|
+
rootInstructionBaseContents = (0, root_instruction_state_1.captureRootInstructionBaseContents)({
|
|
3429
|
+
repoRoot: options.homeDir,
|
|
3430
|
+
targetPaths: plannedRootInstructionTargets,
|
|
3431
|
+
existingBaseContents: rootInstructionBaseContents,
|
|
3432
|
+
managedTargetPaths: (0, root_instruction_state_1.collectManagedRootInstructionTargets)(existingBundles),
|
|
3433
|
+
});
|
|
3434
|
+
const existingDesiredState = existingGlobal?.desired_state ?? [];
|
|
3435
|
+
(0, root_instruction_state_1.assertManagedRootInstructionSyncSourcesCached)({
|
|
3436
|
+
desiredState: existingDesiredState,
|
|
3437
|
+
materializedBundles: existingBundles,
|
|
3438
|
+
targetPaths: plannedRootInstructionTargets,
|
|
3439
|
+
resolveCachedBundle: (entry) => resolveDesiredCachedBundle(options.libraryDir, entry),
|
|
3440
|
+
});
|
|
3441
|
+
let pathsToReplace = null;
|
|
3442
|
+
if (existingBundleState) {
|
|
3443
|
+
const toolsToReplace = options.agents.length > 0
|
|
3444
|
+
? options.agents.filter((t) => t in existingBundleState.tools)
|
|
3445
|
+
: Object.keys(existingBundleState.tools);
|
|
3446
|
+
pathsToReplace = flattenBundleState({
|
|
3447
|
+
tools: Object.fromEntries(toolsToReplace.map((t) => [t, existingBundleState.tools[t]])),
|
|
3448
|
+
});
|
|
3449
|
+
const replacementAllowed = await confirmManagedFileRemovals(options.homeDir, pathsToReplace, options.prompts, "replace");
|
|
3450
|
+
if (!replacementAllowed) {
|
|
3451
|
+
throw new Error("Replacement aborted because a modified managed file was kept");
|
|
3452
|
+
}
|
|
3453
|
+
}
|
|
3454
|
+
const sharedRootInstructionState = (0, root_instruction_state_1.collectSharedRootInstructionState)(existingBundles, plannedWriteTargets, preparedBundle.cachedBundle.bundle);
|
|
3455
|
+
if (sharedRootInstructionState.files.length > 0) {
|
|
3456
|
+
const replacementAllowed = await confirmManagedFileRemovals(options.homeDir, sharedRootInstructionState, options.prompts, "replace");
|
|
3457
|
+
if (!replacementAllowed) {
|
|
3458
|
+
throw new Error("Replacement aborted because a modified managed file was kept");
|
|
3459
|
+
}
|
|
3460
|
+
}
|
|
3461
|
+
if (pathsToReplace) {
|
|
3462
|
+
removeManagedPaths(options.homeDir, pathsToReplace);
|
|
3463
|
+
}
|
|
3464
|
+
const materializedResult = await (0, bundle_materialization_1.materializeBundle)({
|
|
3465
|
+
repoRoot: options.homeDir,
|
|
3466
|
+
bundleDir: node_path_1.default.dirname(preparedBundle.cachedBundle.manifestFile),
|
|
3467
|
+
manifest: preparedBundle.cachedBundle.manifest,
|
|
3468
|
+
tools: availableGlobalTools,
|
|
3469
|
+
bundleName: preparedBundle.cachedBundle.bundle,
|
|
3470
|
+
bundleSource: preparedBundle.bundleSource,
|
|
3471
|
+
allowFileOverwriteTargets: (0, root_instruction_state_1.collectManagedRootInstructionTargets)(existingBundles),
|
|
3472
|
+
rootInstructionBaseContents,
|
|
3473
|
+
resolveFileConflict: options.prompts.resolveFileConflict,
|
|
3474
|
+
pathLayout: tool_mapping_1.GLOBAL_TOOL_MATERIALIZATION_LAYOUT,
|
|
3475
|
+
itemSelectors: preparedBundle.selectedItems,
|
|
3476
|
+
disableModelInvocation: options.disableModelInvocation,
|
|
3477
|
+
});
|
|
3478
|
+
const newBundleState = buildMaterializedBundleState({
|
|
3479
|
+
existingBundleState,
|
|
3480
|
+
materializedResult,
|
|
3481
|
+
repoRoot: options.homeDir,
|
|
3482
|
+
source: preparedBundle.bundleSource,
|
|
3483
|
+
resolvedCommit: preparedBundle.sourceRevision?.currentCommit,
|
|
3484
|
+
selectedTools: availableGlobalTools,
|
|
3485
|
+
selectedItems: preparedBundle.selectedItems,
|
|
3486
|
+
});
|
|
3487
|
+
const newDesiredEntry = buildDesiredEntryForAppliedBundle({
|
|
3488
|
+
existingDesiredState,
|
|
3489
|
+
cachedBundle: preparedBundle.cachedBundle,
|
|
3490
|
+
requestedSource: options.source,
|
|
3491
|
+
requestedProtocol: options.protocol,
|
|
3492
|
+
requestedRef: options.ref,
|
|
3493
|
+
requestedTools: availableGlobalTools,
|
|
3494
|
+
replaceRequestedTools: options.agents.length === 0,
|
|
3495
|
+
requestedItems: preparedBundle.selectedItems,
|
|
3496
|
+
replaceRequestedItems: preparedBundle.replacesItemSelection,
|
|
3497
|
+
sourceRevision: preparedBundle.sourceRevision,
|
|
3498
|
+
disableModelInvocation: options.disableModelInvocation,
|
|
3499
|
+
});
|
|
3500
|
+
const newDesiredState = [
|
|
3501
|
+
...upsertDesiredEntryPreservingOrder(existingDesiredState, newDesiredEntry),
|
|
3502
|
+
];
|
|
3503
|
+
const newBundles = {
|
|
3504
|
+
...existingBundles,
|
|
3505
|
+
[preparedBundle.cachedBundle.bundle]: newBundleState,
|
|
3506
|
+
};
|
|
3507
|
+
const syncedRootInstructionPaths = (0, root_instruction_state_1.syncManagedRootInstructionFiles)({
|
|
3508
|
+
repoRoot: options.homeDir,
|
|
3509
|
+
desiredState: newDesiredState,
|
|
3510
|
+
materializedBundles: newBundles,
|
|
3511
|
+
rootInstructionBaseContents,
|
|
3512
|
+
targetPaths: plannedRootInstructionTargets,
|
|
3513
|
+
resolveCachedBundle: (entry) => resolveDesiredCachedBundle(options.libraryDir, entry),
|
|
3514
|
+
repoRelPathRemapper,
|
|
3515
|
+
});
|
|
3516
|
+
const refreshedBundles = (0, root_instruction_state_1.refreshManagedFileFingerprintsForPaths)(options.homeDir, newBundles, syncedRootInstructionPaths);
|
|
3517
|
+
const newGlobalState = {
|
|
3518
|
+
desired_state: newDesiredState,
|
|
3519
|
+
materialized_state: {
|
|
3520
|
+
bundles: refreshedBundles,
|
|
3521
|
+
...(rootInstructionBaseContents !== undefined
|
|
3522
|
+
? { root_instruction_base_contents: rootInstructionBaseContents }
|
|
3523
|
+
: {}),
|
|
3524
|
+
},
|
|
3525
|
+
};
|
|
3526
|
+
registry = (0, registry_1.upsertGlobalState)(registry, newGlobalState);
|
|
3527
|
+
(0, registry_1.writeRegistryFile)(options.registryFile, registry);
|
|
3528
|
+
const lines = [
|
|
3529
|
+
...preparedBundle.cloneLines,
|
|
3530
|
+
pc.green(formatAppliedGlobalBundleMessage({
|
|
3531
|
+
bundle: preparedBundle.cachedBundle.bundle,
|
|
3532
|
+
toolLabel: availableGlobalTools.join(", "),
|
|
3533
|
+
items: preparedBundle.replacesItemSelection
|
|
3534
|
+
? preparedBundle.selectedItems
|
|
3535
|
+
: undefined,
|
|
3536
|
+
})),
|
|
3537
|
+
];
|
|
3538
|
+
if (skippedTools.length > 0) {
|
|
3539
|
+
lines.push(pc.yellow(`Note: ${skippedTools.join(", ")} ${skippedTools.length === 1 ? "is" : "are"} not supported in global mode and ${skippedTools.length === 1 ? "was" : "were"} skipped`));
|
|
3540
|
+
}
|
|
3541
|
+
return lines.join("\n");
|
|
3542
|
+
}
|
|
3543
|
+
function formatAppliedGlobalBundleMessage(options) {
|
|
3544
|
+
return formatApplyGlobalBundleMessage({
|
|
3545
|
+
bundle: options.bundle,
|
|
3546
|
+
toolLabel: options.toolLabel,
|
|
3547
|
+
items: options.items,
|
|
3548
|
+
action: "Applied",
|
|
3549
|
+
});
|
|
3550
|
+
}
|
|
3551
|
+
function formatApplyGlobalBundleMessage(options) {
|
|
3552
|
+
const itemLabel = options.items !== undefined && options.items.length > 0
|
|
3553
|
+
? `: ${options.items.join(", ")}`
|
|
3554
|
+
: "";
|
|
3555
|
+
return `${options.action ?? "apply"} ${options.bundle} globally for ${options.toolLabel}${itemLabel}`;
|
|
3556
|
+
}
|
|
3557
|
+
async function applySelectedItemsAcrossGlobalSourceBundles(options) {
|
|
3558
|
+
const refreshedSources = new Set();
|
|
3559
|
+
const cloneLines = refreshBundleSourceForApply({
|
|
3560
|
+
source: options.source,
|
|
3561
|
+
libraryDir: options.libraryDir,
|
|
3562
|
+
protocol: options.protocol,
|
|
3563
|
+
ref: options.ref,
|
|
3564
|
+
}, refreshedSources);
|
|
3565
|
+
const selection = await selectSourceBundleItemApplyTargets({
|
|
3566
|
+
libraryDir: options.libraryDir,
|
|
3567
|
+
source: options.source,
|
|
3568
|
+
requestedTools: options.agents,
|
|
3569
|
+
requestedItems: options.includeItems,
|
|
3570
|
+
prompts: options.prompts,
|
|
3571
|
+
existingDesiredState: options.existingDesiredState,
|
|
3572
|
+
global: true,
|
|
3573
|
+
});
|
|
3574
|
+
const outputLines = [];
|
|
3575
|
+
for (const target of selection.removeTargets) {
|
|
3576
|
+
outputLines.push(await removeGlobalBundle({
|
|
3577
|
+
homeDir: options.homeDir,
|
|
3578
|
+
prompts: options.prompts,
|
|
3579
|
+
registryFile: options.registryFile,
|
|
3580
|
+
libraryDir: options.libraryDir,
|
|
3581
|
+
bundle: target.bundle,
|
|
3582
|
+
source: target.source,
|
|
3583
|
+
includeItems: [],
|
|
3584
|
+
selectItems: false,
|
|
3585
|
+
dryRun: options.dryRun,
|
|
3586
|
+
}));
|
|
3587
|
+
}
|
|
3588
|
+
for (const target of selection.applyTargets) {
|
|
3589
|
+
outputLines.push(await applyBundleGlobal({
|
|
3590
|
+
homeDir: options.homeDir,
|
|
3591
|
+
prompts: options.prompts,
|
|
3592
|
+
registryFile: options.registryFile,
|
|
3593
|
+
libraryDir: options.libraryDir,
|
|
3594
|
+
bundle: target.bundle,
|
|
3595
|
+
source: target.source,
|
|
3596
|
+
protocol: options.protocol,
|
|
3597
|
+
agents: target.tools,
|
|
3598
|
+
includeItems: target.items,
|
|
3599
|
+
selectItems: false,
|
|
3600
|
+
replaceItems: true,
|
|
3601
|
+
dryRun: options.dryRun,
|
|
3602
|
+
ref: options.ref,
|
|
3603
|
+
refreshedSources,
|
|
3604
|
+
disableModelInvocation: options.disableModelInvocation,
|
|
3605
|
+
}));
|
|
3606
|
+
}
|
|
3607
|
+
return [...cloneLines, ...outputLines].filter(Boolean).join("\n");
|
|
3608
|
+
}
|
|
3609
|
+
function renderGlobalStatus(options) {
|
|
3610
|
+
const registry = readRegistryWithGuidance(options.registryFile);
|
|
3611
|
+
const globalState = registry.global;
|
|
3612
|
+
if (options.json) {
|
|
3613
|
+
return JSON.stringify({
|
|
3614
|
+
desired_state: globalState?.desired_state ?? [],
|
|
3615
|
+
materialized: {
|
|
3616
|
+
bundles: Object.fromEntries(Object.entries(globalState?.materialized_state.bundles ?? {}).map(([bundleName, bundleState]) => [
|
|
3617
|
+
bundleName,
|
|
3618
|
+
{
|
|
3619
|
+
tools: Object.fromEntries(Object.entries(bundleState.tools).map(([t, s]) => [
|
|
3620
|
+
t,
|
|
3621
|
+
{ files: s.files },
|
|
3622
|
+
])),
|
|
3623
|
+
},
|
|
3624
|
+
])),
|
|
3625
|
+
},
|
|
3626
|
+
}, null, 2);
|
|
3627
|
+
}
|
|
3628
|
+
const lines = [pc.bold("Global Desired State")];
|
|
3629
|
+
if (globalState && globalState.desired_state.length > 0) {
|
|
3630
|
+
for (const entry of globalState.desired_state) {
|
|
3631
|
+
lines.push(`Bundle: ${pc.cyan(entry.bundle)}`);
|
|
3632
|
+
}
|
|
3633
|
+
}
|
|
3634
|
+
else {
|
|
3635
|
+
lines.push(pc.dim("Configured: no"));
|
|
3636
|
+
lines.push(pc.dim('Run "skul add --global <bundle>" to get started'));
|
|
3637
|
+
}
|
|
3638
|
+
lines.push("", pc.bold("Global Materialized State"));
|
|
3639
|
+
if (!globalState ||
|
|
3640
|
+
Object.keys(globalState.materialized_state.bundles).length === 0) {
|
|
3641
|
+
lines.push(pc.dim("Materialized: no"));
|
|
3642
|
+
return lines.join("\n");
|
|
3643
|
+
}
|
|
3644
|
+
lines.push(pc.green("Materialized: yes"), "", "Files:");
|
|
3645
|
+
for (const [bundleName, bundleState] of Object.entries(globalState.materialized_state.bundles)) {
|
|
3646
|
+
lines.push(` Bundle: ${pc.cyan(bundleName)}`);
|
|
3647
|
+
for (const [toolName, toolState] of Object.entries(bundleState.tools)) {
|
|
3648
|
+
lines.push(` Tool: ${toolName}`);
|
|
3649
|
+
for (const file of toolState.files) {
|
|
3650
|
+
lines.push(` ${file}`);
|
|
3651
|
+
}
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
return lines.join("\n");
|
|
3655
|
+
}
|
|
3656
|
+
async function removeGlobalBundle(options) {
|
|
3657
|
+
const repoRelPathRemapper = tool_mapping_1.GLOBAL_TOOL_MATERIALIZATION_LAYOUT.remapRepoRelPath;
|
|
3658
|
+
let registry = readRegistryWithGuidance(options.registryFile);
|
|
3659
|
+
const globalState = registry.global;
|
|
3660
|
+
if (shouldRemoveItemsAcrossBundles(options)) {
|
|
3661
|
+
return removeGlobalBundleItemsAcrossActiveBundles({
|
|
3662
|
+
homeDir: options.homeDir,
|
|
3663
|
+
prompts: options.prompts,
|
|
3664
|
+
registryFile: options.registryFile,
|
|
3665
|
+
libraryDir: options.libraryDir,
|
|
3666
|
+
globalState,
|
|
3667
|
+
source: options.source,
|
|
3668
|
+
bundle: options.inferredBundleFromSource ? undefined : options.bundle,
|
|
3669
|
+
includeItems: options.includeItems,
|
|
3670
|
+
selectItems: options.selectItems,
|
|
3671
|
+
dryRun: options.dryRun,
|
|
3672
|
+
});
|
|
3673
|
+
}
|
|
3674
|
+
const selection = await resolveRemoveGlobalBundleSelection({
|
|
3675
|
+
requestedBundle: options.bundle,
|
|
3676
|
+
requestedSource: options.source,
|
|
3677
|
+
inferredBundleFromSource: options.inferredBundleFromSource,
|
|
3678
|
+
globalState,
|
|
3679
|
+
prompts: options.prompts,
|
|
3680
|
+
});
|
|
3681
|
+
const bundle = selection.bundle;
|
|
3682
|
+
const source = selection.source;
|
|
3683
|
+
const isInDesiredState = globalState?.desired_state.some((e) => e.bundle === bundle && matchesOptionalSource(e.source, source)) ?? false;
|
|
3684
|
+
const desiredEntry = globalState?.desired_state.find((e) => e.bundle === bundle && matchesOptionalSource(e.source, source));
|
|
3685
|
+
const bundleMaterializedState = findGlobalMaterializedBundleState({
|
|
3686
|
+
globalState,
|
|
3687
|
+
bundle,
|
|
3688
|
+
source,
|
|
3689
|
+
});
|
|
3690
|
+
if (!isInDesiredState && !bundleMaterializedState) {
|
|
3691
|
+
const configured = globalState?.desired_state
|
|
3692
|
+
.filter((entry) => matchesOptionalSource(entry.source, source))
|
|
3693
|
+
.map((e) => e.bundle) ?? [];
|
|
3694
|
+
const hint = configured.length > 0
|
|
3695
|
+
? `Configured global bundles: ${configured.join(", ")}`
|
|
3696
|
+
: `No global bundles configured. Run "skul add --global <bundle>" to add one`;
|
|
3697
|
+
throw new Error(`Bundle not found in global active set: ${bundle}. ${hint}`);
|
|
3698
|
+
}
|
|
3699
|
+
if (options.includeItems.length > 0 || options.selectItems) {
|
|
3700
|
+
const itemRemoval = await removeGlobalBundleItems({
|
|
3701
|
+
homeDir: options.homeDir,
|
|
3702
|
+
prompts: options.prompts,
|
|
3703
|
+
registryFile: options.registryFile,
|
|
3704
|
+
libraryDir: options.libraryDir,
|
|
3705
|
+
registry,
|
|
3706
|
+
globalState,
|
|
3707
|
+
desiredEntry,
|
|
3708
|
+
bundle,
|
|
3709
|
+
source,
|
|
3710
|
+
includeItems: options.includeItems,
|
|
3711
|
+
selectItems: options.selectItems,
|
|
3712
|
+
dryRun: options.dryRun,
|
|
3713
|
+
});
|
|
3714
|
+
if (itemRemoval.kind === "completed") {
|
|
3715
|
+
return itemRemoval.output;
|
|
3716
|
+
}
|
|
3717
|
+
}
|
|
3718
|
+
if (options.dryRun) {
|
|
3719
|
+
if (bundleMaterializedState) {
|
|
3720
|
+
const { files } = flattenBundleState(bundleMaterializedState);
|
|
3721
|
+
const lines = [
|
|
3722
|
+
`${pc.yellow("DRY RUN:")} Would remove global ${bundle} (${files.length} file(s))`,
|
|
3723
|
+
];
|
|
3724
|
+
for (const file of files)
|
|
3725
|
+
lines.push(` ${file}`);
|
|
3726
|
+
return lines.join("\n");
|
|
3727
|
+
}
|
|
3728
|
+
return `${pc.yellow("DRY RUN:")} Would remove ${bundle} from global desired state`;
|
|
3729
|
+
}
|
|
3730
|
+
if (bundleMaterializedState) {
|
|
3731
|
+
const bundlePaths = flattenBundleState(bundleMaterializedState);
|
|
3732
|
+
const rootInstructionBaseContents = globalState?.materialized_state.root_instruction_base_contents;
|
|
3733
|
+
const removedRootInstructionPaths = new Set(bundlePaths.files.filter((p) => (0, root_instruction_render_1.isRootInstructionPath)(p)));
|
|
3734
|
+
const remainingBundles = { ...globalState.materialized_state.bundles };
|
|
3735
|
+
delete remainingBundles[bundle];
|
|
3736
|
+
const remainingDesiredState = globalState?.desired_state.filter((entry) => !matchesBundleIdentity(entry, bundle, source)) ?? [];
|
|
3737
|
+
const rewrittenRootInstructionPaths = new Set(Array.from((0, root_instruction_state_1.collectManagedRootInstructionTargets)(remainingBundles)).filter((p) => removedRootInstructionPaths.has(p)));
|
|
3738
|
+
(0, root_instruction_state_1.assertManagedRootInstructionSyncSourcesCached)({
|
|
3739
|
+
desiredState: remainingDesiredState,
|
|
3740
|
+
materializedBundles: remainingBundles,
|
|
3741
|
+
targetPaths: rewrittenRootInstructionPaths,
|
|
3742
|
+
resolveCachedBundle: (entry) => resolveDesiredCachedBundle(options.libraryDir, entry),
|
|
3743
|
+
});
|
|
3744
|
+
const removeAllowed = await confirmManagedFileRemovals(options.homeDir, bundlePaths, options.prompts, "remove");
|
|
3745
|
+
if (!removeAllowed) {
|
|
3746
|
+
throw new Error("Removal aborted because a modified managed file was kept");
|
|
3747
|
+
}
|
|
3748
|
+
removeManagedPaths(options.homeDir, bundlePaths);
|
|
3749
|
+
const remainingRootInstructionTargets = (0, root_instruction_state_1.collectManagedRootInstructionTargets)(remainingBundles);
|
|
3750
|
+
const restoredRootInstructionPaths = new Set(Array.from(removedRootInstructionPaths).filter((p) => !remainingRootInstructionTargets.has(p)));
|
|
3751
|
+
(0, root_instruction_state_1.restoreRootInstructionBaseContents)({
|
|
3752
|
+
repoRoot: options.homeDir,
|
|
3753
|
+
baseContents: rootInstructionBaseContents,
|
|
3754
|
+
targetPaths: restoredRootInstructionPaths,
|
|
3755
|
+
});
|
|
3756
|
+
const nextRootInstructionBaseContents = rootInstructionBaseContents
|
|
3757
|
+
? Object.fromEntries(Object.entries(rootInstructionBaseContents).filter(([p]) => !restoredRootInstructionPaths.has(p)))
|
|
3758
|
+
: undefined;
|
|
3759
|
+
if (Object.keys(remainingBundles).length > 0) {
|
|
3760
|
+
const syncedRootInstructionPaths = (0, root_instruction_state_1.syncManagedRootInstructionFiles)({
|
|
3761
|
+
repoRoot: options.homeDir,
|
|
3762
|
+
desiredState: remainingDesiredState,
|
|
3763
|
+
materializedBundles: remainingBundles,
|
|
3764
|
+
rootInstructionBaseContents: nextRootInstructionBaseContents,
|
|
3765
|
+
targetPaths: rewrittenRootInstructionPaths,
|
|
3766
|
+
resolveCachedBundle: (entry) => resolveDesiredCachedBundle(options.libraryDir, entry),
|
|
3767
|
+
repoRelPathRemapper,
|
|
3768
|
+
});
|
|
3769
|
+
const refreshedBundles = (0, root_instruction_state_1.refreshManagedFileFingerprintsForPaths)(options.homeDir, remainingBundles, syncedRootInstructionPaths);
|
|
3770
|
+
const newGlobalState = {
|
|
3771
|
+
desired_state: remainingDesiredState,
|
|
3772
|
+
materialized_state: {
|
|
3773
|
+
bundles: refreshedBundles,
|
|
3774
|
+
...(nextRootInstructionBaseContents &&
|
|
3775
|
+
Object.keys(nextRootInstructionBaseContents).length > 0
|
|
3776
|
+
? {
|
|
3777
|
+
root_instruction_base_contents: nextRootInstructionBaseContents,
|
|
3778
|
+
}
|
|
3779
|
+
: {}),
|
|
3780
|
+
},
|
|
3781
|
+
};
|
|
3782
|
+
registry = (0, registry_1.upsertGlobalState)(registry, newGlobalState);
|
|
3783
|
+
}
|
|
3784
|
+
else {
|
|
3785
|
+
if (remainingDesiredState.length > 0) {
|
|
3786
|
+
registry = (0, registry_1.upsertGlobalState)(registry, {
|
|
3787
|
+
desired_state: remainingDesiredState,
|
|
3788
|
+
materialized_state: { bundles: {} },
|
|
3789
|
+
});
|
|
3790
|
+
}
|
|
3791
|
+
else {
|
|
3792
|
+
registry = { ...registry, global: undefined };
|
|
3793
|
+
}
|
|
3794
|
+
}
|
|
3795
|
+
}
|
|
3796
|
+
else if (isInDesiredState && globalState) {
|
|
3797
|
+
const newDesiredState = globalState.desired_state.filter((entry) => !matchesBundleIdentity(entry, bundle, source));
|
|
3798
|
+
if (newDesiredState.length > 0 ||
|
|
3799
|
+
Object.keys(globalState.materialized_state.bundles).length > 0) {
|
|
3800
|
+
registry = (0, registry_1.upsertGlobalState)(registry, {
|
|
3801
|
+
...globalState,
|
|
3802
|
+
desired_state: newDesiredState,
|
|
3803
|
+
});
|
|
3804
|
+
}
|
|
3805
|
+
else {
|
|
3806
|
+
registry = { ...registry, global: undefined };
|
|
3807
|
+
}
|
|
3808
|
+
}
|
|
3809
|
+
(0, registry_1.writeRegistryFile)(options.registryFile, registry);
|
|
3810
|
+
return pc.green(`Removed global ${bundle}`);
|
|
3811
|
+
}
|
|
3812
|
+
async function removeGlobalBundleItemsAcrossActiveBundles(options) {
|
|
3813
|
+
if (!options.globalState || options.globalState.desired_state.length === 0) {
|
|
3814
|
+
throw new Error(options.source
|
|
3815
|
+
? `No active global bundles found for ${options.source}. Run "skul add --global ${options.source} <bundle>" to add one first`
|
|
3816
|
+
: 'No active global bundles found. Run "skul add --global <bundle>" to add one first');
|
|
3817
|
+
}
|
|
3818
|
+
const choices = listActiveGlobalBundleItemRemovalChoices({
|
|
3819
|
+
libraryDir: options.libraryDir,
|
|
3820
|
+
desiredState: options.globalState.desired_state,
|
|
3821
|
+
source: options.source,
|
|
3822
|
+
bundle: options.bundle,
|
|
3823
|
+
});
|
|
3824
|
+
const requestedItems = (0, bundle_items_1.normalizeBundleItemSelectors)(options.includeItems);
|
|
3825
|
+
const selectedValues = options.selectItems
|
|
3826
|
+
? await promptForBundleItemRemovalChoices({
|
|
3827
|
+
prompts: options.prompts,
|
|
3828
|
+
choices,
|
|
3829
|
+
requestedItems,
|
|
3830
|
+
})
|
|
3831
|
+
: selectRequestedBundleItemRemovalChoices({
|
|
3832
|
+
choices,
|
|
3833
|
+
requestedItems,
|
|
3834
|
+
});
|
|
3835
|
+
const removalPlan = planBundleItemRemovals({
|
|
3836
|
+
desiredState: options.globalState.desired_state,
|
|
3837
|
+
choices,
|
|
3838
|
+
selectedValues,
|
|
3839
|
+
});
|
|
3840
|
+
if (options.dryRun) {
|
|
3841
|
+
return `${pc.yellow("DRY RUN:")} Would remove ${formatBundleItemRemovalSummary(removalPlan.removedItems)} from global bundles`;
|
|
3842
|
+
}
|
|
3843
|
+
for (const target of groupBundleItemRemovalTargets(removalPlan.removedItems)) {
|
|
3844
|
+
await removeGlobalBundle({
|
|
3845
|
+
homeDir: options.homeDir,
|
|
3846
|
+
prompts: options.prompts,
|
|
3847
|
+
registryFile: options.registryFile,
|
|
3848
|
+
libraryDir: options.libraryDir,
|
|
3849
|
+
bundle: target.bundle,
|
|
3850
|
+
source: target.source,
|
|
3851
|
+
includeItems: target.items,
|
|
3852
|
+
selectItems: false,
|
|
3853
|
+
dryRun: false,
|
|
3854
|
+
});
|
|
3855
|
+
}
|
|
3856
|
+
return pc.green(`Removed ${formatBundleItemRemovalSummary(removalPlan.removedItems)} from global bundles`);
|
|
3857
|
+
}
|
|
3858
|
+
function listActiveGlobalBundleItemRemovalChoices(options) {
|
|
3859
|
+
const choices = options.desiredState.flatMap((entry) => {
|
|
3860
|
+
if (!matchesOptionalSource(entry.source, options.source))
|
|
3861
|
+
return [];
|
|
3862
|
+
if (options.bundle !== undefined && entry.bundle !== options.bundle) {
|
|
3863
|
+
return [];
|
|
3864
|
+
}
|
|
3865
|
+
return listDesiredGlobalBundleItemRemovalChoices({
|
|
3866
|
+
libraryDir: options.libraryDir,
|
|
3867
|
+
desiredEntry: entry,
|
|
3868
|
+
});
|
|
3869
|
+
});
|
|
3870
|
+
if (choices.length === 0) {
|
|
3871
|
+
throw new Error(options.source
|
|
3872
|
+
? `No active global bundle items found for ${options.source}`
|
|
3873
|
+
: "No active global bundle items found");
|
|
3874
|
+
}
|
|
3875
|
+
return choices;
|
|
3876
|
+
}
|
|
3877
|
+
function listDesiredGlobalBundleItemRemovalChoices(options) {
|
|
3878
|
+
const cachedBundle = findCachedBundleWithGuidance({
|
|
3879
|
+
libraryDir: options.libraryDir,
|
|
3880
|
+
bundle: options.desiredEntry.bundle,
|
|
3881
|
+
source: options.desiredEntry.source,
|
|
3882
|
+
});
|
|
3883
|
+
const selectedTools = options.desiredEntry.tools ??
|
|
3884
|
+
Object.keys(cachedBundle.manifest.tools).filter((toolName) => (0, tool_mapping_1.globalCapableToolNames)().includes(toolName));
|
|
3885
|
+
const availableItems = (0, bundle_items_1.listSelectableBundleItems)({
|
|
3886
|
+
bundleDir: node_path_1.default.dirname(cachedBundle.manifestFile),
|
|
3887
|
+
manifest: cachedBundle.manifest,
|
|
3888
|
+
tools: selectedTools,
|
|
3889
|
+
});
|
|
3890
|
+
const activeItems = options.desiredEntry.items ?? availableItems;
|
|
3891
|
+
return activeItems.map((item) => ({
|
|
3892
|
+
value: encodeBundleItemRemovalChoice({
|
|
3893
|
+
bundle: options.desiredEntry.bundle,
|
|
3894
|
+
source: options.desiredEntry.source,
|
|
3895
|
+
item,
|
|
3896
|
+
}),
|
|
3897
|
+
label: formatBundleItemRemovalChoiceLabel({
|
|
3898
|
+
bundle: options.desiredEntry.bundle,
|
|
3899
|
+
source: options.desiredEntry.source,
|
|
3900
|
+
item,
|
|
3901
|
+
}),
|
|
3902
|
+
bundle: options.desiredEntry.bundle,
|
|
3903
|
+
source: options.desiredEntry.source,
|
|
3904
|
+
item,
|
|
3905
|
+
activeItems,
|
|
3906
|
+
}));
|
|
3907
|
+
}
|
|
3908
|
+
async function resolveRemoveGlobalBundleSelection(options) {
|
|
3909
|
+
if (options.requestedBundle &&
|
|
3910
|
+
isGlobalRemoveBundleActive({
|
|
3911
|
+
globalState: options.globalState,
|
|
3912
|
+
bundle: options.requestedBundle,
|
|
3913
|
+
source: options.requestedSource,
|
|
3914
|
+
})) {
|
|
3915
|
+
return {
|
|
3916
|
+
bundle: options.requestedBundle,
|
|
3917
|
+
...(options.requestedSource !== undefined
|
|
3918
|
+
? { source: options.requestedSource }
|
|
3919
|
+
: {}),
|
|
3920
|
+
};
|
|
3921
|
+
}
|
|
3922
|
+
if (options.requestedBundle && !options.inferredBundleFromSource) {
|
|
3923
|
+
return {
|
|
3924
|
+
bundle: options.requestedBundle,
|
|
3925
|
+
...(options.requestedSource !== undefined
|
|
3926
|
+
? { source: options.requestedSource }
|
|
3927
|
+
: {}),
|
|
3928
|
+
};
|
|
3929
|
+
}
|
|
3930
|
+
if (options.requestedSource !== undefined &&
|
|
3931
|
+
options.inferredBundleFromSource) {
|
|
3932
|
+
return promptForActiveGlobalRemoveBundleSelection({
|
|
3933
|
+
globalState: options.globalState,
|
|
3934
|
+
prompts: options.prompts,
|
|
3935
|
+
source: options.requestedSource,
|
|
3936
|
+
});
|
|
3937
|
+
}
|
|
3938
|
+
if (options.requestedBundle !== undefined) {
|
|
3939
|
+
return { bundle: options.requestedBundle };
|
|
3940
|
+
}
|
|
3941
|
+
return promptForActiveGlobalRemoveBundleSelection({
|
|
3942
|
+
globalState: options.globalState,
|
|
3943
|
+
prompts: options.prompts,
|
|
3944
|
+
});
|
|
3945
|
+
}
|
|
3946
|
+
async function promptForActiveGlobalRemoveBundleSelection(options) {
|
|
3947
|
+
const activeSelections = listActiveGlobalRemoveBundleSelections({
|
|
3948
|
+
globalState: options.globalState,
|
|
3949
|
+
source: options.source,
|
|
3950
|
+
});
|
|
3951
|
+
if (activeSelections.length === 0) {
|
|
3952
|
+
throw new Error(options.source
|
|
3953
|
+
? `No active global bundles found for ${options.source}. Run "skul add --global ${options.source} <bundle>" to add one first`
|
|
3954
|
+
: 'No active global bundles found. Run "skul add --global <bundle>" to add one first');
|
|
3955
|
+
}
|
|
3956
|
+
if (activeSelections.length === 1) {
|
|
3957
|
+
return activeSelections[0];
|
|
3958
|
+
}
|
|
3959
|
+
const selection = await options.prompts.selectBundleFromSelections(activeSelections, options.source);
|
|
3960
|
+
return {
|
|
3961
|
+
bundle: selection.bundle,
|
|
3962
|
+
...(selection.source !== undefined ? { source: selection.source } : {}),
|
|
3963
|
+
};
|
|
3964
|
+
}
|
|
3965
|
+
function listActiveGlobalRemoveBundleSelections(options) {
|
|
3966
|
+
const selections = [];
|
|
3967
|
+
const seen = new Set();
|
|
3968
|
+
for (const entry of options.globalState?.desired_state ?? []) {
|
|
3969
|
+
if (!matchesOptionalSource(entry.source, options.source))
|
|
3970
|
+
continue;
|
|
3971
|
+
addActiveRemoveBundleSelection(selections, seen, {
|
|
3972
|
+
bundle: entry.bundle,
|
|
3973
|
+
...(entry.source !== undefined ? { source: entry.source } : {}),
|
|
3974
|
+
protocol: entry.protocol,
|
|
3975
|
+
});
|
|
3976
|
+
}
|
|
3977
|
+
for (const [bundle, state] of Object.entries(options.globalState?.materialized_state.bundles ?? {})) {
|
|
3978
|
+
if (!matchesOptionalSource(state.source, options.source))
|
|
3979
|
+
continue;
|
|
3980
|
+
addActiveRemoveBundleSelection(selections, seen, {
|
|
3981
|
+
bundle,
|
|
3982
|
+
...(state.source !== undefined ? { source: state.source } : {}),
|
|
3983
|
+
});
|
|
3984
|
+
}
|
|
3985
|
+
return selections.sort(compareBundleSelections);
|
|
3986
|
+
}
|
|
3987
|
+
function isGlobalRemoveBundleActive(options) {
|
|
3988
|
+
const desiredMatch = options.globalState?.desired_state.some((entry) => entry.bundle === options.bundle &&
|
|
3989
|
+
matchesOptionalSource(entry.source, options.source)) ?? false;
|
|
3990
|
+
return (desiredMatch ||
|
|
3991
|
+
findGlobalMaterializedBundleState({
|
|
3992
|
+
globalState: options.globalState,
|
|
3993
|
+
bundle: options.bundle,
|
|
3994
|
+
source: options.source,
|
|
3995
|
+
}) !== undefined);
|
|
3996
|
+
}
|
|
3997
|
+
function findGlobalMaterializedBundleState(options) {
|
|
3998
|
+
const bundleState = options.globalState?.materialized_state.bundles[options.bundle];
|
|
3999
|
+
if (!bundleState ||
|
|
4000
|
+
!matchesOptionalSource(bundleState.source, options.source)) {
|
|
4001
|
+
return undefined;
|
|
4002
|
+
}
|
|
4003
|
+
return bundleState;
|
|
4004
|
+
}
|
|
4005
|
+
async function removeGlobalBundleItems(options) {
|
|
4006
|
+
if (!options.globalState || !options.desiredEntry) {
|
|
4007
|
+
throw new Error(`Cannot remove selected items from global ${options.bundle} because it is not in desired state`);
|
|
4008
|
+
}
|
|
4009
|
+
const cachedBundle = findCachedBundleWithGuidance({
|
|
4010
|
+
libraryDir: options.libraryDir,
|
|
4011
|
+
bundle: options.bundle,
|
|
4012
|
+
source: options.desiredEntry.source ?? options.source,
|
|
4013
|
+
});
|
|
4014
|
+
const selectedTools = options.desiredEntry.tools ??
|
|
4015
|
+
Object.keys(cachedBundle.manifest.tools).filter((toolName) => (0, tool_mapping_1.globalCapableToolNames)().includes(toolName));
|
|
4016
|
+
const availableItems = (0, bundle_items_1.listSelectableBundleItems)({
|
|
4017
|
+
bundleDir: node_path_1.default.dirname(cachedBundle.manifestFile),
|
|
4018
|
+
manifest: cachedBundle.manifest,
|
|
4019
|
+
tools: selectedTools,
|
|
4020
|
+
});
|
|
4021
|
+
const currentItems = options.desiredEntry.items ?? availableItems;
|
|
4022
|
+
const requestedItems = (0, bundle_items_1.normalizeBundleItemSelectors)(options.includeItems);
|
|
4023
|
+
(0, bundle_items_1.assertBundleSupportsRequestedItems)({
|
|
4024
|
+
requestedItems,
|
|
4025
|
+
availableItems,
|
|
4026
|
+
});
|
|
4027
|
+
const inactiveRequestedItems = requestedItems.filter((item) => !currentItems.includes(item));
|
|
4028
|
+
if (inactiveRequestedItems.length > 0) {
|
|
4029
|
+
throw new Error(`Bundle item(s) are not active in global ${options.bundle}: ${inactiveRequestedItems.join(", ")}`);
|
|
4030
|
+
}
|
|
4031
|
+
const selectedItems = options.selectItems
|
|
4032
|
+
? await options.prompts.selectBundleItems(currentItems, requestedItems, "remove")
|
|
4033
|
+
: requestedItems;
|
|
4034
|
+
const normalizedSelectedItems = (0, bundle_items_1.normalizeBundleItemSelectors)(selectedItems);
|
|
4035
|
+
const inactiveItems = normalizedSelectedItems.filter((item) => !currentItems.includes(item));
|
|
4036
|
+
if (inactiveItems.length > 0) {
|
|
4037
|
+
throw new Error(`Bundle item(s) are not active in global ${options.bundle}: ${inactiveItems.join(", ")}`);
|
|
4038
|
+
}
|
|
4039
|
+
const selectedItemSet = new Set(normalizedSelectedItems);
|
|
4040
|
+
const remainingItems = currentItems.filter((item) => !selectedItemSet.has(item));
|
|
4041
|
+
if (remainingItems.length === 0) {
|
|
4042
|
+
return { kind: "remove-bundle" };
|
|
4043
|
+
}
|
|
4044
|
+
if (options.dryRun) {
|
|
4045
|
+
return {
|
|
4046
|
+
kind: "completed",
|
|
4047
|
+
output: `${pc.yellow("DRY RUN:")} Would remove ${normalizedSelectedItems.join(", ")} from global ${options.bundle}`,
|
|
4048
|
+
};
|
|
4049
|
+
}
|
|
4050
|
+
const nextRegistry = (0, registry_1.upsertGlobalState)(options.registry, {
|
|
4051
|
+
...options.globalState,
|
|
4052
|
+
desired_state: options.globalState.desired_state.map((entry) => entry.bundle === options.bundle &&
|
|
4053
|
+
matchesOptionalSource(entry.source, options.source)
|
|
4054
|
+
? { ...entry, items: remainingItems }
|
|
4055
|
+
: entry),
|
|
4056
|
+
});
|
|
4057
|
+
(0, registry_1.writeRegistryFile)(options.registryFile, nextRegistry);
|
|
4058
|
+
try {
|
|
4059
|
+
await applyGlobal({
|
|
4060
|
+
homeDir: options.homeDir,
|
|
4061
|
+
prompts: options.prompts,
|
|
4062
|
+
registryFile: options.registryFile,
|
|
4063
|
+
libraryDir: options.libraryDir,
|
|
4064
|
+
dryRun: false,
|
|
4065
|
+
});
|
|
4066
|
+
}
|
|
4067
|
+
catch (error) {
|
|
4068
|
+
(0, registry_1.writeRegistryFile)(options.registryFile, options.registry);
|
|
4069
|
+
throw error;
|
|
4070
|
+
}
|
|
4071
|
+
return {
|
|
4072
|
+
kind: "completed",
|
|
4073
|
+
output: pc.green(`Removed ${normalizedSelectedItems.join(", ")} from global ${options.bundle}`),
|
|
4074
|
+
};
|
|
4075
|
+
}
|
|
4076
|
+
async function resetGlobal(options) {
|
|
4077
|
+
let registry = readRegistryWithGuidance(options.registryFile);
|
|
4078
|
+
const globalState = registry.global;
|
|
4079
|
+
if (!globalState ||
|
|
4080
|
+
Object.keys(globalState.materialized_state.bundles).length === 0) {
|
|
4081
|
+
return "No globally materialized Skul bundles found";
|
|
4082
|
+
}
|
|
4083
|
+
const allBundlePaths = Object.values(globalState.materialized_state.bundles).map(flattenBundleState);
|
|
4084
|
+
const allFiles = allBundlePaths.flatMap((bp) => bp.files);
|
|
4085
|
+
if (options.dryRun) {
|
|
4086
|
+
const lines = [
|
|
4087
|
+
`${pc.yellow("DRY RUN:")} Would remove ${allFiles.length} globally managed file(s)`,
|
|
4088
|
+
];
|
|
4089
|
+
for (const file of allFiles)
|
|
4090
|
+
lines.push(` ${file}`);
|
|
4091
|
+
return lines.join("\n");
|
|
4092
|
+
}
|
|
4093
|
+
for (const bundlePaths of allBundlePaths) {
|
|
4094
|
+
const resetAllowed = await confirmManagedFileRemovals(options.homeDir, bundlePaths, options.prompts, "reset");
|
|
4095
|
+
if (!resetAllowed) {
|
|
4096
|
+
throw new Error("Reset aborted because a modified managed file was kept");
|
|
4097
|
+
}
|
|
4098
|
+
}
|
|
4099
|
+
for (const bundlePaths of allBundlePaths) {
|
|
4100
|
+
removeManagedPaths(options.homeDir, bundlePaths);
|
|
4101
|
+
}
|
|
4102
|
+
// reset --global removes all bundle materialization entirely; shared root-instruction files
|
|
4103
|
+
// (e.g. .claude/CLAUDE.md) are not re-composed for remaining bundles — base content is restored
|
|
4104
|
+
// as-is. Run `apply --global` afterward to re-materialize from desired state.
|
|
4105
|
+
(0, root_instruction_state_1.restoreRootInstructionBaseContents)({
|
|
4106
|
+
repoRoot: options.homeDir,
|
|
4107
|
+
baseContents: globalState.materialized_state.root_instruction_base_contents,
|
|
4108
|
+
targetPaths: (0, root_instruction_state_1.collectManagedRootInstructionTargets)(globalState.materialized_state.bundles),
|
|
4109
|
+
});
|
|
4110
|
+
registry = (0, registry_1.upsertGlobalState)(registry, {
|
|
4111
|
+
desired_state: globalState.desired_state,
|
|
4112
|
+
materialized_state: { bundles: {} },
|
|
4113
|
+
});
|
|
4114
|
+
(0, registry_1.writeRegistryFile)(options.registryFile, registry);
|
|
4115
|
+
return pc.green("Reset globally managed Skul files");
|
|
4116
|
+
}
|
|
4117
|
+
async function applyGlobal(options) {
|
|
4118
|
+
const registry = readRegistryWithGuidance(options.registryFile);
|
|
4119
|
+
const globalState = registry.global;
|
|
4120
|
+
if (!globalState || globalState.desired_state.length === 0) {
|
|
4121
|
+
return `No global bundles configured. Run "skul add --global <bundle>" to add one`;
|
|
4122
|
+
}
|
|
4123
|
+
const outputLines = [];
|
|
4124
|
+
const toApply = globalState.desired_state.filter((entry) => {
|
|
4125
|
+
const mat = globalState.materialized_state.bundles[entry.bundle];
|
|
4126
|
+
if (!mat)
|
|
4127
|
+
return true;
|
|
4128
|
+
if (!isDesiredBundleMaterialized({
|
|
4129
|
+
desiredEntry: entry,
|
|
4130
|
+
materializedBundleState: mat,
|
|
4131
|
+
availableTools: (0, tool_mapping_1.globalCapableToolNames)(),
|
|
4132
|
+
}))
|
|
4133
|
+
return true;
|
|
4134
|
+
if (entry.source &&
|
|
4135
|
+
!(0, bundle_fetch_1.readCachedSourceRevision)({
|
|
4136
|
+
source: entry.source,
|
|
4137
|
+
libraryDir: options.libraryDir,
|
|
4138
|
+
}).cached)
|
|
4139
|
+
return true;
|
|
4140
|
+
return false;
|
|
4141
|
+
});
|
|
4142
|
+
if (toApply.length === 0) {
|
|
4143
|
+
return options.dryRun
|
|
4144
|
+
? "DRY RUN: All global bundles are already materialized"
|
|
4145
|
+
: "All global bundles are already materialized";
|
|
4146
|
+
}
|
|
4147
|
+
if (options.dryRun) {
|
|
4148
|
+
return toApply
|
|
4149
|
+
.map((e) => `${pc.yellow("DRY RUN:")} Would apply ${e.bundle} globally`)
|
|
4150
|
+
.join("\n");
|
|
4151
|
+
}
|
|
4152
|
+
for (const entry of toApply) {
|
|
4153
|
+
try {
|
|
4154
|
+
const result = await applyBundleGlobal({
|
|
4155
|
+
homeDir: options.homeDir,
|
|
4156
|
+
prompts: options.prompts,
|
|
4157
|
+
registryFile: options.registryFile,
|
|
4158
|
+
libraryDir: options.libraryDir,
|
|
4159
|
+
bundle: entry.bundle,
|
|
4160
|
+
source: entry.source,
|
|
4161
|
+
protocol: entry.protocol,
|
|
4162
|
+
agents: entry.tools ?? [],
|
|
4163
|
+
includeItems: entry.items ?? [],
|
|
4164
|
+
selectItems: false,
|
|
4165
|
+
dryRun: false,
|
|
4166
|
+
ref: entry.ref,
|
|
4167
|
+
});
|
|
4168
|
+
outputLines.push(result);
|
|
4169
|
+
}
|
|
4170
|
+
catch (err) {
|
|
4171
|
+
outputLines.push(`${pc.red("ERROR:")} Failed to apply ${entry.bundle}: ${err instanceof Error ? err.message : String(err)}`);
|
|
4172
|
+
}
|
|
4173
|
+
}
|
|
4174
|
+
return outputLines.join("\n");
|
|
4175
|
+
}
|
|
4176
|
+
function assertUnreachable(_value) {
|
|
4177
|
+
throw new Error("Unhandled command");
|
|
4178
|
+
}
|
|
4179
|
+
if (require.main === module) {
|
|
4180
|
+
void run(process.argv.slice(2))
|
|
4181
|
+
.then((output) => {
|
|
4182
|
+
console.log(output);
|
|
4183
|
+
})
|
|
4184
|
+
.catch((error) => {
|
|
4185
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4186
|
+
console.error(message);
|
|
4187
|
+
process.exitCode = 1;
|
|
4188
|
+
});
|
|
4189
|
+
}
|
|
4190
|
+
//# sourceMappingURL=index.js.map
|