@devflow-core/dsh-devflow 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +13 -0
- package/README.md +82 -0
- package/assets/commands/devflow-adversarial.toml +11 -0
- package/assets/commands/devflow-audit.toml +32 -0
- package/assets/commands/devflow-debt.toml +42 -0
- package/assets/commands/devflow-find-fault.toml +11 -0
- package/assets/commands/devflow-learn.toml +21 -0
- package/assets/commands/devflow-plan.toml +58 -0
- package/assets/commands/devflow-prove.toml +20 -0
- package/assets/commands/devflow-pua.toml +40 -0
- package/assets/commands/devflow-review.toml +36 -0
- package/assets/commands/devflow-spec.toml +49 -0
- package/assets/commands/devflow.toml +35 -0
- package/assets/presets/devflow-2/NOTICE +4 -0
- package/assets/presets/devflow-2/README.md +71 -0
- package/assets/presets/devflow-2/agent.cordis.yml +337 -0
- package/assets/presets/devflow-2/custom-bash.mjs +213 -0
- package/assets/presets/devflow-2/preset.yml +3 -0
- package/assets/presets/devflow-2/tool-bootstrap.mjs +496 -0
- package/assets/scripts/devflow-audit.js +275 -0
- package/assets/scripts/devflow-debt.js +196 -0
- package/assets/scripts/devflow-doctor.js +90 -0
- package/assets/scripts/devflow-plan.js +638 -0
- package/assets/scripts/devflow-review.js +93 -0
- package/assets/scripts/devflow-spec.js +238 -0
- package/assets/skills/devflow-adversarial/SKILL.md +71 -0
- package/assets/skills/devflow-audit/SKILL.md +78 -0
- package/assets/skills/devflow-brainstorm/SKILL.md +176 -0
- package/assets/skills/devflow-brainstorm/references/interview-discipline.md +184 -0
- package/assets/skills/devflow-build/SKILL.md +238 -0
- package/assets/skills/devflow-build/references/build-methods.md +40 -0
- package/assets/skills/devflow-core/SKILL.md +93 -0
- package/assets/skills/devflow-core/references/core-methods.md +131 -0
- package/assets/skills/devflow-core/references/reference-projects.md +133 -0
- package/assets/skills/devflow-core/references/skill-guide.md +63 -0
- package/assets/skills/devflow-cut/SKILL.md +208 -0
- package/assets/skills/devflow-cut/references/cut-methods.md +65 -0
- package/assets/skills/devflow-cut/references/native-capability-checklist.md +112 -0
- package/assets/skills/devflow-docs-followup/SKILL.md +132 -0
- package/assets/skills/devflow-docs-followup/agents/openai.yaml +4 -0
- package/assets/skills/devflow-find-fault/SKILL.md +109 -0
- package/assets/skills/devflow-learn/SKILL.md +176 -0
- package/assets/skills/devflow-plan/SKILL.md +142 -0
- package/assets/skills/devflow-plan/references/plan-methods.md +74 -0
- package/assets/skills/devflow-project-knowledge/SKILL.md +354 -0
- package/assets/skills/devflow-prove/SKILL.md +216 -0
- package/assets/skills/devflow-prove/references/code-review-checklist.md +202 -0
- package/assets/skills/devflow-prove/references/flow-self-test.md +775 -0
- package/assets/skills/devflow-prove/references/proof-recovery-methods.md +26 -0
- package/assets/skills/devflow-pua/SKILL.md +197 -0
- package/assets/skills/devflow-pua/references/flavor-display.md +49 -0
- package/assets/skills/devflow-pua/references/methodology-library.md +193 -0
- package/assets/skills/devflow-pua/references/methodology-router.md +78 -0
- package/assets/skills/devflow-spec/SKILL.md +92 -0
- package/assets/skills/devflow-spec/references/spec-plan-methods.md +15 -0
- package/cordis.patch.yml +11 -0
- package/lib/dsh-home.js +33 -0
- package/lib/index.js +79 -0
- package/lib/mount-once.js +34 -0
- package/lib/sync.js +168 -0
- package/package.json +32 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
|
|
4
|
+
const ignoreDirs = new Set([
|
|
5
|
+
".git",
|
|
6
|
+
".hg",
|
|
7
|
+
".svn",
|
|
8
|
+
".next",
|
|
9
|
+
".nuxt",
|
|
10
|
+
".turbo",
|
|
11
|
+
".cache",
|
|
12
|
+
"build",
|
|
13
|
+
"coverage",
|
|
14
|
+
"dist",
|
|
15
|
+
"node_modules",
|
|
16
|
+
"out"
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
const textExtensions = new Set([
|
|
20
|
+
".c",
|
|
21
|
+
".cc",
|
|
22
|
+
".cpp",
|
|
23
|
+
".cs",
|
|
24
|
+
".css",
|
|
25
|
+
".go",
|
|
26
|
+
".html",
|
|
27
|
+
".java",
|
|
28
|
+
".js",
|
|
29
|
+
".jsx",
|
|
30
|
+
".kt",
|
|
31
|
+
".mjs",
|
|
32
|
+
".py",
|
|
33
|
+
".rb",
|
|
34
|
+
".rs",
|
|
35
|
+
".sh",
|
|
36
|
+
".swift",
|
|
37
|
+
".ts",
|
|
38
|
+
".tsx",
|
|
39
|
+
".vue"
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
const patternChecks = [
|
|
43
|
+
{
|
|
44
|
+
tag: "stdlib",
|
|
45
|
+
pattern: /\b(?:import|require)\s*(?:\(|).*["']lodash(?:\/[^"']*)?["']/,
|
|
46
|
+
message: "lodash dependency candidate",
|
|
47
|
+
replacement: "Prefer standard Array/Object/String helpers when the call site is simple."
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
tag: "native",
|
|
51
|
+
pattern: /\b(?:import|require)\s*(?:\(|).*["'](?:moment|date-fns)["']/,
|
|
52
|
+
message: "date library candidate",
|
|
53
|
+
replacement: "Prefer Intl.DateTimeFormat or native Date for simple formatting."
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
tag: "yagni",
|
|
57
|
+
pattern: /\b(?:abstract\s+class|interface\s+I[A-Z]\w+|class\s+\w*(?:Factory|Manager|Provider|Registry)\b)/,
|
|
58
|
+
message: "abstraction candidate",
|
|
59
|
+
replacement: "Keep only if there is a real second implementation or caller."
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
tag: "delete",
|
|
63
|
+
pattern: /\b(?:enableExperimental|futureProof|placeholder|unusedFlag)\b/,
|
|
64
|
+
message: "speculative flag candidate",
|
|
65
|
+
replacement: "Delete until a current requirement needs it."
|
|
66
|
+
}
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
function usage() {
|
|
70
|
+
console.log("Usage: node scripts/devflow-audit.js [target-directory] [--self-test] [--json]");
|
|
71
|
+
console.log("Scans code for overengineering candidates. Findings are candidates; confirm by reading code before editing.");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isTextFile(file) {
|
|
75
|
+
return textExtensions.has(path.extname(file).toLowerCase());
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function declarationName(line) {
|
|
79
|
+
const functionMatch = line.match(/\b(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(/);
|
|
80
|
+
if (functionMatch) {
|
|
81
|
+
return functionMatch[1];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const arrowMatch = line.match(/\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>/);
|
|
85
|
+
if (arrowMatch) {
|
|
86
|
+
return arrowMatch[1];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const classMatch = line.match(/\bclass\s+([A-Za-z_$][\w$]*)\b/);
|
|
90
|
+
return classMatch?.[1] || null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function walk(dir) {
|
|
94
|
+
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
|
95
|
+
if (entry.isDirectory() && ignoreDirs.has(entry.name)) {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const fullPath = path.join(dir, entry.name);
|
|
100
|
+
if (entry.isDirectory()) {
|
|
101
|
+
return walk(fullPath);
|
|
102
|
+
}
|
|
103
|
+
return [fullPath];
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function scan(root) {
|
|
108
|
+
const findings = [];
|
|
109
|
+
const declarations = new Map();
|
|
110
|
+
|
|
111
|
+
for (const file of walk(root)) {
|
|
112
|
+
if (!isTextFile(file)) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const rel = path.relative(root, file).replaceAll(path.sep, "/");
|
|
117
|
+
const lines = fs.readFileSync(file, "utf8").split(/\r?\n/);
|
|
118
|
+
const fullText = lines.join("\n");
|
|
119
|
+
|
|
120
|
+
// 文件级结构候选:仅启发式提示,人工确认后编辑,不构成缺失证据(judgment 恒 PASS)。
|
|
121
|
+
const hasController = /\bclass\s+\w*Controller\b/.test(fullText);
|
|
122
|
+
const hasEntity = /\bclass\s+\w*(?:Entity|Model|Dto|DTO)\b/.test(fullText);
|
|
123
|
+
if (hasController && hasEntity) {
|
|
124
|
+
findings.push({
|
|
125
|
+
rel,
|
|
126
|
+
line: 1,
|
|
127
|
+
tag: "layering",
|
|
128
|
+
message: "Controller 与实体同文件候选",
|
|
129
|
+
replacement: "检查 Controller 是否直接持有实体/模型定义;建议分层。"
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
if (lines.length > 400) {
|
|
133
|
+
findings.push({
|
|
134
|
+
rel,
|
|
135
|
+
line: 1,
|
|
136
|
+
tag: "megaclass",
|
|
137
|
+
message: "单文件超长候选",
|
|
138
|
+
replacement: "检查是否需要拆分。"
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
// no-cache:仅提示读路径是否应查缓存,不宣称一定缺失(数据流无法静态证明)。
|
|
142
|
+
if (/\b(?:get|fetch|find|list|query)\w*\s*\(/.test(fullText) && !/\bcache\b/i.test(fullText)) {
|
|
143
|
+
findings.push({
|
|
144
|
+
rel,
|
|
145
|
+
line: 1,
|
|
146
|
+
tag: "no-cache",
|
|
147
|
+
message: "读路径无缓存候选(启发式)",
|
|
148
|
+
replacement: "检查高频读路径是否应查缓存;仅提示不构成缺失证据。"
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
lines.forEach((line, index) => {
|
|
153
|
+
const declaredName = declarationName(line);
|
|
154
|
+
if (declaredName) {
|
|
155
|
+
const entries = declarations.get(declaredName) || [];
|
|
156
|
+
entries.push({ rel, line: index + 1 });
|
|
157
|
+
declarations.set(declaredName, entries);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
for (const check of patternChecks) {
|
|
161
|
+
if (!check.pattern.test(line)) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
findings.push({
|
|
166
|
+
rel,
|
|
167
|
+
line: index + 1,
|
|
168
|
+
tag: check.tag,
|
|
169
|
+
message: check.message,
|
|
170
|
+
replacement: check.replacement
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
for (const [name, entries] of declarations) {
|
|
177
|
+
const files = new Set(entries.map((entry) => entry.rel));
|
|
178
|
+
if (files.size < 2) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
for (const entry of entries.slice(1)) {
|
|
183
|
+
findings.push({
|
|
184
|
+
rel: entry.rel,
|
|
185
|
+
line: entry.line,
|
|
186
|
+
tag: "reuse",
|
|
187
|
+
message: `duplicate declaration candidate: ${name}`,
|
|
188
|
+
replacement: "Check whether the existing project helper, type, or pattern can be reused."
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return findings;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function report(root, json) {
|
|
197
|
+
const findings = scan(root);
|
|
198
|
+
const judgment = "PASS";
|
|
199
|
+
|
|
200
|
+
if (json) {
|
|
201
|
+
console.log(JSON.stringify({ checker: "audit", findings, count: findings.length, judgment }));
|
|
202
|
+
return 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (findings.length === 0) {
|
|
206
|
+
console.log("Lean already. Ship.");
|
|
207
|
+
return 0;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
console.log("DevFlow audit report");
|
|
211
|
+
console.log("Scope: overengineering candidates only; confirm findings by reading code before editing.");
|
|
212
|
+
for (const finding of findings) {
|
|
213
|
+
console.log(`${finding.rel}:L${finding.line}: ${finding.tag}: ${finding.message}. ${finding.replacement}`);
|
|
214
|
+
}
|
|
215
|
+
console.log(`net: ${findings.length} candidates, 0 deps confirmed removable without code review.`);
|
|
216
|
+
return 0;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function selfTest() {
|
|
220
|
+
const tmp = fs.mkdtempSync(path.join(require("node:os").tmpdir(), "devflow-audit-"));
|
|
221
|
+
fs.writeFileSync(
|
|
222
|
+
path.join(tmp, "sample.ts"),
|
|
223
|
+
[
|
|
224
|
+
"import _ from 'lodash';",
|
|
225
|
+
"import moment from 'moment';",
|
|
226
|
+
"interface IReportFactory {}",
|
|
227
|
+
"const enableExperimental = false;",
|
|
228
|
+
"function formatTotal(value) { return String(value); }",
|
|
229
|
+
"class ReportController { entityId = 1; }",
|
|
230
|
+
"class OrderEntity {}",
|
|
231
|
+
"function getOrderData() { return String(value); }"
|
|
232
|
+
].join("\n"),
|
|
233
|
+
"utf8"
|
|
234
|
+
);
|
|
235
|
+
fs.writeFileSync(
|
|
236
|
+
path.join(tmp, "other.ts"),
|
|
237
|
+
"function formatTotal(value) { return value.toString(); }",
|
|
238
|
+
"utf8"
|
|
239
|
+
);
|
|
240
|
+
fs.writeFileSync(
|
|
241
|
+
path.join(tmp, "megaclass.ts"),
|
|
242
|
+
Array.from({ length: 401 }, (_, index) => `// line ${index + 1}`).join("\n"),
|
|
243
|
+
"utf8"
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const findings = scan(tmp);
|
|
247
|
+
const tags = new Set(findings.map((finding) => finding.tag));
|
|
248
|
+
for (const tag of ["reuse", "stdlib", "native", "yagni", "delete", "layering", "megaclass", "no-cache"]) {
|
|
249
|
+
if (!tags.has(tag)) {
|
|
250
|
+
throw new Error(`Self-test expected ${tag} finding`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
console.log("DevFlow audit self-test passed");
|
|
255
|
+
console.log("Checked reuse, stdlib, native, yagni, delete, layering, megaclass, and no-cache candidate detection");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const args = process.argv.slice(2);
|
|
259
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
260
|
+
usage();
|
|
261
|
+
process.exit(0);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (args.includes("--self-test")) {
|
|
265
|
+
selfTest();
|
|
266
|
+
process.exit(0);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const targetArg = args.find((arg) => !arg.startsWith("-")) || ".";
|
|
270
|
+
const targetRoot = path.resolve(process.cwd(), targetArg);
|
|
271
|
+
if (!fs.existsSync(targetRoot) || !fs.statSync(targetRoot).isDirectory()) {
|
|
272
|
+
throw new Error(`Target directory does not exist: ${targetRoot}`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
process.exitCode = report(targetRoot, args.includes("--json"));
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const os = require("node:os");
|
|
3
|
+
const path = require("node:path");
|
|
4
|
+
|
|
5
|
+
const ignoreDirs = new Set([
|
|
6
|
+
".git",
|
|
7
|
+
".hg",
|
|
8
|
+
".svn",
|
|
9
|
+
"node_modules",
|
|
10
|
+
"dist",
|
|
11
|
+
"build",
|
|
12
|
+
"coverage",
|
|
13
|
+
".next",
|
|
14
|
+
".nuxt",
|
|
15
|
+
".turbo",
|
|
16
|
+
".cache"
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
const markerPattern = /^\s*(?:(?:\/\/|#|--|;|\*)\s*|<!--\s*)?devflow:\s*(.+?)(?:\s*-->)?\s*$/i;
|
|
20
|
+
|
|
21
|
+
function usage() {
|
|
22
|
+
console.log("Usage: node scripts/devflow-debt.js [target-directory] [--self-test] [--json]");
|
|
23
|
+
console.log("Scans for devflow: intentional-simplification markers and reports ceiling/revisit gaps.");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function walk(dir) {
|
|
27
|
+
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
|
28
|
+
if (entry.isDirectory() && ignoreDirs.has(entry.name)) {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const fullPath = path.join(dir, entry.name);
|
|
33
|
+
if (entry.isDirectory()) {
|
|
34
|
+
return walk(fullPath);
|
|
35
|
+
}
|
|
36
|
+
return [fullPath];
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isTextFile(file) {
|
|
41
|
+
const ext = path.extname(file).toLowerCase();
|
|
42
|
+
return [
|
|
43
|
+
".c",
|
|
44
|
+
".cc",
|
|
45
|
+
".cpp",
|
|
46
|
+
".cs",
|
|
47
|
+
".css",
|
|
48
|
+
".go",
|
|
49
|
+
".html",
|
|
50
|
+
".java",
|
|
51
|
+
".js",
|
|
52
|
+
".json",
|
|
53
|
+
".jsx",
|
|
54
|
+
".kt",
|
|
55
|
+
".md",
|
|
56
|
+
".mdc",
|
|
57
|
+
".mjs",
|
|
58
|
+
".py",
|
|
59
|
+
".rb",
|
|
60
|
+
".rs",
|
|
61
|
+
".sh",
|
|
62
|
+
".swift",
|
|
63
|
+
".toml",
|
|
64
|
+
".ts",
|
|
65
|
+
".tsx",
|
|
66
|
+
".txt",
|
|
67
|
+
".yaml",
|
|
68
|
+
".yml"
|
|
69
|
+
].includes(ext);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function classifyMarker(text) {
|
|
73
|
+
const normalized = text.toLowerCase();
|
|
74
|
+
const hasCeiling = normalized.includes("ceiling:") || normalized.includes("ceiling ");
|
|
75
|
+
const hasRevisit = normalized.includes("revisit:") || normalized.includes("revisit when");
|
|
76
|
+
|
|
77
|
+
if (hasCeiling && hasRevisit) {
|
|
78
|
+
return "ok";
|
|
79
|
+
}
|
|
80
|
+
if (!hasCeiling && !hasRevisit) {
|
|
81
|
+
return "no-ceiling,no-trigger";
|
|
82
|
+
}
|
|
83
|
+
if (!hasCeiling) {
|
|
84
|
+
return "no-ceiling";
|
|
85
|
+
}
|
|
86
|
+
return "no-trigger";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function scan(root) {
|
|
90
|
+
const markers = [];
|
|
91
|
+
|
|
92
|
+
for (const file of walk(root)) {
|
|
93
|
+
if (!isTextFile(file)) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const rel = path.relative(root, file).replaceAll(path.sep, "/");
|
|
98
|
+
const lines = fs.readFileSync(file, "utf8").split(/\r?\n/);
|
|
99
|
+
|
|
100
|
+
lines.forEach((line, index) => {
|
|
101
|
+
const match = line.match(markerPattern);
|
|
102
|
+
if (!match) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const detail = match[1].trim();
|
|
107
|
+
if (detail.includes("<") && detail.includes(">")) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
markers.push({
|
|
111
|
+
rel,
|
|
112
|
+
line: index + 1,
|
|
113
|
+
detail,
|
|
114
|
+
status: classifyMarker(detail)
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return markers;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function report(root, json) {
|
|
123
|
+
const markers = scan(root);
|
|
124
|
+
const invalid = markers.filter((marker) => marker.status !== "ok");
|
|
125
|
+
|
|
126
|
+
if (json) {
|
|
127
|
+
console.log(
|
|
128
|
+
JSON.stringify({
|
|
129
|
+
checker: "debt",
|
|
130
|
+
markers,
|
|
131
|
+
count: markers.length,
|
|
132
|
+
invalidCount: invalid.length,
|
|
133
|
+
judgment: invalid.length === 0 ? "PASS" : "FAIL"
|
|
134
|
+
})
|
|
135
|
+
);
|
|
136
|
+
return invalid.length === 0 ? 0 : 1;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (markers.length === 0) {
|
|
140
|
+
console.log("No devflow debt markers found.");
|
|
141
|
+
return 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
for (const marker of markers) {
|
|
145
|
+
console.log(`${marker.rel}:${marker.line} - ${marker.detail}. status: ${marker.status}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
console.log(`${markers.length} markers, ${invalid.length} no-trigger`);
|
|
149
|
+
return invalid.length === 0 ? 0 : 1;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function selfTest() {
|
|
153
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "devflow-debt-"));
|
|
154
|
+
fs.writeFileSync(
|
|
155
|
+
path.join(tmp, "sample.js"),
|
|
156
|
+
[
|
|
157
|
+
"// devflow: ceiling: temporary direct parser, revisit when marker syntax expands",
|
|
158
|
+
"// devflow: missing structured fields",
|
|
159
|
+
"// devflow: <ceiling>, revisit when <trigger>"
|
|
160
|
+
].join("\n"),
|
|
161
|
+
"utf8"
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const markers = scan(tmp);
|
|
165
|
+
if (markers.length !== 2) {
|
|
166
|
+
throw new Error(`Self-test expected 2 markers, found ${markers.length}`);
|
|
167
|
+
}
|
|
168
|
+
if (markers[0].status !== "ok") {
|
|
169
|
+
throw new Error(`Self-test expected first marker ok, found ${markers[0].status}`);
|
|
170
|
+
}
|
|
171
|
+
if (markers[1].status !== "no-ceiling,no-trigger") {
|
|
172
|
+
throw new Error(`Self-test expected second marker invalid, found ${markers[1].status}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
console.log("DevFlow debt self-test passed");
|
|
176
|
+
console.log("Checked marker discovery, ceiling detection, revisit trigger detection, and placeholder filtering");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const args = process.argv.slice(2);
|
|
180
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
181
|
+
usage();
|
|
182
|
+
process.exit(0);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (args.includes("--self-test")) {
|
|
186
|
+
selfTest();
|
|
187
|
+
process.exit(0);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const targetArg = args.find((arg) => !arg.startsWith("-")) || ".";
|
|
191
|
+
const targetRoot = path.resolve(process.cwd(), targetArg);
|
|
192
|
+
if (!fs.existsSync(targetRoot) || !fs.statSync(targetRoot).isDirectory()) {
|
|
193
|
+
throw new Error(`Target directory does not exist: ${targetRoot}`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
process.exitCode = report(targetRoot, args.includes("--json"));
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// DevFlow doctor: post-install integrity check for the user-level runtime.
|
|
5
|
+
// Deliberately thin: it reuses `install-devflow-user.js --check` per home
|
|
6
|
+
// instead of duplicating the runtime file lists, so list drift stays in one place.
|
|
7
|
+
const fs = require("node:fs");
|
|
8
|
+
const os = require("node:os");
|
|
9
|
+
const path = require("node:path");
|
|
10
|
+
const { spawnSync } = require("node:child_process");
|
|
11
|
+
|
|
12
|
+
const root = path.resolve(__dirname, "..");
|
|
13
|
+
const installer = path.join(root, "scripts", "install-devflow-user.js");
|
|
14
|
+
|
|
15
|
+
// User-level runtime homes the doctor knows about. Only homes that exist are
|
|
16
|
+
// checked, so a clean machine reports "nothing installed" instead of six failures.
|
|
17
|
+
const homes = ["~/.dsh", "~/.codex", "~/.claude", "~/.codebuddy", "~/.workbuddy", "~/.zcode"].map(
|
|
18
|
+
(home) => path.join(os.homedir(), home.slice(2))
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
function checkHome(home) {
|
|
22
|
+
const result = spawnSync(process.execPath, [installer, "--check", "--home", home], {
|
|
23
|
+
cwd: root,
|
|
24
|
+
encoding: "utf8"
|
|
25
|
+
});
|
|
26
|
+
return { home, ok: result.status === 0, output: result.stdout.trim() };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function runChecks() {
|
|
30
|
+
const existing = homes.filter((home) => fs.existsSync(home));
|
|
31
|
+
if (existing.length === 0) {
|
|
32
|
+
console.log("DevFlow doctor: no user-level runtime found.");
|
|
33
|
+
console.log("Install it with: npm run install:user -- --home ~/.dsh --write --force");
|
|
34
|
+
return 1;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const results = existing.map(checkHome);
|
|
38
|
+
for (const { home, ok, output } of results) {
|
|
39
|
+
console.log(`${ok ? "pass" : "fail"}: ${home}`);
|
|
40
|
+
if (!ok) {
|
|
41
|
+
// Surface the installer's own verdict lines so a failing home names its
|
|
42
|
+
// missing/changed files instead of forcing a manual --check rerun.
|
|
43
|
+
for (const line of output.split("\n")) {
|
|
44
|
+
if (line.startsWith("missing:") || line.startsWith("changed:")) console.log(` ${line}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const failed = results.filter((result) => !result.ok).length;
|
|
49
|
+
console.log(`DevFlow doctor: ${results.length} homes checked, ${failed} failed.`);
|
|
50
|
+
return failed > 0 ? 1 : 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Self-test: a temp home prepared by the installer must pass, and one deleted
|
|
54
|
+
// runtime file must flip the verdict to fail — proving the doctor detects both
|
|
55
|
+
// the clean and the missing state, not just that it runs.
|
|
56
|
+
function selfTest() {
|
|
57
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "devflow-doctor-"));
|
|
58
|
+
try {
|
|
59
|
+
const install = spawnSync(process.execPath, [installer, "--write", "--home", tmp], {
|
|
60
|
+
cwd: root,
|
|
61
|
+
encoding: "utf8"
|
|
62
|
+
});
|
|
63
|
+
if (install.status !== 0) throw new Error(`install into temp home failed:\n${install.stdout}`);
|
|
64
|
+
|
|
65
|
+
const clean = checkHome(tmp);
|
|
66
|
+
if (!clean.ok) throw new Error(`clean temp home must pass, got:\n${clean.output}`);
|
|
67
|
+
|
|
68
|
+
fs.rmSync(path.join(tmp, "skills", "devflow-core", "SKILL.md"));
|
|
69
|
+
const degraded = checkHome(tmp);
|
|
70
|
+
if (degraded.ok) throw new Error("home with a deleted runtime file must fail");
|
|
71
|
+
|
|
72
|
+
console.log("DevFlow doctor self-test passed");
|
|
73
|
+
return 0;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error(error.message);
|
|
76
|
+
return 1;
|
|
77
|
+
} finally {
|
|
78
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const args = process.argv.slice(2);
|
|
83
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
84
|
+
console.log("Usage: node scripts/devflow-doctor.js [--self-test]");
|
|
85
|
+
console.log("Checks every existing user-level runtime home for missing or changed DevFlow files.");
|
|
86
|
+
console.log("--self-test verifies the clean and missing detection states against a temp home.");
|
|
87
|
+
process.exit(0);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
process.exit(args.includes("--self-test") ? selfTest() : runChecks());
|