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,168 @@
|
|
|
1
|
+
import { readSourceRecords } from './sourceRegistry.js';
|
|
2
|
+
import {
|
|
3
|
+
applySourceUpgrade,
|
|
4
|
+
planSourceUpgrade
|
|
5
|
+
} from './sourceUpgrade.js';
|
|
6
|
+
|
|
7
|
+
export async function updateRegisteredGitSources(
|
|
8
|
+
context,
|
|
9
|
+
{ allowBreaking = false } = {}
|
|
10
|
+
) {
|
|
11
|
+
const records = await readSourceRecords(context.rootDir);
|
|
12
|
+
const sources = [];
|
|
13
|
+
const networkFailureLimit = normalizeNetworkFailureLimit(
|
|
14
|
+
context.gitNetworkFailureLimit
|
|
15
|
+
);
|
|
16
|
+
let consecutiveNetworkFailures = 0;
|
|
17
|
+
|
|
18
|
+
for (const record of records.filter((candidate) => candidate.type === 'git')) {
|
|
19
|
+
if (consecutiveNetworkFailures >= networkFailureLimit) {
|
|
20
|
+
sources.push(batchResult(
|
|
21
|
+
record.sourceId,
|
|
22
|
+
'failed',
|
|
23
|
+
'git-network-circuit-open',
|
|
24
|
+
false,
|
|
25
|
+
undefined,
|
|
26
|
+
`Skipped after ${networkFailureLimit} consecutive Git network failures`
|
|
27
|
+
));
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const plan = await planSourceUpgrade(context, {
|
|
33
|
+
request: { sourceId: record.sourceId },
|
|
34
|
+
authorization: {
|
|
35
|
+
kind: allowBreaking ? 'breaking' : 'ordinary'
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
const application = await applySourceUpgrade(context, plan);
|
|
39
|
+
if (plan.changes.removedOrRelocated.length > 0) {
|
|
40
|
+
sources.push(batchResult(
|
|
41
|
+
record.sourceId,
|
|
42
|
+
'breaking',
|
|
43
|
+
'breaking-replacement',
|
|
44
|
+
true,
|
|
45
|
+
plan.affectedProjectLinks,
|
|
46
|
+
undefined,
|
|
47
|
+
buildUpdateSummary(plan),
|
|
48
|
+
plan.affectedGlobalLinks,
|
|
49
|
+
plan.affectedHermesLinks
|
|
50
|
+
));
|
|
51
|
+
} else {
|
|
52
|
+
sources.push(
|
|
53
|
+
application.result.status === 'dirty'
|
|
54
|
+
? batchResult(
|
|
55
|
+
record.sourceId,
|
|
56
|
+
'dirty',
|
|
57
|
+
'dirty-worktree',
|
|
58
|
+
false,
|
|
59
|
+
undefined,
|
|
60
|
+
'Local Git changes found; update skipped'
|
|
61
|
+
)
|
|
62
|
+
: batchResult(
|
|
63
|
+
record.sourceId,
|
|
64
|
+
application.result.status,
|
|
65
|
+
undefined,
|
|
66
|
+
undefined,
|
|
67
|
+
undefined,
|
|
68
|
+
undefined,
|
|
69
|
+
application.result.status === 'updated'
|
|
70
|
+
? buildUpdateSummary(plan)
|
|
71
|
+
: undefined
|
|
72
|
+
)
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
consecutiveNetworkFailures = 0;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
consecutiveNetworkFailures = error.category === 'git-network'
|
|
78
|
+
? consecutiveNetworkFailures + 1
|
|
79
|
+
: 0;
|
|
80
|
+
if (error.category === 'breaking-replacement') {
|
|
81
|
+
sources.push(batchResult(
|
|
82
|
+
record.sourceId,
|
|
83
|
+
'breaking',
|
|
84
|
+
'breaking-replacement',
|
|
85
|
+
false,
|
|
86
|
+
allowBreaking ? [] : error.affectedProjectLinks,
|
|
87
|
+
undefined,
|
|
88
|
+
undefined,
|
|
89
|
+
allowBreaking ? [] : error.affectedGlobalLinks,
|
|
90
|
+
allowBreaking ? [] : error.affectedHermesLinks
|
|
91
|
+
));
|
|
92
|
+
} else {
|
|
93
|
+
sources.push(batchResult(
|
|
94
|
+
record.sourceId,
|
|
95
|
+
'failed',
|
|
96
|
+
error.category || 'failure',
|
|
97
|
+
false,
|
|
98
|
+
undefined,
|
|
99
|
+
error.message || 'Git source update failed'
|
|
100
|
+
));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { sources };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function normalizeNetworkFailureLimit(value) {
|
|
109
|
+
if (value === undefined) return 3;
|
|
110
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
111
|
+
throw new TypeError('gitNetworkFailureLimit must be a positive integer');
|
|
112
|
+
}
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function batchResult(
|
|
117
|
+
sourceId,
|
|
118
|
+
status,
|
|
119
|
+
category,
|
|
120
|
+
applied,
|
|
121
|
+
affected,
|
|
122
|
+
message,
|
|
123
|
+
updateSummary,
|
|
124
|
+
affectedGlobalLinks,
|
|
125
|
+
affectedHermesLinks
|
|
126
|
+
) {
|
|
127
|
+
return {
|
|
128
|
+
sourceId,
|
|
129
|
+
status,
|
|
130
|
+
...(category ? { category } : {}),
|
|
131
|
+
...(applied === undefined ? {} : { applied }),
|
|
132
|
+
...(message ? { message } : {}),
|
|
133
|
+
...(updateSummary ? { updateSummary } : {}),
|
|
134
|
+
...(Array.isArray(affected) && affected.length > 0
|
|
135
|
+
? {
|
|
136
|
+
affected: affected.map((link) => ({
|
|
137
|
+
alias: link.alias,
|
|
138
|
+
skillPath: link.skillPath
|
|
139
|
+
}))
|
|
140
|
+
}
|
|
141
|
+
: {}),
|
|
142
|
+
...(Array.isArray(affectedGlobalLinks) && affectedGlobalLinks.length > 0
|
|
143
|
+
? {
|
|
144
|
+
affectedGlobalLinks: affectedGlobalLinks.map((link) => ({
|
|
145
|
+
alias: link.alias,
|
|
146
|
+
skillPath: link.skillPath
|
|
147
|
+
}))
|
|
148
|
+
}
|
|
149
|
+
: {}),
|
|
150
|
+
...(Array.isArray(affectedHermesLinks) && affectedHermesLinks.length > 0
|
|
151
|
+
? {
|
|
152
|
+
affectedHermesLinks: affectedHermesLinks.map((link) => ({
|
|
153
|
+
alias: link.alias,
|
|
154
|
+
skillPath: link.skillPath
|
|
155
|
+
}))
|
|
156
|
+
}
|
|
157
|
+
: {})
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function buildUpdateSummary(plan) {
|
|
162
|
+
if (!plan.skillChanges) return undefined;
|
|
163
|
+
return {
|
|
164
|
+
fromCommit: plan.currentCommit,
|
|
165
|
+
toCommit: plan.incomingCommit,
|
|
166
|
+
skillChanges: plan.skillChanges
|
|
167
|
+
};
|
|
168
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { SourceAcquisitionError } from './sourceAcquisitionError.js';
|
|
3
|
+
import { canonicalJson } from './canonicalJson.js';
|
|
4
|
+
import { inspectGitRegistryRepair } from './sourceGitUpgradeAdapter.js';
|
|
5
|
+
import {
|
|
6
|
+
assertNoAffectedLinks,
|
|
7
|
+
classifySkillChanges,
|
|
8
|
+
findAffectedGlobalLinks,
|
|
9
|
+
findAffectedHermesLinks,
|
|
10
|
+
findAffectedProjectLinks
|
|
11
|
+
} from './sourceUpgradePolicy.js';
|
|
12
|
+
import { readSourceRecords, writeSourceRecord } from './sourceRegistry.js';
|
|
13
|
+
import { validateSourceId } from './sourceRecord.js';
|
|
14
|
+
|
|
15
|
+
const repairPlans = new WeakMap();
|
|
16
|
+
|
|
17
|
+
export async function planGitSourceRepair(context, command) {
|
|
18
|
+
const authorization = requireAuthorization(command?.authorization);
|
|
19
|
+
const prepared = await buildRepairPlan(context, command?.request);
|
|
20
|
+
authorization.enforce(prepared.plan);
|
|
21
|
+
repairPlans.set(prepared.plan, {
|
|
22
|
+
authorization,
|
|
23
|
+
originalFingerprint: fingerprintRepairPlan(prepared.plan),
|
|
24
|
+
request: { sourceId: prepared.plan.sourceId }
|
|
25
|
+
});
|
|
26
|
+
return prepared.plan;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function applyGitSourceRepair(context, plan) {
|
|
30
|
+
const session = repairPlans.get(plan);
|
|
31
|
+
if (!session) {
|
|
32
|
+
assertRepairPlan(plan);
|
|
33
|
+
throw new Error(
|
|
34
|
+
'The Git registry repair plan must be applied by the process that created it'
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
assertSameRepairPlan(
|
|
39
|
+
session.originalFingerprint,
|
|
40
|
+
plan,
|
|
41
|
+
'Git source changed since the registry repair plan'
|
|
42
|
+
);
|
|
43
|
+
assertRepairPlan(plan);
|
|
44
|
+
|
|
45
|
+
const replanned = await buildRepairPlan(context, session.request);
|
|
46
|
+
session.authorization.enforce(replanned.plan);
|
|
47
|
+
assertSameRepairPlan(
|
|
48
|
+
session.originalFingerprint,
|
|
49
|
+
replanned.plan,
|
|
50
|
+
'Git source changed since the registry repair plan'
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
if (replanned.plan.status !== 'ready') {
|
|
54
|
+
return buildRepairResult(replanned.plan);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const rootDir = requireRootDir(context);
|
|
58
|
+
const record = replanned.record;
|
|
59
|
+
await writeSourceRecord(rootDir, {
|
|
60
|
+
...record,
|
|
61
|
+
origin: {
|
|
62
|
+
...record.origin,
|
|
63
|
+
commit: replanned.plan.currentCommit
|
|
64
|
+
},
|
|
65
|
+
skills: replanned.plan.skills
|
|
66
|
+
});
|
|
67
|
+
return buildRepairResult(replanned.plan, 'repaired');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function buildRepairPlan(context, request) {
|
|
71
|
+
const rootDir = requireRootDir(context);
|
|
72
|
+
const projectPath = requireProjectPath(context);
|
|
73
|
+
const sourceId = requireSourceId(request);
|
|
74
|
+
const record = await findGitRecord(rootDir, sourceId);
|
|
75
|
+
const candidate = await inspectGitRegistryRepair(context, record);
|
|
76
|
+
const changes = classifySkillChanges(record.skills, candidate.skills);
|
|
77
|
+
const affectedProjectLinks = candidate.status === 'dirty'
|
|
78
|
+
? []
|
|
79
|
+
: await findAffectedProjectLinks({
|
|
80
|
+
rootDir,
|
|
81
|
+
projectPath,
|
|
82
|
+
installPath: record.installPath,
|
|
83
|
+
removedSkillPaths: changes.removedOrRelocated
|
|
84
|
+
});
|
|
85
|
+
const affectedGlobalLinks = candidate.status === 'dirty'
|
|
86
|
+
? []
|
|
87
|
+
: await findAffectedGlobalLinks({
|
|
88
|
+
rootDir,
|
|
89
|
+
globalDir: context.globalDir,
|
|
90
|
+
installPath: record.installPath,
|
|
91
|
+
removedSkillPaths: changes.removedOrRelocated
|
|
92
|
+
});
|
|
93
|
+
const affectedHermesLinks = candidate.status === 'dirty'
|
|
94
|
+
? []
|
|
95
|
+
: await findAffectedHermesLinks({
|
|
96
|
+
rootDir,
|
|
97
|
+
hermesDir: context.hermesDir,
|
|
98
|
+
installPath: record.installPath,
|
|
99
|
+
removedSkillPaths: changes.removedOrRelocated
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
record,
|
|
104
|
+
plan: {
|
|
105
|
+
operation: 'repair-source-registry',
|
|
106
|
+
status: candidate.status,
|
|
107
|
+
sourceId,
|
|
108
|
+
installPath: record.installPath,
|
|
109
|
+
input: {
|
|
110
|
+
type: 'git',
|
|
111
|
+
remote: record.origin.remote
|
|
112
|
+
},
|
|
113
|
+
registeredCommit: record.origin.commit,
|
|
114
|
+
currentCommit: candidate.currentCommit,
|
|
115
|
+
skills: candidate.skills,
|
|
116
|
+
warnings: candidate.warnings,
|
|
117
|
+
changes,
|
|
118
|
+
affectedProjectLinks,
|
|
119
|
+
...(affectedGlobalLinks.length > 0 ? { affectedGlobalLinks } : {}),
|
|
120
|
+
...(affectedHermesLinks.length > 0 ? { affectedHermesLinks } : {})
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function findGitRecord(rootDir, sourceId) {
|
|
126
|
+
const records = await readSourceRecords(rootDir);
|
|
127
|
+
const record = records.find((candidate) => candidate.sourceId === sourceId);
|
|
128
|
+
if (!record) {
|
|
129
|
+
throw new SourceAcquisitionError(
|
|
130
|
+
'unresolved-identity',
|
|
131
|
+
`Source is not registered: ${sourceId}`,
|
|
132
|
+
3
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
if (record.type !== 'git') {
|
|
136
|
+
throw new SourceAcquisitionError(
|
|
137
|
+
'unsupported-source',
|
|
138
|
+
`Git registry repair requires a Git source: ${sourceId}`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
return record;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function buildRepairResult(plan, status = plan.status) {
|
|
145
|
+
return {
|
|
146
|
+
status: status === 'ready' ? 'repaired' : status,
|
|
147
|
+
sourceId: plan.sourceId,
|
|
148
|
+
installPath: plan.installPath,
|
|
149
|
+
commit: plan.currentCommit,
|
|
150
|
+
skills: plan.skills,
|
|
151
|
+
warnings: plan.warnings,
|
|
152
|
+
changes: plan.changes
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function requireAuthorization(authorization) {
|
|
157
|
+
if (authorization?.kind === 'breaking') return { enforce() {} };
|
|
158
|
+
if (authorization?.kind === 'ordinary') {
|
|
159
|
+
return {
|
|
160
|
+
enforce: (plan) => assertNoAffectedLinks(
|
|
161
|
+
plan.affectedProjectLinks,
|
|
162
|
+
plan.affectedGlobalLinks,
|
|
163
|
+
plan.affectedHermesLinks
|
|
164
|
+
)
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
throw new Error('Git registry repair requires an authorization policy');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function requireRootDir(context) {
|
|
171
|
+
if (!context || typeof context.rootDir !== 'string' || !context.rootDir.trim()) {
|
|
172
|
+
throw new Error('Source management requires context.rootDir');
|
|
173
|
+
}
|
|
174
|
+
return path.resolve(context.rootDir);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function requireProjectPath(context) {
|
|
178
|
+
if (!context || typeof context.projectPath !== 'string' || !context.projectPath.trim()) {
|
|
179
|
+
throw new Error('Git registry repair requires context.projectPath');
|
|
180
|
+
}
|
|
181
|
+
return path.resolve(context.projectPath);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function requireSourceId(request) {
|
|
185
|
+
if (!request || typeof request.sourceId !== 'string' || !request.sourceId.trim()) {
|
|
186
|
+
throw new Error('Git registry repair requires request.sourceId');
|
|
187
|
+
}
|
|
188
|
+
if (request.input !== undefined) {
|
|
189
|
+
throw new Error('Git registry repair does not accept replacement input');
|
|
190
|
+
}
|
|
191
|
+
validateSourceId(request.sourceId);
|
|
192
|
+
return request.sourceId;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function assertRepairPlan(plan) {
|
|
196
|
+
if (
|
|
197
|
+
!plan ||
|
|
198
|
+
plan.operation !== 'repair-source-registry' ||
|
|
199
|
+
!['ready', 'current', 'dirty'].includes(plan.status)
|
|
200
|
+
) {
|
|
201
|
+
throw new Error('A valid Git registry repair plan is required');
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function fingerprintRepairPlan(plan) {
|
|
206
|
+
return canonicalJson({
|
|
207
|
+
operation: plan.operation,
|
|
208
|
+
status: plan.status,
|
|
209
|
+
sourceId: plan.sourceId,
|
|
210
|
+
installPath: plan.installPath,
|
|
211
|
+
input: plan.input,
|
|
212
|
+
registeredCommit: plan.registeredCommit,
|
|
213
|
+
currentCommit: plan.currentCommit,
|
|
214
|
+
skills: plan.skills,
|
|
215
|
+
warnings: plan.warnings,
|
|
216
|
+
changes: plan.changes,
|
|
217
|
+
affectedProjectLinks: plan.affectedProjectLinks,
|
|
218
|
+
affectedGlobalLinks: plan.affectedGlobalLinks,
|
|
219
|
+
affectedHermesLinks: plan.affectedHermesLinks
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function assertSameRepairPlan(originalFingerprint, plan, message) {
|
|
224
|
+
if (originalFingerprint !== fingerprintRepairPlan(plan)) {
|
|
225
|
+
throw new SourceAcquisitionError('stale-plan', message);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { execFile as execFileCallback } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
import { SourceAcquisitionError } from './sourceAcquisitionError.js';
|
|
5
|
+
import { SOURCE_INSTALLING_MARKER } from './sourcePolicy.js';
|
|
6
|
+
import { sourcePathExists, validateSourceTree } from './sourceTree.js';
|
|
7
|
+
import { validateStagedSource } from './sourceValidation.js';
|
|
8
|
+
|
|
9
|
+
const execFile = promisify(execFileCallback);
|
|
10
|
+
|
|
11
|
+
export async function cloneAndInspectGitSource(stagingRoot, parsed) {
|
|
12
|
+
const contentRoot = path.join(stagingRoot, 'repository');
|
|
13
|
+
const cloneArguments = ['clone', '--', parsed.cloneRemote, contentRoot];
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
await execFile('git', cloneArguments);
|
|
17
|
+
} catch (error) {
|
|
18
|
+
throw gitError(`Could not clone Git source: ${safeGitError(error)}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const focus = parsed.treeSegments
|
|
23
|
+
? await checkoutGitHubTreeRef(contentRoot, parsed.treeSegments)
|
|
24
|
+
: undefined;
|
|
25
|
+
if (await sourcePathExists(path.join(contentRoot, SOURCE_INSTALLING_MARKER))) {
|
|
26
|
+
throw new SourceAcquisitionError(
|
|
27
|
+
'source-validation',
|
|
28
|
+
`Git source contains reserved publication marker: ${SOURCE_INSTALLING_MARKER}`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
await validateSourceTree(stagingRoot, contentRoot);
|
|
32
|
+
const validated = await validateStagedSource(contentRoot);
|
|
33
|
+
assertFocusContainsSkill(validated.skills, focus);
|
|
34
|
+
const [{ stdout: commitOutput }, ref] = await Promise.all([
|
|
35
|
+
execFile('git', ['-C', contentRoot, 'rev-parse', 'HEAD']),
|
|
36
|
+
readCheckedOutRef(contentRoot)
|
|
37
|
+
]);
|
|
38
|
+
return {
|
|
39
|
+
contentRoot,
|
|
40
|
+
commit: commitOutput.trim(),
|
|
41
|
+
ref,
|
|
42
|
+
...(focus ? { focus } : {}),
|
|
43
|
+
skills: validated.skills,
|
|
44
|
+
warnings: validated.warnings
|
|
45
|
+
};
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (error instanceof SourceAcquisitionError) throw error;
|
|
48
|
+
throw gitError(error.message);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function checkoutGitHubTreeRef(sourcePath, treeSegments) {
|
|
53
|
+
const { stdout } = await execFile('git', [
|
|
54
|
+
'-C',
|
|
55
|
+
sourcePath,
|
|
56
|
+
'for-each-ref',
|
|
57
|
+
'--format=%(refname:strip=3)',
|
|
58
|
+
'refs/remotes/origin'
|
|
59
|
+
]);
|
|
60
|
+
const branches = stdout
|
|
61
|
+
.split('\n')
|
|
62
|
+
.map((branch) => branch.trim())
|
|
63
|
+
.filter((branch) => branch && branch !== 'HEAD')
|
|
64
|
+
.filter((branch) => {
|
|
65
|
+
const branchSegments = branch.split('/');
|
|
66
|
+
return branchSegments.every((segment, index) => treeSegments[index] === segment);
|
|
67
|
+
})
|
|
68
|
+
.sort((left, right) => right.split('/').length - left.split('/').length);
|
|
69
|
+
const ref = branches[0];
|
|
70
|
+
if (!ref) {
|
|
71
|
+
throw gitError('GitHub tree URL does not match a repository branch');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
await execFile('git', [
|
|
75
|
+
'-C',
|
|
76
|
+
sourcePath,
|
|
77
|
+
'checkout',
|
|
78
|
+
'--quiet',
|
|
79
|
+
'-B',
|
|
80
|
+
ref,
|
|
81
|
+
`origin/${ref}`
|
|
82
|
+
]);
|
|
83
|
+
const focusSegments = treeSegments.slice(ref.split('/').length);
|
|
84
|
+
return {
|
|
85
|
+
ref,
|
|
86
|
+
...(focusSegments.length > 0 ? { path: focusSegments.join('/') } : {})
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function prioritizeFocusedSkills(skills, focus) {
|
|
91
|
+
if (!focus?.path) return skills;
|
|
92
|
+
return [...skills].sort((left, right) => {
|
|
93
|
+
const leftFocused = left === focus.path || left.startsWith(`${focus.path}/`);
|
|
94
|
+
const rightFocused = right === focus.path || right.startsWith(`${focus.path}/`);
|
|
95
|
+
return Number(rightFocused) - Number(leftFocused) || left.localeCompare(right);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function readCheckedOutRef(sourcePath) {
|
|
100
|
+
try {
|
|
101
|
+
const { stdout } = await execFile('git', [
|
|
102
|
+
'-C',
|
|
103
|
+
sourcePath,
|
|
104
|
+
'symbolic-ref',
|
|
105
|
+
'--quiet',
|
|
106
|
+
'--short',
|
|
107
|
+
'HEAD'
|
|
108
|
+
]);
|
|
109
|
+
return stdout.trim();
|
|
110
|
+
} catch {
|
|
111
|
+
throw gitError('Git source must check out a branch');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function assertFocusContainsSkill(skills, focus) {
|
|
116
|
+
if (!focus?.path) return;
|
|
117
|
+
const containsSkill = skills.some(
|
|
118
|
+
(skillPath) => skillPath === focus.path || skillPath.startsWith(`${focus.path}/`)
|
|
119
|
+
);
|
|
120
|
+
if (!containsSkill) {
|
|
121
|
+
throw new SourceAcquisitionError(
|
|
122
|
+
'source-validation',
|
|
123
|
+
`GitHub source focus contains no scanner-visible skill: ${focus.path}`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function safeGitError(error) {
|
|
129
|
+
const message = typeof error?.stderr === 'string' && error.stderr.trim()
|
|
130
|
+
? error.stderr.trim()
|
|
131
|
+
: error.message;
|
|
132
|
+
return String(message)
|
|
133
|
+
.replace(/(?:https|ssh):\/\/[^\s'"]+/gi, '[sanitized Git URL]')
|
|
134
|
+
.replace(/(?:[^\s@'"]+@)?[^\s:/'"]+:[^\s'"]+/g, '[sanitized Git URL]');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function gitError(message) {
|
|
138
|
+
return new SourceAcquisitionError('git-acquisition', message);
|
|
139
|
+
}
|