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,596 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source upgrade lifecycle
|
|
3
|
+
*
|
|
4
|
+
* This module owns planning, re-planning, authorization, execution,
|
|
5
|
+
* and result projection for all source upgrades.
|
|
6
|
+
*
|
|
7
|
+
* Architecture note: Preparation and execution logic is inlined as private
|
|
8
|
+
* helpers rather than separate modules, because they have only one caller.
|
|
9
|
+
* Local, Remote Archive, and Remote File preparation (38-44 lines each)
|
|
10
|
+
* and execution (197 lines) are now internal to this lifecycle module.
|
|
11
|
+
* The Git upgrade adapter remains separate (327 lines) due to its depth.
|
|
12
|
+
*
|
|
13
|
+
* See ADR-0004 for the centralized upgrade lifecycle decision.
|
|
14
|
+
*/
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { SourceAcquisitionError } from './sourceAcquisitionError.js';
|
|
17
|
+
import { createGitUpgradeAdapter } from './sourceGitUpgradeAdapter.js';
|
|
18
|
+
import { isRemoteArchiveInput, parseRemoteArchiveInput, parseRemoteSkillFileInput } from './sourceHttp.js';
|
|
19
|
+
import { inspectLocalInput, stageLocalInput } from './sourceLocal.js';
|
|
20
|
+
import { inspectRemoteArchive, stageRemoteArchive } from './sourceArchiveWorkspace.js';
|
|
21
|
+
import { inspectRemoteSkillFile, stageRemoteSkillFile } from './sourceRemoteFileWorkspace.js';
|
|
22
|
+
import {
|
|
23
|
+
readSourceRecords,
|
|
24
|
+
validateSourceId
|
|
25
|
+
} from './sourceRegistry.js';
|
|
26
|
+
import {
|
|
27
|
+
cleanupDirectorySourceReplacement,
|
|
28
|
+
commitDirectorySourceReplacement,
|
|
29
|
+
commitGitSourceFastForward,
|
|
30
|
+
defaultSourceUpdateFileOperations,
|
|
31
|
+
discardDirectorySourceReplacement,
|
|
32
|
+
discardGitSourceFastForward,
|
|
33
|
+
prepareDirectorySourceReplacement,
|
|
34
|
+
prepareGitSourceFastForward,
|
|
35
|
+
rollbackDirectorySourceReplacement,
|
|
36
|
+
rollbackGitSourceFastForward
|
|
37
|
+
} from './sourceUpgradeTransaction.js';
|
|
38
|
+
import { defineSourceUpgradeAdapter } from './sourceUpgradeAdapter.js';
|
|
39
|
+
import {
|
|
40
|
+
assertActiveSourceMatchesRecord,
|
|
41
|
+
assertNoAffectedLinks,
|
|
42
|
+
assertSameSourceUpgradePlan,
|
|
43
|
+
classifySkillChanges,
|
|
44
|
+
findAffectedGlobalLinks,
|
|
45
|
+
findAffectedHermesLinks,
|
|
46
|
+
findAffectedProjectLinks,
|
|
47
|
+
fingerprintSourceUpgradePlan
|
|
48
|
+
} from './sourceUpgradePolicy.js';
|
|
49
|
+
import {
|
|
50
|
+
createManagedSourceWorkspace,
|
|
51
|
+
removeManagedSourceWorkspace
|
|
52
|
+
} from './sourceWorkspace.js';
|
|
53
|
+
const sourceUpgradePlans = new WeakMap();
|
|
54
|
+
const SOURCE_UPGRADE_AUTHORIZATIONS = new Map([
|
|
55
|
+
['ordinary', Object.freeze({
|
|
56
|
+
enforce(plan) {
|
|
57
|
+
assertNoAffectedLinks(
|
|
58
|
+
plan.affectedProjectLinks,
|
|
59
|
+
plan.affectedGlobalLinks,
|
|
60
|
+
plan.affectedHermesLinks
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
})],
|
|
64
|
+
['breaking', Object.freeze({
|
|
65
|
+
enforce() {}
|
|
66
|
+
})]
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
export async function planSourceUpgrade(context, command) {
|
|
70
|
+
const request = command?.request;
|
|
71
|
+
const authorizationPolicy = requireAuthorizationPolicy(
|
|
72
|
+
command?.authorization
|
|
73
|
+
);
|
|
74
|
+
const preparedPlan = await buildSourceUpgradePlan(context, request);
|
|
75
|
+
authorizationPolicy.enforce(preparedPlan.plan);
|
|
76
|
+
sourceUpgradePlans.set(preparedPlan.plan, {
|
|
77
|
+
authorizationPolicy,
|
|
78
|
+
originalFingerprint: fingerprintSourceUpgradePlan(preparedPlan.plan),
|
|
79
|
+
stalePlanMessage: preparedPlan.stalePlanMessage,
|
|
80
|
+
request: {
|
|
81
|
+
sourceId: preparedPlan.plan.sourceId,
|
|
82
|
+
input: request.input
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
return preparedPlan.plan;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function applySourceUpgrade(context, plan) {
|
|
89
|
+
const session = sourceUpgradePlans.get(plan);
|
|
90
|
+
if (!session) {
|
|
91
|
+
assertUpdatePlan(plan);
|
|
92
|
+
throw copiedPlanError(plan);
|
|
93
|
+
}
|
|
94
|
+
assertOriginalPlan(
|
|
95
|
+
session.originalFingerprint,
|
|
96
|
+
plan,
|
|
97
|
+
session.stalePlanMessage
|
|
98
|
+
);
|
|
99
|
+
assertUpdatePlan(plan);
|
|
100
|
+
|
|
101
|
+
const replannedUpgrade = await buildSourceUpgradePlan(
|
|
102
|
+
context,
|
|
103
|
+
session.request
|
|
104
|
+
);
|
|
105
|
+
session.authorizationPolicy.enforce(replannedUpgrade.plan);
|
|
106
|
+
assertOriginalPlan(
|
|
107
|
+
session.originalFingerprint,
|
|
108
|
+
replannedUpgrade.plan,
|
|
109
|
+
session.stalePlanMessage
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
await runSourceUpgrade(
|
|
113
|
+
context,
|
|
114
|
+
replannedUpgrade,
|
|
115
|
+
session.stalePlanMessage
|
|
116
|
+
);
|
|
117
|
+
return {
|
|
118
|
+
status: 'applied',
|
|
119
|
+
result: buildUpdateResult(replannedUpgrade.plan)
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function requireAuthorizationPolicy(authorization) {
|
|
124
|
+
const policy = SOURCE_UPGRADE_AUTHORIZATIONS.get(authorization?.kind);
|
|
125
|
+
if (!policy) {
|
|
126
|
+
throw new Error('Source upgrade requires an authorization policy');
|
|
127
|
+
}
|
|
128
|
+
return policy;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function buildSourceUpgradePlan(context, request) {
|
|
132
|
+
const sourceId = requireSourceId(request);
|
|
133
|
+
const record = await findSourceUpgradeRecord(context.rootDir, sourceId);
|
|
134
|
+
const adapter = selectUpgradeAdapter(context, request, record);
|
|
135
|
+
const candidate = await adapter.inspect();
|
|
136
|
+
const changes = classifySkillChanges(record.skills, candidate.skills);
|
|
137
|
+
const affectedProjectLinks = await findAffectedProjectLinks({
|
|
138
|
+
rootDir: context.rootDir,
|
|
139
|
+
projectPath: context.projectPath,
|
|
140
|
+
installPath: record.installPath,
|
|
141
|
+
removedSkillPaths: changes.removedOrRelocated
|
|
142
|
+
});
|
|
143
|
+
const affectedGlobalLinks = await findAffectedGlobalLinks({
|
|
144
|
+
rootDir: context.rootDir,
|
|
145
|
+
globalDir: context.globalDir,
|
|
146
|
+
installPath: record.installPath,
|
|
147
|
+
removedSkillPaths: changes.removedOrRelocated
|
|
148
|
+
});
|
|
149
|
+
const affectedHermesLinks = await findAffectedHermesLinks({
|
|
150
|
+
rootDir: context.rootDir,
|
|
151
|
+
hermesDir: context.hermesDir,
|
|
152
|
+
installPath: record.installPath,
|
|
153
|
+
removedSkillPaths: changes.removedOrRelocated
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
adapter,
|
|
158
|
+
kind: adapter.kind,
|
|
159
|
+
record,
|
|
160
|
+
stalePlanMessage: adapter.stalePlanMessage,
|
|
161
|
+
plan: buildUpgradePlan({
|
|
162
|
+
sourceId,
|
|
163
|
+
record,
|
|
164
|
+
candidate,
|
|
165
|
+
changes,
|
|
166
|
+
affectedProjectLinks,
|
|
167
|
+
affectedGlobalLinks,
|
|
168
|
+
affectedHermesLinks
|
|
169
|
+
})
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function selectUpgradeAdapter(context, request, record) {
|
|
174
|
+
if (record.type === 'git') {
|
|
175
|
+
return createGitUpgradeAdapter(context, request, record);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (record.type === 'remote-file') {
|
|
179
|
+
return createInlineRemoteFileUpgradeAdapter(context, request, record);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (isRemoteArchiveInput(request?.input)) {
|
|
183
|
+
if (record.type !== 'archive') {
|
|
184
|
+
throw new SourceAcquisitionError(
|
|
185
|
+
'unsupported-source',
|
|
186
|
+
`Remote Archive update requires an Archive source: ${record.sourceId}`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
return createInlineRemoteArchiveUpgradeAdapter(context, request);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!['local', 'archive'].includes(record.type)) {
|
|
193
|
+
throw new SourceAcquisitionError(
|
|
194
|
+
'unsupported-source',
|
|
195
|
+
`Local replacement requires a Local or Archive source: ${record.sourceId}`
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
return createInlineLocalUpgradeAdapter(context, request);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function buildUpgradePlan({
|
|
202
|
+
sourceId,
|
|
203
|
+
record,
|
|
204
|
+
candidate,
|
|
205
|
+
changes,
|
|
206
|
+
affectedProjectLinks,
|
|
207
|
+
affectedGlobalLinks,
|
|
208
|
+
affectedHermesLinks
|
|
209
|
+
}) {
|
|
210
|
+
return {
|
|
211
|
+
operation: 'update-source',
|
|
212
|
+
status: candidate.status || 'ready',
|
|
213
|
+
sourceId,
|
|
214
|
+
installPath: record.installPath,
|
|
215
|
+
input: candidate.input,
|
|
216
|
+
...(candidate.origin ? { origin: candidate.origin } : {}),
|
|
217
|
+
...(candidate.integrity ? { integrity: candidate.integrity } : {}),
|
|
218
|
+
...(candidate.currentCommit ? {
|
|
219
|
+
currentCommit: candidate.currentCommit,
|
|
220
|
+
incomingCommit: candidate.incomingCommit
|
|
221
|
+
} : {}),
|
|
222
|
+
skills: candidate.skills,
|
|
223
|
+
warnings: candidate.warnings,
|
|
224
|
+
...(candidate.skillChanges ? { skillChanges: candidate.skillChanges } : {}),
|
|
225
|
+
changes,
|
|
226
|
+
affectedProjectLinks,
|
|
227
|
+
...(affectedGlobalLinks.length > 0 ? { affectedGlobalLinks } : {}),
|
|
228
|
+
...(affectedHermesLinks.length > 0 ? { affectedHermesLinks } : {})
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function findSourceUpgradeRecord(rootDir, sourceId) {
|
|
233
|
+
const records = await readSourceRecords(rootDir);
|
|
234
|
+
const record = records.find((candidate) => candidate.sourceId === sourceId);
|
|
235
|
+
if (!record) {
|
|
236
|
+
throw new SourceAcquisitionError(
|
|
237
|
+
'unresolved-identity',
|
|
238
|
+
`Source is not registered: ${sourceId}`,
|
|
239
|
+
3
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
return record;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function requireSourceId(request) {
|
|
246
|
+
if (!request || typeof request.sourceId !== 'string' || !request.sourceId.trim()) {
|
|
247
|
+
throw new Error('Source update requires request.sourceId');
|
|
248
|
+
}
|
|
249
|
+
validateSourceId(request.sourceId);
|
|
250
|
+
return request.sourceId;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function assertUpdatePlan(plan) {
|
|
254
|
+
const validStatus = plan?.input?.type === 'git'
|
|
255
|
+
? ['ready', 'current', 'dirty'].includes(plan?.status)
|
|
256
|
+
: plan?.status === 'ready';
|
|
257
|
+
if (!plan || plan.operation !== 'update-source' || !validStatus) {
|
|
258
|
+
throw new Error('A valid update-source plan is required');
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function copiedPlanError(plan) {
|
|
263
|
+
return new Error(
|
|
264
|
+
plan.input?.type === 'git'
|
|
265
|
+
? 'The Git update-source plan must be applied by the process that created it'
|
|
266
|
+
: 'The update-source plan must be applied by the process that created it'
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function assertOriginalPlan(originalFingerprint, plan, stalePlanMessage) {
|
|
271
|
+
assertSameSourceUpgradePlan(
|
|
272
|
+
originalFingerprint,
|
|
273
|
+
plan,
|
|
274
|
+
stalePlanMessage
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function buildUpdateResult(plan, status = plan.status) {
|
|
279
|
+
if (plan.input.type === 'git') {
|
|
280
|
+
return {
|
|
281
|
+
status: status === 'ready' ? 'updated' : status,
|
|
282
|
+
sourceId: plan.sourceId,
|
|
283
|
+
installPath: plan.installPath,
|
|
284
|
+
commit: plan.incomingCommit,
|
|
285
|
+
skills: plan.skills,
|
|
286
|
+
warnings: plan.warnings,
|
|
287
|
+
...(plan.skillChanges ? { skillChanges: plan.skillChanges } : {}),
|
|
288
|
+
changes: plan.changes
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
status: status === 'ready' ? 'updated' : status,
|
|
293
|
+
sourceId: plan.sourceId,
|
|
294
|
+
installPath: plan.installPath,
|
|
295
|
+
input: plan.input,
|
|
296
|
+
...(plan.origin ? { origin: plan.origin } : {}),
|
|
297
|
+
integrity: plan.integrity,
|
|
298
|
+
skills: plan.skills,
|
|
299
|
+
warnings: plan.warnings,
|
|
300
|
+
changes: plan.changes
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// === Private: Inline upgrade adapters ===
|
|
305
|
+
|
|
306
|
+
function createInlineLocalUpgradeAdapter(context, request) {
|
|
307
|
+
const inputPath = requireUpdateInput(request);
|
|
308
|
+
|
|
309
|
+
return defineSourceUpgradeAdapter({
|
|
310
|
+
stalePlanMessage: 'Local source or replacement content changed since the update plan',
|
|
311
|
+
kind: 'directory',
|
|
312
|
+
inspect: async () => projectLocalCandidate(
|
|
313
|
+
await inspectLocalInput(inputPath, { archiveLimits: context?.archiveLimits })
|
|
314
|
+
),
|
|
315
|
+
prepare: async (workspaceRoot) => projectLocalCandidate(
|
|
316
|
+
await stageLocalInput(inputPath, workspaceRoot, { archiveLimits: context?.archiveLimits })
|
|
317
|
+
)
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function createInlineRemoteArchiveUpgradeAdapter(context, request) {
|
|
322
|
+
const parsed = parseRemoteArchiveInput(request?.input);
|
|
323
|
+
|
|
324
|
+
return defineSourceUpgradeAdapter({
|
|
325
|
+
stalePlanMessage: 'Remote Archive changed since the update plan',
|
|
326
|
+
kind: 'directory',
|
|
327
|
+
async inspect() {
|
|
328
|
+
const candidate = await inspectRemoteArchive(
|
|
329
|
+
context,
|
|
330
|
+
parsed,
|
|
331
|
+
'skillcaddy-archive-update-plan-'
|
|
332
|
+
);
|
|
333
|
+
return projectRemoteArchiveCandidate(parsed, candidate);
|
|
334
|
+
},
|
|
335
|
+
async prepare(stagingRoot) {
|
|
336
|
+
const candidate = await stageRemoteArchive(
|
|
337
|
+
context,
|
|
338
|
+
parsed,
|
|
339
|
+
stagingRoot
|
|
340
|
+
);
|
|
341
|
+
return projectRemoteArchiveCandidate(parsed, candidate);
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function createInlineRemoteFileUpgradeAdapter(context, request, record) {
|
|
347
|
+
const parsed = parseRemoteSkillFileInput(
|
|
348
|
+
request?.input ?? record.origin.display
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
return defineSourceUpgradeAdapter({
|
|
352
|
+
stalePlanMessage: 'Remote SKILL.md changed since the update plan',
|
|
353
|
+
kind: 'directory',
|
|
354
|
+
async inspect() {
|
|
355
|
+
const candidate = await inspectRemoteSkillFile(
|
|
356
|
+
context,
|
|
357
|
+
parsed,
|
|
358
|
+
'skillcaddy-remote-file-update-plan-'
|
|
359
|
+
);
|
|
360
|
+
return projectRemoteFileCandidate(parsed, candidate);
|
|
361
|
+
},
|
|
362
|
+
async prepare(stagingRoot) {
|
|
363
|
+
const candidate = await stageRemoteSkillFile(
|
|
364
|
+
context,
|
|
365
|
+
parsed,
|
|
366
|
+
stagingRoot
|
|
367
|
+
);
|
|
368
|
+
return projectRemoteFileCandidate(parsed, candidate);
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// === Private: Projection helpers ===
|
|
374
|
+
|
|
375
|
+
function requireUpdateInput(request) {
|
|
376
|
+
if (typeof request?.input !== 'string' || !request.input.trim()) {
|
|
377
|
+
throw new Error('Source update requires request.input');
|
|
378
|
+
}
|
|
379
|
+
return path.resolve(request.input);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function projectLocalCandidate(prepared) {
|
|
383
|
+
return {
|
|
384
|
+
...(prepared.contentRoot ? { contentRoot: prepared.contentRoot } : {}),
|
|
385
|
+
input: {
|
|
386
|
+
type: prepared.type,
|
|
387
|
+
name: prepared.basename
|
|
388
|
+
},
|
|
389
|
+
integrity: prepared.integrity,
|
|
390
|
+
skills: prepared.skills,
|
|
391
|
+
warnings: prepared.warnings
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function projectRemoteArchiveCandidate(parsed, candidate) {
|
|
396
|
+
return {
|
|
397
|
+
...candidate,
|
|
398
|
+
input: {
|
|
399
|
+
type: 'remote-zip',
|
|
400
|
+
display: parsed.display
|
|
401
|
+
},
|
|
402
|
+
origin: {
|
|
403
|
+
kind: parsed.protocol,
|
|
404
|
+
display: parsed.display
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function projectRemoteFileCandidate(parsed, candidate) {
|
|
410
|
+
return {
|
|
411
|
+
...candidate,
|
|
412
|
+
input: {
|
|
413
|
+
type: 'remote-file',
|
|
414
|
+
display: parsed.display
|
|
415
|
+
},
|
|
416
|
+
origin: {
|
|
417
|
+
kind: parsed.protocol,
|
|
418
|
+
display: parsed.display
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// === Private: Execution phase ===
|
|
424
|
+
|
|
425
|
+
async function runSourceUpgrade(
|
|
426
|
+
context,
|
|
427
|
+
upgrade,
|
|
428
|
+
stalePlanMessage
|
|
429
|
+
) {
|
|
430
|
+
if (upgrade.kind === 'git') {
|
|
431
|
+
return executeGitFastForward(context, upgrade, stalePlanMessage);
|
|
432
|
+
}
|
|
433
|
+
return executeDirectoryReplacement(context, upgrade, stalePlanMessage);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async function executeDirectoryReplacement(
|
|
437
|
+
context,
|
|
438
|
+
upgrade,
|
|
439
|
+
stalePlanMessage
|
|
440
|
+
) {
|
|
441
|
+
const destination = path.resolve(
|
|
442
|
+
context.rootDir,
|
|
443
|
+
upgrade.record.installPath
|
|
444
|
+
);
|
|
445
|
+
await assertActiveSourceMatchesRecord(destination, upgrade.record);
|
|
446
|
+
const workspace = await createManagedSourceWorkspace(
|
|
447
|
+
context.rootDir,
|
|
448
|
+
'update-'
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
try {
|
|
452
|
+
const candidate = await upgrade.adapter.prepare(workspace.root);
|
|
453
|
+
assertPreparedCandidateMatchesPlan(
|
|
454
|
+
candidate,
|
|
455
|
+
upgrade.plan,
|
|
456
|
+
stalePlanMessage
|
|
457
|
+
);
|
|
458
|
+
await assertActiveSourceMatchesRecord(destination, upgrade.record);
|
|
459
|
+
const publication = await prepareDirectorySourceReplacement({
|
|
460
|
+
rootDir: context.rootDir,
|
|
461
|
+
destination,
|
|
462
|
+
stagingRoot: workspace.root,
|
|
463
|
+
preparedContentRoot: candidate.contentRoot,
|
|
464
|
+
nextRecord: buildUpdatedSourceRecord(upgrade.record, upgrade.plan),
|
|
465
|
+
fileOperations: context.fileOperations ||
|
|
466
|
+
defaultSourceUpdateFileOperations
|
|
467
|
+
});
|
|
468
|
+
await runDirectoryReplacement(publication);
|
|
469
|
+
} finally {
|
|
470
|
+
await removeManagedSourceWorkspace(workspace);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
async function executeGitFastForward(context, upgrade, stalePlanMessage) {
|
|
475
|
+
const candidate = await upgrade.adapter.prepare();
|
|
476
|
+
assertPreparedCandidateMatchesPlan(
|
|
477
|
+
candidate,
|
|
478
|
+
upgrade.plan,
|
|
479
|
+
stalePlanMessage
|
|
480
|
+
);
|
|
481
|
+
if (upgrade.plan.status !== 'ready') return;
|
|
482
|
+
const publication = await prepareGitSourceFastForward({
|
|
483
|
+
rootDir: context.rootDir,
|
|
484
|
+
record: upgrade.record,
|
|
485
|
+
nextRecord: buildUpdatedSourceRecord(upgrade.record, upgrade.plan),
|
|
486
|
+
incomingCommit: upgrade.plan.incomingCommit,
|
|
487
|
+
publishRecord: context.publishRecord
|
|
488
|
+
});
|
|
489
|
+
await runGitFastForward(publication);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function assertPreparedCandidateMatchesPlan(
|
|
493
|
+
candidate,
|
|
494
|
+
plan,
|
|
495
|
+
stalePlanMessage
|
|
496
|
+
) {
|
|
497
|
+
assertSameSourceUpgradePlan(
|
|
498
|
+
fingerprintSourceUpgradePlan(plan),
|
|
499
|
+
{
|
|
500
|
+
...plan,
|
|
501
|
+
status: candidate.status || 'ready',
|
|
502
|
+
input: candidate.input,
|
|
503
|
+
...(candidate.origin ? { origin: candidate.origin } : {}),
|
|
504
|
+
...(candidate.integrity ? { integrity: candidate.integrity } : {}),
|
|
505
|
+
...(candidate.currentCommit ? {
|
|
506
|
+
currentCommit: candidate.currentCommit,
|
|
507
|
+
incomingCommit: candidate.incomingCommit
|
|
508
|
+
} : {}),
|
|
509
|
+
skills: candidate.skills,
|
|
510
|
+
warnings: candidate.warnings
|
|
511
|
+
},
|
|
512
|
+
stalePlanMessage
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function buildUpdatedSourceRecord(record, plan) {
|
|
517
|
+
if (record.type === 'git') {
|
|
518
|
+
return {
|
|
519
|
+
...record,
|
|
520
|
+
origin: {
|
|
521
|
+
...record.origin,
|
|
522
|
+
commit: plan.incomingCommit
|
|
523
|
+
},
|
|
524
|
+
skills: plan.skills
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
return {
|
|
528
|
+
...record,
|
|
529
|
+
origin: plan.origin || (
|
|
530
|
+
record.type === 'archive'
|
|
531
|
+
? record.origin
|
|
532
|
+
: {
|
|
533
|
+
kind: 'local',
|
|
534
|
+
name: plan.input.name
|
|
535
|
+
}
|
|
536
|
+
),
|
|
537
|
+
integrity: plan.integrity,
|
|
538
|
+
skills: plan.skills
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
async function runDirectoryReplacement(publication) {
|
|
543
|
+
let phase = 'prepared';
|
|
544
|
+
|
|
545
|
+
try {
|
|
546
|
+
phase = 'committing';
|
|
547
|
+
await commitDirectorySourceReplacement(publication);
|
|
548
|
+
phase = 'committed';
|
|
549
|
+
phase = 'cleanup';
|
|
550
|
+
await cleanupDirectorySourceReplacement(publication);
|
|
551
|
+
} catch (error) {
|
|
552
|
+
if (phase === 'committing') {
|
|
553
|
+
await rollbackBeforeCommit(
|
|
554
|
+
publication,
|
|
555
|
+
error,
|
|
556
|
+
rollbackDirectorySourceReplacement
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
throw error;
|
|
560
|
+
} finally {
|
|
561
|
+
await discardDirectorySourceReplacement(publication);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
async function runGitFastForward(publication) {
|
|
566
|
+
let phase = 'prepared';
|
|
567
|
+
|
|
568
|
+
try {
|
|
569
|
+
phase = 'committing';
|
|
570
|
+
await commitGitSourceFastForward(publication);
|
|
571
|
+
phase = 'committed';
|
|
572
|
+
phase = 'cleanup';
|
|
573
|
+
} catch (error) {
|
|
574
|
+
if (phase === 'committing') {
|
|
575
|
+
await rollbackBeforeCommit(
|
|
576
|
+
publication,
|
|
577
|
+
error,
|
|
578
|
+
rollbackGitSourceFastForward
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
throw error;
|
|
582
|
+
} finally {
|
|
583
|
+
await discardGitSourceFastForward(publication);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
async function rollbackBeforeCommit(publication, originalError, rollback) {
|
|
588
|
+
try {
|
|
589
|
+
await rollback(publication);
|
|
590
|
+
} catch (rollbackError) {
|
|
591
|
+
throw new AggregateError(
|
|
592
|
+
[originalError, rollbackError],
|
|
593
|
+
'Source update failed and rollback could not restore the previous source'
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const REQUIRED_ADAPTER_METHODS = ['inspect', 'prepare'];
|
|
2
|
+
const VALID_KINDS = ['git', 'directory'];
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {object} SourceUpgradeAdapter
|
|
6
|
+
* @property {string} stalePlanMessage
|
|
7
|
+
* @property {'git' | 'directory'} kind
|
|
8
|
+
* @property {() => Promise<object>} inspect
|
|
9
|
+
* @property {(workspaceRoot?: string) => Promise<object>} prepare
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {SourceUpgradeAdapter} adapter
|
|
14
|
+
* @returns {Readonly<SourceUpgradeAdapter>}
|
|
15
|
+
*/
|
|
16
|
+
export function defineSourceUpgradeAdapter(adapter) {
|
|
17
|
+
if (!adapter || typeof adapter !== 'object' || Array.isArray(adapter)) {
|
|
18
|
+
throw new TypeError('Source upgrade adapter must be an object');
|
|
19
|
+
}
|
|
20
|
+
if (
|
|
21
|
+
typeof adapter.stalePlanMessage !== 'string' ||
|
|
22
|
+
!adapter.stalePlanMessage.trim()
|
|
23
|
+
) {
|
|
24
|
+
throw new TypeError(
|
|
25
|
+
'Source upgrade adapter requires stalePlanMessage'
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
if (!VALID_KINDS.includes(adapter.kind)) {
|
|
29
|
+
throw new TypeError(
|
|
30
|
+
`Source upgrade adapter requires kind ('git' or 'directory')`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
for (const method of REQUIRED_ADAPTER_METHODS) {
|
|
34
|
+
if (typeof adapter[method] !== 'function') {
|
|
35
|
+
throw new TypeError(
|
|
36
|
+
`Source upgrade adapter requires ${method}()`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return Object.freeze(adapter);
|
|
41
|
+
}
|