worclaude 2.5.0 → 2.6.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 +65 -0
- package/package.json +4 -2
- package/src/commands/doctor.js +4 -6
- package/src/commands/init.js +9 -6
- package/src/commands/scan.js +126 -0
- package/src/commands/setup-state.js +113 -0
- package/src/commands/status.js +6 -4
- package/src/commands/upgrade.js +32 -20
- package/src/core/drift-checks.js +2 -1
- package/src/core/file-categorizer.js +38 -0
- package/src/core/merger.js +14 -7
- package/src/core/migration.js +77 -1
- package/src/core/project-scanner/detectors/ci.js +51 -0
- package/src/core/project-scanner/detectors/deployment.js +32 -0
- package/src/core/project-scanner/detectors/env-variables.js +78 -0
- package/src/core/project-scanner/detectors/external-apis.js +45 -0
- package/src/core/project-scanner/detectors/frameworks.js +56 -0
- package/src/core/project-scanner/detectors/language.js +131 -0
- package/src/core/project-scanner/detectors/linting.js +58 -0
- package/src/core/project-scanner/detectors/monorepo.js +93 -0
- package/src/core/project-scanner/detectors/orm.js +72 -0
- package/src/core/project-scanner/detectors/package-manager.js +57 -0
- package/src/core/project-scanner/detectors/readme.js +123 -0
- package/src/core/project-scanner/detectors/scripts.js +128 -0
- package/src/core/project-scanner/detectors/spec-docs.js +110 -0
- package/src/core/project-scanner/detectors/testing.js +88 -0
- package/src/core/project-scanner/index.js +126 -0
- package/src/core/project-scanner/manifests.js +120 -0
- package/src/core/remover.js +6 -3
- package/src/core/scaffolder.js +1 -0
- package/src/core/setup-state.js +213 -0
- package/src/index.js +39 -0
- package/templates/commands/setup.md +512 -113
package/src/core/merger.js
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
scaffoldPluginJson,
|
|
11
11
|
scaffoldMemoryDocs,
|
|
12
12
|
} from './scaffolder.js';
|
|
13
|
+
import { workflowRefRelPath } from './file-categorizer.js';
|
|
13
14
|
import { promptHookConflict } from '../prompts/conflict-resolution.js';
|
|
14
15
|
import {
|
|
15
16
|
detectMissingSections,
|
|
@@ -124,10 +125,11 @@ async function mergeSkills(projectRoot, existingScan, variables, report, selecti
|
|
|
124
125
|
const existsAsFlat = existingScan.existingSkills.includes(`${skill.name}.md`);
|
|
125
126
|
|
|
126
127
|
if (existsAsDir || existsAsFlat) {
|
|
127
|
-
// Tier 2: conflict — save
|
|
128
|
+
// Tier 2: conflict — save under .claude/workflow-ref/ so the live
|
|
129
|
+
// SKILL.md stays authoritative and the ref cannot shadow it.
|
|
128
130
|
await scaffoldFile(
|
|
129
131
|
skill.templatePath,
|
|
130
|
-
|
|
132
|
+
workflowRefRelPath(`skills/${skill.name}/SKILL.md`),
|
|
131
133
|
skill.vars,
|
|
132
134
|
projectRoot
|
|
133
135
|
);
|
|
@@ -147,7 +149,7 @@ async function mergeSkills(projectRoot, existingScan, variables, report, selecti
|
|
|
147
149
|
|
|
148
150
|
if (routingExistsAsDir || routingExistsAsFlat) {
|
|
149
151
|
await writeFile(
|
|
150
|
-
path.join(projectRoot,
|
|
152
|
+
path.join(projectRoot, workflowRefRelPath('skills/agent-routing/SKILL.md')),
|
|
151
153
|
routingContent
|
|
152
154
|
);
|
|
153
155
|
report.conflicts.skills.push('agent-routing');
|
|
@@ -166,7 +168,7 @@ async function mergeAgents(projectRoot, existingScan, selectedAgents, report) {
|
|
|
166
168
|
if (existingScan.existingAgents.includes(filename)) {
|
|
167
169
|
await scaffoldFile(
|
|
168
170
|
`agents/universal/${agent}.md`,
|
|
169
|
-
|
|
171
|
+
workflowRefRelPath(`agents/${filename}`),
|
|
170
172
|
{},
|
|
171
173
|
projectRoot
|
|
172
174
|
);
|
|
@@ -191,7 +193,7 @@ async function mergeAgents(projectRoot, existingScan, selectedAgents, report) {
|
|
|
191
193
|
if (existingScan.existingAgents.includes(filename)) {
|
|
192
194
|
await scaffoldFile(
|
|
193
195
|
`agents/optional/${category}/${agent}.md`,
|
|
194
|
-
|
|
196
|
+
workflowRefRelPath(`agents/${filename}`),
|
|
195
197
|
{},
|
|
196
198
|
projectRoot
|
|
197
199
|
);
|
|
@@ -216,7 +218,7 @@ async function mergeCommands(projectRoot, existingScan, report) {
|
|
|
216
218
|
if (existingScan.existingCommands.includes(filename)) {
|
|
217
219
|
await scaffoldFile(
|
|
218
220
|
`commands/${cmd}.md`,
|
|
219
|
-
|
|
221
|
+
workflowRefRelPath(`commands/${filename}`),
|
|
220
222
|
{},
|
|
221
223
|
projectRoot
|
|
222
224
|
);
|
|
@@ -431,7 +433,12 @@ async function mergeAgentsMd(projectRoot, existingScan, variables, report) {
|
|
|
431
433
|
await scaffoldFile('core/agents-md.md', 'AGENTS.md', variables, projectRoot);
|
|
432
434
|
report.agentsMdHandling = 'created';
|
|
433
435
|
} else {
|
|
434
|
-
await scaffoldFile(
|
|
436
|
+
await scaffoldFile(
|
|
437
|
+
'core/agents-md.md',
|
|
438
|
+
workflowRefRelPath('root/AGENTS.md'),
|
|
439
|
+
variables,
|
|
440
|
+
projectRoot
|
|
441
|
+
);
|
|
435
442
|
report.agentsMdHandling = 'saved-alongside';
|
|
436
443
|
}
|
|
437
444
|
}
|
package/src/core/migration.js
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { AGENT_CATALOG } from '../data/agents.js';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
fileExists,
|
|
5
|
+
readFile,
|
|
6
|
+
writeFile,
|
|
7
|
+
dirExists,
|
|
8
|
+
moveFile,
|
|
9
|
+
listFiles,
|
|
10
|
+
listFilesRecursive,
|
|
11
|
+
} from '../utils/file.js';
|
|
4
12
|
import { hashFile } from '../utils/hash.js';
|
|
13
|
+
import { WORKFLOW_REF_DIR } from './file-categorizer.js';
|
|
5
14
|
|
|
6
15
|
// --- Version comparison ---
|
|
7
16
|
|
|
@@ -142,3 +151,70 @@ export async function patchAgentDescriptions(projectRoot, meta, promptFn) {
|
|
|
142
151
|
|
|
143
152
|
return report;
|
|
144
153
|
}
|
|
154
|
+
|
|
155
|
+
// --- Workflow-ref relocation (v2.5.1) ---
|
|
156
|
+
|
|
157
|
+
const LEGACY_ROOT_REFS = ['CLAUDE.md.workflow-ref.md', 'AGENTS.md.workflow-ref'];
|
|
158
|
+
|
|
159
|
+
// Legacy ref files could only live inside these subdirs. Scoping the scan
|
|
160
|
+
// here instead of walking all of .claude/ matters because `sessions/` and
|
|
161
|
+
// `learnings/` can accumulate hundreds of files that will never be refs.
|
|
162
|
+
const LEGACY_REF_SUBDIRS = ['commands', 'agents', 'skills'];
|
|
163
|
+
|
|
164
|
+
function legacyClaudeRefTarget(relKey) {
|
|
165
|
+
// relKey is relative to .claude/, e.g. "commands/sync.workflow-ref.md".
|
|
166
|
+
// Lookahead `(?=\.[^.]+$)` ensures ".workflow-ref" is stripped only when
|
|
167
|
+
// followed by a final extension — so "sync.workflow-ref.md" → "sync.md",
|
|
168
|
+
// but a hypothetical "sync.workflow-ref" (no final ext) wouldn't match.
|
|
169
|
+
const parts = relKey.split('/');
|
|
170
|
+
const name = parts.pop();
|
|
171
|
+
parts.push(name.replace(/\.workflow-ref(?=\.[^.]+$)/, ''));
|
|
172
|
+
return path.join(WORKFLOW_REF_DIR, ...parts);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function migrateWorkflowRefLocation(projectRoot) {
|
|
176
|
+
const report = { moved: 0, names: [], skipped: [] };
|
|
177
|
+
const claudeDir = path.join(projectRoot, '.claude');
|
|
178
|
+
const refDir = path.join(claudeDir, WORKFLOW_REF_DIR);
|
|
179
|
+
|
|
180
|
+
// 1. Scan only the subdirs where legacy `.workflow-ref.md` siblings could
|
|
181
|
+
// live. Walking all of .claude/ would re-stat hundreds of session files
|
|
182
|
+
// on every upgrade for no benefit.
|
|
183
|
+
for (const subdir of LEGACY_REF_SUBDIRS) {
|
|
184
|
+
const scanRoot = path.join(claudeDir, subdir);
|
|
185
|
+
if (!(await dirExists(scanRoot))) continue;
|
|
186
|
+
|
|
187
|
+
const allFiles = await listFilesRecursive(scanRoot);
|
|
188
|
+
for (const fp of allFiles) {
|
|
189
|
+
const rel = path.relative(claudeDir, fp).split(path.sep).join('/');
|
|
190
|
+
if (!rel.endsWith('.workflow-ref.md')) continue;
|
|
191
|
+
|
|
192
|
+
const target = path.join(claudeDir, legacyClaudeRefTarget(rel));
|
|
193
|
+
if (await fileExists(target)) {
|
|
194
|
+
report.skipped.push(rel);
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
await moveFile(fp, target);
|
|
198
|
+
report.moved++;
|
|
199
|
+
report.names.push(rel);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// 2. Legacy root-level refs (CLAUDE.md.workflow-ref.md, AGENTS.md.workflow-ref).
|
|
204
|
+
for (const legacyName of LEGACY_ROOT_REFS) {
|
|
205
|
+
const src = path.join(projectRoot, legacyName);
|
|
206
|
+
if (!(await fileExists(src))) continue;
|
|
207
|
+
|
|
208
|
+
const original = legacyName.replace(/\.workflow-ref(?:\.md)?$/, '');
|
|
209
|
+
const target = path.join(refDir, original);
|
|
210
|
+
if (await fileExists(target)) {
|
|
211
|
+
report.skipped.push(legacyName);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
await moveFile(src, target);
|
|
215
|
+
report.moved++;
|
|
216
|
+
report.names.push(legacyName);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return report;
|
|
220
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import { fileExists, dirExists } from '../../../utils/file.js';
|
|
4
|
+
|
|
5
|
+
export default async function detectCi(projectRoot) {
|
|
6
|
+
const results = [];
|
|
7
|
+
|
|
8
|
+
const workflowsDir = path.join(projectRoot, '.github', 'workflows');
|
|
9
|
+
if (await dirExists(workflowsDir)) {
|
|
10
|
+
try {
|
|
11
|
+
const entries = await fs.readdir(workflowsDir, { withFileTypes: true });
|
|
12
|
+
const workflows = entries
|
|
13
|
+
.filter((e) => e.isFile() && /\.ya?ml$/.test(e.name))
|
|
14
|
+
.map((e) => e.name)
|
|
15
|
+
.sort();
|
|
16
|
+
if (workflows.length > 0) {
|
|
17
|
+
results.push({
|
|
18
|
+
field: 'ci',
|
|
19
|
+
value: { provider: 'GitHub Actions', workflows },
|
|
20
|
+
confidence: 'high',
|
|
21
|
+
source: '.github/workflows/',
|
|
22
|
+
candidates: null,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
/* missing or unreadable — non-fatal */
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const other = [
|
|
31
|
+
{ file: '.gitlab-ci.yml', provider: 'GitLab CI' },
|
|
32
|
+
{ file: '.circleci/config.yml', provider: 'CircleCI' },
|
|
33
|
+
{ file: 'azure-pipelines.yml', provider: 'Azure Pipelines' },
|
|
34
|
+
{ file: 'Jenkinsfile', provider: 'Jenkins' },
|
|
35
|
+
{ file: '.drone.yml', provider: 'Drone' },
|
|
36
|
+
{ file: 'bitbucket-pipelines.yml', provider: 'Bitbucket Pipelines' },
|
|
37
|
+
];
|
|
38
|
+
for (const { file, provider } of other) {
|
|
39
|
+
if (await fileExists(path.join(projectRoot, file))) {
|
|
40
|
+
results.push({
|
|
41
|
+
field: 'ci',
|
|
42
|
+
value: { provider, workflows: [path.basename(file)] },
|
|
43
|
+
confidence: 'high',
|
|
44
|
+
source: file,
|
|
45
|
+
candidates: null,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return results;
|
|
51
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileExists } from '../../../utils/file.js';
|
|
3
|
+
|
|
4
|
+
const DEPLOYMENTS = [
|
|
5
|
+
{ file: 'vercel.json', target: 'Vercel' },
|
|
6
|
+
{ file: 'netlify.toml', target: 'Netlify' },
|
|
7
|
+
{ file: 'Dockerfile', target: 'Docker' },
|
|
8
|
+
{ file: 'fly.toml', target: 'Fly.io' },
|
|
9
|
+
{ file: 'railway.toml', target: 'Railway' },
|
|
10
|
+
{ file: 'app.yaml', target: 'Google App Engine' },
|
|
11
|
+
{ file: 'serverless.yml', target: 'Serverless Framework' },
|
|
12
|
+
{ file: 'render.yaml', target: 'Render' },
|
|
13
|
+
{ file: '.platform.app.yaml', target: 'Platform.sh' },
|
|
14
|
+
{ file: 'wrangler.toml', target: 'Cloudflare Workers' },
|
|
15
|
+
{ file: 'amplify.yml', target: 'AWS Amplify' },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
export default async function detectDeployment(projectRoot) {
|
|
19
|
+
const results = [];
|
|
20
|
+
for (const { file, target } of DEPLOYMENTS) {
|
|
21
|
+
if (await fileExists(path.join(projectRoot, file))) {
|
|
22
|
+
results.push({
|
|
23
|
+
field: 'deployment',
|
|
24
|
+
value: target,
|
|
25
|
+
confidence: 'high',
|
|
26
|
+
source: file,
|
|
27
|
+
candidates: null,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return results;
|
|
32
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileExists, readFile } from '../../../utils/file.js';
|
|
3
|
+
import { getAllDeps, depMatches } from '../manifests.js';
|
|
4
|
+
|
|
5
|
+
const ENV_FILES = ['.env.example', '.env.template', '.env.sample'];
|
|
6
|
+
|
|
7
|
+
const SDK_SERVICES = [
|
|
8
|
+
{ sdkPrefix: '@stripe/', service: 'Stripe', envPrefix: 'STRIPE_' },
|
|
9
|
+
{ sdkPrefix: 'stripe', service: 'Stripe', envPrefix: 'STRIPE_' },
|
|
10
|
+
{ sdkPrefix: '@aws-sdk/', service: 'AWS', envPrefix: 'AWS_' },
|
|
11
|
+
{ sdkPrefix: '@sendgrid/', service: 'SendGrid', envPrefix: 'SENDGRID_' },
|
|
12
|
+
{ sdkPrefix: 'twilio', service: 'Twilio', envPrefix: 'TWILIO_' },
|
|
13
|
+
{ sdkPrefix: 'openai', service: 'OpenAI', envPrefix: 'OPENAI_' },
|
|
14
|
+
{ sdkPrefix: '@anthropic-ai/', service: 'Anthropic', envPrefix: 'ANTHROPIC_' },
|
|
15
|
+
{ sdkPrefix: '@google-cloud/', service: 'Google Cloud', envPrefix: 'GOOGLE_' },
|
|
16
|
+
{ sdkPrefix: '@slack/', service: 'Slack', envPrefix: 'SLACK_' },
|
|
17
|
+
{ sdkPrefix: 'postmark', service: 'Postmark', envPrefix: 'POSTMARK_' },
|
|
18
|
+
{ sdkPrefix: 'resend', service: 'Resend', envPrefix: 'RESEND_' },
|
|
19
|
+
{ sdkPrefix: '@sentry/', service: 'Sentry', envPrefix: 'SENTRY_' },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
function parseEnvNames(content) {
|
|
23
|
+
const names = [];
|
|
24
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
25
|
+
const line = rawLine.trim();
|
|
26
|
+
if (line === '' || line.startsWith('#')) continue;
|
|
27
|
+
const match = line.match(/^(?:export\s+)?([A-Z_][A-Z0-9_]*)\s*=/);
|
|
28
|
+
if (match) names.push(match[1]);
|
|
29
|
+
}
|
|
30
|
+
return names;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default async function detectEnvVariables(projectRoot) {
|
|
34
|
+
let sourceFile = null;
|
|
35
|
+
let content = null;
|
|
36
|
+
for (const f of ENV_FILES) {
|
|
37
|
+
const p = path.join(projectRoot, f);
|
|
38
|
+
if (await fileExists(p)) {
|
|
39
|
+
sourceFile = f;
|
|
40
|
+
content = await readFile(p);
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (!sourceFile) return [];
|
|
45
|
+
|
|
46
|
+
const names = parseEnvNames(content);
|
|
47
|
+
if (names.length === 0) {
|
|
48
|
+
return [
|
|
49
|
+
{
|
|
50
|
+
field: 'envVariables',
|
|
51
|
+
value: { names: [], inferredServices: [] },
|
|
52
|
+
confidence: 'high',
|
|
53
|
+
source: sourceFile,
|
|
54
|
+
candidates: null,
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const { js, py } = await getAllDeps(projectRoot);
|
|
60
|
+
const allDeps = { ...js, ...py };
|
|
61
|
+
const inferredServices = new Set();
|
|
62
|
+
for (const { sdkPrefix, service, envPrefix } of SDK_SERVICES) {
|
|
63
|
+
const pattern = sdkPrefix.endsWith('/') ? `${sdkPrefix}*` : sdkPrefix;
|
|
64
|
+
if (depMatches(allDeps, pattern) && names.some((n) => n.startsWith(envPrefix))) {
|
|
65
|
+
inferredServices.add(service);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return [
|
|
70
|
+
{
|
|
71
|
+
field: 'envVariables',
|
|
72
|
+
value: { names, inferredServices: Array.from(inferredServices) },
|
|
73
|
+
confidence: 'high',
|
|
74
|
+
source: sourceFile,
|
|
75
|
+
candidates: null,
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { getAllDeps, depMatches } from '../manifests.js';
|
|
2
|
+
|
|
3
|
+
const SDK_MAP = [
|
|
4
|
+
{ match: 'stripe', service: 'Stripe' },
|
|
5
|
+
{ match: '@sendgrid/', service: 'SendGrid' },
|
|
6
|
+
{ match: '@aws-sdk/', service: 'AWS' },
|
|
7
|
+
{ match: 'twilio', service: 'Twilio' },
|
|
8
|
+
{ match: 'openai', service: 'OpenAI' },
|
|
9
|
+
{ match: '@anthropic-ai/sdk', service: 'Anthropic' },
|
|
10
|
+
{ match: '@google-cloud/', service: 'Google Cloud' },
|
|
11
|
+
{ match: '@slack/', service: 'Slack' },
|
|
12
|
+
{ match: 'postmark', service: 'Postmark' },
|
|
13
|
+
{ match: 'resend', service: 'Resend' },
|
|
14
|
+
{ match: 'algoliasearch', service: 'Algolia' },
|
|
15
|
+
{ match: 'pusher', service: 'Pusher' },
|
|
16
|
+
{ match: 'posthog-js', service: 'PostHog' },
|
|
17
|
+
{ match: 'posthog-node', service: 'PostHog' },
|
|
18
|
+
{ match: '@sentry/', service: 'Sentry' },
|
|
19
|
+
{ match: 'mixpanel', service: 'Mixpanel' },
|
|
20
|
+
{ match: 'mixpanel-browser', service: 'Mixpanel' },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
export default async function detectExternalApis(projectRoot) {
|
|
24
|
+
const { js, py, hasPackageJson, hasPyproject } = await getAllDeps(projectRoot);
|
|
25
|
+
if (!hasPackageJson && !hasPyproject) return [];
|
|
26
|
+
|
|
27
|
+
const allDeps = { ...js, ...py };
|
|
28
|
+
const services = new Set();
|
|
29
|
+
for (const { match, service } of SDK_MAP) {
|
|
30
|
+
const pattern = match.endsWith('/') ? `${match}*` : match;
|
|
31
|
+
if (depMatches(allDeps, pattern)) services.add(service);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (services.size === 0) return [];
|
|
35
|
+
|
|
36
|
+
return [
|
|
37
|
+
{
|
|
38
|
+
field: 'externalApis',
|
|
39
|
+
value: Array.from(services),
|
|
40
|
+
confidence: 'high',
|
|
41
|
+
source: hasPackageJson ? 'package.json' : 'pyproject.toml',
|
|
42
|
+
candidates: null,
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { getAllDeps } from '../manifests.js';
|
|
2
|
+
|
|
3
|
+
const FRAMEWORKS = [
|
|
4
|
+
{ dep: 'next', name: 'Next.js' },
|
|
5
|
+
{ dep: 'nuxt', name: 'Nuxt' },
|
|
6
|
+
{ dep: 'astro', name: 'Astro' },
|
|
7
|
+
{ dep: 'remix', name: 'Remix' },
|
|
8
|
+
{ dep: 'sveltekit', name: 'SvelteKit' },
|
|
9
|
+
{ dep: '@sveltejs/kit', name: 'SvelteKit' },
|
|
10
|
+
{ dep: 'react', name: 'React' },
|
|
11
|
+
{ dep: 'vue', name: 'Vue' },
|
|
12
|
+
{ dep: 'svelte', name: 'Svelte' },
|
|
13
|
+
{ dep: 'solid-js', name: 'SolidJS' },
|
|
14
|
+
{ dep: 'express', name: 'Express' },
|
|
15
|
+
{ dep: 'fastify', name: 'Fastify' },
|
|
16
|
+
{ dep: 'hono', name: 'Hono' },
|
|
17
|
+
{ dep: 'koa', name: 'Koa' },
|
|
18
|
+
{ dep: '@nestjs/core', name: 'NestJS' },
|
|
19
|
+
{ dep: 'fastapi', name: 'FastAPI' },
|
|
20
|
+
{ dep: 'starlette', name: 'Starlette' },
|
|
21
|
+
{ dep: 'django', name: 'Django' },
|
|
22
|
+
{ dep: 'flask', name: 'Flask' },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
export default async function detectFrameworks(projectRoot) {
|
|
26
|
+
const { js, py, hasPackageJson, hasPyproject } = await getAllDeps(projectRoot);
|
|
27
|
+
if (!hasPackageJson && !hasPyproject) return [];
|
|
28
|
+
|
|
29
|
+
const detected = [];
|
|
30
|
+
const sources = new Set();
|
|
31
|
+
const seen = new Set();
|
|
32
|
+
for (const { dep, name } of FRAMEWORKS) {
|
|
33
|
+
if (seen.has(name)) continue;
|
|
34
|
+
if (js[dep] !== undefined) {
|
|
35
|
+
detected.push({ name, version: typeof js[dep] === 'string' ? js[dep] : '' });
|
|
36
|
+
sources.add('package.json');
|
|
37
|
+
seen.add(name);
|
|
38
|
+
} else if (py[dep] !== undefined) {
|
|
39
|
+
detected.push({ name, version: typeof py[dep] === 'string' ? py[dep] : '' });
|
|
40
|
+
sources.add('pyproject.toml');
|
|
41
|
+
seen.add(name);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (detected.length === 0) return [];
|
|
46
|
+
|
|
47
|
+
return [
|
|
48
|
+
{
|
|
49
|
+
field: 'frameworks',
|
|
50
|
+
value: detected,
|
|
51
|
+
confidence: 'high',
|
|
52
|
+
source: Array.from(sources).join(', '),
|
|
53
|
+
candidates: null,
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import { fileExists } from '../../../utils/file.js';
|
|
4
|
+
|
|
5
|
+
async function hasAnyJsFile(projectRoot) {
|
|
6
|
+
try {
|
|
7
|
+
const entries = await fs.readdir(projectRoot, { withFileTypes: true });
|
|
8
|
+
for (const entry of entries) {
|
|
9
|
+
if (entry.isFile() && /\.(js|mjs|cjs)$/.test(entry.name)) return true;
|
|
10
|
+
}
|
|
11
|
+
const srcDir = path.join(projectRoot, 'src');
|
|
12
|
+
if (await fileExists(srcDir)) {
|
|
13
|
+
const srcEntries = await fs.readdir(srcDir, { withFileTypes: true });
|
|
14
|
+
for (const entry of srcEntries) {
|
|
15
|
+
if (entry.isFile() && /\.(js|mjs|cjs)$/.test(entry.name)) return true;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
} catch {
|
|
19
|
+
/* missing or unreadable — non-fatal */
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function hasTsFiles(projectRoot) {
|
|
25
|
+
try {
|
|
26
|
+
const srcDir = path.join(projectRoot, 'src');
|
|
27
|
+
if (await fileExists(srcDir)) {
|
|
28
|
+
const entries = await fs.readdir(srcDir, { withFileTypes: true });
|
|
29
|
+
for (const entry of entries) {
|
|
30
|
+
if (entry.isFile() && /\.(ts|tsx)$/.test(entry.name)) return true;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
/* missing or unreadable — non-fatal */
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default async function detectLanguage(projectRoot) {
|
|
40
|
+
const results = [];
|
|
41
|
+
const has = async (f) => fileExists(path.join(projectRoot, f));
|
|
42
|
+
|
|
43
|
+
if (await has('tsconfig.json')) {
|
|
44
|
+
results.push({
|
|
45
|
+
field: 'language',
|
|
46
|
+
value: 'TypeScript',
|
|
47
|
+
confidence: 'high',
|
|
48
|
+
source: 'tsconfig.json',
|
|
49
|
+
candidates: null,
|
|
50
|
+
});
|
|
51
|
+
} else if (await has('jsconfig.json')) {
|
|
52
|
+
results.push({
|
|
53
|
+
field: 'language',
|
|
54
|
+
value: 'JavaScript',
|
|
55
|
+
confidence: 'high',
|
|
56
|
+
source: 'jsconfig.json',
|
|
57
|
+
candidates: null,
|
|
58
|
+
});
|
|
59
|
+
} else if ((await hasAnyJsFile(projectRoot)) && !(await hasTsFiles(projectRoot))) {
|
|
60
|
+
results.push({
|
|
61
|
+
field: 'language',
|
|
62
|
+
value: 'JavaScript',
|
|
63
|
+
confidence: 'high',
|
|
64
|
+
source: '*.js files',
|
|
65
|
+
candidates: null,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const pythonFiles = ['pyproject.toml', 'setup.py', 'requirements.txt'];
|
|
70
|
+
for (const f of pythonFiles) {
|
|
71
|
+
if (await has(f)) {
|
|
72
|
+
results.push({
|
|
73
|
+
field: 'language',
|
|
74
|
+
value: 'Python',
|
|
75
|
+
confidence: 'high',
|
|
76
|
+
source: f,
|
|
77
|
+
candidates: null,
|
|
78
|
+
});
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (!results.some((r) => r.value === 'Python')) {
|
|
83
|
+
try {
|
|
84
|
+
const entries = await fs.readdir(projectRoot, { withFileTypes: true });
|
|
85
|
+
const reqFile = entries.find((e) => e.isFile() && /^requirements.*\.txt$/.test(e.name));
|
|
86
|
+
if (reqFile) {
|
|
87
|
+
results.push({
|
|
88
|
+
field: 'language',
|
|
89
|
+
value: 'Python',
|
|
90
|
+
confidence: 'high',
|
|
91
|
+
source: reqFile.name,
|
|
92
|
+
candidates: null,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
/* missing or unreadable — non-fatal */
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (await has('Cargo.toml')) {
|
|
101
|
+
results.push({
|
|
102
|
+
field: 'language',
|
|
103
|
+
value: 'Rust',
|
|
104
|
+
confidence: 'high',
|
|
105
|
+
source: 'Cargo.toml',
|
|
106
|
+
candidates: null,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (await has('go.mod')) {
|
|
111
|
+
results.push({
|
|
112
|
+
field: 'language',
|
|
113
|
+
value: 'Go',
|
|
114
|
+
confidence: 'high',
|
|
115
|
+
source: 'go.mod',
|
|
116
|
+
candidates: null,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (await has('Gemfile')) {
|
|
121
|
+
results.push({
|
|
122
|
+
field: 'language',
|
|
123
|
+
value: 'Ruby',
|
|
124
|
+
confidence: 'high',
|
|
125
|
+
source: 'Gemfile',
|
|
126
|
+
candidates: null,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return results;
|
|
131
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import { fileExists } from '../../../utils/file.js';
|
|
4
|
+
import { readPyprojectToml } from '../manifests.js';
|
|
5
|
+
|
|
6
|
+
const CONFIGS = [
|
|
7
|
+
{ tool: 'ESLint', patterns: [/^\.eslintrc(\..+)?$/, /^eslint\.config\.(js|ts|mjs|cjs)$/] },
|
|
8
|
+
{ tool: 'Prettier', patterns: [/^\.prettierrc(\..+)?$/, /^prettier\.config\.(js|ts|mjs|cjs)$/] },
|
|
9
|
+
{ tool: 'Biome', patterns: [/^biome\.jsonc?$/] },
|
|
10
|
+
{ tool: 'Stylelint', patterns: [/^\.stylelintrc(\..+)?$/] },
|
|
11
|
+
{ tool: 'RuboCop', patterns: [/^\.rubocop\.ya?ml$/] },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
async function rootFiles(projectRoot) {
|
|
15
|
+
try {
|
|
16
|
+
const entries = await fs.readdir(projectRoot, { withFileTypes: true });
|
|
17
|
+
return entries.filter((e) => e.isFile()).map((e) => e.name);
|
|
18
|
+
} catch {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default async function detectLinting(projectRoot) {
|
|
24
|
+
const files = await rootFiles(projectRoot);
|
|
25
|
+
const tools = new Set();
|
|
26
|
+
const sources = [];
|
|
27
|
+
|
|
28
|
+
for (const { tool, patterns } of CONFIGS) {
|
|
29
|
+
const match = files.find((f) => patterns.some((p) => p.test(f)));
|
|
30
|
+
if (match) {
|
|
31
|
+
tools.add(tool);
|
|
32
|
+
sources.push(match);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (await fileExists(path.join(projectRoot, 'ruff.toml'))) {
|
|
37
|
+
tools.add('Ruff');
|
|
38
|
+
sources.push('ruff.toml');
|
|
39
|
+
} else {
|
|
40
|
+
const pyproject = await readPyprojectToml(projectRoot);
|
|
41
|
+
if (pyproject && pyproject.tool && pyproject.tool.ruff) {
|
|
42
|
+
tools.add('Ruff');
|
|
43
|
+
sources.push('pyproject.toml');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (tools.size === 0) return [];
|
|
48
|
+
|
|
49
|
+
return [
|
|
50
|
+
{
|
|
51
|
+
field: 'linting',
|
|
52
|
+
value: Array.from(tools),
|
|
53
|
+
confidence: 'high',
|
|
54
|
+
source: sources.join(', '),
|
|
55
|
+
candidates: null,
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
}
|