worclaude 2.5.1 → 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.
@@ -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
+ }
@@ -0,0 +1,93 @@
1
+ import path from 'node:path';
2
+ import YAML from 'yaml';
3
+ import fs from 'fs-extra';
4
+ import { fileExists, readFile } from '../../../utils/file.js';
5
+ import { readPackageJson } from '../manifests.js';
6
+
7
+ async function readYamlWorkspacePackages(projectRoot) {
8
+ const filePath = path.join(projectRoot, 'pnpm-workspace.yaml');
9
+ if (!(await fileExists(filePath))) return null;
10
+ try {
11
+ const raw = await readFile(filePath);
12
+ const parsed = YAML.parse(raw);
13
+ if (parsed && Array.isArray(parsed.packages)) return parsed.packages;
14
+ } catch {
15
+ /* missing or unreadable — non-fatal */
16
+ }
17
+ return null;
18
+ }
19
+
20
+ async function expandPackagePaths(projectRoot, patterns) {
21
+ const paths = [];
22
+ for (const pattern of patterns) {
23
+ if (pattern.endsWith('/*')) {
24
+ const parent = pattern.slice(0, -2);
25
+ const parentPath = path.join(projectRoot, parent);
26
+ try {
27
+ const entries = await fs.readdir(parentPath, { withFileTypes: true });
28
+ for (const entry of entries) {
29
+ if (entry.isDirectory()) paths.push(path.posix.join(parent, entry.name));
30
+ }
31
+ } catch {
32
+ // directory missing — pnpm-workspace.yaml lists a path the user hasn't created
33
+ }
34
+ } else {
35
+ paths.push(pattern);
36
+ }
37
+ }
38
+ return paths;
39
+ }
40
+
41
+ export default async function detectMonorepo(projectRoot) {
42
+ const results = [];
43
+ const sources = [];
44
+ let packagePaths = [];
45
+ let tool = null;
46
+
47
+ const pnpmPackages = await readYamlWorkspacePackages(projectRoot);
48
+ if (pnpmPackages) {
49
+ tool = 'pnpm';
50
+ sources.push('pnpm-workspace.yaml');
51
+ packagePaths = await expandPackagePaths(projectRoot, pnpmPackages);
52
+ }
53
+
54
+ if (!tool) {
55
+ const pkg = await readPackageJson(projectRoot);
56
+ if (pkg && pkg.workspaces) {
57
+ const patterns = Array.isArray(pkg.workspaces)
58
+ ? pkg.workspaces
59
+ : Array.isArray(pkg.workspaces.packages)
60
+ ? pkg.workspaces.packages
61
+ : [];
62
+ if (patterns.length > 0) {
63
+ tool = 'npm-or-yarn';
64
+ sources.push('package.json');
65
+ packagePaths = await expandPackagePaths(projectRoot, patterns);
66
+ }
67
+ }
68
+ }
69
+
70
+ const markers = [
71
+ { file: 'lerna.json', tool: 'lerna' },
72
+ { file: 'nx.json', tool: 'nx' },
73
+ { file: 'turbo.json', tool: 'turbo' },
74
+ { file: 'rush.json', tool: 'rush' },
75
+ ];
76
+ for (const { file, tool: markerTool } of markers) {
77
+ if (await fileExists(path.join(projectRoot, file))) {
78
+ if (!tool) tool = markerTool;
79
+ sources.push(file);
80
+ }
81
+ }
82
+
83
+ if (!tool) return [];
84
+
85
+ results.push({
86
+ field: 'monorepo',
87
+ value: { isMonorepo: true, tool, packagePaths },
88
+ confidence: 'high',
89
+ source: sources.join(', '),
90
+ candidates: null,
91
+ });
92
+ return results;
93
+ }
@@ -0,0 +1,72 @@
1
+ import path from 'node:path';
2
+ import fs from 'fs-extra';
3
+ import { fileExists } from '../../../utils/file.js';
4
+ import { getAllDeps } from '../manifests.js';
5
+
6
+ async function findFile(projectRoot, patterns) {
7
+ for (const p of patterns) {
8
+ if (await fileExists(path.join(projectRoot, p))) return p;
9
+ }
10
+ return null;
11
+ }
12
+
13
+ async function findDrizzleConfig(projectRoot) {
14
+ try {
15
+ const entries = await fs.readdir(projectRoot, { withFileTypes: true });
16
+ const match = entries.find(
17
+ (e) => e.isFile() && /^drizzle\.config\.(js|ts|mjs|cjs)$/.test(e.name)
18
+ );
19
+ return match ? match.name : null;
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+
25
+ const SIMPLE_ORMS = [
26
+ { deps: ['sequelize', '@sequelize/core'], name: 'Sequelize', source: 'package.json', from: 'js' },
27
+ { deps: ['typeorm'], name: 'TypeORM', source: 'package.json', from: 'js' },
28
+ { deps: ['mongoose'], name: 'Mongoose', source: 'package.json', from: 'js' },
29
+ { deps: ['sqlalchemy', 'SQLAlchemy'], name: 'SQLAlchemy', source: 'pyproject.toml', from: 'py' },
30
+ { deps: ['tortoise-orm'], name: 'Tortoise ORM', source: 'pyproject.toml', from: 'py' },
31
+ ];
32
+
33
+ function ormResult(name, schemaFile, source) {
34
+ return {
35
+ field: 'orm',
36
+ value: { name, schemaFile: schemaFile ?? null },
37
+ confidence: 'high',
38
+ source,
39
+ candidates: null,
40
+ };
41
+ }
42
+
43
+ export default async function detectOrm(projectRoot) {
44
+ const { js, py } = await getAllDeps(projectRoot);
45
+ const results = [];
46
+
47
+ if (js['prisma'] !== undefined || js['@prisma/client'] !== undefined) {
48
+ const schemaFile = await findFile(projectRoot, ['prisma/schema.prisma']);
49
+ results.push(ormResult('Prisma', schemaFile, schemaFile || 'package.json'));
50
+ }
51
+
52
+ if (js['drizzle-orm'] !== undefined) {
53
+ const schemaFile = await findDrizzleConfig(projectRoot);
54
+ results.push(ormResult('Drizzle', schemaFile, schemaFile || 'package.json'));
55
+ }
56
+
57
+ for (const { deps, name, source, from } of SIMPLE_ORMS) {
58
+ const map = from === 'js' ? js : py;
59
+ if (deps.some((d) => map[d] !== undefined)) {
60
+ results.push(ormResult(name, null, source));
61
+ }
62
+ }
63
+
64
+ const alembicIni = (await fileExists(path.join(projectRoot, 'alembic.ini')))
65
+ ? 'alembic.ini'
66
+ : null;
67
+ if (py['alembic'] !== undefined || alembicIni) {
68
+ results.push(ormResult('Alembic', alembicIni, alembicIni || 'pyproject.toml'));
69
+ }
70
+
71
+ return results;
72
+ }
@@ -0,0 +1,57 @@
1
+ import path from 'node:path';
2
+ import { fileExists } from '../../../utils/file.js';
3
+
4
+ const LOCKFILES = [
5
+ { file: 'pnpm-lock.yaml', manager: 'pnpm', group: 'js' },
6
+ { file: 'yarn.lock', manager: 'yarn', group: 'js' },
7
+ { file: 'package-lock.json', manager: 'npm', group: 'js' },
8
+ { file: 'bun.lock', manager: 'bun', group: 'js' },
9
+ { file: 'bun.lockb', manager: 'bun', group: 'js' },
10
+ { file: 'poetry.lock', manager: 'poetry', group: 'python' },
11
+ { file: 'uv.lock', manager: 'uv', group: 'python' },
12
+ { file: 'Pipfile.lock', manager: 'pipenv', group: 'python' },
13
+ { file: 'Cargo.lock', manager: 'cargo', group: 'rust' },
14
+ { file: 'Gemfile.lock', manager: 'bundler', group: 'ruby' },
15
+ { file: 'go.sum', manager: 'go', group: 'go' },
16
+ ];
17
+
18
+ export default async function detectPackageManager(projectRoot) {
19
+ const present = [];
20
+ for (const entry of LOCKFILES) {
21
+ if (await fileExists(path.join(projectRoot, entry.file))) {
22
+ present.push(entry);
23
+ }
24
+ }
25
+ if (present.length === 0) return [];
26
+
27
+ const byGroup = new Map();
28
+ for (const entry of present) {
29
+ if (!byGroup.has(entry.group)) byGroup.set(entry.group, []);
30
+ byGroup.get(entry.group).push(entry);
31
+ }
32
+
33
+ const results = [];
34
+ for (const group of byGroup.values()) {
35
+ if (group.length === 1) {
36
+ const { file, manager } = group[0];
37
+ results.push({
38
+ field: 'packageManager',
39
+ value: manager,
40
+ confidence: 'high',
41
+ source: file,
42
+ candidates: null,
43
+ });
44
+ } else {
45
+ const managers = group.map((g) => g.manager);
46
+ const sources = group.map((g) => g.file);
47
+ results.push({
48
+ field: 'packageManager',
49
+ value: group[0].manager,
50
+ confidence: 'medium',
51
+ source: sources.join(', '),
52
+ candidates: managers,
53
+ });
54
+ }
55
+ }
56
+ return results;
57
+ }
@@ -0,0 +1,123 @@
1
+ import path from 'node:path';
2
+ import fs from 'fs-extra';
3
+ import { fileExists, readFile } from '../../../utils/file.js';
4
+
5
+ const README_CANDIDATES = ['README.md', 'README.rst', 'README.txt'];
6
+ const SETUP_HEADING = /installation|getting\s*started|setup|quick\s*start/i;
7
+ const SETUP_CAP = 2000;
8
+ const MIN_DESCRIPTION_LENGTH = 20;
9
+ // Cap full-file read to keep a pathological 50MB README from blocking the scan.
10
+ const MAX_README_BYTES = 512 * 1024;
11
+ const BADGE_PATTERNS = [/^\s*\[!\[.*?\]\(.*?\)\]\(.*?\)\s*$/, /^\s*!\[.*?\]\(.*?\)\s*$/];
12
+
13
+ function stripHtmlComments(text) {
14
+ return text.replace(/<!--[\s\S]*?-->/g, '');
15
+ }
16
+
17
+ function stripHtmlTags(text) {
18
+ return text.replace(/<[^>]+>/g, '');
19
+ }
20
+
21
+ function isSkippable(line) {
22
+ if (line.trim() === '') return true;
23
+ return BADGE_PATTERNS.some((p) => p.test(line));
24
+ }
25
+
26
+ function stripLeadingBadges(lines) {
27
+ const result = [...lines];
28
+ while (result.length > 0 && isSkippable(result[0])) {
29
+ result.shift();
30
+ }
31
+ return result;
32
+ }
33
+
34
+ function firstNonEmptyParagraph(lines, startIndex) {
35
+ let i = startIndex;
36
+ while (i < lines.length && isSkippable(lines[i])) i++;
37
+ if (i >= lines.length) return null;
38
+ const paragraph = [];
39
+ while (i < lines.length && lines[i].trim() !== '') {
40
+ if (/^#{1,6}\s/.test(lines[i])) break;
41
+ if (isSkippable(lines[i])) {
42
+ i++;
43
+ continue;
44
+ }
45
+ paragraph.push(lines[i]);
46
+ i++;
47
+ }
48
+ const text = paragraph.join(' ').trim();
49
+ return text.length > 0 ? text : null;
50
+ }
51
+
52
+ function findH1Index(lines) {
53
+ return lines.findIndex((line) => /^#\s+/.test(line));
54
+ }
55
+
56
+ function extractSetupInstructions(lines) {
57
+ const startIdx = lines.findIndex(
58
+ (line) => /^#{1,6}\s/.test(line) && SETUP_HEADING.test(line.replace(/^#+\s+/, ''))
59
+ );
60
+ if (startIdx === -1) return null;
61
+ const body = [];
62
+ for (let i = startIdx + 1; i < lines.length; i++) {
63
+ if (/^#{1,6}\s/.test(lines[i])) break;
64
+ body.push(lines[i]);
65
+ }
66
+ const text = body.join('\n').trim();
67
+ if (text.length === 0) return null;
68
+ return text.length > SETUP_CAP ? text.slice(0, SETUP_CAP) : text;
69
+ }
70
+
71
+ export default async function detectReadme(projectRoot) {
72
+ let sourceFile = null;
73
+ let content = null;
74
+ for (const candidate of README_CANDIDATES) {
75
+ const p = path.join(projectRoot, candidate);
76
+ if (await fileExists(p)) {
77
+ sourceFile = candidate;
78
+ const stat = await fs.stat(p);
79
+ if (stat.size > MAX_README_BYTES) {
80
+ // Oversized README — read just the head; description/setup headings are near the top.
81
+ const handle = await fs.open(p, 'r');
82
+ try {
83
+ const buf = Buffer.alloc(MAX_README_BYTES);
84
+ const { bytesRead } = await handle.read(buf, 0, MAX_README_BYTES, 0);
85
+ content = buf.subarray(0, bytesRead).toString('utf-8');
86
+ } finally {
87
+ await handle.close();
88
+ }
89
+ } else {
90
+ content = await readFile(p);
91
+ }
92
+ break;
93
+ }
94
+ }
95
+ if (!sourceFile) return [];
96
+
97
+ const cleaned = stripHtmlTags(stripHtmlComments(content));
98
+ const rawLines = cleaned.split(/\r?\n/);
99
+ const lines = stripLeadingBadges(rawLines);
100
+
101
+ const h1Index = findH1Index(lines);
102
+ const descStart = h1Index === -1 ? 0 : h1Index + 1;
103
+ let projectDescription = firstNonEmptyParagraph(lines, descStart);
104
+ if (projectDescription && projectDescription.length < MIN_DESCRIPTION_LENGTH) {
105
+ projectDescription = null;
106
+ }
107
+
108
+ const setupInstructions = extractSetupInstructions(lines);
109
+
110
+ return [
111
+ {
112
+ field: 'readme',
113
+ value: {
114
+ projectDescription,
115
+ setupInstructions,
116
+ fullPath: sourceFile,
117
+ },
118
+ confidence: 'medium',
119
+ source: sourceFile,
120
+ candidates: null,
121
+ },
122
+ ];
123
+ }
@@ -0,0 +1,128 @@
1
+ import path from 'node:path';
2
+ import YAML from 'yaml';
3
+ import { fileExists, readFile } from '../../../utils/file.js';
4
+ import { readPackageJson } from '../manifests.js';
5
+
6
+ const ALL_SCRIPTS_CAP = 50;
7
+
8
+ function pickScript(scripts, candidates) {
9
+ for (const key of candidates) {
10
+ if (scripts[key]) return { key, value: scripts[key] };
11
+ }
12
+ return null;
13
+ }
14
+
15
+ function firstTestScript(scripts) {
16
+ if (scripts['test']) return { key: 'test', value: scripts['test'] };
17
+ if (scripts['test:unit']) return { key: 'test:unit', value: scripts['test:unit'] };
18
+ const key = Object.keys(scripts).find((k) => k.startsWith('test:'));
19
+ return key ? { key, value: scripts[key] } : null;
20
+ }
21
+
22
+ async function readMakefileTargets(projectRoot) {
23
+ const filePath = path.join(projectRoot, 'Makefile');
24
+ if (!(await fileExists(filePath))) return [];
25
+ try {
26
+ const raw = await readFile(filePath);
27
+ const targets = new Set();
28
+ for (const line of raw.split(/\r?\n/)) {
29
+ const match = line.match(/^([A-Za-z][A-Za-z0-9_-]*)\s*:(?!=)/);
30
+ if (match) targets.add(match[1]);
31
+ }
32
+ return Array.from(targets);
33
+ } catch {
34
+ return [];
35
+ }
36
+ }
37
+
38
+ async function readTaskfile(projectRoot) {
39
+ const filePath = path.join(projectRoot, 'Taskfile.yml');
40
+ if (!(await fileExists(filePath))) return [];
41
+ try {
42
+ const raw = await readFile(filePath);
43
+ const parsed = YAML.parse(raw);
44
+ if (parsed && parsed.tasks && typeof parsed.tasks === 'object') {
45
+ return Object.keys(parsed.tasks);
46
+ }
47
+ return [];
48
+ } catch {
49
+ return [];
50
+ }
51
+ }
52
+
53
+ async function readJustfile(projectRoot) {
54
+ const filePath = path.join(projectRoot, 'justfile');
55
+ if (!(await fileExists(filePath))) return [];
56
+ try {
57
+ const raw = await readFile(filePath);
58
+ const recipes = new Set();
59
+ for (const line of raw.split(/\r?\n/)) {
60
+ const match = line.match(/^([a-zA-Z][a-zA-Z0-9_-]*)\s*(?::|\s+[a-zA-Z])/);
61
+ if (match) recipes.add(match[1]);
62
+ }
63
+ return Array.from(recipes);
64
+ } catch {
65
+ return [];
66
+ }
67
+ }
68
+
69
+ export default async function detectScripts(projectRoot) {
70
+ const pkg = await readPackageJson(projectRoot);
71
+ const scripts = pkg && pkg.scripts ? pkg.scripts : {};
72
+
73
+ const dev = pickScript(scripts, ['dev', 'start', 'serve']);
74
+ const test = firstTestScript(scripts);
75
+ const build = pickScript(scripts, ['build', 'compile']);
76
+ const lint = pickScript(scripts, ['lint', 'check']);
77
+
78
+ let truncated = false;
79
+ let allScripts = {};
80
+ const entries = Object.entries(scripts);
81
+ if (entries.length > ALL_SCRIPTS_CAP) {
82
+ truncated = true;
83
+ for (const [k, v] of entries.slice(0, ALL_SCRIPTS_CAP)) allScripts[k] = v;
84
+ } else {
85
+ allScripts = { ...scripts };
86
+ }
87
+
88
+ const sources = [];
89
+ if (pkg) sources.push('package.json');
90
+
91
+ const makefileTargets = await readMakefileTargets(projectRoot);
92
+ const taskfileTasks = await readTaskfile(projectRoot);
93
+ const justfileRecipes = await readJustfile(projectRoot);
94
+ if (makefileTargets.length > 0) sources.push('Makefile');
95
+ if (taskfileTasks.length > 0) sources.push('Taskfile.yml');
96
+ if (justfileRecipes.length > 0) sources.push('justfile');
97
+
98
+ if (
99
+ !pkg &&
100
+ makefileTargets.length === 0 &&
101
+ taskfileTasks.length === 0 &&
102
+ justfileRecipes.length === 0
103
+ ) {
104
+ return [];
105
+ }
106
+
107
+ const value = {
108
+ dev: dev ? { key: dev.key, command: dev.value } : null,
109
+ test: test ? { key: test.key, command: test.value } : null,
110
+ build: build ? { key: build.key, command: build.value } : null,
111
+ lint: lint ? { key: lint.key, command: lint.value } : null,
112
+ allScripts,
113
+ makefileTargets,
114
+ taskfileTasks,
115
+ justfileRecipes,
116
+ };
117
+ if (truncated) value.truncated = true;
118
+
119
+ return [
120
+ {
121
+ field: 'scripts',
122
+ value,
123
+ confidence: 'high',
124
+ source: sources.join(', '),
125
+ candidates: null,
126
+ },
127
+ ];
128
+ }