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,109 @@
|
|
|
1
|
+
import { access, lstat, mkdir, readlink, symlink } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { normalizeRoots } from './roots.js';
|
|
5
|
+
|
|
6
|
+
const MANAGER_SKILL_NAME = 'skillcaddy-manager';
|
|
7
|
+
|
|
8
|
+
export function getManagerInstallPaths(roots, options = {}) {
|
|
9
|
+
const homeDir = options.homeDir || homedir();
|
|
10
|
+
const { codeRoot } = normalizeRoots(roots);
|
|
11
|
+
return {
|
|
12
|
+
sourcePath: path.join(codeRoot, 'skills', MANAGER_SKILL_NAME),
|
|
13
|
+
targetPath: path.join(path.resolve(homeDir), '.agents', 'skills', MANAGER_SKILL_NAME)
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function checkManagerInstall(rootDir, options = {}) {
|
|
18
|
+
const paths = getManagerInstallPaths(rootDir, options);
|
|
19
|
+
const sourceOk = await isSkillDirectory(paths.sourcePath);
|
|
20
|
+
if (!sourceOk) {
|
|
21
|
+
return {
|
|
22
|
+
ok: false,
|
|
23
|
+
status: 'source-missing',
|
|
24
|
+
...paths,
|
|
25
|
+
message: `manager skill source is missing: ${paths.sourcePath}`
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const target = await safeLstat(paths.targetPath);
|
|
30
|
+
if (!target) {
|
|
31
|
+
return {
|
|
32
|
+
ok: false,
|
|
33
|
+
status: 'missing',
|
|
34
|
+
...paths,
|
|
35
|
+
message: `manager skill is not installed: ${paths.targetPath}`
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!target.isSymbolicLink()) {
|
|
40
|
+
return {
|
|
41
|
+
ok: false,
|
|
42
|
+
status: 'conflict',
|
|
43
|
+
...paths,
|
|
44
|
+
message: `target exists but is not a symlink: ${paths.targetPath}`
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const linkTarget = await readlink(paths.targetPath);
|
|
49
|
+
const resolvedTarget = path.resolve(path.dirname(paths.targetPath), linkTarget);
|
|
50
|
+
if (resolvedTarget !== paths.sourcePath) {
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
status: 'conflict',
|
|
54
|
+
...paths,
|
|
55
|
+
linkTarget: resolvedTarget,
|
|
56
|
+
message: `target symlink points elsewhere: ${resolvedTarget}`
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
ok: true,
|
|
62
|
+
status: 'installed',
|
|
63
|
+
...paths,
|
|
64
|
+
linkTarget: resolvedTarget,
|
|
65
|
+
message: `manager skill is installed: ${paths.targetPath}`
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function installManagerSkill(rootDir, options = {}) {
|
|
70
|
+
const current = await checkManagerInstall(rootDir, options);
|
|
71
|
+
if (current.status === 'installed') {
|
|
72
|
+
return { ...current, unchanged: true };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (current.status !== 'missing') {
|
|
76
|
+
throw new Error(current.message);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
await mkdir(path.dirname(current.targetPath), { recursive: true });
|
|
80
|
+
await symlink(current.sourcePath, current.targetPath, 'dir');
|
|
81
|
+
|
|
82
|
+
const installed = await checkManagerInstall(rootDir, options);
|
|
83
|
+
if (!installed.ok) {
|
|
84
|
+
throw new Error(installed.message);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { ...installed, unchanged: false };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function isSkillDirectory(skillPath) {
|
|
91
|
+
const stat = await safeLstat(skillPath);
|
|
92
|
+
if (!stat?.isDirectory()) return false;
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
await access(path.join(skillPath, 'SKILL.md'));
|
|
96
|
+
return true;
|
|
97
|
+
} catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function safeLstat(targetPath) {
|
|
103
|
+
try {
|
|
104
|
+
return await lstat(targetPath);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (error?.code === 'ENOENT') return null;
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { buildSkillEnablePlan } from './enablePlan.js';
|
|
3
|
+
import { enableProjectSkill } from './projectActions.js';
|
|
4
|
+
import {
|
|
5
|
+
applyAddSource,
|
|
6
|
+
applyUpdateSource,
|
|
7
|
+
listSources,
|
|
8
|
+
planAddSource,
|
|
9
|
+
planBreakingUpdateSource,
|
|
10
|
+
planUpdateSource
|
|
11
|
+
} from './sourceManager.js';
|
|
12
|
+
import { getState } from './skillStore.js';
|
|
13
|
+
import { normalizeEnablementScope } from './enablementScope.js';
|
|
14
|
+
import { normalizeRoots } from './roots.js';
|
|
15
|
+
|
|
16
|
+
const workflowPlans = new WeakMap();
|
|
17
|
+
|
|
18
|
+
export async function planManagerSourceWorkflow(context, request) {
|
|
19
|
+
const acquisitionRequest = request?.acquisition || null;
|
|
20
|
+
const enablementRequest = request?.enablement || null;
|
|
21
|
+
if (!acquisitionRequest && !enablementRequest) {
|
|
22
|
+
throw new Error('Manager workflow requires acquisition or enablement intent');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const acquisition = acquisitionRequest
|
|
26
|
+
? await planAcquisition(context, acquisitionRequest)
|
|
27
|
+
: null;
|
|
28
|
+
const enablement = enablementRequest
|
|
29
|
+
? await planEnablement(context, acquisition, enablementRequest)
|
|
30
|
+
: null;
|
|
31
|
+
const plan = {
|
|
32
|
+
operation: 'manager-source-workflow',
|
|
33
|
+
acquisition: acquisition && {
|
|
34
|
+
action: acquisition.action,
|
|
35
|
+
plan: acquisition.plan
|
|
36
|
+
},
|
|
37
|
+
enablement
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
workflowPlans.set(plan, {
|
|
41
|
+
acquisition,
|
|
42
|
+
enablementRequest
|
|
43
|
+
});
|
|
44
|
+
return plan;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function applyManagerSourceWorkflow(context, plan) {
|
|
48
|
+
const privatePlan = workflowPlans.get(plan);
|
|
49
|
+
if (!privatePlan) {
|
|
50
|
+
throw new Error('The Manager workflow plan must be applied by the process that created it');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const acquisition = await applyAcquisition(context, privatePlan.acquisition);
|
|
54
|
+
const enablement = privatePlan.enablementRequest
|
|
55
|
+
? await applyEnablement(
|
|
56
|
+
context,
|
|
57
|
+
plan.acquisition?.plan || null,
|
|
58
|
+
privatePlan.enablementRequest
|
|
59
|
+
)
|
|
60
|
+
: null;
|
|
61
|
+
|
|
62
|
+
return { acquisition, enablement };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function planAcquisition(context, request) {
|
|
66
|
+
const registeredSource = request.sourceId
|
|
67
|
+
? await findRegisteredSource(context, request.sourceId)
|
|
68
|
+
: null;
|
|
69
|
+
|
|
70
|
+
if (request.sourceId && !registeredSource) {
|
|
71
|
+
throw new Error(`Source is not registered: ${request.sourceId}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (registeredSource) {
|
|
75
|
+
return planRegisteredUpdate(context, request, registeredSource.sourceId);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const addRequest = {
|
|
79
|
+
input: request.input,
|
|
80
|
+
...(request.name === undefined ? {} : { name: request.name }),
|
|
81
|
+
...(request.namespace === undefined ? {} : { namespace: request.namespace })
|
|
82
|
+
};
|
|
83
|
+
try {
|
|
84
|
+
return {
|
|
85
|
+
action: 'add',
|
|
86
|
+
plan: await planAddSource(context, addRequest)
|
|
87
|
+
};
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (!error.registeredSourceId) throw error;
|
|
90
|
+
return planRegisteredUpdate(context, request, error.registeredSourceId);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function planRegisteredUpdate(context, request, sourceId) {
|
|
95
|
+
const planUpdate = request.allowBreaking
|
|
96
|
+
? planBreakingUpdateSource
|
|
97
|
+
: planUpdateSource;
|
|
98
|
+
return {
|
|
99
|
+
action: 'update',
|
|
100
|
+
plan: await planUpdate(context, {
|
|
101
|
+
sourceId,
|
|
102
|
+
...(request.input === undefined ? {} : { input: request.input })
|
|
103
|
+
})
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function findRegisteredSource(context, sourceId) {
|
|
108
|
+
const inventory = await listSources(context);
|
|
109
|
+
return inventory.sources.find(
|
|
110
|
+
(source) => source.status === 'registered' && source.sourceId === sourceId
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Manager workflow 的两个根。
|
|
116
|
+
*
|
|
117
|
+
* `context.rootDir` 是原件库根(source 获取只写这里);`context.roots` 可选,
|
|
118
|
+
* 用来把代码根一并带进来 —— 仓库自带 skills 与合集初始化契约都在代码根侧。
|
|
119
|
+
*/
|
|
120
|
+
function resolveWorkflowRoots(context) {
|
|
121
|
+
if (context.roots && typeof context.roots === 'object') return normalizeRoots(context.roots);
|
|
122
|
+
return normalizeRoots(context.rootDir);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function planEnablement(context, acquisition, request) {
|
|
126
|
+
const roots = resolveWorkflowRoots(context);
|
|
127
|
+
const scope = normalizeEnablementScope(request.scope);
|
|
128
|
+
if (acquisition) {
|
|
129
|
+
const skillId = resolveAcquiredSkillId(acquisition.plan, request);
|
|
130
|
+
return {
|
|
131
|
+
skillId,
|
|
132
|
+
alias: request.alias || path.posix.basename(skillId),
|
|
133
|
+
scope,
|
|
134
|
+
status: 'pending-acquisition',
|
|
135
|
+
reminders: []
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (typeof request.skillId !== 'string' || !request.skillId.trim()) {
|
|
140
|
+
throw new Error('Enablement-only Manager requests require enablement.skillId');
|
|
141
|
+
}
|
|
142
|
+
const state = await getState(
|
|
143
|
+
roots,
|
|
144
|
+
scope === 'project' ? requireProjectPath(context) : context.projectPath || roots.libraryRoot,
|
|
145
|
+
{ globalDir: context.globalDir, hermesDir: context.hermesDir }
|
|
146
|
+
);
|
|
147
|
+
return buildSkillEnablePlan(state, request.skillId, request.alias, scope);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function applyAcquisition(context, acquisition) {
|
|
151
|
+
if (!acquisition) return null;
|
|
152
|
+
if (acquisition.action === 'add') {
|
|
153
|
+
return applyAddSource(context, acquisition.plan);
|
|
154
|
+
}
|
|
155
|
+
return applyUpdateSource(context, acquisition.plan);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function applyEnablement(context, acquisitionPlan, request) {
|
|
159
|
+
const roots = resolveWorkflowRoots(context);
|
|
160
|
+
const scope = normalizeEnablementScope(request.scope);
|
|
161
|
+
const state = await getState(
|
|
162
|
+
roots,
|
|
163
|
+
scope === 'project' ? requireProjectPath(context) : context.projectPath || roots.libraryRoot,
|
|
164
|
+
{ globalDir: context.globalDir, hermesDir: context.hermesDir }
|
|
165
|
+
);
|
|
166
|
+
const skillId = acquisitionPlan
|
|
167
|
+
? resolveAcquiredSkillId(acquisitionPlan, request)
|
|
168
|
+
: request.skillId;
|
|
169
|
+
const enablementPlan = buildSkillEnablePlan(state, skillId, request.alias, scope);
|
|
170
|
+
const result = await enableProjectSkill(roots, {
|
|
171
|
+
...(scope === 'project' ? { projectPath: context.projectPath } : {}),
|
|
172
|
+
scope,
|
|
173
|
+
...(context.globalDir ? { globalDir: context.globalDir } : {}),
|
|
174
|
+
...(context.hermesDir ? { hermesDir: context.hermesDir } : {}),
|
|
175
|
+
skillPath: enablementPlan.skillPath,
|
|
176
|
+
alias: enablementPlan.alias
|
|
177
|
+
});
|
|
178
|
+
const verifiedState = await getState(
|
|
179
|
+
roots,
|
|
180
|
+
scope === 'project' ? context.projectPath : context.projectPath || roots.libraryRoot,
|
|
181
|
+
{ globalDir: context.globalDir, hermesDir: context.hermesDir }
|
|
182
|
+
);
|
|
183
|
+
const verifiedPlan = buildSkillEnablePlan(
|
|
184
|
+
verifiedState,
|
|
185
|
+
skillId,
|
|
186
|
+
enablementPlan.alias,
|
|
187
|
+
scope
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
...result,
|
|
192
|
+
skillId,
|
|
193
|
+
reminders: verifiedPlan.reminders
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function resolveAcquiredSkillId(acquisitionPlan, request) {
|
|
198
|
+
const availableIds = new Map(
|
|
199
|
+
acquisitionPlan.skills.map((sourceSkillPath) => [
|
|
200
|
+
path.posix.join(acquisitionPlan.installPath, sourceSkillPath),
|
|
201
|
+
sourceSkillPath
|
|
202
|
+
])
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
if (request.skillId) {
|
|
206
|
+
if (!availableIds.has(request.skillId)) {
|
|
207
|
+
throw new Error(
|
|
208
|
+
`Selected skill is not part of the acquired source: ${request.skillId}`
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
return request.skillId;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (
|
|
215
|
+
typeof request.sourceSkillPath !== 'string' ||
|
|
216
|
+
!acquisitionPlan.skills.includes(request.sourceSkillPath)
|
|
217
|
+
) {
|
|
218
|
+
throw new Error(
|
|
219
|
+
`Select one acquired skill: ${[...availableIds.keys()].join(', ')}`
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
return path.posix.join(acquisitionPlan.installPath, request.sourceSkillPath);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function requireProjectPath(context) {
|
|
226
|
+
if (
|
|
227
|
+
!context ||
|
|
228
|
+
typeof context.projectPath !== 'string' ||
|
|
229
|
+
!context.projectPath.trim()
|
|
230
|
+
) {
|
|
231
|
+
throw new Error('Manager enablement requires context.projectPath');
|
|
232
|
+
}
|
|
233
|
+
return context.projectPath;
|
|
234
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { syncClaudeSkills } from './claudeStore.js';
|
|
2
|
+
import { enableSkill } from './skillStore.js';
|
|
3
|
+
|
|
4
|
+
export async function enableProjectSkill(roots, input) {
|
|
5
|
+
const result = await enableSkill(roots, input);
|
|
6
|
+
|
|
7
|
+
if (result.scope !== 'project') return result;
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
...result,
|
|
11
|
+
claudeSync: await syncClaudeBestEffort(input.projectPath)
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function syncClaudeBestEffort(projectPath) {
|
|
16
|
+
try {
|
|
17
|
+
return await syncClaudeSkills(projectPath);
|
|
18
|
+
} catch (error) {
|
|
19
|
+
return {
|
|
20
|
+
ok: false,
|
|
21
|
+
error: error.message || 'Claude Code 同步失败'
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const SYSTEM_PROJECT_PATHS = new Set([
|
|
5
|
+
path.parse(process.cwd()).root,
|
|
6
|
+
'/Applications',
|
|
7
|
+
'/Library',
|
|
8
|
+
'/System',
|
|
9
|
+
'/bin',
|
|
10
|
+
'/etc',
|
|
11
|
+
'/opt',
|
|
12
|
+
'/private',
|
|
13
|
+
'/sbin',
|
|
14
|
+
'/tmp',
|
|
15
|
+
'/usr',
|
|
16
|
+
'/var'
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
export function normalizeProjectPath(value) {
|
|
20
|
+
const projectPath = requirePath(value, 'projectPath');
|
|
21
|
+
assertSafeProjectPath(projectPath);
|
|
22
|
+
return projectPath;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function requirePath(value, label) {
|
|
26
|
+
if (!value || typeof value !== 'string') {
|
|
27
|
+
throw new Error(`${label} 不能为空`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return path.resolve(value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function assertSafeProjectPath(projectPath) {
|
|
34
|
+
if (SYSTEM_PROJECT_PATHS.has(projectPath)) {
|
|
35
|
+
throw new Error(`projectPath 不能是系统目录:${projectPath}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const homePath = path.resolve(homedir());
|
|
39
|
+
if (projectPath === homePath) {
|
|
40
|
+
throw new Error(`projectPath 不能是用户主目录:${projectPath}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (isAncestor(projectPath, homePath)) {
|
|
44
|
+
throw new Error(`projectPath 不能是用户主目录的父目录:${projectPath}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isAncestor(parentPath, childPath) {
|
|
49
|
+
const relative = path.relative(parentPath, childPath);
|
|
50
|
+
return relative && !relative.startsWith('..') && !path.isAbsolute(relative);
|
|
51
|
+
}
|
package/lib/roots.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Skillcaddy 的两个根目录。
|
|
7
|
+
*
|
|
8
|
+
* - code root(代码根):运行中的代码所在目录。clone 出来的仓库、npm 包目录、
|
|
9
|
+
* Homebrew Cellar 目录都属于这一类。它提供 package.json、server.js、
|
|
10
|
+
* scripts/、skills/、collection-metadata/ 这些"随发行形态一起走"的内容。
|
|
11
|
+
* - library root(原件库根):原件库所在目录。它承载 official/github/personal/
|
|
12
|
+
* archived 四个 source bucket,以及 .skillcaddy/sources 与 .skillcaddy/metadata。
|
|
13
|
+
*
|
|
14
|
+
* 两者解耦之前,原件库只能跟着代码走:`npx skillcaddy` 之类的临时安装形态会把
|
|
15
|
+
* 1GB 级原件库落在 npm 缓存目录里,缓存一清就没了。解耦之后原件库有一个稳定的、
|
|
16
|
+
* 与安装形态无关的位置。
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** 覆盖原件库根的环境变量。历史脚本(pull-github.sh 等)已经在用这个名字。 */
|
|
20
|
+
export const LIBRARY_ROOT_ENV = 'SKILLCADDY_ROOT';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 默认原件库根:`~/.skillcaddy/library`。
|
|
24
|
+
*
|
|
25
|
+
* 刻意不直接用 `~/.skillcaddy`:原件库内部结构固定为
|
|
26
|
+
* `<libraryRoot>/{official,github,personal,archived}` 与
|
|
27
|
+
* `<libraryRoot>/.skillcaddy/{sources,metadata}`,与 library image 的成员定义
|
|
28
|
+
* (ADR-0011)保持一致,这样导出的镜像可以被另一台机器原样导入。
|
|
29
|
+
*/
|
|
30
|
+
export function getDefaultLibraryRoot(homeDir = homedir()) {
|
|
31
|
+
return path.join(homeDir, '.skillcaddy', 'library');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 是否是 git checkout。clone 出来的仓库与 git worktree 都会带 `.git`
|
|
36
|
+
* (worktree 是文件,clone 是目录),npm 包目录与发行 tarball 都不会带。
|
|
37
|
+
*/
|
|
38
|
+
export function isCheckoutRoot(codeRoot) {
|
|
39
|
+
if (!codeRoot) return false;
|
|
40
|
+
return existsSync(path.join(codeRoot, '.git'));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 解析原件库根。优先级:
|
|
45
|
+
*
|
|
46
|
+
* 1. 显式传入的 libraryRoot(CLI 参数、调用方决策);
|
|
47
|
+
* 2. `SKILLCADDY_ROOT` 环境变量;
|
|
48
|
+
* 3. 代码根是 git checkout —— 原件库继续留在 checkout 里,保持既有行为不变;
|
|
49
|
+
* 4. `~/.skillcaddy/library`。
|
|
50
|
+
*
|
|
51
|
+
* 第 3 条是兼容既有 clone 用户的关键:他们已经装好的原件库不会因为这次重构
|
|
52
|
+
* 而"凭空消失"。
|
|
53
|
+
*/
|
|
54
|
+
export function resolveLibraryRoot({ codeRoot, libraryRoot, env = process.env } = {}) {
|
|
55
|
+
if (libraryRoot) return path.resolve(libraryRoot);
|
|
56
|
+
|
|
57
|
+
const fromEnv = env?.[LIBRARY_ROOT_ENV];
|
|
58
|
+
if (fromEnv) return path.resolve(fromEnv);
|
|
59
|
+
|
|
60
|
+
if (codeRoot && isCheckoutRoot(codeRoot)) return path.resolve(codeRoot);
|
|
61
|
+
|
|
62
|
+
return getDefaultLibraryRoot();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 同时解析两个根。`codeRoot` 缺省为当前工作目录。
|
|
67
|
+
*/
|
|
68
|
+
export function resolveRoots({ codeRoot, libraryRoot, env = process.env } = {}) {
|
|
69
|
+
const resolvedCodeRoot = path.resolve(codeRoot || process.cwd());
|
|
70
|
+
return {
|
|
71
|
+
codeRoot: resolvedCodeRoot,
|
|
72
|
+
libraryRoot: resolveLibraryRoot({ codeRoot: resolvedCodeRoot, libraryRoot, env })
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 归一化"两个根"的入参。
|
|
78
|
+
*
|
|
79
|
+
* 接受两种形态:
|
|
80
|
+
* - 对象 `{ libraryRoot, codeRoot }` —— 当前形态;
|
|
81
|
+
* - 字符串 —— 历史单根形态,等价于 `{ libraryRoot: str, codeRoot: str }`。
|
|
82
|
+
*
|
|
83
|
+
* 单根形态存在的意义是让"原件库与代码同目录"这种既有调用点继续成立:
|
|
84
|
+
* 解耦之后它们仍然是合法的双根组合,只是两个根指向同一个目录。
|
|
85
|
+
*/
|
|
86
|
+
export function normalizeRoots(input, fallback = {}) {
|
|
87
|
+
if (typeof input === 'string') {
|
|
88
|
+
const libraryRoot = path.resolve(input);
|
|
89
|
+
return {
|
|
90
|
+
libraryRoot,
|
|
91
|
+
codeRoot: path.resolve(fallback.codeRoot || libraryRoot)
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const libraryRoot = path.resolve(input?.libraryRoot || fallback.libraryRoot || process.cwd());
|
|
96
|
+
const codeRoot = path.resolve(input?.codeRoot || fallback.codeRoot || libraryRoot);
|
|
97
|
+
|
|
98
|
+
return { libraryRoot, codeRoot };
|
|
99
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { access, lstat, readdir, readlink } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
export async function scanSkillLinks(skillsDir) {
|
|
6
|
+
const entries = await safeReaddir(skillsDir);
|
|
7
|
+
const records = await Promise.all(
|
|
8
|
+
entries
|
|
9
|
+
.filter((entry) => !entry.name.startsWith('.'))
|
|
10
|
+
.map(async (entry) => {
|
|
11
|
+
const linkPath = path.join(skillsDir, entry.name);
|
|
12
|
+
const stat = await lstat(linkPath);
|
|
13
|
+
|
|
14
|
+
if (!stat.isSymbolicLink()) {
|
|
15
|
+
if (!stat.isDirectory()) return null;
|
|
16
|
+
return {
|
|
17
|
+
alias: entry.name,
|
|
18
|
+
linkPath,
|
|
19
|
+
targetPath: null,
|
|
20
|
+
isSymlink: false,
|
|
21
|
+
exists: true
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const targetPath = await resolveLinkTarget(linkPath);
|
|
26
|
+
return {
|
|
27
|
+
alias: entry.name,
|
|
28
|
+
linkPath,
|
|
29
|
+
targetPath,
|
|
30
|
+
isSymlink: true,
|
|
31
|
+
exists: await pathExists(targetPath)
|
|
32
|
+
};
|
|
33
|
+
})
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
return records.filter(Boolean).sort((left, right) => left.alias.localeCompare(right.alias));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function resolveLinkTarget(linkPath) {
|
|
40
|
+
const target = await readlink(linkPath);
|
|
41
|
+
return path.resolve(path.dirname(linkPath), target);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function pathExists(targetPath) {
|
|
45
|
+
try {
|
|
46
|
+
await access(targetPath, constants.F_OK);
|
|
47
|
+
return true;
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function linkPathExists(targetPath) {
|
|
54
|
+
try {
|
|
55
|
+
await lstat(targetPath);
|
|
56
|
+
return true;
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function safeReaddir(dir) {
|
|
63
|
+
try {
|
|
64
|
+
return await readdir(dir, { withFileTypes: true });
|
|
65
|
+
} catch {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
}
|