devworkspace 0.2.0 → 0.2.1
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 +15 -0
- package/dist/cli/render.js +8 -4
- package/dist/cli/render.js.map +1 -1
- package/dist/config/schemas.d.ts +7 -1
- package/dist/config/schemas.js +10 -0
- package/dist/config/schemas.js.map +1 -1
- package/dist/discovery/commands.d.ts +34 -3
- package/dist/discovery/commands.js +95 -33
- package/dist/discovery/commands.js.map +1 -1
- package/dist/discovery/detect.d.ts +17 -0
- package/dist/discovery/detect.js +101 -42
- package/dist/discovery/detect.js.map +1 -1
- package/dist/integrations/adapters/graphify.js +46 -8
- package/dist/integrations/adapters/graphify.js.map +1 -1
- package/dist/integrations/manager.js +14 -1
- package/dist/integrations/manager.js.map +1 -1
- package/dist/integrations/persistentPath.d.ts +20 -0
- package/dist/integrations/persistentPath.js +47 -0
- package/dist/integrations/persistentPath.js.map +1 -0
- package/dist/integrations/types.d.ts +6 -0
- package/dist/integrations/types.js.map +1 -1
- package/dist/prompts/onboarding.js +4 -4
- package/dist/prompts/onboarding.js.map +1 -1
- package/dist/services/analysis.d.ts +13 -1
- package/dist/services/analysis.js +17 -18
- package/dist/services/analysis.js.map +1 -1
- package/dist/services/doctor.js +78 -4
- package/dist/services/doctor.js.map +1 -1
- package/dist/services/refresh.js +1 -1
- package/dist/services/refresh.js.map +1 -1
- package/dist/services/status.d.ts +1 -0
- package/dist/services/status.js +1 -1
- package/dist/services/status.js.map +1 -1
- package/docs/integrations.md +1 -1
- package/docs/testing.md +4 -0
- package/docs/troubleshooting.md +4 -1
- package/package.json +1 -1
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
export const TRUST_NOTE = 'Commands marked "repository-script" come from files in the repository (e.g. package.json scripts) and are untrusted input. ' +
|
|
2
2
|
'Inspect what a script does before running it, especially in automation or on unfamiliar repositories.';
|
|
3
|
+
/** First match wins, so more specific scripts (test:unit) are preferred over catch-alls (test). */
|
|
3
4
|
const SCRIPT_CANDIDATES = {
|
|
4
5
|
typecheck: ['typecheck', 'type-check', 'check-types', 'tsc', 'types'],
|
|
5
|
-
unit: ['test', '
|
|
6
|
+
unit: ['test:unit', 'unit', 'test', 'test:run'],
|
|
6
7
|
integration: ['test:integration', 'test:int', 'integration'],
|
|
7
8
|
e2e: ['test:e2e', 'e2e', 'test:playwright', 'test:cypress', 'cypress:run'],
|
|
8
9
|
lint: ['lint', 'eslint', 'lint:check'],
|
|
9
10
|
format: ['format:check', 'prettier:check', 'format-check', 'check:format'],
|
|
10
11
|
build: ['build', 'compile'],
|
|
12
|
+
migrations: ['db:migrate', 'migrate'],
|
|
13
|
+
security: ['test:security', 'security:test', 'security', 'audit:security'],
|
|
11
14
|
};
|
|
12
15
|
const REQUIRED = {
|
|
13
16
|
typecheck: 'yes',
|
|
@@ -20,73 +23,132 @@ const REQUIRED = {
|
|
|
20
23
|
migrations: 'context',
|
|
21
24
|
security: 'context',
|
|
22
25
|
};
|
|
23
|
-
const
|
|
26
|
+
export const CHECK_ORDER = ['typecheck', 'unit', 'integration', 'e2e', 'lint', 'format', 'build', 'migrations', 'security'];
|
|
24
27
|
function scriptCommand(pm, script) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (mgr === 'yarn')
|
|
28
|
+
if (script === 'test' && (pm === 'npm' || pm === 'pnpm' || pm === 'yarn' || pm === 'bun'))
|
|
29
|
+
return `${pm} test`;
|
|
30
|
+
if (pm === 'yarn')
|
|
29
31
|
return `yarn ${script}`;
|
|
30
|
-
if (
|
|
32
|
+
if (pm === 'bun')
|
|
31
33
|
return `bun run ${script}`;
|
|
32
|
-
return `${
|
|
34
|
+
return `${pm} run ${script}`;
|
|
33
35
|
}
|
|
34
36
|
/** Placeholder scripts such as npm's default "no test specified" do not count as tests. */
|
|
35
37
|
function isRealScript(body) {
|
|
36
38
|
return !/no test specified/i.test(body) && body.trim() !== '';
|
|
37
39
|
}
|
|
40
|
+
const byCwd = (a, b) => (a.cwd ?? '').localeCompare(b.cwd ?? '');
|
|
41
|
+
/**
|
|
42
|
+
* Detected verification commands. Node commands are produced per package and only for scripts that exist in that
|
|
43
|
+
* package's own manifest. Every check that no package provides gets one unavailable entry so the contract always
|
|
44
|
+
* lists all nine checks.
|
|
45
|
+
*/
|
|
38
46
|
export function buildContract(input) {
|
|
39
|
-
const found =
|
|
40
|
-
const
|
|
41
|
-
if (!found.
|
|
42
|
-
found.
|
|
47
|
+
const found = [];
|
|
48
|
+
const add = (id, command, source, origin, cwd) => {
|
|
49
|
+
if (!found.some((f) => f.id === id && (f.cwd ?? '') === (cwd ?? ''))) {
|
|
50
|
+
found.push({ id, available: true, command, required: REQUIRED[id], source, origin, ...(cwd ? { cwd } : {}) });
|
|
51
|
+
}
|
|
43
52
|
};
|
|
44
|
-
|
|
53
|
+
for (const np of input.nodePackages) {
|
|
54
|
+
const manifest = np.dir ? `${np.dir}/package.json` : 'package.json';
|
|
45
55
|
for (const [id, names] of Object.entries(SCRIPT_CANDIDATES)) {
|
|
46
56
|
for (const name of names) {
|
|
47
|
-
const body =
|
|
57
|
+
const body = np.pkg.scripts[name];
|
|
48
58
|
if (body !== undefined && isRealScript(body)) {
|
|
49
|
-
|
|
59
|
+
add(id, scriptCommand(np.packageManager, name), `${manifest}#scripts.${name}`, 'repository-script', np.dir);
|
|
50
60
|
break;
|
|
51
61
|
}
|
|
52
62
|
}
|
|
53
63
|
}
|
|
54
|
-
if (!found.
|
|
55
|
-
|
|
64
|
+
if (np.pkg.scripts.typecheck === undefined && np.hasTsconfig && np.pkg.hasTypes && !found.some((f) => f.id === 'typecheck' && (f.cwd ?? '') === np.dir)) {
|
|
65
|
+
add('typecheck', `${np.packageManager === 'pnpm' ? 'pnpm exec' : 'npx'} tsc --noEmit`, `${np.dir ? `${np.dir}/` : ''}tsconfig.json`, 'inferred', np.dir);
|
|
56
66
|
}
|
|
57
67
|
}
|
|
58
68
|
if (input.ecosystems.has('python')) {
|
|
59
69
|
const ev = input.pythonEvidence;
|
|
60
70
|
const hasTestsDir = [...input.files].some((f) => /^tests?\//.test(f) && f.endsWith('.py'));
|
|
61
71
|
if (ev.pytest || hasTestsDir || input.has('pytest.ini'))
|
|
62
|
-
|
|
72
|
+
add('unit', 'pytest', ev.pytest ?? 'tests/', 'inferred');
|
|
63
73
|
if (ev.ruff)
|
|
64
|
-
|
|
74
|
+
add('lint', 'ruff check .', ev.ruff, 'inferred');
|
|
65
75
|
if (ev.mypy)
|
|
66
|
-
|
|
76
|
+
add('typecheck', 'mypy .', ev.mypy, 'inferred');
|
|
67
77
|
if (ev.black)
|
|
68
|
-
|
|
78
|
+
add('format', 'black --check .', ev.black, 'inferred');
|
|
69
79
|
if (ev.alembic)
|
|
70
|
-
|
|
80
|
+
add('migrations', 'alembic check', ev.alembic, 'inferred');
|
|
71
81
|
}
|
|
72
82
|
if (input.ecosystems.has('go')) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
83
|
+
add('unit', 'go test ./...', 'go.mod', 'inferred');
|
|
84
|
+
add('build', 'go build ./...', 'go.mod', 'inferred');
|
|
85
|
+
add('lint', 'go vet ./...', 'go.mod', 'inferred');
|
|
76
86
|
}
|
|
77
87
|
if (input.ecosystems.has('rust')) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
88
|
+
add('unit', 'cargo test', 'Cargo.toml', 'inferred');
|
|
89
|
+
add('build', 'cargo build', 'Cargo.toml', 'inferred');
|
|
90
|
+
add('lint', 'cargo clippy', 'Cargo.toml', 'inferred');
|
|
81
91
|
}
|
|
82
92
|
if (input.has('pom.xml')) {
|
|
83
|
-
|
|
84
|
-
|
|
93
|
+
add('unit', 'mvn test', 'pom.xml', 'inferred');
|
|
94
|
+
add('build', 'mvn package', 'pom.xml', 'inferred');
|
|
85
95
|
}
|
|
86
96
|
if (input.has('build.gradle') || input.has('build.gradle.kts')) {
|
|
87
|
-
|
|
88
|
-
|
|
97
|
+
add('unit', 'gradle test', 'build.gradle', 'inferred');
|
|
98
|
+
add('build', 'gradle build', 'build.gradle', 'inferred');
|
|
99
|
+
}
|
|
100
|
+
return withPlaceholders(found);
|
|
101
|
+
}
|
|
102
|
+
function withPlaceholders(entries) {
|
|
103
|
+
const out = [];
|
|
104
|
+
for (const id of CHECK_ORDER) {
|
|
105
|
+
const mine = entries.filter((e) => e.id === id && e.available).sort(byCwd);
|
|
106
|
+
if (mine.length > 0)
|
|
107
|
+
out.push(...mine);
|
|
108
|
+
else {
|
|
109
|
+
const unavailable = entries.find((e) => e.id === id);
|
|
110
|
+
out.push(unavailable ?? { id, available: false, command: null, required: 'no', source: null, origin: 'inferred' });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Combine the previous contract with fresh detection.
|
|
117
|
+
* - Entries confirmed by a person (origin "user") are never dropped or overwritten. A user entry without a cwd
|
|
118
|
+
* covers its whole check; one with a cwd covers only that directory, and detection fills in the other packages.
|
|
119
|
+
* - If detection is incomplete, previously available entries survive too, with their original provenance.
|
|
120
|
+
*/
|
|
121
|
+
export function mergeContract(previous, detected, opts = {}) {
|
|
122
|
+
const prev = previous ?? [];
|
|
123
|
+
const detectedAvailable = detected.filter((d) => d.available);
|
|
124
|
+
const incomplete = detectedAvailable.length === 0 || !!opts.truncated;
|
|
125
|
+
let preservedUser = 0;
|
|
126
|
+
let preservedStale = 0;
|
|
127
|
+
const out = [];
|
|
128
|
+
for (const id of CHECK_ORDER) {
|
|
129
|
+
const user = prev.filter((p) => p.id === id && p.origin === 'user' && p.available);
|
|
130
|
+
const globalUser = user.some((u) => !u.cwd);
|
|
131
|
+
const det = detectedAvailable.filter((d) => d.id === id);
|
|
132
|
+
const chosen = [...user];
|
|
133
|
+
preservedUser += user.length;
|
|
134
|
+
if (!globalUser) {
|
|
135
|
+
for (const d of det)
|
|
136
|
+
if (!user.some((u) => (u.cwd ?? '') === (d.cwd ?? '')))
|
|
137
|
+
chosen.push(d);
|
|
138
|
+
}
|
|
139
|
+
if (chosen.length === 0 && incomplete) {
|
|
140
|
+
const old = prev.filter((p) => p.id === id && p.available);
|
|
141
|
+
chosen.push(...old);
|
|
142
|
+
preservedStale += old.length;
|
|
143
|
+
}
|
|
144
|
+
if (chosen.length > 0)
|
|
145
|
+
out.push(...chosen.sort(byCwd));
|
|
146
|
+
else {
|
|
147
|
+
// nothing confirmed, nothing detected: list the check as unavailable so the contract always names all nine checks
|
|
148
|
+
const unavailable = detected.find((d) => d.id === id) ?? { id, available: false, command: null, required: 'no', source: null, origin: 'inferred' };
|
|
149
|
+
out.push(unavailable);
|
|
150
|
+
}
|
|
89
151
|
}
|
|
90
|
-
return
|
|
152
|
+
return { checks: out, preservedUser, preservedStale, incomplete };
|
|
91
153
|
}
|
|
92
154
|
//# sourceMappingURL=commands.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commands.js","sourceRoot":"","sources":["../../src/discovery/commands.ts"],"names":[],"mappings":"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","sourcesContent":["import type { CheckId, CommandEntry } from '../config/schemas.js';\nimport type { PackageInfo } from './detect.js';\n\nexport const TRUST_NOTE =\n 'Commands marked \"repository-script\" come from files in the repository (e.g. package.json scripts) and are untrusted input. ' +\n 'Inspect what a script does before running it, especially in automation or on unfamiliar repositories.';\n\nexport interface ContractInput {\n pkg: PackageInfo | null;\n packageManager: string | null;\n ecosystems: Set<string>;\n has: (rel: string) => boolean;\n pythonEvidence: Record<string, string>;\n files: Set<string>;\n}\n\nconst SCRIPT_CANDIDATES: Record<string, string[]> = {\n typecheck: ['typecheck', 'type-check', 'check-types', 'tsc', 'types'],\n unit: ['test', 'test:unit', 'unit', 'test:run'],\n integration: ['test:integration', 'test:int', 'integration'],\n e2e: ['test:e2e', 'e2e', 'test:playwright', 'test:cypress', 'cypress:run'],\n lint: ['lint', 'eslint', 'lint:check'],\n format: ['format:check', 'prettier:check', 'format-check', 'check:format'],\n build: ['build', 'compile'],\n};\n\nconst REQUIRED: Record<CheckId, CommandEntry['required']> = {\n typecheck: 'yes',\n unit: 'yes',\n integration: 'context',\n e2e: 'context',\n lint: 'yes',\n format: 'context',\n build: 'yes',\n migrations: 'context',\n security: 'context',\n};\n\nconst ORDER: CheckId[] = ['typecheck', 'unit', 'integration', 'e2e', 'lint', 'format', 'build', 'migrations', 'security'];\n\nfunction scriptCommand(pm: string | null, script: string): string {\n const mgr = pm ?? 'npm';\n if (script === 'test' && (mgr === 'npm' || mgr === 'pnpm' || mgr === 'yarn' || mgr === 'bun')) return `${mgr} test`;\n if (mgr === 'yarn') return `yarn ${script}`;\n if (mgr === 'bun') return `bun run ${script}`;\n return `${mgr} run ${script}`;\n}\n\n/** Placeholder scripts such as npm's default \"no test specified\" do not count as tests. */\nfunction isRealScript(body: string): boolean {\n return !/no test specified/i.test(body) && body.trim() !== '';\n}\n\nexport function buildContract(input: ContractInput): CommandEntry[] {\n const found = new Map<CheckId, CommandEntry>();\n const set = (id: CheckId, command: string, source: string, origin: CommandEntry['origin']): void => {\n if (!found.has(id)) found.set(id, { id, available: true, command, required: REQUIRED[id], source, origin });\n };\n\n if (input.pkg) {\n for (const [id, names] of Object.entries(SCRIPT_CANDIDATES)) {\n for (const name of names) {\n const body = input.pkg.scripts[name];\n if (body !== undefined && isRealScript(body)) {\n set(id as CheckId, scriptCommand(input.packageManager, name), `${input.pkg.rel}#scripts.${name}`, 'repository-script');\n break;\n }\n }\n }\n if (!found.has('typecheck') && input.has('tsconfig.json') && input.pkg.hasTypes) {\n set('typecheck', `${input.packageManager === 'pnpm' ? 'pnpm exec' : 'npx'} tsc --noEmit`, 'tsconfig.json', 'inferred');\n }\n }\n\n if (input.ecosystems.has('python')) {\n const ev = input.pythonEvidence;\n const hasTestsDir = [...input.files].some((f) => /^tests?\\//.test(f) && f.endsWith('.py'));\n if (ev.pytest || hasTestsDir || input.has('pytest.ini')) set('unit', 'pytest', ev.pytest ?? 'tests/', 'inferred');\n if (ev.ruff) set('lint', 'ruff check .', ev.ruff, 'inferred');\n if (ev.mypy) set('typecheck', 'mypy .', ev.mypy, 'inferred');\n if (ev.black) set('format', 'black --check .', ev.black, 'inferred');\n if (ev.alembic) set('migrations', 'alembic check', ev.alembic, 'inferred');\n }\n if (input.ecosystems.has('go')) {\n set('unit', 'go test ./...', 'go.mod', 'inferred');\n set('build', 'go build ./...', 'go.mod', 'inferred');\n set('lint', 'go vet ./...', 'go.mod', 'inferred');\n }\n if (input.ecosystems.has('rust')) {\n set('unit', 'cargo test', 'Cargo.toml', 'inferred');\n set('build', 'cargo build', 'Cargo.toml', 'inferred');\n set('lint', 'cargo clippy', 'Cargo.toml', 'inferred');\n }\n if (input.has('pom.xml')) {\n set('unit', 'mvn test', 'pom.xml', 'inferred');\n set('build', 'mvn package', 'pom.xml', 'inferred');\n }\n if (input.has('build.gradle') || input.has('build.gradle.kts')) {\n set('unit', 'gradle test', 'build.gradle', 'inferred');\n set('build', 'gradle build', 'build.gradle', 'inferred');\n }\n\n return ORDER.map(\n (id) => found.get(id) ?? { id, available: false, command: null, required: 'no' as const, source: null, origin: 'inferred' as const },\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"commands.js","sourceRoot":"","sources":["../../src/discovery/commands.ts"],"names":[],"mappings":"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","sourcesContent":["import type { CheckId, CommandEntry } from '../config/schemas.js';\nimport type { PackageInfo } from './detect.js';\n\nexport const TRUST_NOTE =\n 'Commands marked \"repository-script\" come from files in the repository (e.g. package.json scripts) and are untrusted input. ' +\n 'Inspect what a script does before running it, especially in automation or on unfamiliar repositories.';\n\n/** One Node.js package: the manifest, where it lives, and which package manager its own lockfile says it uses. */\nexport interface NodePackageInput {\n /** Directory relative to the project root, POSIX separators; '' for the root. */\n dir: string;\n pkg: PackageInfo;\n packageManager: string;\n hasTsconfig: boolean;\n}\n\nexport interface ContractInput {\n nodePackages: NodePackageInput[];\n ecosystems: Set<string>;\n has: (rel: string) => boolean;\n pythonEvidence: Record<string, string>;\n files: Set<string>;\n}\n\n/** First match wins, so more specific scripts (test:unit) are preferred over catch-alls (test). */\nconst SCRIPT_CANDIDATES: Record<string, string[]> = {\n typecheck: ['typecheck', 'type-check', 'check-types', 'tsc', 'types'],\n unit: ['test:unit', 'unit', 'test', 'test:run'],\n integration: ['test:integration', 'test:int', 'integration'],\n e2e: ['test:e2e', 'e2e', 'test:playwright', 'test:cypress', 'cypress:run'],\n lint: ['lint', 'eslint', 'lint:check'],\n format: ['format:check', 'prettier:check', 'format-check', 'check:format'],\n build: ['build', 'compile'],\n migrations: ['db:migrate', 'migrate'],\n security: ['test:security', 'security:test', 'security', 'audit:security'],\n};\n\nconst REQUIRED: Record<CheckId, CommandEntry['required']> = {\n typecheck: 'yes',\n unit: 'yes',\n integration: 'context',\n e2e: 'context',\n lint: 'yes',\n format: 'context',\n build: 'yes',\n migrations: 'context',\n security: 'context',\n};\n\nexport const CHECK_ORDER: CheckId[] = ['typecheck', 'unit', 'integration', 'e2e', 'lint', 'format', 'build', 'migrations', 'security'];\n\nfunction scriptCommand(pm: string, script: string): string {\n if (script === 'test' && (pm === 'npm' || pm === 'pnpm' || pm === 'yarn' || pm === 'bun')) return `${pm} test`;\n if (pm === 'yarn') return `yarn ${script}`;\n if (pm === 'bun') return `bun run ${script}`;\n return `${pm} run ${script}`;\n}\n\n/** Placeholder scripts such as npm's default \"no test specified\" do not count as tests. */\nfunction isRealScript(body: string): boolean {\n return !/no test specified/i.test(body) && body.trim() !== '';\n}\n\nconst byCwd = (a: CommandEntry, b: CommandEntry): number => (a.cwd ?? '').localeCompare(b.cwd ?? '');\n\n/**\n * Detected verification commands. Node commands are produced per package and only for scripts that exist in that\n * package's own manifest. Every check that no package provides gets one unavailable entry so the contract always\n * lists all nine checks.\n */\nexport function buildContract(input: ContractInput): CommandEntry[] {\n const found: CommandEntry[] = [];\n const add = (id: CheckId, command: string, source: string, origin: CommandEntry['origin'], cwd?: string): void => {\n if (!found.some((f) => f.id === id && (f.cwd ?? '') === (cwd ?? ''))) {\n found.push({ id, available: true, command, required: REQUIRED[id], source, origin, ...(cwd ? { cwd } : {}) });\n }\n };\n\n for (const np of input.nodePackages) {\n const manifest = np.dir ? `${np.dir}/package.json` : 'package.json';\n for (const [id, names] of Object.entries(SCRIPT_CANDIDATES)) {\n for (const name of names) {\n const body = np.pkg.scripts[name];\n if (body !== undefined && isRealScript(body)) {\n add(id as CheckId, scriptCommand(np.packageManager, name), `${manifest}#scripts.${name}`, 'repository-script', np.dir);\n break;\n }\n }\n }\n if (np.pkg.scripts.typecheck === undefined && np.hasTsconfig && np.pkg.hasTypes && !found.some((f) => f.id === 'typecheck' && (f.cwd ?? '') === np.dir)) {\n add('typecheck', `${np.packageManager === 'pnpm' ? 'pnpm exec' : 'npx'} tsc --noEmit`, `${np.dir ? `${np.dir}/` : ''}tsconfig.json`, 'inferred', np.dir);\n }\n }\n\n if (input.ecosystems.has('python')) {\n const ev = input.pythonEvidence;\n const hasTestsDir = [...input.files].some((f) => /^tests?\\//.test(f) && f.endsWith('.py'));\n if (ev.pytest || hasTestsDir || input.has('pytest.ini')) add('unit', 'pytest', ev.pytest ?? 'tests/', 'inferred');\n if (ev.ruff) add('lint', 'ruff check .', ev.ruff, 'inferred');\n if (ev.mypy) add('typecheck', 'mypy .', ev.mypy, 'inferred');\n if (ev.black) add('format', 'black --check .', ev.black, 'inferred');\n if (ev.alembic) add('migrations', 'alembic check', ev.alembic, 'inferred');\n }\n if (input.ecosystems.has('go')) {\n add('unit', 'go test ./...', 'go.mod', 'inferred');\n add('build', 'go build ./...', 'go.mod', 'inferred');\n add('lint', 'go vet ./...', 'go.mod', 'inferred');\n }\n if (input.ecosystems.has('rust')) {\n add('unit', 'cargo test', 'Cargo.toml', 'inferred');\n add('build', 'cargo build', 'Cargo.toml', 'inferred');\n add('lint', 'cargo clippy', 'Cargo.toml', 'inferred');\n }\n if (input.has('pom.xml')) {\n add('unit', 'mvn test', 'pom.xml', 'inferred');\n add('build', 'mvn package', 'pom.xml', 'inferred');\n }\n if (input.has('build.gradle') || input.has('build.gradle.kts')) {\n add('unit', 'gradle test', 'build.gradle', 'inferred');\n add('build', 'gradle build', 'build.gradle', 'inferred');\n }\n\n return withPlaceholders(found);\n}\n\nfunction withPlaceholders(entries: CommandEntry[]): CommandEntry[] {\n const out: CommandEntry[] = [];\n for (const id of CHECK_ORDER) {\n const mine = entries.filter((e) => e.id === id && e.available).sort(byCwd);\n if (mine.length > 0) out.push(...mine);\n else {\n const unavailable = entries.find((e) => e.id === id);\n out.push(unavailable ?? { id, available: false, command: null, required: 'no', source: null, origin: 'inferred' });\n }\n }\n return out;\n}\n\nexport interface MergeResult {\n checks: CommandEntry[];\n /** Entries kept from the previous contract because a human confirmed them (origin \"user\"). */\n preservedUser: number;\n /** Previously detected entries kept because this analysis found nothing or was cut short. */\n preservedStale: number;\n /** The new detection found no verification commands at all, or the scan was truncated. */\n incomplete: boolean;\n}\n\n/**\n * Combine the previous contract with fresh detection.\n * - Entries confirmed by a person (origin \"user\") are never dropped or overwritten. A user entry without a cwd\n * covers its whole check; one with a cwd covers only that directory, and detection fills in the other packages.\n * - If detection is incomplete, previously available entries survive too, with their original provenance.\n */\nexport function mergeContract(previous: CommandEntry[] | null, detected: CommandEntry[], opts: { truncated?: boolean } = {}): MergeResult {\n const prev = previous ?? [];\n const detectedAvailable = detected.filter((d) => d.available);\n const incomplete = detectedAvailable.length === 0 || !!opts.truncated;\n let preservedUser = 0;\n let preservedStale = 0;\n const out: CommandEntry[] = [];\n\n for (const id of CHECK_ORDER) {\n const user = prev.filter((p) => p.id === id && p.origin === 'user' && p.available);\n const globalUser = user.some((u) => !u.cwd);\n const det = detectedAvailable.filter((d) => d.id === id);\n const chosen: CommandEntry[] = [...user];\n preservedUser += user.length;\n if (!globalUser) {\n for (const d of det) if (!user.some((u) => (u.cwd ?? '') === (d.cwd ?? ''))) chosen.push(d);\n }\n if (chosen.length === 0 && incomplete) {\n const old = prev.filter((p) => p.id === id && p.available);\n chosen.push(...old);\n preservedStale += old.length;\n }\n if (chosen.length > 0) out.push(...chosen.sort(byCwd));\n else {\n // nothing confirmed, nothing detected: list the check as unavailable so the contract always names all nine checks\n const unavailable = detected.find((d) => d.id === id) ?? { id, available: false, command: null, required: 'no' as const, source: null, origin: 'inferred' as const };\n out.push(unavailable);\n }\n }\n return { checks: out, preservedUser, preservedStale, incomplete };\n}\n"]}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ScanResult } from './scan.js';
|
|
2
|
+
import { type NodePackageInput } from './commands.js';
|
|
2
3
|
import type { CommandEntry } from '../config/schemas.js';
|
|
3
4
|
export interface Detected {
|
|
4
5
|
name: string;
|
|
@@ -10,6 +11,7 @@ export interface SubProject {
|
|
|
10
11
|
name: string;
|
|
11
12
|
ecosystem: string;
|
|
12
13
|
frameworks: string[];
|
|
14
|
+
packageManager?: string;
|
|
13
15
|
}
|
|
14
16
|
export interface PackageInfo {
|
|
15
17
|
rel: string;
|
|
@@ -49,6 +51,8 @@ export interface DiscoveryReport {
|
|
|
49
51
|
packages: SubProject[];
|
|
50
52
|
};
|
|
51
53
|
commands: CommandEntry[];
|
|
54
|
+
/** Recognised package manifests (project-relative). Used as evidence for derived facts. */
|
|
55
|
+
manifests: string[];
|
|
52
56
|
testConfigs: string[];
|
|
53
57
|
ci: string[];
|
|
54
58
|
docker: string[];
|
|
@@ -69,4 +73,17 @@ export interface DetectInput {
|
|
|
69
73
|
projectName: string;
|
|
70
74
|
scan: ScanResult;
|
|
71
75
|
}
|
|
76
|
+
export interface FoundNodePackage extends NodePackageInput {
|
|
77
|
+
manifest: string;
|
|
78
|
+
/** Where the package manager came from, for transparency in discovery.json consumers. */
|
|
79
|
+
pmSource: 'lockfile' | 'packageManager field' | 'inherited from root' | 'default';
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Find every Node.js package, not just the root one. Each package gets its own package manager from its own lockfile
|
|
83
|
+
* (falling back to the packageManager field, then the root lockfile, then npm). No script is ever executed.
|
|
84
|
+
*/
|
|
85
|
+
export declare function discoverNodePackages(root: string, files: Set<string>): Promise<{
|
|
86
|
+
packages: FoundNodePackage[];
|
|
87
|
+
unparsable: string[];
|
|
88
|
+
}>;
|
|
72
89
|
export declare function detectProject(input: DetectInput): Promise<DiscoveryReport>;
|
package/dist/discovery/detect.js
CHANGED
|
@@ -221,6 +221,71 @@ function languagesFrom(byExt) {
|
|
|
221
221
|
}
|
|
222
222
|
return [...acc.entries()].map(([name, files]) => ({ name, files })).sort((a, b) => b.files - a.files || a.name.localeCompare(b.name));
|
|
223
223
|
}
|
|
224
|
+
const LOCKFILES = [
|
|
225
|
+
['pnpm-lock.yaml', 'pnpm'],
|
|
226
|
+
['yarn.lock', 'yarn'],
|
|
227
|
+
['bun.lockb', 'bun'],
|
|
228
|
+
['bun.lock', 'bun'],
|
|
229
|
+
['package-lock.json', 'npm'],
|
|
230
|
+
['npm-shrinkwrap.json', 'npm'],
|
|
231
|
+
];
|
|
232
|
+
const MAX_NODE_PACKAGES = 200;
|
|
233
|
+
const MAX_MANIFEST_DEPTH = 5;
|
|
234
|
+
/** A package.json that only sets {"type":"module"} (a marker file) is not a package. */
|
|
235
|
+
function isRealManifest(p) {
|
|
236
|
+
return !!p.name || Object.keys(p.scripts).length > 0 || Object.keys(p.deps).length > 0 || p.workspaces.length > 0;
|
|
237
|
+
}
|
|
238
|
+
function lockfilePm(files, dir) {
|
|
239
|
+
for (const [lock, pm] of LOCKFILES)
|
|
240
|
+
if (files.has(dir ? `${dir}/${lock}` : lock))
|
|
241
|
+
return pm;
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Find every Node.js package, not just the root one. Each package gets its own package manager from its own lockfile
|
|
246
|
+
* (falling back to the packageManager field, then the root lockfile, then npm). No script is ever executed.
|
|
247
|
+
*/
|
|
248
|
+
export async function discoverNodePackages(root, files) {
|
|
249
|
+
const manifests = [...files]
|
|
250
|
+
.filter((f) => path.posix.basename(f) === 'package.json' && !isAuxPath(f) && f.split('/').length <= MAX_MANIFEST_DEPTH)
|
|
251
|
+
.sort((a, b) => a.split('/').length - b.split('/').length || a.localeCompare(b))
|
|
252
|
+
.slice(0, MAX_NODE_PACKAGES);
|
|
253
|
+
const rootLockPm = lockfilePm(files, '');
|
|
254
|
+
const packages = [];
|
|
255
|
+
const unparsable = [];
|
|
256
|
+
for (const manifest of manifests) {
|
|
257
|
+
const pkg = await readPackageJson(root, manifest);
|
|
258
|
+
if (!pkg) {
|
|
259
|
+
unparsable.push(manifest);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (!isRealManifest(pkg))
|
|
263
|
+
continue;
|
|
264
|
+
const dir = path.posix.dirname(manifest) === '.' ? '' : path.posix.dirname(manifest);
|
|
265
|
+
const own = lockfilePm(files, dir);
|
|
266
|
+
const field = pkg.packageManagerField?.split('@')[0] ?? null;
|
|
267
|
+
let packageManager;
|
|
268
|
+
let pmSource;
|
|
269
|
+
if (own) {
|
|
270
|
+
packageManager = own;
|
|
271
|
+
pmSource = 'lockfile';
|
|
272
|
+
}
|
|
273
|
+
else if (field) {
|
|
274
|
+
packageManager = field;
|
|
275
|
+
pmSource = 'packageManager field';
|
|
276
|
+
}
|
|
277
|
+
else if (rootLockPm && dir !== '') {
|
|
278
|
+
packageManager = rootLockPm;
|
|
279
|
+
pmSource = 'inherited from root';
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
packageManager = 'npm';
|
|
283
|
+
pmSource = 'default';
|
|
284
|
+
}
|
|
285
|
+
packages.push({ dir, pkg, packageManager, hasTsconfig: files.has(dir ? `${dir}/tsconfig.json` : 'tsconfig.json'), manifest, pmSource });
|
|
286
|
+
}
|
|
287
|
+
return { packages, unparsable };
|
|
288
|
+
}
|
|
224
289
|
export async function detectProject(input) {
|
|
225
290
|
const { root, scan } = input;
|
|
226
291
|
const files = scan.files;
|
|
@@ -232,27 +297,23 @@ export async function detectProject(input) {
|
|
|
232
297
|
if (!frameworks.has(d.name))
|
|
233
298
|
frameworks.set(d.name, d);
|
|
234
299
|
};
|
|
235
|
-
// Node / JS
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
warnings.push(
|
|
239
|
-
|
|
300
|
+
// Node / JS: every package.json, not only the one at the root
|
|
301
|
+
const { packages: nodePkgs, unparsable } = await discoverNodePackages(root, files);
|
|
302
|
+
for (const m of unparsable.slice(0, 5))
|
|
303
|
+
warnings.push(`${m} exists but could not be parsed.`);
|
|
304
|
+
const rootNode = nodePkgs.find((p) => p.dir === '');
|
|
305
|
+
const rootPkg = rootNode?.pkg ?? null;
|
|
306
|
+
if (nodePkgs.length > 0) {
|
|
240
307
|
ecosystems.add('node');
|
|
241
|
-
|
|
308
|
+
for (const p of nodePkgs)
|
|
309
|
+
nodeFrameworks(p.pkg).forEach(addFw);
|
|
242
310
|
}
|
|
243
311
|
let packageManager = null;
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
packageManager = 'bun';
|
|
250
|
-
else if (has('package-lock.json') || has('npm-shrinkwrap.json'))
|
|
251
|
-
packageManager = 'npm';
|
|
252
|
-
else if (rootPkg?.packageManagerField)
|
|
253
|
-
packageManager = rootPkg.packageManagerField.split('@')[0] ?? null;
|
|
254
|
-
else if (rootPkg)
|
|
255
|
-
packageManager = 'npm';
|
|
312
|
+
const pms = [...new Set(nodePkgs.map((p) => p.packageManager))].sort();
|
|
313
|
+
if (pms.length === 1)
|
|
314
|
+
packageManager = pms[0];
|
|
315
|
+
else if (pms.length > 1)
|
|
316
|
+
packageManager = `mixed (${pms.join(', ')})`;
|
|
256
317
|
// Python
|
|
257
318
|
const py = await pythonText(root, scan);
|
|
258
319
|
const hasPy = has('pyproject.toml') || has('requirements.txt') || has('Pipfile') || has('setup.py') || [...files].some((f) => f.endsWith('.py') && !isAuxPath(f));
|
|
@@ -301,7 +362,7 @@ export async function detectProject(input) {
|
|
|
301
362
|
workspacePatterns = parseWorkspaceYaml((await safeReadText(root, 'pnpm-workspace.yaml', 64 * 1024).catch(() => null)) ?? '');
|
|
302
363
|
}
|
|
303
364
|
else if (rootPkg && rootPkg.workspaces.length > 0) {
|
|
304
|
-
monoTool = packageManager === 'yarn' ? 'yarn-workspaces' : 'npm-workspaces';
|
|
365
|
+
monoTool = rootNode?.packageManager === 'yarn' ? 'yarn-workspaces' : 'npm-workspaces';
|
|
305
366
|
workspacePatterns = rootPkg.workspaces;
|
|
306
367
|
}
|
|
307
368
|
else if (has('lerna.json'))
|
|
@@ -321,28 +382,19 @@ export async function detectProject(input) {
|
|
|
321
382
|
else if (has('go.work'))
|
|
322
383
|
monoTool = 'go-work';
|
|
323
384
|
const packages = [];
|
|
324
|
-
|
|
325
|
-
if (
|
|
326
|
-
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
packages.push({
|
|
333
|
-
path: d,
|
|
334
|
-
name: p?.name ?? path.posix.basename(d),
|
|
335
|
-
ecosystem: 'node',
|
|
336
|
-
frameworks: p ? nodeFrameworks(p).map((f) => f.name) : [],
|
|
337
|
-
});
|
|
338
|
-
if (p)
|
|
339
|
-
nodeFrameworks(p).forEach(addFw);
|
|
340
|
-
}
|
|
341
|
-
else {
|
|
342
|
-
packages.push({ path: d, name: path.posix.basename(d), ecosystem: 'rust', frameworks: [] });
|
|
343
|
-
}
|
|
385
|
+
for (const p of nodePkgs) {
|
|
386
|
+
if (p.dir === '')
|
|
387
|
+
continue;
|
|
388
|
+
packages.push({ path: p.dir, name: p.pkg.name ?? path.posix.basename(p.dir), ecosystem: 'node', frameworks: nodeFrameworks(p.pkg).map((f) => f.name), packageManager: p.packageManager });
|
|
389
|
+
}
|
|
390
|
+
if (monoTool === 'cargo-workspace') {
|
|
391
|
+
for (const d of expandWorkspaceGlobs(workspacePatterns, files, manifest).slice(0, 100)) {
|
|
392
|
+
packages.push({ path: d, name: path.posix.basename(d), ecosystem: 'rust', frameworks: [] });
|
|
344
393
|
}
|
|
345
394
|
}
|
|
395
|
+
// Several packages without any workspace tooling (e.g. backend/ + frontend/ and no root package.json) is still one repository of many packages.
|
|
396
|
+
if (!monoTool && nodePkgs.length >= 2)
|
|
397
|
+
monoTool = 'multi-package';
|
|
346
398
|
const testConfigs = [...files].filter((f) => TEST_CONFIG_RE.test(f) && f.split('/').length <= 4 && !isAuxPath(f)).sort().slice(0, 30);
|
|
347
399
|
const ci = [
|
|
348
400
|
...[...files].filter((f) => f.startsWith('.github/workflows/')),
|
|
@@ -377,13 +429,19 @@ export async function detectProject(input) {
|
|
|
377
429
|
entryHints.push(cand);
|
|
378
430
|
}
|
|
379
431
|
const commands = buildContract({
|
|
380
|
-
|
|
381
|
-
packageManager,
|
|
432
|
+
nodePackages: nodePkgs,
|
|
382
433
|
ecosystems,
|
|
383
434
|
has,
|
|
384
435
|
pythonEvidence: py.evidence,
|
|
385
436
|
files,
|
|
386
437
|
});
|
|
438
|
+
const languages = languagesFrom(scan.byExtension).slice(0, 12);
|
|
439
|
+
if (ecosystems.size === 0 && languages.length > 0) {
|
|
440
|
+
warnings.push(`No ecosystem was recognised although source files were found (${languages.slice(0, 3).map((l) => l.name).join(', ')}). Package manifests may be missing or unsupported; record verification commands by hand in .devworkspace/context/commands.json.`);
|
|
441
|
+
}
|
|
442
|
+
if (ecosystems.size > 0 && !commands.some((c) => c.available)) {
|
|
443
|
+
warnings.push('No verification commands (tests, lint, typecheck, build) were found in the package manifests.');
|
|
444
|
+
}
|
|
387
445
|
if (scan.truncated)
|
|
388
446
|
warnings.push('Scan hit its file or depth limit; discovery is partial.');
|
|
389
447
|
if (scan.sensitiveFiles.length > 0)
|
|
@@ -400,13 +458,14 @@ export async function detectProject(input) {
|
|
|
400
458
|
skippedSymlinks: scan.skippedSymlinks,
|
|
401
459
|
ignoredEntries: scan.ignoredEntries,
|
|
402
460
|
},
|
|
403
|
-
languages
|
|
461
|
+
languages,
|
|
404
462
|
topDirs: scan.topDirs,
|
|
405
463
|
ecosystems: [...ecosystems].sort(),
|
|
406
464
|
packageManager,
|
|
407
465
|
frameworks: [...frameworks.values()].sort((a, b) => a.name.localeCompare(b.name)),
|
|
408
466
|
monorepo: { tool: monoTool, packages },
|
|
409
467
|
commands,
|
|
468
|
+
manifests: [...nodePkgs.map((p) => p.manifest), ...['pyproject.toml', 'requirements.txt', 'Pipfile', 'go.mod', 'Cargo.toml', 'pom.xml', 'build.gradle', 'build.gradle.kts', 'composer.json', 'Gemfile'].filter(has)].sort(),
|
|
410
469
|
testConfigs,
|
|
411
470
|
ci,
|
|
412
471
|
docker,
|