@eltonssouza/development-utility-kit 0.10.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/.claude/agents/README.md +24 -0
- package/.claude/agents/analyst.md +198 -0
- package/.claude/agents/backend-developer.md +126 -0
- package/.claude/agents/brain-keeper.md +229 -0
- package/.claude/agents/code-reviewer.md +181 -0
- package/.claude/agents/database-engineer.md +94 -0
- package/.claude/agents/devops-engineer.md +141 -0
- package/.claude/agents/frontend-developer.md +97 -0
- package/.claude/agents/gate-keeper.md +118 -0
- package/.claude/agents/migrator.md +291 -0
- package/.claude/agents/mobile-developer.md +80 -0
- package/.claude/agents/n8n-specialist.md +94 -0
- package/.claude/agents/product-owner.md +115 -0
- package/.claude/agents/qa-engineer.md +232 -0
- package/.claude/agents/release-engineer.md +204 -0
- package/.claude/agents/scaffold.md +87 -0
- package/.claude/agents/security-engineer.md +199 -0
- package/.claude/agents/sprint-runner.md +46 -0
- package/.claude/agents/stack-resolver.md +104 -0
- package/.claude/agents/tech-lead.md +182 -0
- package/.claude/agents/update-template.md +54 -0
- package/.claude/agents/ux-designer.md +118 -0
- package/.claude/hooks/flow-guard.js +261 -0
- package/.claude/hooks/flow-state.js +197 -0
- package/.claude/local/CLAUDE.md +71 -0
- package/.claude/settings.json +55 -0
- package/.claude/skills/README.md +331 -0
- package/.claude/skills/active-project/SKILL.md +131 -0
- package/.claude/skills/api-integration-test/SKILL.md +84 -0
- package/.claude/skills/auto-test-guard/SKILL.md +239 -0
- package/.claude/skills/auto-test-guard/resources/backend-tests.md +20 -0
- package/.claude/skills/auto-test-guard/resources/e2e-tests.md +24 -0
- package/.claude/skills/auto-test-guard/resources/execution-report.md +49 -0
- package/.claude/skills/auto-test-guard/resources/frontend-tests.md +18 -0
- package/.claude/skills/auto-test-guard/resources/initial-setup.md +108 -0
- package/.claude/skills/auto-test-guard/resources/run-suite.md +48 -0
- package/.claude/skills/auto-test-guard/resources/senior-gate.md +19 -0
- package/.claude/skills/brain-keeper/SKILL.md +62 -0
- package/.claude/skills/brain-keeper/obsidian/app.json +9 -0
- package/.claude/skills/brain-keeper/obsidian/appearance.json +4 -0
- package/.claude/skills/brain-keeper/obsidian/core-plugins.json +20 -0
- package/.claude/skills/brain-keeper/obsidian/daily-notes.json +5 -0
- package/.claude/skills/brain-keeper/obsidian/graph.json +32 -0
- package/.claude/skills/brain-keeper/obsidian/snippets/folder-colors.css +90 -0
- package/.claude/skills/brain-keeper/obsidian/templates.json +5 -0
- package/.claude/skills/brain-keeper/templates/README.md +51 -0
- package/.claude/skills/brain-keeper/templates/adr.md +40 -0
- package/.claude/skills/brain-keeper/templates/bug.md +35 -0
- package/.claude/skills/brain-keeper/templates/daily.md +38 -0
- package/.claude/skills/brain-keeper/templates/feature.md +62 -0
- package/.claude/skills/brain-keeper/templates/meeting.md +34 -0
- package/.claude/skills/brain-keeper/templates/tech-debt.md +21 -0
- package/.claude/skills/caveman/SKILL.md +189 -0
- package/.claude/skills/create-stack-pack/SKILL.md +281 -0
- package/.claude/skills/grill-me/SKILL.md +80 -0
- package/.claude/skills/pair-debug/SKILL.md +288 -0
- package/.claude/skills/prd-ready-check/SKILL.md +86 -0
- package/.claude/skills/project-manager/SKILL.md +334 -0
- package/.claude/skills/quality-standards/SKILL.md +203 -0
- package/.claude/skills/quick-feature/SKILL.md +266 -0
- package/.claude/skills/run-sprint/SKILL.md +41 -0
- package/.claude/skills/scaffold/SKILL.md +60 -0
- package/.claude/skills/stack-discovery/SKILL.md +161 -0
- package/.claude/skills/test-coverage-auditor/SKILL.md +87 -0
- package/.claude/skills/to-issues/SKILL.md +163 -0
- package/.claude/skills/to-prd/SKILL.md +130 -0
- package/.claude/skills/update-template/SKILL.md +256 -0
- package/.claude/stacks/CODEOWNERS +30 -0
- package/.claude/stacks/README.md +97 -0
- package/.claude/stacks/_template.md +116 -0
- package/.claude/stacks/dotnet/aspire-9.md +528 -0
- package/.claude/stacks/go/gin-1.10.md +570 -0
- package/.claude/stacks/java/spring-boot-3.md +376 -0
- package/.claude/stacks/java/spring-boot-4.md +438 -0
- package/.claude/stacks/node/express-5.md +538 -0
- package/.claude/stacks/python/django-5.md +483 -0
- package/.claude/stacks/python/fastapi-0.115.md +522 -0
- package/.claude/stacks/typescript/angular-18.md +420 -0
- package/.claude/stacks/typescript/angular-19.md +397 -0
- package/.claude/stacks/typescript/angular-21.md +494 -0
- package/CLAUDE.md +472 -0
- package/README.md +412 -0
- package/bin/cli.js +848 -0
- package/bin/lib/adr.js +146 -0
- package/bin/lib/backup.js +62 -0
- package/bin/lib/detect-stack.js +476 -0
- package/bin/lib/doctor.js +527 -0
- package/bin/lib/help.js +328 -0
- package/bin/lib/identity.js +108 -0
- package/bin/lib/lint-allowlist.json +15 -0
- package/bin/lib/lint.js +798 -0
- package/bin/lib/local-dir.js +68 -0
- package/bin/lib/manifest.js +236 -0
- package/bin/lib/sync-all.js +394 -0
- package/bin/lib/version-check.js +398 -0
- package/dashboard/db.js +321 -0
- package/dashboard/package.json +22 -0
- package/dashboard/public/app.js +853 -0
- package/dashboard/public/content/docs/agents-reference.en.md +911 -0
- package/dashboard/public/content/docs/architecture-overview.en.md +252 -0
- package/dashboard/public/content/docs/autonomy-matrix.en.md +186 -0
- package/dashboard/public/content/docs/cli-reference.en.md +538 -0
- package/dashboard/public/content/docs/git-flow.en.md +525 -0
- package/dashboard/public/content/docs/honcho-memory.en.md +394 -0
- package/dashboard/public/content/docs/hooks-reference.en.md +404 -0
- package/dashboard/public/content/docs/pipeline.en.md +414 -0
- package/dashboard/public/content/docs/plugins.en.md +289 -0
- package/dashboard/public/content/docs/quality-gate.en.md +315 -0
- package/dashboard/public/content/docs/skills-reference.en.md +484 -0
- package/dashboard/public/content/docs/stack-rules.en.md +362 -0
- package/dashboard/public/content/docs/troubleshooting.en.md +565 -0
- package/dashboard/public/content/manifest.json +114 -0
- package/dashboard/public/content/manual/backend.en.md +1053 -0
- package/dashboard/public/content/manual/existing-project.en.md +848 -0
- package/dashboard/public/content/manual/frontend.en.md +1008 -0
- package/dashboard/public/content/manual/fullstack.en.md +1459 -0
- package/dashboard/public/content/manual/mobile.en.md +837 -0
- package/dashboard/public/content/manual/quickstart.en.md +169 -0
- package/dashboard/public/index.html +217 -0
- package/dashboard/public/style.css +857 -0
- package/dashboard/public/vendor/marked.min.js +69 -0
- package/dashboard/rtk.js +143 -0
- package/dashboard/server-app.js +421 -0
- package/dashboard/server.js +104 -0
- package/dashboard/test/sprint1.test.js +406 -0
- package/dashboard/test/sprint2.test.js +571 -0
- package/dashboard/test/sprint3.test.js +560 -0
- package/package.json +33 -0
- package/scripts/hooks/subagent-telemetry.sh +14 -0
- package/scripts/hooks/telemetry-writer.js +250 -0
- package/scripts/latest-versions.json +56 -0
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `duk doctor` — environment validation for the development-utility-kit harness.
|
|
5
|
+
*
|
|
6
|
+
* Mechanical, deterministic, no LLM. Per ADR-034:
|
|
7
|
+
* "Mecânico, determinístico, scriptável → CLI duk."
|
|
8
|
+
*
|
|
9
|
+
* Checks:
|
|
10
|
+
* 1. environment — Node version, git, npx, python3 (warn-only for hooks)
|
|
11
|
+
* 2. harness — root directory, .claude/ structure, CLAUDE.md, package.json
|
|
12
|
+
* 3. settings — ~/.claude/settings.json exists, parseable, hook paths live
|
|
13
|
+
* 4. stacks — packs available in .claude/stacks/, README index present
|
|
14
|
+
* 5. credentials — C:/development/tools/credentials/vps.txt (warn if missing)
|
|
15
|
+
* 6. project — only if CWD is an adopted project: MANIFEST + Project Identity
|
|
16
|
+
*
|
|
17
|
+
* Output: aligned table (Category | Check | Status | Detail).
|
|
18
|
+
* Exit code: 0 if no FAIL, 1 if any FAIL. WARN never fails unless --strict.
|
|
19
|
+
*
|
|
20
|
+
* Flags:
|
|
21
|
+
* --json machine-readable output
|
|
22
|
+
* --strict exit non-zero on WARN too
|
|
23
|
+
* --help show help
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const path = require('path');
|
|
28
|
+
const os = require('os');
|
|
29
|
+
const { spawnSync } = require('child_process');
|
|
30
|
+
|
|
31
|
+
// ── Status constants ────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
const PASS = 'PASS';
|
|
34
|
+
const WARN = 'WARN';
|
|
35
|
+
const FAIL = 'FAIL';
|
|
36
|
+
|
|
37
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Run a command and return { stdout, status }. Never throws.
|
|
41
|
+
* @param {string} cmd
|
|
42
|
+
* @param {string[]} args
|
|
43
|
+
* @returns {{ stdout: string, status: number|null }}
|
|
44
|
+
*/
|
|
45
|
+
function runCmd(cmd, args) {
|
|
46
|
+
try {
|
|
47
|
+
const r = spawnSync(cmd, args, {
|
|
48
|
+
encoding: 'utf8',
|
|
49
|
+
shell: process.platform === 'win32',
|
|
50
|
+
timeout: 5000,
|
|
51
|
+
});
|
|
52
|
+
return { stdout: (r.stdout || '').trim(), status: r.status };
|
|
53
|
+
} catch {
|
|
54
|
+
return { stdout: '', status: null };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function fileExists(p) {
|
|
59
|
+
try {
|
|
60
|
+
return fs.existsSync(p);
|
|
61
|
+
} catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isDir(p) {
|
|
67
|
+
try {
|
|
68
|
+
return fs.statSync(p).isDirectory();
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function readJsonSafe(p) {
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function packageRoot() {
|
|
83
|
+
return path.resolve(__dirname, '..', '..');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function harnessVersion() {
|
|
87
|
+
const pkg = readJsonSafe(path.join(packageRoot(), 'package.json'));
|
|
88
|
+
return pkg && pkg.version ? pkg.version : '0.0.0';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── Individual checks ───────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @returns {Array<{category: string, name: string, status: string, detail: string}>}
|
|
95
|
+
*/
|
|
96
|
+
function checkEnvironment() {
|
|
97
|
+
const out = [];
|
|
98
|
+
|
|
99
|
+
// Node version
|
|
100
|
+
const nodeVer = process.version.replace(/^v/, '');
|
|
101
|
+
const major = parseInt(nodeVer.split('.')[0], 10);
|
|
102
|
+
out.push({
|
|
103
|
+
category: 'environment',
|
|
104
|
+
name: 'node',
|
|
105
|
+
status: major >= 18 ? PASS : FAIL,
|
|
106
|
+
detail: `${process.version} (need >= 18)`,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// git
|
|
110
|
+
const git = runCmd('git', ['--version']);
|
|
111
|
+
out.push({
|
|
112
|
+
category: 'environment',
|
|
113
|
+
name: 'git',
|
|
114
|
+
status: git.status === 0 ? PASS : FAIL,
|
|
115
|
+
detail: git.status === 0 ? git.stdout : 'not in PATH',
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// npx
|
|
119
|
+
const npx = runCmd('npx', ['--version']);
|
|
120
|
+
out.push({
|
|
121
|
+
category: 'environment',
|
|
122
|
+
name: 'npx',
|
|
123
|
+
status: npx.status === 0 ? PASS : WARN,
|
|
124
|
+
detail: npx.status === 0 ? `v${npx.stdout}` : 'not in PATH (some skills need it)',
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// python3 (used by some plugin hooks)
|
|
128
|
+
let py = runCmd('python3', ['--version']);
|
|
129
|
+
if (py.status !== 0) py = runCmd('python', ['--version']);
|
|
130
|
+
out.push({
|
|
131
|
+
category: 'environment',
|
|
132
|
+
name: 'python',
|
|
133
|
+
status: py.status === 0 ? PASS : WARN,
|
|
134
|
+
detail: py.status === 0 ? py.stdout : 'not in PATH (some hooks need it)',
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function checkHarness() {
|
|
141
|
+
const out = [];
|
|
142
|
+
const root = packageRoot();
|
|
143
|
+
|
|
144
|
+
// Root exists and is a git repo
|
|
145
|
+
const isGitRepo = isDir(path.join(root, '.git'));
|
|
146
|
+
out.push({
|
|
147
|
+
category: 'harness',
|
|
148
|
+
name: 'root',
|
|
149
|
+
status: isGitRepo ? PASS : FAIL,
|
|
150
|
+
detail: `${root} ${isGitRepo ? '(git repo)' : '(NOT a git repo)'}`,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Required structure
|
|
154
|
+
const required = [
|
|
155
|
+
{ rel: '.claude', kind: 'dir' },
|
|
156
|
+
{ rel: '.claude/agents', kind: 'dir' },
|
|
157
|
+
{ rel: '.claude/skills', kind: 'dir' },
|
|
158
|
+
{ rel: '.claude/stacks', kind: 'dir' },
|
|
159
|
+
{ rel: 'CLAUDE.md', kind: 'file' },
|
|
160
|
+
{ rel: 'package.json', kind: 'file' },
|
|
161
|
+
{ rel: 'bin/cli.js', kind: 'file' },
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
for (const r of required) {
|
|
165
|
+
const full = path.join(root, r.rel);
|
|
166
|
+
const ok = r.kind === 'dir' ? isDir(full) : fileExists(full);
|
|
167
|
+
out.push({
|
|
168
|
+
category: 'harness',
|
|
169
|
+
name: r.rel,
|
|
170
|
+
status: ok ? PASS : FAIL,
|
|
171
|
+
detail: ok ? r.kind : `missing (${r.kind})`,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Harness version
|
|
176
|
+
out.push({
|
|
177
|
+
category: 'harness',
|
|
178
|
+
name: 'version',
|
|
179
|
+
status: PASS,
|
|
180
|
+
detail: harnessVersion(),
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function checkSettings() {
|
|
187
|
+
const out = [];
|
|
188
|
+
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
189
|
+
|
|
190
|
+
if (!fileExists(settingsPath)) {
|
|
191
|
+
out.push({
|
|
192
|
+
category: 'settings',
|
|
193
|
+
name: '~/.claude/settings.json',
|
|
194
|
+
status: WARN,
|
|
195
|
+
detail: 'not found (hooks/permissions empty)',
|
|
196
|
+
});
|
|
197
|
+
return out;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const settings = readJsonSafe(settingsPath);
|
|
201
|
+
if (!settings) {
|
|
202
|
+
out.push({
|
|
203
|
+
category: 'settings',
|
|
204
|
+
name: '~/.claude/settings.json',
|
|
205
|
+
status: FAIL,
|
|
206
|
+
detail: 'exists but not valid JSON',
|
|
207
|
+
});
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
out.push({
|
|
212
|
+
category: 'settings',
|
|
213
|
+
name: '~/.claude/settings.json',
|
|
214
|
+
status: PASS,
|
|
215
|
+
detail: 'valid JSON',
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Walk hooks looking for absolute paths that don't exist on disk.
|
|
219
|
+
const brokenHooks = [];
|
|
220
|
+
const allHooks = [];
|
|
221
|
+
try {
|
|
222
|
+
const hooks = settings.hooks || {};
|
|
223
|
+
for (const eventName of Object.keys(hooks)) {
|
|
224
|
+
const arr = Array.isArray(hooks[eventName]) ? hooks[eventName] : [];
|
|
225
|
+
for (const entry of arr) {
|
|
226
|
+
const hookList = (entry && Array.isArray(entry.hooks)) ? entry.hooks : [];
|
|
227
|
+
for (const h of hookList) {
|
|
228
|
+
if (h && typeof h.command === 'string') {
|
|
229
|
+
// Extract first absolute-looking path from the command.
|
|
230
|
+
const m = h.command.match(/[A-Za-z]:[/\\][^\s"']+|\/[^\s"']+/);
|
|
231
|
+
if (m) {
|
|
232
|
+
const p = m[0];
|
|
233
|
+
allHooks.push({ event: eventName, path: p });
|
|
234
|
+
if (!fileExists(p)) brokenHooks.push({ event: eventName, path: p });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
} catch {
|
|
241
|
+
// ignore — best-effort scan
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
out.push({
|
|
245
|
+
category: 'settings',
|
|
246
|
+
name: 'hooks.total',
|
|
247
|
+
status: PASS,
|
|
248
|
+
detail: `${allHooks.length} hook command(s) detected`,
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
if (brokenHooks.length > 0) {
|
|
252
|
+
for (const b of brokenHooks) {
|
|
253
|
+
out.push({
|
|
254
|
+
category: 'settings',
|
|
255
|
+
name: `hook.${b.event}`,
|
|
256
|
+
status: FAIL,
|
|
257
|
+
detail: `path does not exist: ${b.path}`,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
} else if (allHooks.length > 0) {
|
|
261
|
+
out.push({
|
|
262
|
+
category: 'settings',
|
|
263
|
+
name: 'hooks.paths',
|
|
264
|
+
status: PASS,
|
|
265
|
+
detail: 'all hook paths exist',
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return out;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function checkStacks() {
|
|
273
|
+
const out = [];
|
|
274
|
+
const stacksDir = path.join(packageRoot(), '.claude', 'stacks');
|
|
275
|
+
|
|
276
|
+
if (!isDir(stacksDir)) {
|
|
277
|
+
out.push({
|
|
278
|
+
category: 'stacks',
|
|
279
|
+
name: 'directory',
|
|
280
|
+
status: FAIL,
|
|
281
|
+
detail: 'missing .claude/stacks/',
|
|
282
|
+
});
|
|
283
|
+
return out;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Index
|
|
287
|
+
const readme = path.join(stacksDir, 'README.md');
|
|
288
|
+
out.push({
|
|
289
|
+
category: 'stacks',
|
|
290
|
+
name: 'README.md',
|
|
291
|
+
status: fileExists(readme) ? PASS : WARN,
|
|
292
|
+
detail: fileExists(readme) ? 'present' : 'missing (index lost)',
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// Walk languages/<framework>-<major>.md
|
|
296
|
+
let packs = [];
|
|
297
|
+
try {
|
|
298
|
+
const langs = fs.readdirSync(stacksDir, { withFileTypes: true })
|
|
299
|
+
.filter((d) => d.isDirectory())
|
|
300
|
+
.map((d) => d.name);
|
|
301
|
+
for (const lang of langs) {
|
|
302
|
+
const langDir = path.join(stacksDir, lang);
|
|
303
|
+
const files = fs.readdirSync(langDir).filter((f) => f.endsWith('.md'));
|
|
304
|
+
for (const f of files) {
|
|
305
|
+
packs.push(`${lang}/${f.replace(/\.md$/, '')}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
} catch {
|
|
309
|
+
// ignore
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
out.push({
|
|
313
|
+
category: 'stacks',
|
|
314
|
+
name: 'packs.count',
|
|
315
|
+
status: packs.length > 0 ? PASS : WARN,
|
|
316
|
+
detail: packs.length === 0 ? 'no packs found' : `${packs.length} pack(s): ${packs.join(', ')}`,
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
return out;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function checkOperationalGuardRail() {
|
|
323
|
+
return [{
|
|
324
|
+
category: 'guidance',
|
|
325
|
+
name: 'mass-edits',
|
|
326
|
+
status: PASS,
|
|
327
|
+
detail: 'Use Cowork/sandbox for mass reads & validates — avoid mass native-git edits in Claude Code CLI on Windows (OS locks, AV scans).',
|
|
328
|
+
}];
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function checkCredentials() {
|
|
332
|
+
const out = [];
|
|
333
|
+
const credPath = process.platform === 'win32'
|
|
334
|
+
? 'C:/development/tools/credentials/vps.txt'
|
|
335
|
+
: path.join(os.homedir(), 'development/tools/credentials/vps.txt');
|
|
336
|
+
|
|
337
|
+
out.push({
|
|
338
|
+
category: 'credentials',
|
|
339
|
+
name: 'vps.txt',
|
|
340
|
+
status: fileExists(credPath) ? PASS : WARN,
|
|
341
|
+
detail: fileExists(credPath) ? credPath : `not found at ${credPath} (skills that need VPS access will prompt)`,
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
return out;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Check whether a .claude.bak/ directory exists and is restorable
|
|
349
|
+
* (i.e., it contains at least one .md file — not an empty dir).
|
|
350
|
+
* @param {string} dir Project directory to inspect.
|
|
351
|
+
* @returns {Array<{category,name,status,detail}>}
|
|
352
|
+
*/
|
|
353
|
+
function checkBackupRestore(dir) {
|
|
354
|
+
const out = [];
|
|
355
|
+
const bakDir = path.join(dir, '.claude.bak');
|
|
356
|
+
|
|
357
|
+
if (!isDir(bakDir)) {
|
|
358
|
+
// No backup present — this is fine (first install or intentionally cleaned).
|
|
359
|
+
out.push({
|
|
360
|
+
category: 'backup',
|
|
361
|
+
name: '.claude.bak',
|
|
362
|
+
status: PASS,
|
|
363
|
+
detail: 'no backup present (normal for first install)',
|
|
364
|
+
});
|
|
365
|
+
return out;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Backup exists — verify it is restorable (contains agent/skill files).
|
|
369
|
+
let fileCount = 0;
|
|
370
|
+
try {
|
|
371
|
+
const walk = (d) => {
|
|
372
|
+
for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
|
|
373
|
+
const full = path.join(d, entry.name);
|
|
374
|
+
if (entry.isDirectory()) walk(full);
|
|
375
|
+
else if (entry.isFile()) fileCount++;
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
walk(bakDir);
|
|
379
|
+
} catch {
|
|
380
|
+
// ignore
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (fileCount === 0) {
|
|
384
|
+
out.push({
|
|
385
|
+
category: 'backup',
|
|
386
|
+
name: '.claude.bak',
|
|
387
|
+
status: WARN,
|
|
388
|
+
detail: `backup directory exists but is empty — not restorable (rm -rf .claude.bak to clear)`,
|
|
389
|
+
});
|
|
390
|
+
} else {
|
|
391
|
+
out.push({
|
|
392
|
+
category: 'backup',
|
|
393
|
+
name: '.claude.bak',
|
|
394
|
+
status: PASS,
|
|
395
|
+
detail: `backup present and restorable (${fileCount} file(s) — restore: cp -r .claude.bak .claude)`,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return out;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function checkProject() {
|
|
403
|
+
// Only check the current working directory — and only if it looks like an adopted project (not the harness itself).
|
|
404
|
+
const out = [];
|
|
405
|
+
const cwd = process.cwd();
|
|
406
|
+
const root = packageRoot();
|
|
407
|
+
|
|
408
|
+
// Skip when invoked from the harness root itself.
|
|
409
|
+
if (path.resolve(cwd) === path.resolve(root)) {
|
|
410
|
+
return out;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Always check backup regardless of whether .claude/ exists.
|
|
414
|
+
out.push(...checkBackupRestore(cwd));
|
|
415
|
+
|
|
416
|
+
const cwdClaude = path.join(cwd, '.claude');
|
|
417
|
+
if (!isDir(cwdClaude)) {
|
|
418
|
+
out.push({
|
|
419
|
+
category: 'project',
|
|
420
|
+
name: 'cwd',
|
|
421
|
+
status: WARN,
|
|
422
|
+
detail: 'CWD is not an adopted project (no .claude/) — run `duk install` to adopt',
|
|
423
|
+
});
|
|
424
|
+
return out;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// MANIFEST baseline
|
|
428
|
+
const manifest = path.join(cwdClaude, '.MANIFEST');
|
|
429
|
+
out.push({
|
|
430
|
+
category: 'project',
|
|
431
|
+
name: '.MANIFEST',
|
|
432
|
+
status: fileExists(manifest) ? PASS : WARN,
|
|
433
|
+
detail: fileExists(manifest) ? 'baseline present' : 'missing (run `duk install` to create)',
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
// Project Identity in CLAUDE.md (not the placeholder)
|
|
437
|
+
const claudeMd = path.join(cwd, 'CLAUDE.md');
|
|
438
|
+
if (fileExists(claudeMd)) {
|
|
439
|
+
const txt = fs.readFileSync(claudeMd, 'utf8');
|
|
440
|
+
const filled = !/Project name.*<project-name>/i.test(txt) && /## Project Identity/i.test(txt);
|
|
441
|
+
out.push({
|
|
442
|
+
category: 'project',
|
|
443
|
+
name: 'Project Identity',
|
|
444
|
+
status: filled ? PASS : WARN,
|
|
445
|
+
detail: filled ? 'filled' : 'placeholder `<project-name>` still present — run stack discovery',
|
|
446
|
+
});
|
|
447
|
+
} else {
|
|
448
|
+
out.push({
|
|
449
|
+
category: 'project',
|
|
450
|
+
name: 'CLAUDE.md',
|
|
451
|
+
status: FAIL,
|
|
452
|
+
detail: 'missing',
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return out;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ── Formatting ──────────────────────────────────────────────────────────────
|
|
460
|
+
|
|
461
|
+
function formatTable(rows) {
|
|
462
|
+
if (rows.length === 0) return '';
|
|
463
|
+
const headers = ['CATEGORY', 'CHECK', 'STATUS', 'DETAIL'];
|
|
464
|
+
const widths = headers.map((h, i) => {
|
|
465
|
+
const colKey = ['category', 'name', 'status', 'detail'][i];
|
|
466
|
+
return Math.max(h.length, ...rows.map((r) => String(r[colKey] || '').length));
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
const pad = (s, w) => String(s || '').padEnd(w);
|
|
470
|
+
const sep = widths.map((w) => '─'.repeat(w)).join('─┼─');
|
|
471
|
+
const header = headers.map((h, i) => pad(h, widths[i])).join(' │ ');
|
|
472
|
+
const body = rows.map((r) =>
|
|
473
|
+
[r.category, r.name, r.status, r.detail].map((v, i) => pad(v, widths[i])).join(' │ ')
|
|
474
|
+
).join('\n');
|
|
475
|
+
|
|
476
|
+
return `${header}\n${sep}\n${body}`;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function summarize(rows) {
|
|
480
|
+
const counts = { PASS: 0, WARN: 0, FAIL: 0 };
|
|
481
|
+
for (const r of rows) counts[r.status] = (counts[r.status] || 0) + 1;
|
|
482
|
+
return counts;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// ── Entry point ─────────────────────────────────────────────────────────────
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* @param {{ json?: boolean, strict?: boolean }} [options]
|
|
489
|
+
* @returns {number} exit code
|
|
490
|
+
*/
|
|
491
|
+
function runDoctor(options) {
|
|
492
|
+
const opts = options || {};
|
|
493
|
+
const rows = [
|
|
494
|
+
...checkEnvironment(),
|
|
495
|
+
...checkHarness(),
|
|
496
|
+
...checkSettings(),
|
|
497
|
+
...checkStacks(),
|
|
498
|
+
...checkCredentials(),
|
|
499
|
+
...checkOperationalGuardRail(),
|
|
500
|
+
...checkProject(),
|
|
501
|
+
];
|
|
502
|
+
|
|
503
|
+
const counts = summarize(rows);
|
|
504
|
+
|
|
505
|
+
if (opts.json) {
|
|
506
|
+
process.stdout.write(JSON.stringify({ summary: counts, checks: rows }, null, 2) + '\n');
|
|
507
|
+
} else {
|
|
508
|
+
process.stdout.write('duk doctor — environment validation\n\n');
|
|
509
|
+
process.stdout.write(formatTable(rows) + '\n\n');
|
|
510
|
+
process.stdout.write(
|
|
511
|
+
`Summary: ${counts.PASS} PASS, ${counts.WARN} WARN, ${counts.FAIL} FAIL\n`
|
|
512
|
+
);
|
|
513
|
+
if (counts.FAIL > 0) {
|
|
514
|
+
process.stdout.write('\nAction required: fix FAIL items before continuing.\n');
|
|
515
|
+
} else if (counts.WARN > 0) {
|
|
516
|
+
process.stdout.write('\nNon-blocking warnings present. Review when convenient.\n');
|
|
517
|
+
} else {
|
|
518
|
+
process.stdout.write('\nAll clear.\n');
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (counts.FAIL > 0) return 1;
|
|
523
|
+
if (opts.strict && counts.WARN > 0) return 1;
|
|
524
|
+
return 0;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
module.exports = { runDoctor };
|