gentle-pi 0.1.19 → 0.1.21
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/extensions/sdd-init.ts +513 -113
- package/package.json +1 -1
package/extensions/sdd-init.ts
CHANGED
|
@@ -5,11 +5,11 @@ import {
|
|
|
5
5
|
readdirSync,
|
|
6
6
|
writeFileSync,
|
|
7
7
|
} from "node:fs";
|
|
8
|
-
import { basename, dirname, join } from "node:path";
|
|
8
|
+
import { basename, dirname, join, relative } from "node:path";
|
|
9
9
|
type ExtensionAPI = any;
|
|
10
10
|
|
|
11
11
|
const CONFIG_REL_PATH = "openspec/config.yaml";
|
|
12
|
-
const MAX_SCAN_FILES =
|
|
12
|
+
const MAX_SCAN_FILES = 20_000;
|
|
13
13
|
const IGNORED_DIRS = new Set([
|
|
14
14
|
".git",
|
|
15
15
|
".hg",
|
|
@@ -36,21 +36,32 @@ interface PackageJson {
|
|
|
36
36
|
peerDependencies?: Record<string, string>;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
interface CommandInfo {
|
|
40
|
+
scope: string;
|
|
41
|
+
command: string;
|
|
42
|
+
framework: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
39
45
|
interface Detection {
|
|
40
46
|
projectName: string;
|
|
41
47
|
stack: string[];
|
|
42
|
-
|
|
48
|
+
packageManagers: string[];
|
|
43
49
|
markers: string[];
|
|
50
|
+
evidence: string[];
|
|
44
51
|
testCommand?: string;
|
|
45
52
|
testFramework?: string;
|
|
46
53
|
coverageCommand?: string;
|
|
47
54
|
lintCommand?: string;
|
|
48
55
|
typecheckCommand?: string;
|
|
49
56
|
formatCommand?: string;
|
|
50
|
-
|
|
51
|
-
unit
|
|
52
|
-
integration
|
|
53
|
-
e2e
|
|
57
|
+
commands: {
|
|
58
|
+
unit: CommandInfo[];
|
|
59
|
+
integration: CommandInfo[];
|
|
60
|
+
e2e: CommandInfo[];
|
|
61
|
+
coverage: CommandInfo[];
|
|
62
|
+
lint: CommandInfo[];
|
|
63
|
+
typecheck: CommandInfo[];
|
|
64
|
+
format: CommandInfo[];
|
|
54
65
|
};
|
|
55
66
|
}
|
|
56
67
|
|
|
@@ -94,7 +105,7 @@ function walkProject(cwd: string): string[] {
|
|
|
94
105
|
if (!IGNORED_DIRS.has(entry.name)) stack.push(join(dir, entry.name));
|
|
95
106
|
continue;
|
|
96
107
|
}
|
|
97
|
-
if (entry.isFile()) out.push(join(dir, entry.name)
|
|
108
|
+
if (entry.isFile()) out.push(relative(cwd, join(dir, entry.name)));
|
|
98
109
|
}
|
|
99
110
|
}
|
|
100
111
|
return out.sort();
|
|
@@ -108,15 +119,24 @@ function deps(pkg: PackageJson | undefined): Set<string> {
|
|
|
108
119
|
]);
|
|
109
120
|
}
|
|
110
121
|
|
|
111
|
-
function
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (
|
|
122
|
+
function detectPackageManagerAt(
|
|
123
|
+
cwd: string,
|
|
124
|
+
relDir: string,
|
|
125
|
+
): string | undefined {
|
|
126
|
+
const base = join(cwd, relDir);
|
|
127
|
+
if (existsSync(join(base, "pnpm-lock.yaml"))) return "pnpm";
|
|
128
|
+
if (existsSync(join(base, "yarn.lock"))) return "yarn";
|
|
129
|
+
if (existsSync(join(base, "bun.lockb")) || existsSync(join(base, "bun.lock")))
|
|
130
|
+
return "bun";
|
|
131
|
+
if (existsSync(join(base, "package-lock.json"))) return "npm";
|
|
132
|
+
if (existsSync(join(base, "package.json"))) return "npm";
|
|
117
133
|
return undefined;
|
|
118
134
|
}
|
|
119
135
|
|
|
136
|
+
function commandInScope(scope: string, command: string): string {
|
|
137
|
+
return scope === "." ? command : `cd ${scope} && ${command}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
120
140
|
function runScript(pm: string | undefined, script: string): string {
|
|
121
141
|
if (pm === "yarn") return `yarn ${script}`;
|
|
122
142
|
if (pm === "bun") return `bun run ${script}`;
|
|
@@ -127,117 +147,369 @@ function scriptCommand(
|
|
|
127
147
|
pm: string | undefined,
|
|
128
148
|
scripts: Record<string, string> | undefined,
|
|
129
149
|
candidates: string[],
|
|
130
|
-
): string | undefined {
|
|
150
|
+
): { name: string; command: string } | undefined {
|
|
131
151
|
if (!scripts) return undefined;
|
|
132
152
|
for (const name of candidates) {
|
|
133
|
-
if (scripts[name])
|
|
134
|
-
|
|
153
|
+
if (!scripts[name]) continue;
|
|
154
|
+
const command =
|
|
155
|
+
name === "test" && pm !== "bun"
|
|
135
156
|
? `${pm ?? "npm"} test`
|
|
136
157
|
: runScript(pm, name);
|
|
158
|
+
return { name, command };
|
|
137
159
|
}
|
|
138
160
|
return undefined;
|
|
139
161
|
}
|
|
140
162
|
|
|
141
|
-
function
|
|
142
|
-
const pkg = readJson<PackageJson>(join(cwd, "package.json"));
|
|
163
|
+
function addUnique(list: CommandInfo[], command: CommandInfo): void {
|
|
143
164
|
if (
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
f.endsWith(".ts") ||
|
|
148
|
-
f.endsWith(".tsx") ||
|
|
149
|
-
f.endsWith(".js") ||
|
|
150
|
-
f.endsWith(".jsx"),
|
|
165
|
+
list.some(
|
|
166
|
+
(item) =>
|
|
167
|
+
item.scope === command.scope && item.command === command.command,
|
|
151
168
|
)
|
|
152
169
|
)
|
|
153
170
|
return;
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
171
|
+
list.push(command);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function addMarker(detection: Detection, marker: string): void {
|
|
175
|
+
if (!detection.markers.includes(marker)) detection.markers.push(marker);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function addStack(detection: Detection, stack: string): void {
|
|
179
|
+
if (!detection.stack.includes(stack)) detection.stack.push(stack);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function addEvidence(detection: Detection, evidence: string): void {
|
|
183
|
+
if (!detection.evidence.includes(evidence)) detection.evidence.push(evidence);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
interface GenericHint {
|
|
187
|
+
marker: string;
|
|
188
|
+
stack: string;
|
|
189
|
+
testCommand?: string;
|
|
190
|
+
framework?: string;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const GENERIC_HINTS: GenericHint[] = [
|
|
194
|
+
{
|
|
195
|
+
marker: "mix.exs",
|
|
196
|
+
stack: "Elixir",
|
|
197
|
+
testCommand: "mix test",
|
|
198
|
+
framework: "ExUnit",
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
marker: "rebar.config",
|
|
202
|
+
stack: "Erlang",
|
|
203
|
+
testCommand: "rebar3 eunit",
|
|
204
|
+
framework: "EUnit",
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
marker: "gleam.toml",
|
|
208
|
+
stack: "Gleam",
|
|
209
|
+
testCommand: "gleam test",
|
|
210
|
+
framework: "gleam test",
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
marker: "deno.json",
|
|
214
|
+
stack: "Deno",
|
|
215
|
+
testCommand: "deno test",
|
|
216
|
+
framework: "deno test",
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
marker: "deno.jsonc",
|
|
220
|
+
stack: "Deno",
|
|
221
|
+
testCommand: "deno test",
|
|
222
|
+
framework: "deno test",
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
marker: "Gemfile",
|
|
226
|
+
stack: "Ruby",
|
|
227
|
+
testCommand: "bundle exec rake test",
|
|
228
|
+
framework: "Ruby test task",
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
marker: "composer.json",
|
|
232
|
+
stack: "PHP",
|
|
233
|
+
testCommand: "composer test",
|
|
234
|
+
framework: "Composer test script",
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
marker: "pom.xml",
|
|
238
|
+
stack: "Java/Maven",
|
|
239
|
+
testCommand: "mvn test",
|
|
240
|
+
framework: "Maven test",
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
marker: "build.gradle",
|
|
244
|
+
stack: "Java/Gradle",
|
|
245
|
+
testCommand: "./gradlew test",
|
|
246
|
+
framework: "Gradle test",
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
marker: "build.gradle.kts",
|
|
250
|
+
stack: "Java/Kotlin Gradle",
|
|
251
|
+
testCommand: "./gradlew test",
|
|
252
|
+
framework: "Gradle test",
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
marker: "pubspec.yaml",
|
|
256
|
+
stack: "Dart/Flutter",
|
|
257
|
+
testCommand: "dart test",
|
|
258
|
+
framework: "Dart test",
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
marker: "dune-project",
|
|
262
|
+
stack: "OCaml",
|
|
263
|
+
testCommand: "dune runtest",
|
|
264
|
+
framework: "Dune runtest",
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
marker: "shard.yml",
|
|
268
|
+
stack: "Crystal",
|
|
269
|
+
testCommand: "crystal spec",
|
|
270
|
+
framework: "Crystal spec",
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
marker: "stack.yaml",
|
|
274
|
+
stack: "Haskell",
|
|
275
|
+
testCommand: "stack test",
|
|
276
|
+
framework: "Stack test",
|
|
277
|
+
},
|
|
278
|
+
];
|
|
279
|
+
|
|
280
|
+
function setPrimaryTest(
|
|
281
|
+
detection: Detection,
|
|
282
|
+
command: CommandInfo,
|
|
283
|
+
prefer = false,
|
|
284
|
+
): void {
|
|
285
|
+
if (!detection.testCommand || prefer) {
|
|
286
|
+
detection.testCommand = command.command;
|
|
287
|
+
detection.testFramework = command.framework;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function packageDirs(files: string[]): string[] {
|
|
292
|
+
const dirs = files
|
|
293
|
+
.filter((file) => basename(file) === "package.json")
|
|
294
|
+
.map((file) => (dirname(file) === "." ? "." : dirname(file)));
|
|
295
|
+
return [...new Set(dirs)].sort(
|
|
296
|
+
(a, b) => a.split("/").length - b.split("/").length || a.localeCompare(b),
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function filesInScope(files: string[], scope: string): string[] {
|
|
301
|
+
if (scope === ".") return files;
|
|
302
|
+
const prefix = `${scope}/`;
|
|
303
|
+
return files
|
|
304
|
+
.filter((file) => file.startsWith(prefix))
|
|
305
|
+
.map((file) => file.slice(prefix.length));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function detectNodePackage(
|
|
309
|
+
cwd: string,
|
|
310
|
+
files: string[],
|
|
311
|
+
scope: string,
|
|
312
|
+
detection: Detection,
|
|
313
|
+
): void {
|
|
314
|
+
const pkg = readJson<PackageJson>(join(cwd, scope, "package.json"));
|
|
315
|
+
if (!pkg) return;
|
|
316
|
+
const scopedFiles = filesInScope(files, scope);
|
|
317
|
+
const pm = detectPackageManagerAt(cwd, scope);
|
|
318
|
+
if (pm && !detection.packageManagers.includes(pm))
|
|
319
|
+
detection.packageManagers.push(pm);
|
|
320
|
+
addStack(
|
|
321
|
+
detection,
|
|
322
|
+
pkg.type === "module" ? "Node.js/TypeScript ESM" : "Node.js/TypeScript",
|
|
323
|
+
);
|
|
324
|
+
if (pkg.name && detection.projectName === basename(cwd))
|
|
325
|
+
detection.projectName = pkg.name;
|
|
326
|
+
addMarker(
|
|
327
|
+
detection,
|
|
328
|
+
scope === "." ? "package.json" : `${scope}/package.json`,
|
|
158
329
|
);
|
|
159
|
-
if (
|
|
160
|
-
|
|
161
|
-
if (
|
|
162
|
-
|
|
330
|
+
if (existsSync(join(cwd, scope, "tsconfig.json")))
|
|
331
|
+
addMarker(detection, `${scope === "." ? "" : `${scope}/`}tsconfig.json`);
|
|
332
|
+
if (pm)
|
|
333
|
+
addMarker(
|
|
334
|
+
detection,
|
|
335
|
+
`${scope === "." ? "" : `${scope}/`}${pm} package manager`,
|
|
336
|
+
);
|
|
163
337
|
|
|
164
338
|
const allDeps = deps(pkg);
|
|
165
|
-
if (allDeps.has("react")) detection
|
|
166
|
-
if (allDeps.has("next")) detection
|
|
167
|
-
if (allDeps.has("vue")) detection
|
|
168
|
-
if (allDeps.has("svelte")) detection
|
|
339
|
+
if (allDeps.has("react")) addStack(detection, "React");
|
|
340
|
+
if (allDeps.has("next")) addStack(detection, "Next.js");
|
|
341
|
+
if (allDeps.has("vue")) addStack(detection, "Vue");
|
|
342
|
+
if (allDeps.has("svelte")) addStack(detection, "Svelte");
|
|
169
343
|
if (allDeps.has("@earendil-works/pi-coding-agent"))
|
|
170
|
-
detection
|
|
344
|
+
addStack(detection, "Pi extension package");
|
|
171
345
|
|
|
172
|
-
|
|
346
|
+
let unit = scriptCommand(pm, pkg.scripts, [
|
|
347
|
+
"test:run",
|
|
173
348
|
"test",
|
|
174
349
|
"vitest",
|
|
175
350
|
"jest",
|
|
176
351
|
"unit",
|
|
177
352
|
]);
|
|
178
|
-
if (!
|
|
353
|
+
if (!unit) {
|
|
179
354
|
if (
|
|
180
355
|
allDeps.has("vitest") ||
|
|
181
|
-
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
else if (
|
|
356
|
+
scopedFiles.some((file) => /^vitest\.config\./.test(basename(file)))
|
|
357
|
+
) {
|
|
358
|
+
unit = { name: "vitest", command: runScript(pm, "vitest") };
|
|
359
|
+
} else if (
|
|
185
360
|
allDeps.has("jest") ||
|
|
186
|
-
|
|
187
|
-
)
|
|
188
|
-
|
|
361
|
+
scopedFiles.some((file) => /^jest\.config\./.test(basename(file)))
|
|
362
|
+
) {
|
|
363
|
+
unit = { name: "jest", command: runScript(pm, "jest") };
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (unit) {
|
|
367
|
+
const framework =
|
|
368
|
+
allDeps.has("vitest") || /vitest/i.test(unit.command)
|
|
369
|
+
? "Vitest"
|
|
370
|
+
: allDeps.has("jest") || /jest/i.test(unit.command)
|
|
371
|
+
? "Jest"
|
|
372
|
+
: "package script";
|
|
373
|
+
const info = {
|
|
374
|
+
scope,
|
|
375
|
+
command: commandInScope(scope, unit.command),
|
|
376
|
+
framework,
|
|
377
|
+
};
|
|
378
|
+
addUnique(detection.commands.unit, info);
|
|
379
|
+
setPrimaryTest(detection, info);
|
|
189
380
|
}
|
|
190
|
-
if (allDeps.has("vitest")) detection.testFramework = "Vitest";
|
|
191
|
-
else if (allDeps.has("jest")) detection.testFramework = "Jest";
|
|
192
|
-
else if (detection.testCommand) detection.testFramework = "package script";
|
|
193
381
|
|
|
194
|
-
|
|
195
|
-
"
|
|
382
|
+
const integration = scriptCommand(pm, pkg.scripts, [
|
|
383
|
+
"test:integration",
|
|
384
|
+
"integration",
|
|
385
|
+
]);
|
|
386
|
+
if (integration)
|
|
387
|
+
addUnique(detection.commands.integration, {
|
|
388
|
+
scope,
|
|
389
|
+
command: commandInScope(scope, integration.command),
|
|
390
|
+
framework: "package integration script",
|
|
391
|
+
});
|
|
392
|
+
if (allDeps.has("@testing-library/react") || allDeps.has("supertest")) {
|
|
393
|
+
const framework = allDeps.has("supertest")
|
|
394
|
+
? "Supertest"
|
|
395
|
+
: "Testing Library";
|
|
396
|
+
if (unit)
|
|
397
|
+
addUnique(detection.commands.integration, {
|
|
398
|
+
scope,
|
|
399
|
+
command: commandInScope(scope, unit.command),
|
|
400
|
+
framework,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
let e2e = scriptCommand(pm, pkg.scripts, [
|
|
405
|
+
"test:e2e",
|
|
406
|
+
"e2e",
|
|
407
|
+
"playwright",
|
|
408
|
+
"cypress",
|
|
409
|
+
]);
|
|
410
|
+
if (!e2e) {
|
|
411
|
+
if (
|
|
412
|
+
allDeps.has("@playwright/test") ||
|
|
413
|
+
allDeps.has("playwright") ||
|
|
414
|
+
scopedFiles.some((file) => /^playwright\.config\./.test(basename(file)))
|
|
415
|
+
) {
|
|
416
|
+
e2e = { name: "playwright", command: "npx playwright test" };
|
|
417
|
+
} else if (
|
|
418
|
+
allDeps.has("cypress") ||
|
|
419
|
+
scopedFiles.some((file) => /^cypress\.config\./.test(basename(file)))
|
|
420
|
+
) {
|
|
421
|
+
e2e = { name: "cypress", command: "npx cypress run" };
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (e2e) {
|
|
425
|
+
const framework = /cypress/i.test(e2e.command) ? "Cypress" : "Playwright";
|
|
426
|
+
addUnique(detection.commands.e2e, {
|
|
427
|
+
scope,
|
|
428
|
+
command: commandInScope(scope, e2e.command),
|
|
429
|
+
framework,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const coverage = scriptCommand(pm, pkg.scripts, [
|
|
196
434
|
"test:coverage",
|
|
435
|
+
"coverage",
|
|
197
436
|
]);
|
|
198
|
-
|
|
437
|
+
if (coverage)
|
|
438
|
+
addUnique(detection.commands.coverage, {
|
|
439
|
+
scope,
|
|
440
|
+
command: commandInScope(scope, coverage.command),
|
|
441
|
+
framework: "coverage",
|
|
442
|
+
});
|
|
443
|
+
const lint = scriptCommand(pm, pkg.scripts, [
|
|
199
444
|
"lint",
|
|
445
|
+
"lint:check",
|
|
200
446
|
"check:lint",
|
|
201
447
|
]);
|
|
202
|
-
|
|
448
|
+
if (lint)
|
|
449
|
+
addUnique(detection.commands.lint, {
|
|
450
|
+
scope,
|
|
451
|
+
command: commandInScope(scope, lint.command),
|
|
452
|
+
framework: "linter",
|
|
453
|
+
});
|
|
454
|
+
const typecheck = scriptCommand(pm, pkg.scripts, [
|
|
203
455
|
"typecheck",
|
|
204
456
|
"type-check",
|
|
205
457
|
"check:types",
|
|
206
458
|
]);
|
|
207
|
-
|
|
459
|
+
if (typecheck)
|
|
460
|
+
addUnique(detection.commands.typecheck, {
|
|
461
|
+
scope,
|
|
462
|
+
command: commandInScope(scope, typecheck.command),
|
|
463
|
+
framework: "type checker",
|
|
464
|
+
});
|
|
465
|
+
const format = scriptCommand(pm, pkg.scripts, [
|
|
208
466
|
"format",
|
|
467
|
+
"format:check",
|
|
209
468
|
"fmt",
|
|
210
469
|
"prettier",
|
|
211
470
|
]);
|
|
471
|
+
if (format)
|
|
472
|
+
addUnique(detection.commands.format, {
|
|
473
|
+
scope,
|
|
474
|
+
command: commandInScope(scope, format.command),
|
|
475
|
+
framework: "formatter",
|
|
476
|
+
});
|
|
477
|
+
}
|
|
212
478
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if (
|
|
216
|
-
detection
|
|
217
|
-
|
|
218
|
-
detection.testLayers.e2e = "Playwright";
|
|
219
|
-
else if (allDeps.has("cypress")) detection.testLayers.e2e = "Cypress";
|
|
479
|
+
function detectNode(cwd: string, files: string[], detection: Detection): void {
|
|
480
|
+
const dirs = packageDirs(files);
|
|
481
|
+
if (dirs.length === 0 && files.some((file) => /\.[cm]?[tj]sx?$/.test(file)))
|
|
482
|
+
addStack(detection, "Node.js/TypeScript");
|
|
483
|
+
for (const dir of dirs) detectNodePackage(cwd, files, dir, detection);
|
|
220
484
|
}
|
|
221
485
|
|
|
222
486
|
function detectGo(cwd: string, files: string[], detection: Detection): void {
|
|
223
487
|
if (!hasFile(cwd, "go.mod")) return;
|
|
224
|
-
detection
|
|
225
|
-
detection
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
detection
|
|
229
|
-
if (files.some((
|
|
230
|
-
detection.
|
|
231
|
-
|
|
488
|
+
addStack(detection, "Go");
|
|
489
|
+
addMarker(detection, "go.mod");
|
|
490
|
+
const info = { scope: ".", command: "go test ./...", framework: "go test" };
|
|
491
|
+
addUnique(detection.commands.unit, info);
|
|
492
|
+
setPrimaryTest(detection, info);
|
|
493
|
+
if (files.some((file) => file.endsWith("_test.go")))
|
|
494
|
+
addUnique(detection.commands.integration, {
|
|
495
|
+
scope: ".",
|
|
496
|
+
command: "go test ./...",
|
|
497
|
+
framework: "Go integration tests where present",
|
|
498
|
+
});
|
|
499
|
+
addUnique(detection.commands.coverage, {
|
|
500
|
+
scope: ".",
|
|
501
|
+
command: "go test -cover ./...",
|
|
502
|
+
framework: "go coverage",
|
|
503
|
+
});
|
|
232
504
|
}
|
|
233
505
|
|
|
234
506
|
function detectRust(cwd: string, detection: Detection): void {
|
|
235
507
|
if (!hasFile(cwd, "Cargo.toml")) return;
|
|
236
|
-
detection
|
|
237
|
-
detection
|
|
238
|
-
|
|
239
|
-
detection.
|
|
240
|
-
detection
|
|
508
|
+
addStack(detection, "Rust");
|
|
509
|
+
addMarker(detection, "Cargo.toml");
|
|
510
|
+
const info = { scope: ".", command: "cargo test", framework: "cargo test" };
|
|
511
|
+
addUnique(detection.commands.unit, info);
|
|
512
|
+
setPrimaryTest(detection, info);
|
|
241
513
|
}
|
|
242
514
|
|
|
243
515
|
function detectPython(
|
|
@@ -249,41 +521,102 @@ function detectPython(
|
|
|
249
521
|
hasFile(cwd, "pyproject.toml") ||
|
|
250
522
|
hasFile(cwd, "requirements.txt") ||
|
|
251
523
|
hasFile(cwd, "pytest.ini") ||
|
|
252
|
-
files.some((
|
|
524
|
+
files.some((file) => file.endsWith(".py"));
|
|
253
525
|
if (!hasPython) return;
|
|
254
|
-
detection
|
|
526
|
+
addStack(detection, "Python");
|
|
255
527
|
for (const marker of ["pyproject.toml", "requirements.txt", "pytest.ini"]) {
|
|
256
|
-
if (hasFile(cwd, marker)) detection
|
|
528
|
+
if (hasFile(cwd, marker)) addMarker(detection, marker);
|
|
257
529
|
}
|
|
258
530
|
if (
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
531
|
+
files.some(
|
|
532
|
+
(file) =>
|
|
533
|
+
file.startsWith("tests/") ||
|
|
534
|
+
file.endsWith("_test.py") ||
|
|
535
|
+
basename(file) === "conftest.py",
|
|
536
|
+
)
|
|
262
537
|
) {
|
|
263
|
-
|
|
264
|
-
detection.
|
|
265
|
-
detection
|
|
538
|
+
const info = { scope: ".", command: "pytest", framework: "pytest" };
|
|
539
|
+
addUnique(detection.commands.unit, info);
|
|
540
|
+
setPrimaryTest(detection, info);
|
|
266
541
|
}
|
|
267
542
|
}
|
|
268
543
|
|
|
544
|
+
function detectGenericHints(
|
|
545
|
+
cwd: string,
|
|
546
|
+
files: string[],
|
|
547
|
+
detection: Detection,
|
|
548
|
+
): void {
|
|
549
|
+
for (const hint of GENERIC_HINTS) {
|
|
550
|
+
if (!hasFile(cwd, hint.marker)) continue;
|
|
551
|
+
addStack(detection, hint.stack);
|
|
552
|
+
addMarker(detection, hint.marker);
|
|
553
|
+
addEvidence(
|
|
554
|
+
detection,
|
|
555
|
+
`${hint.stack} manifest detected via ${hint.marker}`,
|
|
556
|
+
);
|
|
557
|
+
if (hint.testCommand && hint.framework) {
|
|
558
|
+
const info = {
|
|
559
|
+
scope: ".",
|
|
560
|
+
command: hint.testCommand,
|
|
561
|
+
framework: hint.framework,
|
|
562
|
+
};
|
|
563
|
+
addUnique(detection.commands.unit, info);
|
|
564
|
+
setPrimaryTest(detection, info);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const testFiles = files.filter(
|
|
569
|
+
(file) =>
|
|
570
|
+
/(^|\/)(test|tests|spec|specs)(\/|$)/i.test(file) ||
|
|
571
|
+
/[._-](test|spec)\.[^/]+$/i.test(file),
|
|
572
|
+
);
|
|
573
|
+
if (testFiles.length > 0) {
|
|
574
|
+
addEvidence(
|
|
575
|
+
detection,
|
|
576
|
+
`Test-like files detected (${testFiles.length}); examples: ${testFiles.slice(0, 5).join(", ")}`,
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
if (detection.stack.length === 0)
|
|
580
|
+
addStack(detection, "Unclassified software project");
|
|
581
|
+
}
|
|
582
|
+
|
|
269
583
|
function detectMakefile(cwd: string, detection: Detection): void {
|
|
270
|
-
const makefile = ["Makefile", "makefile"].find((
|
|
584
|
+
const makefile = ["Makefile", "makefile"].find((file) => hasFile(cwd, file));
|
|
271
585
|
if (!makefile) return;
|
|
272
|
-
detection
|
|
586
|
+
addMarker(detection, makefile);
|
|
273
587
|
let content = "";
|
|
274
588
|
try {
|
|
275
589
|
content = readFileSync(join(cwd, makefile), "utf8");
|
|
276
590
|
} catch {
|
|
277
591
|
return;
|
|
278
592
|
}
|
|
279
|
-
if (
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
593
|
+
if (/^test:/m.test(content)) {
|
|
594
|
+
const info = {
|
|
595
|
+
scope: ".",
|
|
596
|
+
command: "make test",
|
|
597
|
+
framework: "pytest via Makefile",
|
|
598
|
+
};
|
|
599
|
+
addUnique(detection.commands.unit, info);
|
|
600
|
+
setPrimaryTest(detection, info, true);
|
|
601
|
+
}
|
|
602
|
+
if (/^coverage:/m.test(content))
|
|
603
|
+
addUnique(detection.commands.coverage, {
|
|
604
|
+
scope: ".",
|
|
605
|
+
command: "make coverage",
|
|
606
|
+
framework: "coverage",
|
|
607
|
+
});
|
|
608
|
+
if (/^lint:/m.test(content))
|
|
609
|
+
addUnique(detection.commands.lint, {
|
|
610
|
+
scope: ".",
|
|
611
|
+
command: "make lint",
|
|
612
|
+
framework: "linter",
|
|
613
|
+
});
|
|
614
|
+
if (/^(fmt|format):/m.test(content))
|
|
615
|
+
addUnique(detection.commands.format, {
|
|
616
|
+
scope: ".",
|
|
617
|
+
command: /^fmt:/m.test(content) ? "make fmt" : "make format",
|
|
618
|
+
framework: "formatter",
|
|
619
|
+
});
|
|
287
620
|
}
|
|
288
621
|
|
|
289
622
|
function detectProject(cwd: string): Detection {
|
|
@@ -291,42 +624,93 @@ function detectProject(cwd: string): Detection {
|
|
|
291
624
|
const detection: Detection = {
|
|
292
625
|
projectName: basename(cwd),
|
|
293
626
|
stack: [],
|
|
627
|
+
packageManagers: [],
|
|
294
628
|
markers: [],
|
|
295
|
-
|
|
629
|
+
evidence: [],
|
|
630
|
+
commands: {
|
|
631
|
+
unit: [],
|
|
632
|
+
integration: [],
|
|
633
|
+
e2e: [],
|
|
634
|
+
coverage: [],
|
|
635
|
+
lint: [],
|
|
636
|
+
typecheck: [],
|
|
637
|
+
format: [],
|
|
638
|
+
},
|
|
296
639
|
};
|
|
297
640
|
detectNode(cwd, files, detection);
|
|
298
641
|
detectGo(cwd, files, detection);
|
|
299
642
|
detectRust(cwd, detection);
|
|
300
643
|
detectPython(cwd, files, detection);
|
|
644
|
+
detectGenericHints(cwd, files, detection);
|
|
301
645
|
detectMakefile(cwd, detection);
|
|
302
|
-
if (hasFile(cwd, ".github/workflows"))
|
|
303
|
-
|
|
304
|
-
detection.
|
|
305
|
-
detection.
|
|
646
|
+
if (hasFile(cwd, ".github/workflows")) addMarker(detection, "GitHub Actions");
|
|
647
|
+
detection.coverageCommand = detection.commands.coverage[0]?.command;
|
|
648
|
+
detection.lintCommand = detection.commands.lint[0]?.command;
|
|
649
|
+
detection.typecheckCommand = detection.commands.typecheck[0]?.command;
|
|
650
|
+
detection.formatCommand = detection.commands.format[0]?.command;
|
|
306
651
|
return detection;
|
|
307
652
|
}
|
|
308
653
|
|
|
654
|
+
function commandSummary(commands: CommandInfo[]): string {
|
|
655
|
+
if (commands.length === 0) return "none";
|
|
656
|
+
return commands
|
|
657
|
+
.map((command) => `${command.framework} (${command.command})`)
|
|
658
|
+
.join("; ");
|
|
659
|
+
}
|
|
660
|
+
|
|
309
661
|
function renderContext(detection: Detection): string {
|
|
310
662
|
const lines = [
|
|
311
663
|
`${detection.projectName} is a ${detection.stack.length > 0 ? detection.stack.join(", ") : "software"} project.`,
|
|
312
664
|
`Detected markers: ${detection.markers.length > 0 ? detection.markers.join(", ") : "none"}.`,
|
|
313
665
|
];
|
|
314
|
-
if (detection.
|
|
315
|
-
lines.push(`Package
|
|
666
|
+
if (detection.packageManagers.length > 0)
|
|
667
|
+
lines.push(`Package managers: ${detection.packageManagers.join(", ")}.`);
|
|
668
|
+
if (detection.evidence.length > 0)
|
|
669
|
+
lines.push(`Additional evidence: ${detection.evidence.join("; ")}.`);
|
|
316
670
|
if (detection.testCommand)
|
|
317
671
|
lines.push(`Primary test command: ${detection.testCommand}.`);
|
|
318
672
|
else
|
|
319
673
|
lines.push(
|
|
320
674
|
"No reliable test runner was detected; verify testing manually before enabling strict TDD.",
|
|
321
675
|
);
|
|
676
|
+
lines.push(`Unit tests: ${commandSummary(detection.commands.unit)}.`);
|
|
677
|
+
lines.push(
|
|
678
|
+
`Integration tests: ${commandSummary(detection.commands.integration)}.`,
|
|
679
|
+
);
|
|
680
|
+
lines.push(`E2E tests: ${commandSummary(detection.commands.e2e)}.`);
|
|
322
681
|
return lines.join("\n");
|
|
323
682
|
}
|
|
324
683
|
|
|
684
|
+
function pushCommandList(
|
|
685
|
+
lines: string[],
|
|
686
|
+
indent: string,
|
|
687
|
+
commands: CommandInfo[],
|
|
688
|
+
): void {
|
|
689
|
+
if (commands.length === 0) {
|
|
690
|
+
lines.push(`${indent}[]`);
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
for (const command of commands) {
|
|
694
|
+
lines.push(`${indent}- scope: ${yamlString(command.scope)}`);
|
|
695
|
+
lines.push(`${indent} command: ${yamlString(command.command)}`);
|
|
696
|
+
lines.push(`${indent} framework: ${yamlString(command.framework)}`);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
325
700
|
function renderConfig(detection: Detection): string {
|
|
326
701
|
const strictTdd = Boolean(detection.testCommand);
|
|
327
702
|
const testCommand = detection.testCommand ?? "";
|
|
328
703
|
const today = new Date().toISOString().slice(0, 10);
|
|
329
704
|
const context = renderContext(detection);
|
|
705
|
+
const unitLayer = detection.commands.unit
|
|
706
|
+
.map((command) => command.framework)
|
|
707
|
+
.join(", ");
|
|
708
|
+
const integrationLayer = detection.commands.integration
|
|
709
|
+
.map((command) => command.framework)
|
|
710
|
+
.join(", ");
|
|
711
|
+
const e2eLayer = detection.commands.e2e
|
|
712
|
+
.map((command) => command.framework)
|
|
713
|
+
.join(", ");
|
|
330
714
|
const lines = [
|
|
331
715
|
`strict_tdd: ${strictTdd}`,
|
|
332
716
|
"context: |",
|
|
@@ -350,17 +734,32 @@ function renderConfig(detection: Detection): string {
|
|
|
350
734
|
` command: ${yamlString(testCommand)}`,
|
|
351
735
|
` framework: ${yamlString(detection.testFramework ?? "")}`,
|
|
352
736
|
" layers:",
|
|
353
|
-
` unit: ${yamlString(
|
|
354
|
-
` integration: ${yamlString(
|
|
355
|
-
` e2e: ${yamlString(
|
|
356
|
-
"
|
|
357
|
-
|
|
358
|
-
"quality:",
|
|
359
|
-
` lint: ${yamlString(detection.lintCommand ?? "")}`,
|
|
360
|
-
` typecheck: ${yamlString(detection.typecheckCommand ?? "")}`,
|
|
361
|
-
` format: ${yamlString(detection.formatCommand ?? "")}`,
|
|
362
|
-
"",
|
|
737
|
+
` unit: ${yamlString(unitLayer)}`,
|
|
738
|
+
` integration: ${yamlString(integrationLayer)}`,
|
|
739
|
+
` e2e: ${yamlString(e2eLayer)}`,
|
|
740
|
+
" commands:",
|
|
741
|
+
" unit:",
|
|
363
742
|
];
|
|
743
|
+
pushCommandList(lines, " ", detection.commands.unit);
|
|
744
|
+
lines.push(" integration:");
|
|
745
|
+
pushCommandList(lines, " ", detection.commands.integration);
|
|
746
|
+
lines.push(" e2e:");
|
|
747
|
+
pushCommandList(lines, " ", detection.commands.e2e);
|
|
748
|
+
lines.push(" coverage:");
|
|
749
|
+
lines.push(` command: ${yamlString(detection.coverageCommand ?? "")}`);
|
|
750
|
+
lines.push(" commands:");
|
|
751
|
+
pushCommandList(lines, " ", detection.commands.coverage);
|
|
752
|
+
lines.push("quality:");
|
|
753
|
+
lines.push(` lint: ${yamlString(detection.lintCommand ?? "")}`);
|
|
754
|
+
lines.push(" lint_commands:");
|
|
755
|
+
pushCommandList(lines, " ", detection.commands.lint);
|
|
756
|
+
lines.push(` typecheck: ${yamlString(detection.typecheckCommand ?? "")}`);
|
|
757
|
+
lines.push(" typecheck_commands:");
|
|
758
|
+
pushCommandList(lines, " ", detection.commands.typecheck);
|
|
759
|
+
lines.push(` format: ${yamlString(detection.formatCommand ?? "")}`);
|
|
760
|
+
lines.push(" format_commands:");
|
|
761
|
+
pushCommandList(lines, " ", detection.commands.format);
|
|
762
|
+
lines.push("");
|
|
364
763
|
return lines.join("\n");
|
|
365
764
|
}
|
|
366
765
|
|
|
@@ -391,8 +790,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
391
790
|
const testSummary = detection.testCommand
|
|
392
791
|
? `strict TDD enabled with \`${detection.testCommand}\``
|
|
393
792
|
: "strict TDD disabled because no test runner was detected";
|
|
793
|
+
const layerSummary = `unit: ${detection.commands.unit.length}, integration: ${detection.commands.integration.length}, e2e: ${detection.commands.e2e.length}`;
|
|
394
794
|
ctx.ui.notify(
|
|
395
|
-
`Wrote ${CONFIG_REL_PATH}: detected ${detection.stack.join(", ") || "project"}; ${testSummary}.`,
|
|
795
|
+
`Wrote ${CONFIG_REL_PATH}: detected ${detection.stack.join(", ") || "project"}; ${testSummary}; tests found: ${layerSummary}.`,
|
|
396
796
|
"info",
|
|
397
797
|
);
|
|
398
798
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gentle-pi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.21",
|
|
4
4
|
"description": "Turn Pi into el Gentleman: a senior-architect development harness with SDD/OpenSpec, subagents, strict TDD evidence, review guardrails, and skill discovery.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|