skillcaddy 0.27.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/CHANGELOG.md +497 -0
- package/CONTEXT.md +149 -0
- package/LICENSE +21 -0
- package/README.md +499 -0
- package/README_CN.md +482 -0
- package/collection-metadata/github/mattpocock.json +16 -0
- package/lib/canonicalJson.js +11 -0
- package/lib/claudeStore.js +135 -0
- package/lib/collectionEnablement.js +115 -0
- package/lib/collectionSetup.js +199 -0
- package/lib/enablePlan.js +116 -0
- package/lib/enablementScope.js +8 -0
- package/lib/hermesStore.js +21 -0
- package/lib/libraryImage.js +1785 -0
- package/lib/managerInstall.js +109 -0
- package/lib/managerSourceWorkflow.js +234 -0
- package/lib/projectActions.js +24 -0
- package/lib/projectPath.js +51 -0
- package/lib/roots.js +99 -0
- package/lib/skillLinkStore.js +68 -0
- package/lib/skillMetadata.js +191 -0
- package/lib/skillStore.js +541 -0
- package/lib/sourceAcquisition.js +475 -0
- package/lib/sourceAcquisitionAdapter.js +47 -0
- package/lib/sourceAcquisitionError.js +18 -0
- package/lib/sourceAcquisitionPolicy.js +139 -0
- package/lib/sourceArchive.js +363 -0
- package/lib/sourceArchiveWorkspace.js +36 -0
- package/lib/sourceGit.js +95 -0
- package/lib/sourceGitAcquisitionAdapter.js +145 -0
- package/lib/sourceGitBatchUpdate.js +168 -0
- package/lib/sourceGitRepair.js +227 -0
- package/lib/sourceGitStaging.js +139 -0
- package/lib/sourceGitUpgradeAdapter.js +583 -0
- package/lib/sourceGitUrl.js +170 -0
- package/lib/sourceHttp.js +302 -0
- package/lib/sourceLocal.js +200 -0
- package/lib/sourceLocalAcquisitionAdapter.js +107 -0
- package/lib/sourceManager.js +242 -0
- package/lib/sourceMigration.js +172 -0
- package/lib/sourceMigrationIssue.js +6 -0
- package/lib/sourcePath.js +6 -0
- package/lib/sourcePolicy.js +53 -0
- package/lib/sourceRecord.js +295 -0
- package/lib/sourceRegistry.js +155 -0
- package/lib/sourceRemoteArchiveAcquisitionAdapter.js +147 -0
- package/lib/sourceRemoteFileAcquisitionAdapter.js +95 -0
- package/lib/sourceRemoteFilePolicy.js +62 -0
- package/lib/sourceRemoteFileWorkspace.js +55 -0
- package/lib/sourceSkillDiscovery.js +117 -0
- package/lib/sourceTree.js +185 -0
- package/lib/sourceUpgrade.js +596 -0
- package/lib/sourceUpgradeAdapter.js +41 -0
- package/lib/sourceUpgradePolicy.js +170 -0
- package/lib/sourceUpgradeTransaction.js +202 -0
- package/lib/sourceValidation.js +120 -0
- package/lib/sourceWorkspace.js +37 -0
- package/lib/specGate.js +235 -0
- package/lib/tuiActions.js +169 -0
- package/lib/tuiInstall.js +133 -0
- package/lib/tuiLayout.js +94 -0
- package/lib/version.js +17 -0
- package/lib/webManager.js +370 -0
- package/package.json +74 -0
- package/public/agentsUi.js +138 -0
- package/public/app.js +1431 -0
- package/public/claude.css +2 -0
- package/public/claudeUi.js +119 -0
- package/public/emptyState.js +12 -0
- package/public/enabled.css +303 -0
- package/public/favicon.svg +5 -0
- package/public/index.html +199 -0
- package/public/skillcaddy_CN.png +0 -0
- package/public/skillcaddy_EN.png +0 -0
- package/public/styles.css +1611 -0
- package/scripts/cli.js +647 -0
- package/scripts/cron-sync-update-git.sh +128 -0
- package/scripts/pull-github.sh +22 -0
- package/scripts/source.js +638 -0
- package/scripts/tui.js +1058 -0
- package/server.js +196 -0
- package/skills/skillcaddy-manager/SKILL.md +93 -0
- package/skills/skillcaddy-manager/agents/openai.yaml +7 -0
- package/skills/skillcaddy-manager/references/METADATA.md +75 -0
- package/skills/skillcaddy-manager/references/OPERATIONS.md +315 -0
- package/skills/skillcaddy-manager/references/RECOMMENDATION_GUIDE.md +51 -0
- package/skills/skillcaddy-manager/references/featured-skills.json +564 -0
- package/skills/skillcaddy-manager/references/skill-platforms.json +48 -0
- package/skills/skillcaddy-manager/scripts/check-conflicts.cjs +117 -0
- package/skills/skillcaddy-manager/scripts/check-global-skills.cjs +169 -0
- package/skills/skillcaddy-manager/scripts/translate-skill-notes.cjs +297 -0
- package/skills/skillcaddy-manager/scripts/version-manager.cjs +146 -0
- package/skills/skillcaddy-manager/scripts/view-recommendations.cjs +279 -0
- package/skills/skillcaddy-manager/skillcaddy.json +9 -0
- package/skills/spec-pipeline/spec-direction-gate/SKILL.md +119 -0
- package/skills/spec-pipeline/spec-direction-gate/check-approval.py +110 -0
- package/skills/spec-pipeline/spec-draft-intake/SKILL.md +116 -0
- package/skills/spec-pipeline/spec-role-contract/SKILL.md +113 -0
- package/skills/spec-pipeline/spec-role-contract/check-roles.py +108 -0
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { createInterface } from 'node:readline/promises';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { resolveLibraryRoot } from '../lib/roots.js';
|
|
7
|
+
import {
|
|
8
|
+
applyAddSource,
|
|
9
|
+
applyRepairSource,
|
|
10
|
+
applySourceMigration,
|
|
11
|
+
applyUpdateSource,
|
|
12
|
+
exportLibraryImage,
|
|
13
|
+
importLibraryImage,
|
|
14
|
+
inspectSource,
|
|
15
|
+
listSources,
|
|
16
|
+
planAddSource,
|
|
17
|
+
planBreakingRepairSource,
|
|
18
|
+
planBreakingUpdateSource,
|
|
19
|
+
planRepairSource,
|
|
20
|
+
planSourceMigration,
|
|
21
|
+
planUpdateSource,
|
|
22
|
+
updateGitSources
|
|
23
|
+
} from '../lib/sourceManager.js';
|
|
24
|
+
|
|
25
|
+
export async function runSourceCli({
|
|
26
|
+
argv = [],
|
|
27
|
+
rootDir = process.cwd(),
|
|
28
|
+
projectPath = process.cwd(),
|
|
29
|
+
globalDir,
|
|
30
|
+
hermesDir,
|
|
31
|
+
tarPath,
|
|
32
|
+
imageLimits,
|
|
33
|
+
stdin = process.stdin,
|
|
34
|
+
stdout = process.stdout,
|
|
35
|
+
stderr = process.stderr,
|
|
36
|
+
confirm
|
|
37
|
+
} = {}) {
|
|
38
|
+
const [command, ...args] = argv;
|
|
39
|
+
const projectOption = parseProjectOption(args);
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
if (projectOption.error) {
|
|
43
|
+
stderr.write(`Error: ${projectOption.error}\n`);
|
|
44
|
+
printUsage(stderr);
|
|
45
|
+
return 2;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (projectOption.specified && !['repair', 'update', 'update-git'].includes(command)) {
|
|
49
|
+
stderr.write('Error: --project is supported only by repair, update, and update-git\n');
|
|
50
|
+
printUsage(stderr);
|
|
51
|
+
return 2;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (['repair', 'update', 'update-git'].includes(command) && !projectOption.specified) {
|
|
55
|
+
stderr.write('Error: source updates require an explicit --project path\n');
|
|
56
|
+
printUsage(stderr);
|
|
57
|
+
return 2;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const commandArgs = projectOption.args;
|
|
61
|
+
const effectiveProjectPath = path.resolve(projectOption.projectPath || projectPath);
|
|
62
|
+
|
|
63
|
+
if (command === 'image') {
|
|
64
|
+
const [operation, filePath, ...flags] = commandArgs;
|
|
65
|
+
if (!['export', 'import'].includes(operation) || !filePath?.endsWith('.tar.gz') ||
|
|
66
|
+
new Set(flags).size !== flags.length || flags.some(flag => !['--dry-run', '--yes'].includes(flag)) ||
|
|
67
|
+
(operation === 'export' && flags.length)) {
|
|
68
|
+
printUsage(stderr);
|
|
69
|
+
return 2;
|
|
70
|
+
}
|
|
71
|
+
const context = {
|
|
72
|
+
rootDir,
|
|
73
|
+
globalDir,
|
|
74
|
+
hermesDir,
|
|
75
|
+
tarPath,
|
|
76
|
+
imageLimits,
|
|
77
|
+
report: message => stderr.write(`${message}\n`)
|
|
78
|
+
};
|
|
79
|
+
if (operation === 'export') {
|
|
80
|
+
const result = await exportLibraryImage(context, filePath);
|
|
81
|
+
stdout.write(`${result.path}\n`);
|
|
82
|
+
} else {
|
|
83
|
+
const result = await importLibraryImage(context, filePath, {
|
|
84
|
+
dryRun: flags.includes('--dry-run'), yes: flags.includes('--yes'),
|
|
85
|
+
confirm: plan => requestConfirmation({ confirm, stdin, stdout: stderr }, plan, 'Apply this image import plan? [y/N] ')
|
|
86
|
+
});
|
|
87
|
+
stderr.write(`Outcome: ${result.cancelled ? 'cancelled' : flags.includes('--dry-run') ? 'dry-run (NO CHANGES)' : 'imported'}\n`);
|
|
88
|
+
}
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (command === 'list' && commandArgs.length === 0) {
|
|
93
|
+
printList(await listSources({ rootDir }), stdout);
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (command === 'update') {
|
|
98
|
+
const parsed = parseUpdateArgs(commandArgs);
|
|
99
|
+
if (!parsed) {
|
|
100
|
+
printUsage(stderr);
|
|
101
|
+
return 2;
|
|
102
|
+
}
|
|
103
|
+
const context = { rootDir, projectPath: effectiveProjectPath, globalDir, hermesDir };
|
|
104
|
+
const planUpdate = parsed.allowBreaking
|
|
105
|
+
? planBreakingUpdateSource
|
|
106
|
+
: planUpdateSource;
|
|
107
|
+
const plan = await planUpdate(context, parsed.request);
|
|
108
|
+
printUpdatePlan(plan, stdout);
|
|
109
|
+
if (
|
|
110
|
+
plan.status === 'ready' &&
|
|
111
|
+
!parsed.yes &&
|
|
112
|
+
!await requestConfirmation(
|
|
113
|
+
{ confirm, stdin, stdout },
|
|
114
|
+
plan,
|
|
115
|
+
'Apply this update plan? [y/N] '
|
|
116
|
+
)
|
|
117
|
+
) {
|
|
118
|
+
stdout.write('Outcome: cancelled\n');
|
|
119
|
+
return 0;
|
|
120
|
+
}
|
|
121
|
+
printUpdateResult(await applyUpdateSource(context, plan), stdout);
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (command === 'update-git') {
|
|
126
|
+
const parsed = parseGitUpdateArgs(commandArgs);
|
|
127
|
+
if (!parsed) {
|
|
128
|
+
printUsage(stderr);
|
|
129
|
+
return 2;
|
|
130
|
+
}
|
|
131
|
+
const result = await updateGitSources({ rootDir, projectPath: effectiveProjectPath, globalDir, hermesDir });
|
|
132
|
+
printGitUpdateBatch(result, stdout, parsed);
|
|
133
|
+
return result.sources.some(
|
|
134
|
+
(source) => source.status === 'failed' ||
|
|
135
|
+
source.status === 'breaking' && source.applied === false
|
|
136
|
+
) ? 1 : 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (command === 'repair') {
|
|
140
|
+
const parsed = parseRepairArgs(commandArgs);
|
|
141
|
+
if (!parsed) {
|
|
142
|
+
printUsage(stderr);
|
|
143
|
+
return 2;
|
|
144
|
+
}
|
|
145
|
+
const context = { rootDir, projectPath: effectiveProjectPath, globalDir, hermesDir };
|
|
146
|
+
const planRepair = parsed.allowBreaking
|
|
147
|
+
? planBreakingRepairSource
|
|
148
|
+
: planRepairSource;
|
|
149
|
+
const plan = await planRepair(context, parsed.request);
|
|
150
|
+
printRepairPlan(plan, stdout);
|
|
151
|
+
if (
|
|
152
|
+
plan.status === 'ready' &&
|
|
153
|
+
!parsed.yes &&
|
|
154
|
+
!await requestConfirmation(
|
|
155
|
+
{ confirm, stdin, stdout },
|
|
156
|
+
plan,
|
|
157
|
+
'Adopt this Git registry state? [y/N] '
|
|
158
|
+
)
|
|
159
|
+
) {
|
|
160
|
+
stdout.write('Outcome: cancelled\n');
|
|
161
|
+
return 0;
|
|
162
|
+
}
|
|
163
|
+
printRepairResult(await applyRepairSource(context, plan), stdout);
|
|
164
|
+
return 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (command === 'inspect' && args.length === 1) {
|
|
168
|
+
printSource(await inspectSource({ rootDir }, args[0]), stdout);
|
|
169
|
+
return 0;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (command === 'add') {
|
|
173
|
+
const parsed = parseAddArgs(args);
|
|
174
|
+
if (!parsed) {
|
|
175
|
+
printUsage(stderr);
|
|
176
|
+
return 2;
|
|
177
|
+
}
|
|
178
|
+
const plan = await planAddSource({ rootDir }, parsed.request);
|
|
179
|
+
printAddPlan(plan, stdout);
|
|
180
|
+
if (
|
|
181
|
+
plan.status === 'ready' &&
|
|
182
|
+
!parsed.yes &&
|
|
183
|
+
!await requestConfirmation(
|
|
184
|
+
{ confirm, stdin, stdout },
|
|
185
|
+
plan,
|
|
186
|
+
'Apply this add plan? [y/N] '
|
|
187
|
+
)
|
|
188
|
+
) {
|
|
189
|
+
stdout.write('Outcome: cancelled\n');
|
|
190
|
+
return 0;
|
|
191
|
+
}
|
|
192
|
+
printAddResult(await applyAddSource({ rootDir }, plan), stdout);
|
|
193
|
+
return 0;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (command === 'migrate' && (args.length === 0 || (args.length === 1 && args[0] === '--yes'))) {
|
|
197
|
+
const plan = await planSourceMigration({ rootDir });
|
|
198
|
+
printMigrationPlan(plan, stdout);
|
|
199
|
+
if (args[0] === '--yes') {
|
|
200
|
+
printMigrationResult(await applySourceMigration({ rootDir }, plan), stdout);
|
|
201
|
+
} else {
|
|
202
|
+
stdout.write('next: npm run source -- migrate --yes\n');
|
|
203
|
+
}
|
|
204
|
+
return plan.unresolved.length > 0 ? 3 : 0;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
printUsage(stderr);
|
|
208
|
+
return 2;
|
|
209
|
+
} catch (error) {
|
|
210
|
+
stderr.write(`Outcome: ${error.category || 'failure'}\n`);
|
|
211
|
+
stderr.write(`${error.message}\n`);
|
|
212
|
+
return error.exitCode || 1;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function printList(result, stdout) {
|
|
217
|
+
if (result.sources.length === 0) {
|
|
218
|
+
stdout.write('No registered or unmanaged sources found.\n');
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
for (const source of result.sources) {
|
|
223
|
+
stdout.write(`[${source.status}] ${source.inventoryId}\n`);
|
|
224
|
+
stdout.write(` install: ${source.installPath}${source.exists ? '' : ' (missing)'}\n`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function printSource(source, stdout) {
|
|
229
|
+
stdout.write(`source: ${source.sourceId}\n`);
|
|
230
|
+
stdout.write(`type: ${source.type}\n`);
|
|
231
|
+
stdout.write(`install: ${source.installPath}\n`);
|
|
232
|
+
stdout.write(`origin: ${formatOrigin(source.origin)}\n`);
|
|
233
|
+
if (source.integrity) {
|
|
234
|
+
stdout.write(`integrity: ${source.integrity.algorithm} ${source.integrity.value}\n`);
|
|
235
|
+
} else {
|
|
236
|
+
stdout.write('integrity: unavailable\n');
|
|
237
|
+
}
|
|
238
|
+
stdout.write('skills:\n');
|
|
239
|
+
for (const skillPath of source.skills) stdout.write(` - ${skillPath}\n`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function printMigrationPlan(plan, stdout) {
|
|
243
|
+
const recordLabel = plan.records.length === 1 ? 'source record' : 'source records';
|
|
244
|
+
stdout.write(`Migration plan: ${plan.records.length} ${recordLabel}, ${plan.unresolved.length} unresolved.\n`);
|
|
245
|
+
for (const record of plan.records) {
|
|
246
|
+
stdout.write(`[adopt] ${record.sourceId} -> ${record.installPath}\n`);
|
|
247
|
+
}
|
|
248
|
+
for (const issue of plan.unresolved) {
|
|
249
|
+
stdout.write(`[unresolved] ${issue.installPath}: ${issue.reason} — ${issue.detail}\n`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function printMigrationResult(result, stdout) {
|
|
254
|
+
if (result.written.length === 0) {
|
|
255
|
+
stdout.write('Applied source migration: no source records to write.\n');
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const label = result.written.length === 1 ? 'record' : 'records';
|
|
259
|
+
stdout.write(`Applied source migration: ${result.written.length} ${label} written.\n`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function printAddPlan(plan, stdout) {
|
|
263
|
+
stdout.write(`Add plan: ${plan.status}\n`);
|
|
264
|
+
stdout.write(`source: ${plan.sourceId}\n`);
|
|
265
|
+
stdout.write(`install: ${plan.installPath}\n`);
|
|
266
|
+
stdout.write(
|
|
267
|
+
`input: ${plan.input.type} ${
|
|
268
|
+
plan.input.name || plan.input.remote || plan.input.display
|
|
269
|
+
}\n`
|
|
270
|
+
);
|
|
271
|
+
if (plan.origin?.kind === 'git') {
|
|
272
|
+
stdout.write(`origin: ${formatOrigin(plan.origin)}\n`);
|
|
273
|
+
if (plan.focus) {
|
|
274
|
+
stdout.write(`focus: ${plan.focus.ref}${plan.focus.path ? ` ${plan.focus.path}` : ''}\n`);
|
|
275
|
+
}
|
|
276
|
+
} else {
|
|
277
|
+
stdout.write(`integrity: ${plan.integrity.algorithm} ${plan.integrity.value}\n`);
|
|
278
|
+
}
|
|
279
|
+
stdout.write(`skills (${plan.skills.length}):\n`);
|
|
280
|
+
for (const skillPath of plan.skills) stdout.write(` - ${skillPath}\n`);
|
|
281
|
+
stdout.write(`warnings (${plan.warnings.length}):\n`);
|
|
282
|
+
for (const warning of plan.warnings) {
|
|
283
|
+
stdout.write(` - [${warning.category}] ${warning.skillPath}: ${warning.message}\n`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function printAddResult(result, stdout) {
|
|
288
|
+
stdout.write(`Outcome: ${result.status}\n`);
|
|
289
|
+
stdout.write(`source: ${result.sourceId}\n`);
|
|
290
|
+
stdout.write(`install: ${result.installPath}\n`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function printUpdatePlan(plan, stdout) {
|
|
294
|
+
stdout.write(`Update plan: ${plan.status}\n`);
|
|
295
|
+
stdout.write(`source: ${plan.sourceId}\n`);
|
|
296
|
+
stdout.write(`install: ${plan.installPath}\n`);
|
|
297
|
+
stdout.write(
|
|
298
|
+
`input: ${plan.input.type} ${
|
|
299
|
+
plan.input.name || plan.input.display || plan.input.remote
|
|
300
|
+
}\n`
|
|
301
|
+
);
|
|
302
|
+
printSkillPaths('unchanged', plan.changes.unchanged, stdout);
|
|
303
|
+
printSkillPaths('added', plan.changes.added, stdout);
|
|
304
|
+
printSkillPaths('removed or relocated', plan.changes.removedOrRelocated, stdout);
|
|
305
|
+
if (plan.skillChanges) {
|
|
306
|
+
printGitUpdateSummary({
|
|
307
|
+
fromCommit: plan.currentCommit,
|
|
308
|
+
toCommit: plan.incomingCommit,
|
|
309
|
+
skillChanges: plan.skillChanges
|
|
310
|
+
}, stdout, { verbose: true });
|
|
311
|
+
}
|
|
312
|
+
if (plan.affectedProjectLinks?.length) {
|
|
313
|
+
printAffectedProjectLinks(plan.affectedProjectLinks, stdout);
|
|
314
|
+
}
|
|
315
|
+
if (plan.affectedGlobalLinks?.length) {
|
|
316
|
+
printAffectedGlobalLinks(plan.affectedGlobalLinks, stdout);
|
|
317
|
+
}
|
|
318
|
+
if (plan.affectedHermesLinks?.length) {
|
|
319
|
+
printAffectedHermesLinks(plan.affectedHermesLinks, stdout);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function printAffectedProjectLinks(links, stdout) {
|
|
324
|
+
if (!Array.isArray(links) || links.length === 0) {
|
|
325
|
+
stdout.write('would break project links:\n (none)\n');
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
stdout.write('would break project links:\n');
|
|
329
|
+
for (const link of links) {
|
|
330
|
+
stdout.write(` - ${link.alias} -> ${link.skillPath}\n`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function printAffectedGlobalLinks(links, stdout) {
|
|
335
|
+
stdout.write('would break global links:\n');
|
|
336
|
+
for (const link of links) {
|
|
337
|
+
stdout.write(` - ${link.alias} -> ${link.skillPath}\n`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function printAffectedHermesLinks(links, stdout) {
|
|
342
|
+
stdout.write('would break Hermes links:\n');
|
|
343
|
+
for (const link of links) {
|
|
344
|
+
stdout.write(` - ${link.alias} -> ${link.skillPath}\n`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function printGitUpdateBatch(result, stdout, { verbose = false } = {}) {
|
|
349
|
+
const counts = {
|
|
350
|
+
updated: 0,
|
|
351
|
+
current: 0,
|
|
352
|
+
dirty: 0,
|
|
353
|
+
failed: 0,
|
|
354
|
+
breaking: 0
|
|
355
|
+
};
|
|
356
|
+
for (const source of result.sources) {
|
|
357
|
+
counts[source.status] += 1;
|
|
358
|
+
const detail = source.status === 'breaking'
|
|
359
|
+
? ` (${source.applied ? 'updated' : 'blocked'})`
|
|
360
|
+
: source.status === 'failed' && source.category
|
|
361
|
+
? ` (${source.category})`
|
|
362
|
+
: '';
|
|
363
|
+
stdout.write(`[${source.status}] ${source.sourceId}${detail}\n`);
|
|
364
|
+
if (source.status === 'breaking' && source.affected?.length) {
|
|
365
|
+
const aliases = source.affected.map((link) => link.alias).join(', ');
|
|
366
|
+
stdout.write(` would break: ${aliases}\n`);
|
|
367
|
+
}
|
|
368
|
+
if (source.status === 'breaking' && source.affectedGlobalLinks?.length) {
|
|
369
|
+
const aliases = source.affectedGlobalLinks.map((link) => link.alias).join(', ');
|
|
370
|
+
stdout.write(` would break global: ${aliases}\n`);
|
|
371
|
+
}
|
|
372
|
+
if (source.status === 'breaking' && source.affectedHermesLinks?.length) {
|
|
373
|
+
const aliases = source.affectedHermesLinks.map((link) => link.alias).join(', ');
|
|
374
|
+
stdout.write(` would break Hermes: ${aliases}\n`);
|
|
375
|
+
}
|
|
376
|
+
if (source.updateSummary) {
|
|
377
|
+
printGitUpdateSummary(source.updateSummary, stdout, { verbose });
|
|
378
|
+
}
|
|
379
|
+
if (source.status === 'dirty') {
|
|
380
|
+
stdout.write(` reminder: ${source.message || 'local Git changes found; update skipped'}\n`);
|
|
381
|
+
}
|
|
382
|
+
if (source.status === 'failed') {
|
|
383
|
+
stdout.write(` reason: ${source.message || source.category || 'Git source update failed'}\n`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
stdout.write(
|
|
387
|
+
`Git source summary: updated=${counts.updated} current=${counts.current} ` +
|
|
388
|
+
`dirty=${counts.dirty} breaking=${counts.breaking} failed=${counts.failed}\n`
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function printGitUpdateSummary(summary, stdout, { verbose = false } = {}) {
|
|
393
|
+
if (summary.fromCommit && summary.toCommit) {
|
|
394
|
+
stdout.write(
|
|
395
|
+
` commit: ${shortCommit(summary.fromCommit)} -> ${shortCommit(summary.toCommit)}\n`
|
|
396
|
+
);
|
|
397
|
+
} else if (summary.toCommit) {
|
|
398
|
+
stdout.write(` commit: ${summary.toCommit}\n`);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const changes = summary.skillChanges;
|
|
402
|
+
stdout.write(
|
|
403
|
+
` skill changes: add=${changes.added.length} ` +
|
|
404
|
+
`edit=${changes.edited.length} delete=${changes.deleted.length}\n`
|
|
405
|
+
);
|
|
406
|
+
if (!verbose) return;
|
|
407
|
+
|
|
408
|
+
printSkillChangePaths('add new skill', changes.added, stdout);
|
|
409
|
+
printSkillChangePaths('edit skill', changes.edited, stdout);
|
|
410
|
+
printSkillChangePaths('deleted skill', changes.deleted, stdout);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function printSkillChangePaths(label, paths, stdout) {
|
|
414
|
+
if (paths.length === 0) return;
|
|
415
|
+
stdout.write(` ${label}:\n`);
|
|
416
|
+
for (const skillPath of paths) stdout.write(` - ${skillPath}\n`);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function shortCommit(commit) {
|
|
420
|
+
return commit.slice(0, 7);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function parseProjectOption(args) {
|
|
424
|
+
const remaining = [];
|
|
425
|
+
let projectPath;
|
|
426
|
+
|
|
427
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
428
|
+
const argument = args[index];
|
|
429
|
+
if (argument === '--project') {
|
|
430
|
+
const value = args[index + 1];
|
|
431
|
+
if (projectPath !== undefined || !value || value.startsWith('--')) {
|
|
432
|
+
return { error: '--project 需要一个目录路径' };
|
|
433
|
+
}
|
|
434
|
+
projectPath = value;
|
|
435
|
+
index += 1;
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
if (argument.startsWith('--project=')) {
|
|
439
|
+
const value = argument.slice('--project='.length);
|
|
440
|
+
if (projectPath !== undefined || !value) {
|
|
441
|
+
return { error: '--project 需要一个目录路径' };
|
|
442
|
+
}
|
|
443
|
+
projectPath = value;
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
remaining.push(argument);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return {
|
|
450
|
+
args: remaining,
|
|
451
|
+
projectPath,
|
|
452
|
+
specified: projectPath !== undefined
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function parseGitUpdateArgs(args) {
|
|
457
|
+
let verbose = false;
|
|
458
|
+
for (const argument of args) {
|
|
459
|
+
if (argument === '--verbose' && !verbose) {
|
|
460
|
+
verbose = true;
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
return { verbose };
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function printUpdateResult(result, stdout) {
|
|
469
|
+
stdout.write(`Outcome: ${result.status}\n`);
|
|
470
|
+
stdout.write(`source: ${result.sourceId}\n`);
|
|
471
|
+
stdout.write(`install: ${result.installPath}\n`);
|
|
472
|
+
if (result.skillChanges) {
|
|
473
|
+
printGitUpdateSummary({
|
|
474
|
+
fromCommit: undefined,
|
|
475
|
+
toCommit: result.commit,
|
|
476
|
+
skillChanges: result.skillChanges
|
|
477
|
+
}, stdout, { verbose: true });
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function printRepairPlan(plan, stdout) {
|
|
482
|
+
stdout.write(`Repair plan: ${plan.status}\n`);
|
|
483
|
+
stdout.write(`source: ${plan.sourceId}\n`);
|
|
484
|
+
stdout.write(`install: ${plan.installPath}\n`);
|
|
485
|
+
stdout.write(`registered commit: ${plan.registeredCommit}\n`);
|
|
486
|
+
stdout.write(`current commit: ${plan.currentCommit}\n`);
|
|
487
|
+
printSkillPaths('unchanged', plan.changes.unchanged, stdout);
|
|
488
|
+
printSkillPaths('added', plan.changes.added, stdout);
|
|
489
|
+
printSkillPaths('removed or relocated', plan.changes.removedOrRelocated, stdout);
|
|
490
|
+
if (plan.affectedProjectLinks?.length) {
|
|
491
|
+
printAffectedProjectLinks(plan.affectedProjectLinks, stdout);
|
|
492
|
+
}
|
|
493
|
+
if (plan.affectedGlobalLinks?.length) {
|
|
494
|
+
printAffectedGlobalLinks(plan.affectedGlobalLinks, stdout);
|
|
495
|
+
}
|
|
496
|
+
if (plan.affectedHermesLinks?.length) {
|
|
497
|
+
printAffectedHermesLinks(plan.affectedHermesLinks, stdout);
|
|
498
|
+
}
|
|
499
|
+
if (plan.status === 'dirty') {
|
|
500
|
+
stdout.write('reminder: local Git changes found; registry was not changed\n');
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function printRepairResult(result, stdout) {
|
|
505
|
+
stdout.write(`Outcome: ${result.status}\n`);
|
|
506
|
+
stdout.write(`source: ${result.sourceId}\n`);
|
|
507
|
+
stdout.write(`install: ${result.installPath}\n`);
|
|
508
|
+
stdout.write(`commit: ${result.commit}\n`);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function printSkillPaths(label, skillPaths, stdout) {
|
|
512
|
+
stdout.write(`${label}:\n`);
|
|
513
|
+
if (skillPaths.length === 0) {
|
|
514
|
+
stdout.write(' (none)\n');
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
for (const skillPath of skillPaths) stdout.write(` - ${skillPath}\n`);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function parseAddArgs(args) {
|
|
521
|
+
if (args.length === 0 || args[0].startsWith('--')) return null;
|
|
522
|
+
const request = { input: args[0] };
|
|
523
|
+
let yes = false;
|
|
524
|
+
|
|
525
|
+
for (let index = 1; index < args.length; index += 1) {
|
|
526
|
+
const argument = args[index];
|
|
527
|
+
if (argument === '--yes') {
|
|
528
|
+
if (yes) return null;
|
|
529
|
+
yes = true;
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
if (argument === '--name' || argument === '--namespace') {
|
|
533
|
+
const key = argument.slice(2);
|
|
534
|
+
const value = args[index + 1];
|
|
535
|
+
if (!value || value.startsWith('--') || request[key] !== undefined) return null;
|
|
536
|
+
request[key] = value;
|
|
537
|
+
index += 1;
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
return null;
|
|
541
|
+
}
|
|
542
|
+
return { request, yes };
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function parseUpdateArgs(args) {
|
|
546
|
+
if (
|
|
547
|
+
args.length < 1 ||
|
|
548
|
+
args[0].startsWith('--')
|
|
549
|
+
) {
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
552
|
+
const request = {
|
|
553
|
+
sourceId: args[0],
|
|
554
|
+
...(args[1] && !args[1].startsWith('--') ? { input: args[1] } : {})
|
|
555
|
+
};
|
|
556
|
+
let allowBreaking = false;
|
|
557
|
+
let yes = false;
|
|
558
|
+
|
|
559
|
+
const optionStart = request.input ? 2 : 1;
|
|
560
|
+
for (const argument of args.slice(optionStart)) {
|
|
561
|
+
if (argument === '--allow-breaking' && !allowBreaking) {
|
|
562
|
+
allowBreaking = true;
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
if (argument === '--yes' && !yes) {
|
|
566
|
+
yes = true;
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
return { request, yes, allowBreaking };
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function parseRepairArgs(args) {
|
|
575
|
+
if (args.length < 1 || args[0].startsWith('--')) return null;
|
|
576
|
+
const request = { sourceId: args[0] };
|
|
577
|
+
let allowBreaking = false;
|
|
578
|
+
let yes = false;
|
|
579
|
+
|
|
580
|
+
for (const argument of args.slice(1)) {
|
|
581
|
+
if (argument === '--allow-breaking' && !allowBreaking) {
|
|
582
|
+
allowBreaking = true;
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
if (argument === '--yes' && !yes) {
|
|
586
|
+
yes = true;
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
return null;
|
|
590
|
+
}
|
|
591
|
+
return { request, yes, allowBreaking };
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
async function requestConfirmation({ confirm, stdin, stdout }, plan, prompt) {
|
|
595
|
+
if (confirm) return Boolean(await confirm(plan));
|
|
596
|
+
const readline = createInterface({ input: stdin, output: stdout });
|
|
597
|
+
try {
|
|
598
|
+
const answer = await readline.question(prompt);
|
|
599
|
+
return /^(?:y|yes)$/i.test(answer.trim());
|
|
600
|
+
} finally {
|
|
601
|
+
readline.close();
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
export function formatOrigin(origin) {
|
|
606
|
+
if (origin.kind === 'git') {
|
|
607
|
+
const ref = origin.ref ? ` ${origin.ref}` : '';
|
|
608
|
+
const commit = origin.commit ? ` @ ${origin.commit}` : '';
|
|
609
|
+
return `git ${origin.remote}${ref}${commit}`;
|
|
610
|
+
}
|
|
611
|
+
if (origin.kind === 'http' || origin.kind === 'https') return `${origin.kind} ${origin.display}`;
|
|
612
|
+
if (origin.kind === 'local') return `local ${origin.name}`;
|
|
613
|
+
return 'unknown';
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function printUsage(stderr) {
|
|
617
|
+
stderr.write('Usage: npm run source -- image export <image.tar.gz>\n');
|
|
618
|
+
stderr.write('Usage: npm run source -- image import <image.tar.gz> [--dry-run] [--yes]\n');
|
|
619
|
+
stderr.write('Usage: npm run source -- list\n');
|
|
620
|
+
stderr.write('Usage: npm run source -- inspect <source-id>\n');
|
|
621
|
+
stderr.write('Usage: npm run source -- add <input> [--name <name>] [--namespace <namespace>] [--yes]\n');
|
|
622
|
+
stderr.write('Usage: npm run source -- repair <source-id> [--allow-breaking] [--yes] --project <dir>\n');
|
|
623
|
+
stderr.write('Usage: npm run source -- update <source-id> [input] [--allow-breaking] [--yes] --project <dir>\n');
|
|
624
|
+
stderr.write('Usage: npm run source -- update-git [--verbose] --project <dir>\n');
|
|
625
|
+
stderr.write('Usage: npm run source -- migrate [--yes]\n');
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const isMain = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
|
|
629
|
+
if (isMain) {
|
|
630
|
+
// 原件库根:SKILLCADDY_ROOT 优先,其次由代码根推导(clone 用 clone 目录,
|
|
631
|
+
// 发行形态用 ~/.skillcaddy/library)。
|
|
632
|
+
const codeRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
633
|
+
process.exitCode = await runSourceCli({
|
|
634
|
+
argv: process.argv.slice(2),
|
|
635
|
+
rootDir: resolveLibraryRoot({ codeRoot }),
|
|
636
|
+
projectPath: process.env.SKILLCADDY_PROJECT || process.cwd()
|
|
637
|
+
});
|
|
638
|
+
}
|