skvlt 0.9.9

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.
@@ -0,0 +1,689 @@
1
+ import { parseRestoreArgs } from "../internal/args/parse-restore-args";
2
+ import {
3
+ diffGlobalSkillState,
4
+ formatGlobalSkillStateMismatch,
5
+ readInstalledGlobalSkillNames,
6
+ readTrackedGlobalSkillNames,
7
+ type GlobalSkillStateDiff,
8
+ } from "../internal/install/global-skill-state";
9
+ import { resolveInstallConcurrency } from "../internal/install/resolve-install-concurrency";
10
+ import {
11
+ buildSkillsAddCommand,
12
+ formatCommandPreview,
13
+ } from "../internal/install/skills-add-command";
14
+ import {
15
+ runWithConcurrency,
16
+ type ConcurrentFailure,
17
+ } from "../internal/install/run-with-concurrency";
18
+ import { parseManifest } from "../internal/manifest/parse-manifest";
19
+ import type { Manifest } from "../internal/manifest/manifest-types";
20
+ import {
21
+ defaultManifestOptionPath,
22
+ defaultGlobalLockFilePath,
23
+ defaultGlobalSkillsPath,
24
+ } from "../internal/paths/defaults";
25
+ import {
26
+ commandErrorPage,
27
+ errorMessage,
28
+ helpExample,
29
+ helpFooter,
30
+ helpHeading,
31
+ infoLine,
32
+ page,
33
+ progressLine,
34
+ summaryLine,
35
+ } from "../internal/cli/theme";
36
+ import { formatCliPath } from "../internal/paths/format-cli-path";
37
+ import { listInstalledSkillNames } from "../internal/process/list-installed-skill-names";
38
+ import { runBunx, type BunxResult } from "../internal/process/run-bunx";
39
+
40
+ type SourceRestoreEntry = {
41
+ source: string;
42
+ count: number;
43
+ skills: string[];
44
+ pendingSkills: string[];
45
+ alreadyInstalledSkills: string[];
46
+ };
47
+
48
+ export type RestoreRunResult = {
49
+ exitCode: number;
50
+ stdout: string;
51
+ stderr: string;
52
+ errorCode?: string;
53
+ payload?: {
54
+ manifestPath: string;
55
+ projectScope: boolean;
56
+ dryRun: boolean;
57
+ sourceCount: number;
58
+ requestedSkillTotal: number;
59
+ pendingSkillTotal: number;
60
+ installTaskCount: number;
61
+ continueOnError: boolean;
62
+ };
63
+ };
64
+
65
+ export type RestoreDependencies = {
66
+ readManifest?: (manifestPath: string) => Manifest;
67
+ getInstalledSkillNames?: (projectScope: boolean) => Promise<Set<string>>;
68
+ readTrackedGlobalSkillNames?: () => Promise<Set<string>>;
69
+ readInstalledGlobalSkillNames?: () => Promise<Set<string>>;
70
+ runBunx?: (args: string[]) => Promise<BunxResult>;
71
+ reportProgress?: (chunk: string) => void;
72
+ streamOutput?: boolean;
73
+ };
74
+
75
+ function printHelp(): string {
76
+ return `\n${[
77
+ "Restore skills from skvlt.yaml.",
78
+ "",
79
+ helpHeading("Usage"),
80
+ " bunx skvlt restore [options]",
81
+ "",
82
+ helpHeading("Options"),
83
+ " --manifest <path> Path to skvlt.yaml",
84
+ " --only-source <source> Restore only one source; repeatable",
85
+ " --agent <agent> Target one agent; repeatable",
86
+ " --project-scope Restore to project scope instead of global",
87
+ " --copy Copy files instead of symlinking to agent directories",
88
+ " --all Install all skills from the selected sources",
89
+ " --reinstall-all Reinstall everything instead of only missing skills",
90
+ " --continue-on-error Keep going if one source fails to install",
91
+ " --concurrency <count> Maximum sources to install at the same time",
92
+ " --dry-run Print manifest-derived bunx commands without running them",
93
+ " --help Show this help",
94
+ "",
95
+ helpHeading("Notes"),
96
+ ` - Manifest defaults to: ${formatCliPath(defaultManifestOptionPath)}`,
97
+ " - Restore scope defaults to the scope recorded in the manifest; --project-scope overrides it to project scope.",
98
+ ` - Global restore verifies ${formatCliPath(defaultGlobalSkillsPath)} against ${formatCliPath(defaultGlobalLockFilePath)}`,
99
+ "",
100
+ helpHeading("Examples"),
101
+ helpExample("bunx skvlt restore --only-source xixu-me/skills"),
102
+ helpExample("bunx skvlt restore --all"),
103
+ helpExample("bunx skvlt restore --project-scope --manifest ./skvlt.yaml"),
104
+ helpFooter("https://github.com/xixu-me/skills-vault"),
105
+ ].join("\n")}`;
106
+ }
107
+
108
+ async function getInstalledSkillNames(
109
+ projectScope: boolean,
110
+ ): Promise<Set<string>> {
111
+ return new Set(await listInstalledSkillNames(projectScope));
112
+ }
113
+
114
+ function resolveRestoreErrorCode(message: string): string {
115
+ if (
116
+ message.startsWith("Unknown argument:") ||
117
+ message.includes("requires a value") ||
118
+ message.includes("requires a path") ||
119
+ message.includes("requires a positive integer")
120
+ ) {
121
+ return "INVALID_ARGUMENT";
122
+ }
123
+ if (message.startsWith("Manifest not found:")) {
124
+ return "MANIFEST_NOT_FOUND";
125
+ }
126
+ if (message.startsWith("Requested source(s) not found in manifest:")) {
127
+ return "SOURCE_NOT_FOUND";
128
+ }
129
+ if (message.startsWith("Unable to list installed skills")) {
130
+ return "SKILLS_LIST_FAILED";
131
+ }
132
+ if (message.startsWith("Install failed for source")) {
133
+ return "INSTALL_FAILED";
134
+ }
135
+ if (message.startsWith("Global skills state mismatch:")) {
136
+ return "GLOBAL_STATE_MISMATCH";
137
+ }
138
+
139
+ return "RESTORE_FAILED";
140
+ }
141
+
142
+ function formatRestoreError(message: string, errorCode: string): string {
143
+ if (errorCode === "INVALID_ARGUMENT") {
144
+ return commandErrorPage(
145
+ message,
146
+ "bunx skvlt restore [options]",
147
+ "bunx skvlt restore --all",
148
+ );
149
+ }
150
+
151
+ return page(errorMessage(message));
152
+ }
153
+
154
+ function selectSources(
155
+ manifest: Manifest,
156
+ onlySources: string[],
157
+ ): SourceRestoreEntry[] {
158
+ const entries: SourceRestoreEntry[] = Array.from(
159
+ manifest.sources.entries(),
160
+ ).map(([source, entry]) => ({
161
+ source,
162
+ count: entry.count,
163
+ skills: [...entry.skills],
164
+ pendingSkills: [],
165
+ alreadyInstalledSkills: [],
166
+ }));
167
+
168
+ if (onlySources.length === 0) {
169
+ return entries;
170
+ }
171
+
172
+ const selected = entries.filter((entry) =>
173
+ onlySources.includes(entry.source),
174
+ );
175
+ const selectedSources = new Set(selected.map((entry) => entry.source));
176
+ const missing = onlySources.filter((source) => !selectedSources.has(source));
177
+
178
+ if (missing.length > 0) {
179
+ throw new Error(
180
+ `Requested source(s) not found in manifest: ${missing.join(", ")}`,
181
+ );
182
+ }
183
+
184
+ return selected;
185
+ }
186
+
187
+ async function verifyGlobalSkillStateConsistency(
188
+ dependencies: Required<RestoreDependencies>,
189
+ ): Promise<GlobalSkillStateDiff> {
190
+ const [trackedSkillNames, installedSkillNames] = await Promise.all([
191
+ dependencies.readTrackedGlobalSkillNames(),
192
+ dependencies.readInstalledGlobalSkillNames(),
193
+ ]);
194
+
195
+ return diffGlobalSkillState(trackedSkillNames, installedSkillNames);
196
+ }
197
+
198
+ function buildRestoreSummary(
199
+ sourceCount: number,
200
+ installTaskCount: number,
201
+ summarySkillTotal: number,
202
+ installAll: boolean,
203
+ ): string {
204
+ if (installAll) {
205
+ return `Restore summary: ${sourceCount} source(s) processed, ${installTaskCount} source install(s) attempted, upstream source state determined the installed skill count.`;
206
+ }
207
+
208
+ return `Restore summary: ${sourceCount} source(s) processed, ${installTaskCount} source install(s) attempted, ${summarySkillTotal} skill(s) scheduled.`;
209
+ }
210
+
211
+ async function runRestoreFromManifest(
212
+ manifest: Manifest,
213
+ manifestPath: string,
214
+ options: ReturnType<typeof parseRestoreArgs>,
215
+ dependencies: Required<RestoreDependencies>,
216
+ ): Promise<RestoreRunResult> {
217
+ const lines: string[] = [];
218
+ const errors: string[] = [];
219
+ const streamOutput = dependencies.streamOutput;
220
+ const reportProgress = dependencies.reportProgress;
221
+ let streamedOutput = false;
222
+ const emitStreamLine = (line: string) => {
223
+ if (!streamedOutput) {
224
+ reportProgress("\n");
225
+ streamedOutput = true;
226
+ }
227
+ reportProgress(`${line}\n`);
228
+ };
229
+ const appendLine = (line: string) => {
230
+ const formattedLine = line.includes("summary:")
231
+ ? summaryLine(line)
232
+ : infoLine(line);
233
+ if (streamOutput) {
234
+ emitStreamLine(formattedLine);
235
+ return;
236
+ }
237
+
238
+ lines.push(formattedLine);
239
+ };
240
+ const entries = selectSources(manifest, options.onlySources);
241
+ const requestedSkillTotal = entries.reduce(
242
+ (sum, entry) => sum + entry.count,
243
+ 0,
244
+ );
245
+ // The manifest becomes the source of truth for scope unless the user forces
246
+ // project scope on the command line.
247
+ const projectScope = options.projectScope || manifest.scope === "project";
248
+ const scopeLabel = projectScope ? "project" : "global";
249
+
250
+ if (options.dryRun) {
251
+ const previewConcurrency = resolveInstallConcurrency({
252
+ configuredConcurrency: options.concurrency,
253
+ taskCount: entries.length,
254
+ });
255
+
256
+ appendLine(
257
+ `Previewing ${requestedSkillTotal} skill(s) from ${entries.length} source(s) using '${scopeLabel}' scope.`,
258
+ );
259
+ appendLine(
260
+ `Parallel install concurrency: ${previewConcurrency}${options.concurrency ? " (configured)" : " (auto)"}.`,
261
+ );
262
+ if (options.installAll) {
263
+ // `skills add --all` delegates skill selection to the upstream source, so
264
+ // it is intentionally broader than the manifest snapshot.
265
+ appendLine(
266
+ "Manifest skill selections are ignored when --all is enabled; upstream source state determines what gets installed.",
267
+ );
268
+ } else {
269
+ appendLine(
270
+ "Dry run does not inspect locally installed skills; commands include every skill listed in the manifest.",
271
+ );
272
+ }
273
+
274
+ for (const entry of entries) {
275
+ const preview = formatCommandPreview([
276
+ "bunx",
277
+ ...buildSkillsAddCommand(entry.source, entry.skills, {
278
+ ...options,
279
+ projectScope,
280
+ }),
281
+ ]);
282
+ appendLine(`[dry-run] ${preview}`);
283
+ }
284
+
285
+ return {
286
+ exitCode: 0,
287
+ stdout: streamOutput
288
+ ? streamedOutput
289
+ ? "\n"
290
+ : ""
291
+ : page(lines.join("\n")),
292
+ stderr: "",
293
+ payload: {
294
+ manifestPath,
295
+ projectScope,
296
+ dryRun: true,
297
+ sourceCount: entries.length,
298
+ requestedSkillTotal,
299
+ pendingSkillTotal: requestedSkillTotal,
300
+ installTaskCount: entries.length,
301
+ continueOnError: options.continueOnError,
302
+ },
303
+ };
304
+ }
305
+
306
+ const installedLookup = options.installAll
307
+ ? null
308
+ : await dependencies.getInstalledSkillNames(projectScope);
309
+ const showSkillProgress = streamOutput && !options.installAll;
310
+ let pendingSkillTotal = 0;
311
+ let alreadyInstalledTotal = 0;
312
+ const scheduledSkillTotal =
313
+ options.installAll || options.reinstallAll ? requestedSkillTotal : 0;
314
+ const installTasks: Array<{
315
+ order: number;
316
+ entry: SourceRestoreEntry;
317
+ skillsToInstall: string[];
318
+ command: string[];
319
+ }> = [];
320
+ appendLine(`Loading manifest from ${formatCliPath(manifestPath)}...`);
321
+ appendLine("Planning restore operations...");
322
+
323
+ for (const [index, entry] of entries.entries()) {
324
+ if (options.installAll) {
325
+ appendLine(
326
+ `Source ${index + 1}/${entries.length}: ${entry.source} (manifest lists ${entry.count} skill(s); --all delegates selection upstream).`,
327
+ );
328
+ installTasks.push({
329
+ order: index + 1,
330
+ entry,
331
+ skillsToInstall: [],
332
+ command: buildSkillsAddCommand(entry.source, entry.skills, {
333
+ ...options,
334
+ projectScope,
335
+ }),
336
+ });
337
+ continue;
338
+ }
339
+
340
+ if (options.reinstallAll) {
341
+ entry.pendingSkills = [...entry.skills];
342
+ pendingSkillTotal += entry.count;
343
+ appendLine(
344
+ `Source ${index + 1}/${entries.length}: ${entry.source} (${entry.pendingSkills.length} skill(s) selected via --reinstall-all).`,
345
+ );
346
+ installTasks.push({
347
+ order: index + 1,
348
+ entry,
349
+ skillsToInstall: [...entry.skills],
350
+ command: buildSkillsAddCommand(entry.source, entry.skills, {
351
+ ...options,
352
+ projectScope,
353
+ }),
354
+ });
355
+ continue;
356
+ }
357
+
358
+ entry.pendingSkills = entry.skills.filter(
359
+ (skill) => !installedLookup!.has(skill),
360
+ );
361
+ entry.alreadyInstalledSkills = entry.skills.filter((skill) =>
362
+ installedLookup!.has(skill),
363
+ );
364
+ pendingSkillTotal += entry.pendingSkills.length;
365
+ alreadyInstalledTotal += entry.alreadyInstalledSkills.length;
366
+ appendLine(
367
+ `Source ${index + 1}/${entries.length}: ${entry.source} (${entry.pendingSkills.length} pending, ${entry.alreadyInstalledSkills.length} already installed).`,
368
+ );
369
+
370
+ if (entry.pendingSkills.length > 0) {
371
+ installTasks.push({
372
+ order: index + 1,
373
+ entry,
374
+ skillsToInstall: [...entry.pendingSkills],
375
+ command: buildSkillsAddCommand(entry.source, entry.pendingSkills, {
376
+ ...options,
377
+ projectScope,
378
+ }),
379
+ });
380
+ }
381
+ }
382
+
383
+ if (options.installAll) {
384
+ appendLine(
385
+ `Installing all skills from ${entries.length} source(s) using '${scopeLabel}' scope.`,
386
+ );
387
+ appendLine(
388
+ "Manifest skill selections are ignored when --all is enabled; upstream source state determines what gets installed.",
389
+ );
390
+ } else if (options.reinstallAll) {
391
+ appendLine(
392
+ `Reinstalling ${requestedSkillTotal} skill(s) from ${entries.length} source(s) using '${scopeLabel}' scope.`,
393
+ );
394
+ } else {
395
+ appendLine(
396
+ `Restoring up to ${requestedSkillTotal} skill(s) from ${entries.length} source(s) using '${scopeLabel}' scope.`,
397
+ );
398
+ appendLine(
399
+ `${pendingSkillTotal} pending, ${alreadyInstalledTotal} already installed.`,
400
+ );
401
+ }
402
+
403
+ for (const entry of entries) {
404
+ if (
405
+ !options.installAll &&
406
+ !options.reinstallAll &&
407
+ entry.pendingSkills.length === 0
408
+ ) {
409
+ appendLine(
410
+ `Skipping ${entry.source}: all ${entry.count} skill(s) already installed.`,
411
+ );
412
+ }
413
+ }
414
+
415
+ if (installTasks.length === 0) {
416
+ if (!projectScope) {
417
+ const globalSkillStateDiff =
418
+ await verifyGlobalSkillStateConsistency(dependencies);
419
+ if (
420
+ globalSkillStateDiff.missingFromLock.length > 0 ||
421
+ globalSkillStateDiff.missingFromDirectory.length > 0
422
+ ) {
423
+ return {
424
+ exitCode: 1,
425
+ stdout: streamOutput
426
+ ? streamedOutput
427
+ ? "\n"
428
+ : ""
429
+ : page(lines.join("\n")),
430
+ stderr: page(
431
+ formatGlobalSkillStateMismatch(globalSkillStateDiff)
432
+ .map((line) => errorMessage(line))
433
+ .join("\n"),
434
+ ),
435
+ errorCode: "GLOBAL_STATE_MISMATCH",
436
+ };
437
+ }
438
+ }
439
+
440
+ appendLine(
441
+ `Restore summary: ${entries.length} source(s) processed, 0 source install(s) attempted, 0 skill(s) scheduled.`,
442
+ );
443
+ return {
444
+ exitCode: 0,
445
+ stdout: streamOutput
446
+ ? streamedOutput
447
+ ? "\n"
448
+ : ""
449
+ : page(lines.join("\n")),
450
+ stderr: "",
451
+ payload: {
452
+ manifestPath,
453
+ projectScope,
454
+ dryRun: false,
455
+ sourceCount: entries.length,
456
+ requestedSkillTotal,
457
+ pendingSkillTotal: 0,
458
+ installTaskCount: 0,
459
+ continueOnError: options.continueOnError,
460
+ },
461
+ };
462
+ }
463
+
464
+ // The Skills CLI mutates a shared global lock file, so live installs stay
465
+ // serialized even when dry-run previews show a higher theoretical limit.
466
+ const concurrency = 1;
467
+ appendLine(
468
+ `Installing from ${installTasks.length} source(s) with concurrency ${concurrency} (lock-safe mode).`,
469
+ );
470
+ const totalProgressSkills = options.reinstallAll
471
+ ? requestedSkillTotal
472
+ : pendingSkillTotal;
473
+ let completedProgressSkills = 0;
474
+ if (showSkillProgress && totalProgressSkills > 0) {
475
+ emitStreamLine(progressLine(0, totalProgressSkills));
476
+ }
477
+
478
+ let failures: ConcurrentFailure[] = [];
479
+
480
+ try {
481
+ failures = await runWithConcurrency(
482
+ installTasks,
483
+ concurrency,
484
+ async (task) => {
485
+ appendLine(
486
+ `Installing source ${task.order}/${entries.length}: ${task.entry.source}`,
487
+ );
488
+ if (options.installAll) {
489
+ appendLine(
490
+ `Installing all skills from ${task.entry.source} via --all...`,
491
+ );
492
+ } else if (
493
+ !options.reinstallAll &&
494
+ task.entry.alreadyInstalledSkills.length > 0
495
+ ) {
496
+ appendLine(
497
+ `Installing ${task.skillsToInstall.length} missing skill(s) from ${task.entry.source} and skipping ${task.entry.alreadyInstalledSkills.length} already installed.`,
498
+ );
499
+ } else {
500
+ appendLine(
501
+ `Installing ${task.skillsToInstall.length} skill(s) from ${task.entry.source}...`,
502
+ );
503
+ }
504
+
505
+ const result = await dependencies.runBunx(task.command);
506
+ if (result.stdout.trim()) {
507
+ appendLine(result.stdout.trimEnd());
508
+ }
509
+ if (result.stderr.trim()) {
510
+ errors.push(errorMessage(result.stderr.trimEnd()));
511
+ }
512
+
513
+ if (showSkillProgress && totalProgressSkills > 0) {
514
+ completedProgressSkills += task.skillsToInstall.length;
515
+ emitStreamLine(
516
+ progressLine(completedProgressSkills, totalProgressSkills),
517
+ );
518
+ }
519
+
520
+ if (result.exitCode !== 0) {
521
+ const message = `Install failed for source '${task.entry.source}' with exit code ${result.exitCode}`;
522
+ if (options.continueOnError) {
523
+ errors.push(message);
524
+ }
525
+ throw new Error(message);
526
+ }
527
+ },
528
+ { continueOnError: options.continueOnError },
529
+ );
530
+ } catch (error) {
531
+ const message = error instanceof Error ? error.message : String(error);
532
+ return {
533
+ exitCode: 1,
534
+ stdout: streamOutput
535
+ ? streamedOutput
536
+ ? "\n"
537
+ : ""
538
+ : lines.length > 0
539
+ ? page(lines.join("\n"))
540
+ : "",
541
+ stderr: page([...errors, errorMessage(message)].join("\n")),
542
+ errorCode: resolveRestoreErrorCode(message),
543
+ };
544
+ }
545
+
546
+ const summarySkillTotal =
547
+ options.installAll || options.reinstallAll
548
+ ? scheduledSkillTotal
549
+ : pendingSkillTotal;
550
+
551
+ if (options.continueOnError && failures.length > 0) {
552
+ appendLine(
553
+ buildRestoreSummary(
554
+ entries.length,
555
+ installTasks.length,
556
+ summarySkillTotal,
557
+ options.installAll,
558
+ ),
559
+ );
560
+ errors.push(
561
+ errorMessage(
562
+ `Completed restore with ${failures.length} source failure(s).`,
563
+ ),
564
+ );
565
+ return {
566
+ exitCode: 0,
567
+ stdout: streamOutput
568
+ ? streamedOutput
569
+ ? "\n"
570
+ : ""
571
+ : lines.length > 0
572
+ ? page(lines.join("\n"))
573
+ : "",
574
+ stderr: page(errors.join("\n")),
575
+ payload: {
576
+ manifestPath,
577
+ projectScope,
578
+ dryRun: false,
579
+ sourceCount: entries.length,
580
+ requestedSkillTotal,
581
+ pendingSkillTotal: summarySkillTotal,
582
+ installTaskCount: installTasks.length,
583
+ continueOnError: options.continueOnError,
584
+ },
585
+ };
586
+ }
587
+
588
+ if (!projectScope) {
589
+ const globalSkillStateDiff =
590
+ await verifyGlobalSkillStateConsistency(dependencies);
591
+ if (
592
+ globalSkillStateDiff.missingFromLock.length > 0 ||
593
+ globalSkillStateDiff.missingFromDirectory.length > 0
594
+ ) {
595
+ return {
596
+ exitCode: 1,
597
+ stdout: streamOutput
598
+ ? streamedOutput
599
+ ? "\n"
600
+ : ""
601
+ : lines.length > 0
602
+ ? page(lines.join("\n"))
603
+ : "",
604
+ stderr: page(
605
+ formatGlobalSkillStateMismatch(globalSkillStateDiff)
606
+ .map((line) => errorMessage(line))
607
+ .join("\n"),
608
+ ),
609
+ errorCode: "GLOBAL_STATE_MISMATCH",
610
+ };
611
+ }
612
+ }
613
+
614
+ appendLine(
615
+ buildRestoreSummary(
616
+ entries.length,
617
+ installTasks.length,
618
+ summarySkillTotal,
619
+ options.installAll,
620
+ ),
621
+ );
622
+ return {
623
+ exitCode: 0,
624
+ stdout: streamOutput
625
+ ? streamedOutput
626
+ ? "\n"
627
+ : ""
628
+ : lines.length > 0
629
+ ? page(lines.join("\n"))
630
+ : "",
631
+ stderr: errors.length > 0 ? page(errors.join("\n")) : "",
632
+ payload: {
633
+ manifestPath,
634
+ projectScope,
635
+ dryRun: false,
636
+ sourceCount: entries.length,
637
+ requestedSkillTotal,
638
+ pendingSkillTotal: summarySkillTotal,
639
+ installTaskCount: installTasks.length,
640
+ continueOnError: options.continueOnError,
641
+ },
642
+ };
643
+ }
644
+
645
+ /**
646
+ * Restores skills from a manifest while preserving the manifest's scope by default.
647
+ */
648
+ export async function runRestore(
649
+ argv: string[],
650
+ dependencies: RestoreDependencies = {},
651
+ ): Promise<RestoreRunResult> {
652
+ try {
653
+ const options = parseRestoreArgs(argv);
654
+ if (options.help) {
655
+ return { exitCode: 0, stdout: printHelp(), stderr: "" };
656
+ }
657
+
658
+ const resolvedDependencies: Required<RestoreDependencies> = {
659
+ readManifest: dependencies.readManifest ?? parseManifest,
660
+ getInstalledSkillNames:
661
+ dependencies.getInstalledSkillNames ?? getInstalledSkillNames,
662
+ readTrackedGlobalSkillNames:
663
+ dependencies.readTrackedGlobalSkillNames ?? readTrackedGlobalSkillNames,
664
+ readInstalledGlobalSkillNames:
665
+ dependencies.readInstalledGlobalSkillNames ??
666
+ readInstalledGlobalSkillNames,
667
+ runBunx: dependencies.runBunx ?? runBunx,
668
+ reportProgress: dependencies.reportProgress ?? (() => {}),
669
+ streamOutput: dependencies.streamOutput ?? false,
670
+ };
671
+
672
+ const manifest = resolvedDependencies.readManifest(options.manifestPath);
673
+ return runRestoreFromManifest(
674
+ manifest,
675
+ options.manifestPath,
676
+ options,
677
+ resolvedDependencies,
678
+ );
679
+ } catch (error) {
680
+ const message = error instanceof Error ? error.message : String(error);
681
+ const errorCode = resolveRestoreErrorCode(message);
682
+ return {
683
+ exitCode: 1,
684
+ stdout: "",
685
+ stderr: formatRestoreError(message, errorCode),
686
+ errorCode,
687
+ };
688
+ }
689
+ }