sdd-cli 0.1.24 → 0.1.26
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/README.md +100 -662
- package/dist/commands/ai-autopilot.js +887 -811
- package/dist/commands/app-lifecycle.js +223 -8
- package/dist/commands/hello.js +1 -1
- package/package.json +7 -7
|
@@ -9,6 +9,41 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
10
|
const child_process_1 = require("child_process");
|
|
11
11
|
const config_1 = require("../config");
|
|
12
|
+
function collectFilesRecursive(root, maxDepth = 8) {
|
|
13
|
+
const results = [];
|
|
14
|
+
const walk = (current, depth) => {
|
|
15
|
+
if (depth > maxDepth) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const entries = fs_1.default.readdirSync(current, { withFileTypes: true });
|
|
19
|
+
for (const entry of entries) {
|
|
20
|
+
const full = path_1.default.join(current, entry.name);
|
|
21
|
+
const rel = path_1.default.relative(root, full).replace(/\\/g, "/");
|
|
22
|
+
if (entry.isDirectory()) {
|
|
23
|
+
if ([".git", "node_modules", "dist", "build", "target", "__pycache__", ".venv", "venv"].includes(entry.name.toLowerCase())) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
walk(full, depth + 1);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
results.push(rel);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
walk(root, 0);
|
|
34
|
+
return results;
|
|
35
|
+
}
|
|
36
|
+
function countJsTsTests(root, maxDepth = 8) {
|
|
37
|
+
const files = collectFilesRecursive(root, maxDepth)
|
|
38
|
+
.filter((rel) => /\.(jsx?|tsx?)$/i.test(rel))
|
|
39
|
+
.filter((rel) => /\.test\.|\.spec\.|__tests__\//i.test(rel));
|
|
40
|
+
let count = 0;
|
|
41
|
+
for (const rel of files) {
|
|
42
|
+
const raw = fs_1.default.readFileSync(path_1.default.join(root, rel), "utf-8");
|
|
43
|
+
count += (raw.match(/\b(test|it)\s*\(/g) || []).length;
|
|
44
|
+
}
|
|
45
|
+
return count;
|
|
46
|
+
}
|
|
12
47
|
function findFileRecursive(root, predicate, maxDepth = 4) {
|
|
13
48
|
const walk = (current, depth) => {
|
|
14
49
|
if (depth > maxDepth) {
|
|
@@ -73,14 +108,21 @@ function countTestsRecursive(root, maxDepth = 8) {
|
|
|
73
108
|
}
|
|
74
109
|
function run(command, args, cwd) {
|
|
75
110
|
let resolved = command;
|
|
76
|
-
if (process.platform === "win32"
|
|
77
|
-
|
|
111
|
+
if (process.platform === "win32") {
|
|
112
|
+
if (command === "npm") {
|
|
113
|
+
resolved = "npm.cmd";
|
|
114
|
+
}
|
|
115
|
+
else if (command === "mvn") {
|
|
116
|
+
resolved = "mvn.cmd";
|
|
117
|
+
}
|
|
78
118
|
}
|
|
79
119
|
const useShell = process.platform === "win32" && resolved.toLowerCase().endsWith(".cmd");
|
|
80
120
|
const result = useShell
|
|
81
121
|
? (0, child_process_1.spawnSync)([resolved, ...args].join(" "), { cwd, encoding: "utf-8", shell: true })
|
|
82
122
|
: (0, child_process_1.spawnSync)(resolved, args, { cwd, encoding: "utf-8", shell: false });
|
|
83
|
-
const
|
|
123
|
+
const rawOutput = `${result.stdout || ""}${result.stderr || ""}`.trim();
|
|
124
|
+
const merged = rawOutput || (result.error ? String(result.error.message || result.error) : "");
|
|
125
|
+
const output = merged.length > 3500 ? `${merged.slice(0, 3500)}\n...[truncated]` : merged;
|
|
84
126
|
return {
|
|
85
127
|
ok: result.status === 0,
|
|
86
128
|
command: [resolved, ...args].join(" "),
|
|
@@ -107,19 +149,58 @@ function runIfScript(cwd, script) {
|
|
|
107
149
|
return null;
|
|
108
150
|
}
|
|
109
151
|
}
|
|
110
|
-
function
|
|
152
|
+
function readPackageJson(cwd) {
|
|
111
153
|
const pkgPath = path_1.default.join(cwd, "package.json");
|
|
112
154
|
if (!fs_1.default.existsSync(pkgPath)) {
|
|
113
|
-
return
|
|
155
|
+
return null;
|
|
114
156
|
}
|
|
115
157
|
try {
|
|
116
|
-
|
|
117
|
-
const depCount = Object.keys(pkg.dependencies ?? {}).length + Object.keys(pkg.devDependencies ?? {}).length;
|
|
118
|
-
return depCount > 0;
|
|
158
|
+
return JSON.parse(fs_1.default.readFileSync(pkgPath, "utf-8"));
|
|
119
159
|
}
|
|
120
160
|
catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function packageNeedsInstall(cwd) {
|
|
165
|
+
const pkg = readPackageJson(cwd);
|
|
166
|
+
if (!pkg) {
|
|
121
167
|
return false;
|
|
122
168
|
}
|
|
169
|
+
const depCount = Object.keys(pkg.dependencies ?? {}).length + Object.keys(pkg.devDependencies ?? {}).length;
|
|
170
|
+
return depCount > 0;
|
|
171
|
+
}
|
|
172
|
+
function parseGoalProfile(context) {
|
|
173
|
+
const goal = normalizeText(context?.goalText ?? "");
|
|
174
|
+
const hasJava = /\bjava\b/.test(goal);
|
|
175
|
+
const hasReact = /\breact\b/.test(goal);
|
|
176
|
+
const relationalHints = [
|
|
177
|
+
"library",
|
|
178
|
+
"biblioteca",
|
|
179
|
+
"inventario",
|
|
180
|
+
"inventory",
|
|
181
|
+
"prestamo",
|
|
182
|
+
"prestamos",
|
|
183
|
+
"loan",
|
|
184
|
+
"loans",
|
|
185
|
+
"usuario",
|
|
186
|
+
"usuarios",
|
|
187
|
+
"user",
|
|
188
|
+
"users",
|
|
189
|
+
"book",
|
|
190
|
+
"books",
|
|
191
|
+
"cita",
|
|
192
|
+
"citas",
|
|
193
|
+
"appointment",
|
|
194
|
+
"appointments",
|
|
195
|
+
"hospital",
|
|
196
|
+
"gestion",
|
|
197
|
+
"management"
|
|
198
|
+
];
|
|
199
|
+
const relationalDataApp = relationalHints.some((hint) => goal.includes(hint));
|
|
200
|
+
return {
|
|
201
|
+
javaReactFullstack: hasJava && hasReact,
|
|
202
|
+
relationalDataApp
|
|
203
|
+
};
|
|
123
204
|
}
|
|
124
205
|
function basicQualityCheck(appDir) {
|
|
125
206
|
const required = ["README.md"];
|
|
@@ -192,6 +273,112 @@ function advancedQualityCheck(appDir, context) {
|
|
|
192
273
|
output: "Missing schemas.md (or equivalent schema markdown document)"
|
|
193
274
|
};
|
|
194
275
|
}
|
|
276
|
+
const profile = parseGoalProfile(context);
|
|
277
|
+
if (profile.relationalDataApp) {
|
|
278
|
+
const sqlSchema = findFileRecursive(appDir, (rel) => rel === "schema.sql" || rel.endsWith("/schema.sql")) ??
|
|
279
|
+
findFileRecursive(appDir, (rel) => rel.endsWith(".sql") && (rel.includes("schema") || rel.includes("init") || rel.includes("migration")), 10);
|
|
280
|
+
if (!sqlSchema) {
|
|
281
|
+
return {
|
|
282
|
+
ok: false,
|
|
283
|
+
command: "advanced-quality-check",
|
|
284
|
+
output: "Missing SQL schema file (expected schema.sql or migration .sql) for relational data app"
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
const schemaText = normalizeText(fs_1.default.readFileSync(path_1.default.join(appDir, schemaDoc), "utf-8"));
|
|
288
|
+
const readmeText = readme;
|
|
289
|
+
if (!/(postgres|postgresql|mysql|mariadb|sqlite)/.test(`${schemaText}\n${readmeText}`)) {
|
|
290
|
+
return {
|
|
291
|
+
ok: false,
|
|
292
|
+
command: "advanced-quality-check",
|
|
293
|
+
output: "Database technology not explicit in README/schemas (expected PostgreSQL/MySQL/MariaDB/SQLite)"
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (profile.javaReactFullstack) {
|
|
298
|
+
const hasBackendPom = fs_1.default.existsSync(path_1.default.join(appDir, "backend", "pom.xml"));
|
|
299
|
+
const hasFrontendPkg = fs_1.default.existsSync(path_1.default.join(appDir, "frontend", "package.json"));
|
|
300
|
+
if (!hasBackendPom || !hasFrontendPkg) {
|
|
301
|
+
return {
|
|
302
|
+
ok: false,
|
|
303
|
+
command: "advanced-quality-check",
|
|
304
|
+
output: "Expected Java+React fullstack structure: backend/pom.xml and frontend/package.json"
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
const frontendPkg = readPackageJson(path_1.default.join(appDir, "frontend"));
|
|
308
|
+
if (frontendPkg) {
|
|
309
|
+
const deps = { ...(frontendPkg.dependencies ?? {}), ...(frontendPkg.devDependencies ?? {}) };
|
|
310
|
+
if (typeof deps["react-query"] === "string") {
|
|
311
|
+
return {
|
|
312
|
+
ok: false,
|
|
313
|
+
command: "advanced-quality-check",
|
|
314
|
+
output: "Outdated frontend dependency detected: react-query. Use @tanstack/react-query."
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const backendRoot = path_1.default.join(appDir, "backend", "src", "main", "java");
|
|
319
|
+
if (!fs_1.default.existsSync(backendRoot)) {
|
|
320
|
+
return {
|
|
321
|
+
ok: false,
|
|
322
|
+
command: "advanced-quality-check",
|
|
323
|
+
output: "Missing backend/src/main/java for Java backend implementation"
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
const backendFiles = collectFilesRecursive(backendRoot, 12).filter((rel) => rel.toLowerCase().endsWith(".java"));
|
|
327
|
+
const hasDto = backendFiles.some((rel) => /\/dto\//i.test(`/${rel}`) || /dto\.java$/i.test(rel));
|
|
328
|
+
if (!hasDto) {
|
|
329
|
+
return {
|
|
330
|
+
ok: false,
|
|
331
|
+
command: "advanced-quality-check",
|
|
332
|
+
output: "Missing Java DTO layer (expected dto package and *Dto.java files)"
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
const hasRecord = backendFiles.some((rel) => /\brecord\b/.test(fs_1.default.readFileSync(path_1.default.join(backendRoot, rel), "utf-8")));
|
|
336
|
+
if (!hasRecord) {
|
|
337
|
+
return {
|
|
338
|
+
ok: false,
|
|
339
|
+
command: "advanced-quality-check",
|
|
340
|
+
output: "Missing Java record usage (expected at least one record for immutable transport/domain models)"
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
const serviceFiles = backendFiles.filter((rel) => /\/service\//i.test(`/${rel}`));
|
|
344
|
+
const repositoryFiles = backendFiles.filter((rel) => /\/repository\//i.test(`/${rel}`));
|
|
345
|
+
const hasServiceInterface = serviceFiles.some((rel) => /\binterface\b/.test(fs_1.default.readFileSync(path_1.default.join(backendRoot, rel), "utf-8")));
|
|
346
|
+
const hasRepositoryInterface = repositoryFiles.some((rel) => /\binterface\b/.test(fs_1.default.readFileSync(path_1.default.join(backendRoot, rel), "utf-8")));
|
|
347
|
+
if (!hasServiceInterface || !hasRepositoryInterface) {
|
|
348
|
+
return {
|
|
349
|
+
ok: false,
|
|
350
|
+
command: "advanced-quality-check",
|
|
351
|
+
output: "Missing service/repository interfaces in Java backend architecture"
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
const frontendRoot = path_1.default.join(appDir, "frontend", "src");
|
|
355
|
+
if (!fs_1.default.existsSync(frontendRoot)) {
|
|
356
|
+
return {
|
|
357
|
+
ok: false,
|
|
358
|
+
command: "advanced-quality-check",
|
|
359
|
+
output: "Missing frontend/src for React frontend implementation"
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
const frontendFiles = collectFilesRecursive(frontendRoot, 10);
|
|
363
|
+
const hasApiLayer = frontendFiles.some((rel) => /^api\//i.test(rel) || /\/api\//i.test(`/${rel}`));
|
|
364
|
+
const hasHooksLayer = frontendFiles.some((rel) => /^hooks\/use[A-Z].*\.(t|j)sx?$/i.test(rel) || /\/hooks\/use[A-Z]/.test(rel));
|
|
365
|
+
const hasComponentsLayer = frontendFiles.some((rel) => /^components\//i.test(rel) || /\/components\//i.test(`/${rel}`));
|
|
366
|
+
if (!hasApiLayer || !hasHooksLayer || !hasComponentsLayer) {
|
|
367
|
+
return {
|
|
368
|
+
ok: false,
|
|
369
|
+
command: "advanced-quality-check",
|
|
370
|
+
output: "Missing frontend layers (expected src/api, src/hooks/use*.ts(x), and src/components)"
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
const frontendTestCount = countJsTsTests(path_1.default.join(appDir, "frontend"), 10);
|
|
374
|
+
if (frontendTestCount < 3) {
|
|
375
|
+
return {
|
|
376
|
+
ok: false,
|
|
377
|
+
command: "advanced-quality-check",
|
|
378
|
+
output: `Expected at least 3 frontend tests for Java+React profile, found ${frontendTestCount}`
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
}
|
|
195
382
|
const dummyLocalDoc = findFileRecursive(appDir, (rel) => rel.includes("dummylocal") && rel.endsWith(".md")) ??
|
|
196
383
|
findFileRecursive(appDir, (rel) => rel.includes("dummy-local") && rel.endsWith(".md")) ??
|
|
197
384
|
findFileRecursive(appDir, (rel) => rel.includes("dummy_local") && rel.endsWith(".md"));
|
|
@@ -463,6 +650,34 @@ function runAppLifecycle(projectRoot, projectName, context) {
|
|
|
463
650
|
const build = runIfScript(appDir, "build");
|
|
464
651
|
if (build)
|
|
465
652
|
qualitySteps.push(build);
|
|
653
|
+
const backendDir = path_1.default.join(appDir, "backend");
|
|
654
|
+
if (fs_1.default.existsSync(path_1.default.join(backendDir, "pom.xml"))) {
|
|
655
|
+
if (hasCommand("mvn")) {
|
|
656
|
+
qualitySteps.push(run("mvn", ["-q", "test"], backendDir));
|
|
657
|
+
}
|
|
658
|
+
else {
|
|
659
|
+
qualitySteps.push({
|
|
660
|
+
ok: false,
|
|
661
|
+
command: "mvn -q test",
|
|
662
|
+
output: "Maven not available to validate Java backend"
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
const frontendDir = path_1.default.join(appDir, "frontend");
|
|
667
|
+
if (fs_1.default.existsSync(path_1.default.join(frontendDir, "package.json"))) {
|
|
668
|
+
if (packageNeedsInstall(frontendDir)) {
|
|
669
|
+
qualitySteps.push(run("npm", ["install"], frontendDir));
|
|
670
|
+
}
|
|
671
|
+
const feLint = runIfScript(frontendDir, "lint");
|
|
672
|
+
if (feLint)
|
|
673
|
+
qualitySteps.push(feLint);
|
|
674
|
+
const feTest = runIfScript(frontendDir, "test");
|
|
675
|
+
if (feTest)
|
|
676
|
+
qualitySteps.push(feTest);
|
|
677
|
+
const feBuild = runIfScript(frontendDir, "build");
|
|
678
|
+
if (feBuild)
|
|
679
|
+
qualitySteps.push(feBuild);
|
|
680
|
+
}
|
|
466
681
|
qualitySteps.push(advancedQualityCheck(appDir, context));
|
|
467
682
|
if (qualitySteps.length === 0) {
|
|
468
683
|
qualitySteps.push(basicQualityCheck(appDir));
|
package/dist/commands/hello.js
CHANGED
|
@@ -418,7 +418,7 @@ async function runHello(input, runQuestions) {
|
|
|
418
418
|
if (!lifecycleDisabled && !lifecycle.qualityPassed) {
|
|
419
419
|
const appDir = path_1.default.join(projectRoot, "generated-app");
|
|
420
420
|
const parsedAttempts = Number.parseInt(process.env.SDD_AI_REPAIR_MAX_ATTEMPTS ?? "", 10);
|
|
421
|
-
const maxRepairAttempts = Number.isFinite(parsedAttempts) && parsedAttempts > 0 ? parsedAttempts :
|
|
421
|
+
const maxRepairAttempts = Number.isFinite(parsedAttempts) && parsedAttempts > 0 ? parsedAttempts : 10;
|
|
422
422
|
printWhy("Quality gates failed. Attempting AI repair iterations.");
|
|
423
423
|
lifecycle.qualityDiagnostics.forEach((issue) => printWhy(`Quality issue: ${issue}`));
|
|
424
424
|
for (let attempt = 1; attempt <= maxRepairAttempts && !lifecycle.qualityPassed; attempt += 1) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sdd-cli",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "AI-orchestrated specification-driven delivery CLI that plans, validates, and ships production-ready software projects.",
|
|
3
|
+
"version": "0.1.26",
|
|
4
|
+
"description": "AI-orchestrated specification-driven delivery CLI that plans, validates, and ships production-ready software projects.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
7
7
|
"specification-driven-development",
|
|
@@ -25,11 +25,11 @@
|
|
|
25
25
|
"bugs": {
|
|
26
26
|
"url": "https://github.com/jdsalasca/sdd-tool/issues"
|
|
27
27
|
},
|
|
28
|
-
"bin": {
|
|
29
|
-
"sdd-cli": "dist/cli.js",
|
|
30
|
-
"sdd": "dist/cli.js",
|
|
31
|
-
"sdd-tool": "dist/cli.js"
|
|
32
|
-
},
|
|
28
|
+
"bin": {
|
|
29
|
+
"sdd-cli": "dist/cli.js",
|
|
30
|
+
"sdd": "dist/cli.js",
|
|
31
|
+
"sdd-tool": "dist/cli.js"
|
|
32
|
+
},
|
|
33
33
|
"main": "dist/cli.js",
|
|
34
34
|
"types": "dist/cli.d.ts",
|
|
35
35
|
"files": [
|