@skilly-hand/skilly-hand 0.1.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 +17 -0
- package/LICENSE +23 -0
- package/README.md +107 -0
- package/catalog/README.md +30 -0
- package/catalog/catalog-index.json +6 -0
- package/catalog/skills/agents-root-orchestrator/SKILL.md +135 -0
- package/catalog/skills/agents-root-orchestrator/assets/AGENTS-ROOT-TEMPLATE.md +67 -0
- package/catalog/skills/agents-root-orchestrator/manifest.json +34 -0
- package/catalog/skills/skill-creator/SKILL.md +176 -0
- package/catalog/skills/skill-creator/assets/SKILL-TEMPLATE.md +94 -0
- package/catalog/skills/skill-creator/manifest.json +36 -0
- package/catalog/skills/spec-driven-development/SKILL.md +168 -0
- package/catalog/skills/spec-driven-development/agents/apply.md +26 -0
- package/catalog/skills/spec-driven-development/agents/orchestrate.md +25 -0
- package/catalog/skills/spec-driven-development/agents/plan.md +27 -0
- package/catalog/skills/spec-driven-development/agents/verify.md +24 -0
- package/catalog/skills/spec-driven-development/assets/delta-spec-template.md +51 -0
- package/catalog/skills/spec-driven-development/assets/design-template.md +31 -0
- package/catalog/skills/spec-driven-development/assets/spec-template.md +56 -0
- package/catalog/skills/spec-driven-development/assets/validation-checklist.md +32 -0
- package/catalog/skills/spec-driven-development/manifest.json +41 -0
- package/catalog/skills/token-optimizer/SKILL.md +141 -0
- package/catalog/skills/token-optimizer/manifest.json +33 -0
- package/catalog/skills/token-optimizer/references/complexity-indicators.md +138 -0
- package/catalog/templates/AGENTS.template.md +37 -0
- package/package.json +41 -0
- package/packages/catalog/package.json +6 -0
- package/packages/catalog/src/index.js +223 -0
- package/packages/cli/package.json +9 -0
- package/packages/cli/src/bin.js +144 -0
- package/packages/core/package.json +6 -0
- package/packages/core/src/index.js +304 -0
- package/packages/detectors/package.json +6 -0
- package/packages/detectors/src/index.js +169 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { cp, lstat, mkdir, readFile, realpath, rm, symlink, unlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { copySkillTo, loadAllSkills, renderAgentsMarkdown, verifyCatalogFiles } from "../../catalog/src/index.js";
|
|
4
|
+
import { detectProject, inspectProjectFiles } from "../../detectors/src/index.js";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_AGENTS = ["codex", "claude", "cursor", "gemini", "copilot"];
|
|
7
|
+
const MANAGED_MARKER = "<!-- Managed by skilly-hand.";
|
|
8
|
+
|
|
9
|
+
function uniq(values) {
|
|
10
|
+
return [...new Set(values)];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function exists(targetPath) {
|
|
14
|
+
try {
|
|
15
|
+
await lstat(targetPath);
|
|
16
|
+
return true;
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function readJson(filePath) {
|
|
23
|
+
return JSON.parse(await readFile(filePath, "utf8"));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function writeJson(filePath, data) {
|
|
27
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
28
|
+
await writeFile(filePath, JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function nowIso() {
|
|
32
|
+
return new Date().toISOString();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizeAgentList(agents) {
|
|
36
|
+
if (!agents || agents.length === 0) {
|
|
37
|
+
return [...DEFAULT_AGENTS];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return uniq(agents.flatMap((item) => String(item).split(",")).map((item) => item.trim()).filter(Boolean));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function parseTags(input) {
|
|
44
|
+
return uniq((input || []).flatMap((value) => String(value).split(",")).map((value) => value.trim()).filter(Boolean));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function resolveSkillSelection({ catalog, detections, includeTags = [], excludeTags = [] }) {
|
|
48
|
+
const coreSkills = catalog.filter((skill) => skill.tags.includes("core"));
|
|
49
|
+
const requested = new Set(coreSkills.map((skill) => skill.id));
|
|
50
|
+
|
|
51
|
+
for (const detection of detections) {
|
|
52
|
+
for (const skillId of detection.recommendedSkillIds) {
|
|
53
|
+
requested.add(skillId);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let selected = catalog.filter((skill) => requested.has(skill.id) && skill.portable);
|
|
58
|
+
|
|
59
|
+
if (includeTags.length > 0) {
|
|
60
|
+
selected = selected.filter((skill) => includeTags.every((tag) => skill.tags.includes(tag)));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (excludeTags.length > 0) {
|
|
64
|
+
selected = selected.filter((skill) => !excludeTags.some((tag) => skill.tags.includes(tag)));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return selected.sort((a, b) => a.id.localeCompare(b.id));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function buildInstallPlan({ cwd, detections, skills, agents }) {
|
|
71
|
+
return {
|
|
72
|
+
cwd,
|
|
73
|
+
detections,
|
|
74
|
+
agents,
|
|
75
|
+
skills: skills.map((skill) => ({
|
|
76
|
+
id: skill.id,
|
|
77
|
+
title: skill.title,
|
|
78
|
+
tags: skill.tags
|
|
79
|
+
})),
|
|
80
|
+
installRoot: path.join(cwd, ".skilly-hand"),
|
|
81
|
+
generatedAt: nowIso()
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function backupPathIfNeeded(targetPath, backupsDir, lockData) {
|
|
86
|
+
if (!(await exists(targetPath))) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const backupName = targetPath.replaceAll(path.sep, "__").replace(/^__+/, "");
|
|
91
|
+
const backupPath = path.join(backupsDir, backupName);
|
|
92
|
+
|
|
93
|
+
if (!(await exists(backupPath))) {
|
|
94
|
+
await mkdir(path.dirname(backupPath), { recursive: true });
|
|
95
|
+
await cp(targetPath, backupPath, { recursive: true, force: true });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
lockData.backups[targetPath] = backupPath;
|
|
99
|
+
return backupPath;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function ensureManagedTextFile(targetPath, content, backupsDir, lockData) {
|
|
103
|
+
if (await exists(targetPath)) {
|
|
104
|
+
const current = await readFile(targetPath, "utf8");
|
|
105
|
+
if (current === content) {
|
|
106
|
+
lockData.managedFiles.push(targetPath);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
await backupPathIfNeeded(targetPath, backupsDir, lockData);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
114
|
+
await writeFile(targetPath, content, "utf8");
|
|
115
|
+
lockData.managedFiles.push(targetPath);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function ensureSymlink(targetPath, sourcePath, backupsDir, lockData) {
|
|
119
|
+
if (await exists(targetPath)) {
|
|
120
|
+
const info = await lstat(targetPath);
|
|
121
|
+
if (info.isSymbolicLink()) {
|
|
122
|
+
const resolved = await realpath(targetPath);
|
|
123
|
+
if (resolved === sourcePath) {
|
|
124
|
+
lockData.managedSymlinks.push(targetPath);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
await unlink(targetPath);
|
|
129
|
+
} else {
|
|
130
|
+
await backupPathIfNeeded(targetPath, backupsDir, lockData);
|
|
131
|
+
await rm(targetPath, { recursive: true, force: true });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
136
|
+
await symlink(sourcePath, targetPath, "dir");
|
|
137
|
+
lockData.managedSymlinks.push(targetPath);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function buildInstructionFiles({ agentsMarkdown, selectedAgents }) {
|
|
141
|
+
const files = [];
|
|
142
|
+
|
|
143
|
+
if (selectedAgents.includes("codex")) {
|
|
144
|
+
files.push({ pathParts: ["AGENTS.md"], content: agentsMarkdown });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (selectedAgents.includes("claude")) {
|
|
148
|
+
files.push({ pathParts: ["CLAUDE.md"], content: agentsMarkdown });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (selectedAgents.includes("gemini")) {
|
|
152
|
+
files.push({ pathParts: ["GEMINI.md"], content: agentsMarkdown });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (selectedAgents.includes("cursor")) {
|
|
156
|
+
files.push({ pathParts: ["cursor-instructions.md"], content: agentsMarkdown });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (selectedAgents.includes("copilot")) {
|
|
160
|
+
files.push({ pathParts: [".github", "copilot-instructions.md"], content: agentsMarkdown });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return files;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function installProject({
|
|
167
|
+
cwd,
|
|
168
|
+
agents,
|
|
169
|
+
dryRun = false,
|
|
170
|
+
includeTags = [],
|
|
171
|
+
excludeTags = []
|
|
172
|
+
}) {
|
|
173
|
+
const selectedAgents = normalizeAgentList(agents);
|
|
174
|
+
const catalog = await loadAllSkills();
|
|
175
|
+
const detections = await detectProject(cwd);
|
|
176
|
+
const skills = resolveSkillSelection({
|
|
177
|
+
catalog,
|
|
178
|
+
detections,
|
|
179
|
+
includeTags: parseTags(includeTags),
|
|
180
|
+
excludeTags: parseTags(excludeTags)
|
|
181
|
+
});
|
|
182
|
+
const plan = buildInstallPlan({ cwd, detections, skills, agents: selectedAgents });
|
|
183
|
+
|
|
184
|
+
if (dryRun) {
|
|
185
|
+
return { plan, applied: false };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const installRoot = plan.installRoot;
|
|
189
|
+
const targetCatalogDir = path.join(installRoot, "catalog");
|
|
190
|
+
const backupsDir = path.join(installRoot, "backups");
|
|
191
|
+
const lockPath = path.join(installRoot, "manifest.lock.json");
|
|
192
|
+
const lockData = {
|
|
193
|
+
version: 1,
|
|
194
|
+
generatedAt: plan.generatedAt,
|
|
195
|
+
cwd,
|
|
196
|
+
agents: selectedAgents,
|
|
197
|
+
skills: skills.map((skill) => skill.id),
|
|
198
|
+
detections,
|
|
199
|
+
managedFiles: [],
|
|
200
|
+
managedSymlinks: [],
|
|
201
|
+
backups: {}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
await mkdir(installRoot, { recursive: true });
|
|
205
|
+
await mkdir(targetCatalogDir, { recursive: true });
|
|
206
|
+
await mkdir(backupsDir, { recursive: true });
|
|
207
|
+
|
|
208
|
+
for (const skill of skills) {
|
|
209
|
+
await copySkillTo(targetCatalogDir, skill.id);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const agentsMarkdown = renderAgentsMarkdown({
|
|
213
|
+
skills,
|
|
214
|
+
detections,
|
|
215
|
+
generatedAt: plan.generatedAt,
|
|
216
|
+
projectName: path.basename(cwd)
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
await writeFile(path.join(installRoot, "AGENTS.md"), agentsMarkdown, "utf8");
|
|
220
|
+
|
|
221
|
+
for (const instructionFile of buildInstructionFiles({ agentsMarkdown, selectedAgents })) {
|
|
222
|
+
await ensureManagedTextFile(path.join(cwd, ...instructionFile.pathParts), instructionFile.content, backupsDir, lockData);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const skillsSourcePath = path.join(installRoot, "catalog");
|
|
226
|
+
|
|
227
|
+
if (selectedAgents.includes("codex")) {
|
|
228
|
+
await ensureSymlink(path.join(cwd, ".codex", "skills"), skillsSourcePath, backupsDir, lockData);
|
|
229
|
+
}
|
|
230
|
+
if (selectedAgents.includes("claude")) {
|
|
231
|
+
await ensureSymlink(path.join(cwd, ".claude", "skills"), skillsSourcePath, backupsDir, lockData);
|
|
232
|
+
}
|
|
233
|
+
if (selectedAgents.includes("gemini")) {
|
|
234
|
+
await ensureSymlink(path.join(cwd, ".gemini", "skills"), skillsSourcePath, backupsDir, lockData);
|
|
235
|
+
}
|
|
236
|
+
if (selectedAgents.includes("cursor")) {
|
|
237
|
+
await ensureSymlink(path.join(cwd, ".cursor", "skills"), skillsSourcePath, backupsDir, lockData);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
await writeJson(lockPath, lockData);
|
|
241
|
+
|
|
242
|
+
return { plan, applied: true, lockPath };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export async function uninstallProject(cwd) {
|
|
246
|
+
const installRoot = path.join(cwd, ".skilly-hand");
|
|
247
|
+
const lockPath = path.join(installRoot, "manifest.lock.json");
|
|
248
|
+
|
|
249
|
+
if (!(await exists(lockPath))) {
|
|
250
|
+
return { removed: false, reason: "No skilly-hand installation found." };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const lockData = await readJson(lockPath);
|
|
254
|
+
|
|
255
|
+
for (const symlinkPath of lockData.managedSymlinks || []) {
|
|
256
|
+
if (await exists(symlinkPath)) {
|
|
257
|
+
await rm(symlinkPath, { recursive: true, force: true });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
for (const filePath of lockData.managedFiles || []) {
|
|
262
|
+
if (await exists(filePath)) {
|
|
263
|
+
const content = await readFile(filePath, "utf8");
|
|
264
|
+
if (content.includes(MANAGED_MARKER)) {
|
|
265
|
+
await rm(filePath, { force: true });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
for (const [targetPath, backupPath] of Object.entries(lockData.backups || {})) {
|
|
271
|
+
if (await exists(backupPath)) {
|
|
272
|
+
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
273
|
+
await cp(backupPath, targetPath, { recursive: true, force: true });
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
await rm(installRoot, { recursive: true, force: true });
|
|
278
|
+
return { removed: true };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export async function runDoctor(cwd) {
|
|
282
|
+
const installRoot = path.join(cwd, ".skilly-hand");
|
|
283
|
+
const lockPath = path.join(installRoot, "manifest.lock.json");
|
|
284
|
+
const fileStatus = await inspectProjectFiles(cwd);
|
|
285
|
+
const catalogIssues = await verifyCatalogFiles();
|
|
286
|
+
const installed = await exists(lockPath);
|
|
287
|
+
const result = {
|
|
288
|
+
cwd,
|
|
289
|
+
installed,
|
|
290
|
+
catalogIssues,
|
|
291
|
+
fileStatus
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
if (installed) {
|
|
295
|
+
const lock = await readJson(lockPath);
|
|
296
|
+
result.lock = {
|
|
297
|
+
agents: lock.agents,
|
|
298
|
+
skills: lock.skills,
|
|
299
|
+
generatedAt: lock.generatedAt
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return result;
|
|
304
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { access, readFile, stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
async function pathExists(filePath) {
|
|
5
|
+
try {
|
|
6
|
+
await access(filePath);
|
|
7
|
+
return true;
|
|
8
|
+
} catch {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function readJson(filePath) {
|
|
14
|
+
if (!(await pathExists(filePath))) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return JSON.parse(await readFile(filePath, "utf8"));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function addDetection(results, detection) {
|
|
22
|
+
const existing = results.find((item) => item.technology === detection.technology);
|
|
23
|
+
if (existing) {
|
|
24
|
+
existing.confidence = Math.max(existing.confidence, detection.confidence);
|
|
25
|
+
existing.reasons.push(...detection.reasons);
|
|
26
|
+
existing.recommendedSkillIds = [
|
|
27
|
+
...new Set([...existing.recommendedSkillIds, ...detection.recommendedSkillIds])
|
|
28
|
+
];
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
results.push({
|
|
33
|
+
technology: detection.technology,
|
|
34
|
+
confidence: detection.confidence,
|
|
35
|
+
reasons: [...detection.reasons],
|
|
36
|
+
recommendedSkillIds: [...new Set(detection.recommendedSkillIds)]
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function detectProject(cwd) {
|
|
41
|
+
const packageJson = await readJson(path.join(cwd, "package.json"));
|
|
42
|
+
const tsconfigExists = await pathExists(path.join(cwd, "tsconfig.json"));
|
|
43
|
+
const angularJsonExists = await pathExists(path.join(cwd, "angular.json"));
|
|
44
|
+
const viteConfigExists =
|
|
45
|
+
(await pathExists(path.join(cwd, "vite.config.js"))) ||
|
|
46
|
+
(await pathExists(path.join(cwd, "vite.config.ts")));
|
|
47
|
+
const nextConfigExists =
|
|
48
|
+
(await pathExists(path.join(cwd, "next.config.js"))) ||
|
|
49
|
+
(await pathExists(path.join(cwd, "next.config.mjs"))) ||
|
|
50
|
+
(await pathExists(path.join(cwd, "next.config.ts")));
|
|
51
|
+
const storybookDirExists = await pathExists(path.join(cwd, ".storybook"));
|
|
52
|
+
const results = [];
|
|
53
|
+
|
|
54
|
+
if (packageJson) {
|
|
55
|
+
addDetection(results, {
|
|
56
|
+
technology: "nodejs",
|
|
57
|
+
confidence: 1,
|
|
58
|
+
reasons: ["Found package.json"],
|
|
59
|
+
recommendedSkillIds: ["token-optimizer", "commit-writer", "pr-writer", "spec-driven-development"]
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (packageJson || tsconfigExists) {
|
|
64
|
+
addDetection(results, {
|
|
65
|
+
technology: "typescript",
|
|
66
|
+
confidence: tsconfigExists ? 0.95 : 0.7,
|
|
67
|
+
reasons: tsconfigExists ? ["Found tsconfig.json"] : ["TypeScript inferred from project layout"],
|
|
68
|
+
recommendedSkillIds: ["token-optimizer"]
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const deps = packageJson ? { ...packageJson.dependencies, ...packageJson.devDependencies } : {};
|
|
73
|
+
|
|
74
|
+
if ("react" in deps) {
|
|
75
|
+
addDetection(results, {
|
|
76
|
+
technology: "react",
|
|
77
|
+
confidence: 0.95,
|
|
78
|
+
reasons: ['Dependency "react" found in package.json'],
|
|
79
|
+
recommendedSkillIds: ["accessibility-audit", "storybook-component-stories", "playwright-ui-testing"]
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if ("next" in deps || nextConfigExists) {
|
|
84
|
+
addDetection(results, {
|
|
85
|
+
technology: "nextjs",
|
|
86
|
+
confidence: nextConfigExists ? 1 : 0.9,
|
|
87
|
+
reasons: nextConfigExists ? ["Found next.config.*"] : ['Dependency "next" found in package.json'],
|
|
88
|
+
recommendedSkillIds: ["accessibility-audit", "playwright-ui-testing", "spec-driven-development"]
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if ("@angular/core" in deps || angularJsonExists) {
|
|
93
|
+
addDetection(results, {
|
|
94
|
+
technology: "angular",
|
|
95
|
+
confidence: angularJsonExists ? 1 : 0.95,
|
|
96
|
+
reasons: angularJsonExists ? ["Found angular.json"] : ['Dependency "@angular/core" found in package.json'],
|
|
97
|
+
recommendedSkillIds: [
|
|
98
|
+
"angular-guidelines",
|
|
99
|
+
"vitest-component-testing",
|
|
100
|
+
"storybook-component-stories",
|
|
101
|
+
"accessibility-audit"
|
|
102
|
+
]
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if ("vite" in deps || viteConfigExists) {
|
|
107
|
+
addDetection(results, {
|
|
108
|
+
technology: "vite",
|
|
109
|
+
confidence: viteConfigExists ? 1 : 0.9,
|
|
110
|
+
reasons: viteConfigExists ? ["Found vite.config.*"] : ['Dependency "vite" found in package.json'],
|
|
111
|
+
recommendedSkillIds: ["storybook-component-stories", "playwright-ui-testing"]
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if ("@playwright/test" in deps) {
|
|
116
|
+
addDetection(results, {
|
|
117
|
+
technology: "playwright",
|
|
118
|
+
confidence: 0.95,
|
|
119
|
+
reasons: ['Dependency "@playwright/test" found in package.json'],
|
|
120
|
+
recommendedSkillIds: ["playwright-ui-testing"]
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if ("vitest" in deps) {
|
|
125
|
+
addDetection(results, {
|
|
126
|
+
technology: "vitest",
|
|
127
|
+
confidence: 0.95,
|
|
128
|
+
reasons: ['Dependency "vitest" found in package.json'],
|
|
129
|
+
recommendedSkillIds: ["vitest-component-testing"]
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if ("tailwindcss" in deps || (await pathExists(path.join(cwd, "tailwind.config.js"))) || (await pathExists(path.join(cwd, "tailwind.config.ts")))) {
|
|
134
|
+
addDetection(results, {
|
|
135
|
+
technology: "tailwindcss",
|
|
136
|
+
confidence: 0.9,
|
|
137
|
+
reasons: ['Dependency "tailwindcss" or config file found'],
|
|
138
|
+
recommendedSkillIds: ["accessibility-audit", "css-modules"]
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if ("@storybook/react" in deps || "@storybook/angular" in deps || storybookDirExists) {
|
|
143
|
+
addDetection(results, {
|
|
144
|
+
technology: "storybook",
|
|
145
|
+
confidence: storybookDirExists ? 1 : 0.9,
|
|
146
|
+
reasons: storybookDirExists ? ["Found .storybook directory"] : ["Storybook dependency found"],
|
|
147
|
+
recommendedSkillIds: ["storybook-component-stories", "playwright-ui-testing"]
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return results.sort((a, b) => b.confidence - a.confidence || a.technology.localeCompare(b.technology));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function inspectProjectFiles(cwd) {
|
|
155
|
+
const probes = ["package.json", "tsconfig.json", "angular.json", ".storybook"];
|
|
156
|
+
const statuses = [];
|
|
157
|
+
|
|
158
|
+
for (const probe of probes) {
|
|
159
|
+
const probePath = path.join(cwd, probe);
|
|
160
|
+
try {
|
|
161
|
+
const info = await stat(probePath);
|
|
162
|
+
statuses.push({ path: probe, exists: true, type: info.isDirectory() ? "directory" : "file" });
|
|
163
|
+
} catch {
|
|
164
|
+
statuses.push({ path: probe, exists: false, type: null });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return statuses;
|
|
169
|
+
}
|