@devflow-tools/cli 0.16.1 → 0.16.3
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/dist/commands/doctor.d.ts +4 -19
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +352 -6
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +13 -3
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/memory-maintain.d.ts +9 -0
- package/dist/commands/memory-maintain.d.ts.map +1 -0
- package/dist/commands/memory-maintain.js +32 -0
- package/dist/commands/memory-maintain.js.map +1 -0
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +14 -0
- package/dist/commands/status.js.map +1 -1
- package/dist/index.js +29 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/plugin-setup.d.ts +36 -2
- package/dist/lib/plugin-setup.d.ts.map +1 -1
- package/dist/lib/plugin-setup.js +183 -11
- package/dist/lib/plugin-setup.js.map +1 -1
- package/dist/plugin-files/.claude-plugin/plugin.json +2 -2
- package/dist/plugin-files/dist/hooks/hook-client.js +6 -6
- package/dist/plugin-files/dist/hooks/hook-daemon.js +11 -11
- package/dist/plugin-files/dist/hooks/post-tool-use-failure.js +2 -2
- package/dist/plugin-files/dist/hooks/post-tool-use.js +3 -3
- package/dist/plugin-files/dist/hooks/pre-compact.js +2 -2
- package/dist/plugin-files/dist/hooks/pre-tool-use.js +2 -2
- package/dist/plugin-files/dist/hooks/session-end.js +2 -2
- package/dist/plugin-files/dist/hooks/stop.js +2 -2
- package/dist/plugin-files/dist/hooks/user-prompt-submit.js +3 -2
- package/dist/plugin-files/dist/skills/devflow:react/SKILL.md +16 -3
- package/dist/plugin-files/hooks/session-start +37 -17
- package/dist/plugin-files/hooks/stop +3 -2
- package/dist/plugin-files/package.json +7 -8
- package/dist/plugin-files/skills/{devflow:react → react}/SKILL.md +16 -3
- package/package.json +27 -27
- /package/dist/plugin-files/{hooks.json → hooks/hooks.json} +0 -0
- /package/dist/plugin-files/skills/{devflow:animation → animation}/SKILL.md +0 -0
- /package/dist/plugin-files/skills/{devflow:context → context}/SKILL.md +0 -0
- /package/dist/plugin-files/skills/{devflow:css → css}/SKILL.md +0 -0
- /package/dist/plugin-files/skills/{devflow:docker → docker}/SKILL.md +0 -0
- /package/dist/plugin-files/skills/{devflow:doctor → doctor}/SKILL.md +0 -0
- /package/dist/plugin-files/skills/{devflow:electron → electron}/SKILL.md +0 -0
- /package/dist/plugin-files/skills/{devflow:git → git}/SKILL.md +0 -0
- /package/dist/plugin-files/skills/{devflow:graph → graph}/SKILL.md +0 -0
- /package/dist/plugin-files/skills/{devflow:graphql → graphql}/SKILL.md +0 -0
- /package/dist/plugin-files/skills/{devflow:knowledge → knowledge}/SKILL.md +0 -0
- /package/dist/plugin-files/skills/{devflow:memory → memory}/SKILL.md +0 -0
- /package/dist/plugin-files/skills/{devflow:nest → nest}/SKILL.md +0 -0
- /package/dist/plugin-files/skills/{devflow:nextjs → nextjs}/SKILL.md +0 -0
- /package/dist/plugin-files/skills/{devflow:performance → performance}/SKILL.md +0 -0
- /package/dist/plugin-files/skills/{devflow:start → start}/SKILL.md +0 -0
- /package/dist/plugin-files/skills/{devflow:tailwind → tailwind}/SKILL.md +0 -0
- /package/dist/plugin-files/skills/{devflow:taro → taro}/SKILL.md +0 -0
- /package/dist/plugin-files/skills/{devflow:ui-layout → ui-layout}/SKILL.md +0 -0
- /package/dist/plugin-files/skills/{devflow:vue → vue}/SKILL.md +0 -0
- /package/dist/plugin-files/skills/{devflow:workflow → workflow}/SKILL.md +0 -0
|
@@ -1,21 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
export type
|
|
3
|
-
export
|
|
4
|
-
key: string;
|
|
5
|
-
value: unknown;
|
|
6
|
-
target?: unknown;
|
|
7
|
-
ok: boolean;
|
|
8
|
-
severity?: DoctorSeverity;
|
|
9
|
-
}
|
|
10
|
-
export interface DoctorSection {
|
|
11
|
-
name: "Isolation" | "Distill" | "Vectors" | "Telemetry" | "Injection" | "Memory quality" | "Knowledge quality" | "Contract" | "Deadcode" | "Data face" | "Legacy paths" | "Runtime security" | "Benchmark" | "UI reachability" | "Hook path health";
|
|
12
|
-
status: DoctorStatus;
|
|
13
|
-
metrics: DoctorMetric[];
|
|
14
|
-
}
|
|
15
|
-
export interface DoctorResult {
|
|
16
|
-
sections: DoctorSection[];
|
|
17
|
-
exitCode: number;
|
|
18
|
-
}
|
|
1
|
+
import type { DoctorReportV1 } from "@devflow-tools/sdk";
|
|
2
|
+
export type { DoctorMetric, DoctorReportV1, DoctorSection, DoctorSeverity, DoctorStatus, } from "@devflow-tools/sdk";
|
|
3
|
+
export type DoctorResult = DoctorReportV1;
|
|
19
4
|
export interface DoctorOptions {
|
|
20
5
|
project?: string;
|
|
21
6
|
json?: boolean;
|
|
@@ -38,6 +23,7 @@ export interface DoctorDependencies {
|
|
|
38
23
|
installedPluginsPath?: string;
|
|
39
24
|
knownMarketplacesPath?: string;
|
|
40
25
|
pluginCachePath?: string;
|
|
26
|
+
activePluginRoot?: string;
|
|
41
27
|
env?: NodeJS.ProcessEnv;
|
|
42
28
|
now?: () => number;
|
|
43
29
|
openDatabase?: (path: string) => DoctorDatabase;
|
|
@@ -110,5 +96,4 @@ export declare function runDoctorReceipts(): {
|
|
|
110
96
|
sessionId: string;
|
|
111
97
|
data: unknown;
|
|
112
98
|
};
|
|
113
|
-
export {};
|
|
114
99
|
//# sourceMappingURL=doctor.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"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"}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -6,6 +6,8 @@ import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep }
|
|
|
6
6
|
import { spawnSync } from "node:child_process";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import chalk from "chalk";
|
|
9
|
+
import { DOCTOR_SCHEMA_VERSION } from "@devflow-tools/sdk";
|
|
10
|
+
import { resolveActivePlugin, scanClaudeSkillShadowing } from "../lib/plugin-setup.js";
|
|
9
11
|
import { serverStatus } from "./server.js";
|
|
10
12
|
import { getGlobalDevFlowDbPath } from "@devflow-tools/database";
|
|
11
13
|
function getProjectHash(rootPath) {
|
|
@@ -108,6 +110,13 @@ function checkIsolation(project, dependencies) {
|
|
|
108
110
|
ok: true,
|
|
109
111
|
severity: "info",
|
|
110
112
|
});
|
|
113
|
+
metrics.push({
|
|
114
|
+
key: "hook_daemon_registration",
|
|
115
|
+
value: { activeProjectRegistrations: 0 },
|
|
116
|
+
target: "0 when idle or 1 while active",
|
|
117
|
+
ok: true,
|
|
118
|
+
severity: "info",
|
|
119
|
+
});
|
|
111
120
|
}
|
|
112
121
|
else {
|
|
113
122
|
try {
|
|
@@ -144,6 +153,13 @@ function checkIsolation(project, dependencies) {
|
|
|
144
153
|
ok: true,
|
|
145
154
|
severity: "info",
|
|
146
155
|
});
|
|
156
|
+
metrics.push({
|
|
157
|
+
key: "hook_daemon_registration",
|
|
158
|
+
value: { activeProjectRegistrations: projectEntries.length },
|
|
159
|
+
target: "0 when idle or 1 while active",
|
|
160
|
+
ok: projectEntries.length <= 1,
|
|
161
|
+
severity: "warning",
|
|
162
|
+
});
|
|
147
163
|
const registeredSocketPaths = projectEntries.map((entry) => getDaemonSocketPath(entry.rootPath));
|
|
148
164
|
metrics.push({
|
|
149
165
|
key: "registered_daemon_socket_paths",
|
|
@@ -176,6 +192,13 @@ function checkIsolation(project, dependencies) {
|
|
|
176
192
|
ok: false,
|
|
177
193
|
severity: "warning",
|
|
178
194
|
});
|
|
195
|
+
metrics.push({
|
|
196
|
+
key: "hook_daemon_registration",
|
|
197
|
+
value: "unreadable daemon registry",
|
|
198
|
+
target: "valid project registration",
|
|
199
|
+
ok: false,
|
|
200
|
+
severity: "warning",
|
|
201
|
+
});
|
|
179
202
|
}
|
|
180
203
|
}
|
|
181
204
|
return { name: "Isolation", status: sectionStatus(metrics), metrics };
|
|
@@ -184,7 +207,9 @@ const REQUIRED_SCHEMA = {
|
|
|
184
207
|
distill_leases: ["event_ids", "expires_at", "status"],
|
|
185
208
|
observations: ["title"],
|
|
186
209
|
session_events: ["created_at", "id", "processed"],
|
|
210
|
+
sessions: ["created_at", "id", "project_root", "status"],
|
|
187
211
|
};
|
|
212
|
+
const SESSION_STALE_MS = 60 * 60 * 1000;
|
|
188
213
|
function checkRequiredSchema(db) {
|
|
189
214
|
const tableRows = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table'").all();
|
|
190
215
|
const tables = new Set(tableRows.map((row) => row.name));
|
|
@@ -340,6 +365,48 @@ function checkDistill(project, dependencies) {
|
|
|
340
365
|
ok: staleUnleased === 0,
|
|
341
366
|
severity: "error",
|
|
342
367
|
});
|
|
368
|
+
const pendingEvents = doctorCount(openedDb, "SELECT COUNT(*) count FROM session_events WHERE processed = 0");
|
|
369
|
+
metrics.push({
|
|
370
|
+
key: "pending_memory_events",
|
|
371
|
+
value: pendingEvents,
|
|
372
|
+
target: "actively leased or younger than 5 minutes",
|
|
373
|
+
ok: staleUnleased === 0,
|
|
374
|
+
severity: "error",
|
|
375
|
+
});
|
|
376
|
+
const oldestPending = openedDb.prepare("SELECT MIN(created_at) oldest FROM session_events WHERE processed = 0").get();
|
|
377
|
+
const oldestPendingAt = oldestPending?.oldest == null ? null : Number(oldestPending.oldest);
|
|
378
|
+
metrics.push({
|
|
379
|
+
key: "pending_memory_oldest_age_ms",
|
|
380
|
+
value: oldestPendingAt === null ? 0 : Math.max(0, now - oldestPendingAt),
|
|
381
|
+
target: "<= 300000 when no active lease is present",
|
|
382
|
+
ok: oldestPendingAt === null || staleUnleased === 0,
|
|
383
|
+
severity: "warning",
|
|
384
|
+
});
|
|
385
|
+
const orphanEvents = doctorCount(openedDb, `
|
|
386
|
+
SELECT COUNT(*) count
|
|
387
|
+
FROM session_events event
|
|
388
|
+
LEFT JOIN sessions session ON session.id = event.session_id
|
|
389
|
+
WHERE TRIM(event.session_id) = '' OR session.id IS NULL
|
|
390
|
+
`);
|
|
391
|
+
metrics.push({
|
|
392
|
+
key: "orphan_memory_events",
|
|
393
|
+
value: orphanEvents,
|
|
394
|
+
target: 0,
|
|
395
|
+
ok: orphanEvents === 0,
|
|
396
|
+
severity: "error",
|
|
397
|
+
});
|
|
398
|
+
const staleSessions = doctorCount(openedDb, `
|
|
399
|
+
SELECT COUNT(*) count
|
|
400
|
+
FROM sessions
|
|
401
|
+
WHERE status = 'active' AND created_at < ?
|
|
402
|
+
`, now - SESSION_STALE_MS);
|
|
403
|
+
metrics.push({
|
|
404
|
+
key: "stale_memory_sessions",
|
|
405
|
+
value: staleSessions,
|
|
406
|
+
target: 0,
|
|
407
|
+
ok: staleSessions === 0,
|
|
408
|
+
severity: "error",
|
|
409
|
+
});
|
|
343
410
|
const duplicates = openedDb.prepare(`
|
|
344
411
|
SELECT COUNT(*) AS group_count, COALESCE(SUM(title_count), 0) AS row_count
|
|
345
412
|
FROM (
|
|
@@ -386,6 +453,13 @@ function doctorOpenDatabase(path, dependencies) {
|
|
|
386
453
|
function doctorTableExists(db, table) {
|
|
387
454
|
return Boolean(db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1").get(table));
|
|
388
455
|
}
|
|
456
|
+
function doctorTableColumns(db, table) {
|
|
457
|
+
if (!doctorTableExists(db, table))
|
|
458
|
+
return new Set();
|
|
459
|
+
return new Set(db.prepare(`PRAGMA table_info(${table})`).all()
|
|
460
|
+
.map((column) => column.name)
|
|
461
|
+
.filter((name) => typeof name === "string"));
|
|
462
|
+
}
|
|
389
463
|
function doctorCount(db, sql, ...params) {
|
|
390
464
|
const row = db.prepare(sql).get(...params);
|
|
391
465
|
return Number(row?.count ?? 0);
|
|
@@ -412,6 +486,14 @@ function checkVectors(project, dependencies) {
|
|
|
412
486
|
codegraphNodes = doctorCount(db, "SELECT COUNT(*) count FROM nodes");
|
|
413
487
|
metrics.push({ key: "codegraph_nodes", value: codegraphNodes, ok: true, severity: "info" });
|
|
414
488
|
}
|
|
489
|
+
const freshnessMs = Math.max(0, (dependencies.now ?? Date.now)() - statSync(codegraphPath).mtimeMs);
|
|
490
|
+
metrics.push({
|
|
491
|
+
key: "index_freshness_ms",
|
|
492
|
+
value: freshnessMs,
|
|
493
|
+
target: "recently updated codegraph database",
|
|
494
|
+
ok: true,
|
|
495
|
+
severity: "info",
|
|
496
|
+
});
|
|
415
497
|
}
|
|
416
498
|
catch (error) {
|
|
417
499
|
metrics.push({ key: "codegraph_db_read", value: error.message, target: "readable SQLite database", ok: false, severity: "error" });
|
|
@@ -574,10 +656,69 @@ function checkTelemetry(project, strict, dependencies) {
|
|
|
574
656
|
? doctorCount(db, "SELECT COUNT(*) count FROM telemetry_failures WHERE created_at >= ?", cutoff)
|
|
575
657
|
: 0;
|
|
576
658
|
metrics.push({ key: "telemetry_failures_24h", value: failures, target: 0, ok: failures === 0, severity: strictSeverity });
|
|
659
|
+
const sessionColumns = doctorTableColumns(db, "sessions");
|
|
660
|
+
const toolEventColumns = doctorTableColumns(db, "tool_call_events");
|
|
661
|
+
const executionColumns = doctorTableColumns(db, "skill_executions");
|
|
662
|
+
const sessionSchemaReady = ["id", "project_root", "started_at", "status"]
|
|
663
|
+
.every((column) => sessionColumns.has(column));
|
|
664
|
+
metrics.push({
|
|
665
|
+
key: "telemetry_session_schema",
|
|
666
|
+
value: sessionSchemaReady ? "canonical" : "sessions table or identity columns missing",
|
|
667
|
+
target: "canonical sessions table",
|
|
668
|
+
ok: sessionSchemaReady,
|
|
669
|
+
severity: "error",
|
|
670
|
+
});
|
|
671
|
+
if (sessionSchemaReady) {
|
|
672
|
+
const recentSessionStarts = doctorCount(db, `
|
|
673
|
+
SELECT COUNT(*) count FROM sessions
|
|
674
|
+
WHERE project_root = ? AND started_at >= ?
|
|
675
|
+
`, project, cutoff);
|
|
676
|
+
metrics.push({
|
|
677
|
+
key: "session_start_evidence_24h",
|
|
678
|
+
value: recentSessionStarts,
|
|
679
|
+
target: "present after a Claude SessionStart",
|
|
680
|
+
ok: true,
|
|
681
|
+
severity: "info",
|
|
682
|
+
});
|
|
683
|
+
const staleSessions = doctorCount(db, `
|
|
684
|
+
SELECT COUNT(*) count FROM sessions
|
|
685
|
+
WHERE project_root = ? AND status = 'running' AND started_at < ?
|
|
686
|
+
`, project, (dependencies.now ?? Date.now)() - SESSION_STALE_MS);
|
|
687
|
+
metrics.push({
|
|
688
|
+
key: "stale_telemetry_sessions",
|
|
689
|
+
value: staleSessions,
|
|
690
|
+
target: 0,
|
|
691
|
+
ok: staleSessions === 0,
|
|
692
|
+
severity: "error",
|
|
693
|
+
});
|
|
694
|
+
let orphanToolEvents = 0;
|
|
695
|
+
if (toolEventColumns.has("session_id")) {
|
|
696
|
+
orphanToolEvents = doctorCount(db, `
|
|
697
|
+
SELECT COUNT(*) count
|
|
698
|
+
FROM tool_call_events event
|
|
699
|
+
LEFT JOIN sessions session ON session.id = event.session_id
|
|
700
|
+
WHERE TRIM(COALESCE(event.session_id, '')) = '' OR session.id IS NULL
|
|
701
|
+
`);
|
|
702
|
+
}
|
|
703
|
+
let orphanExecutions = 0;
|
|
704
|
+
if (executionColumns.has("session_id")) {
|
|
705
|
+
orphanExecutions = doctorCount(db, `
|
|
706
|
+
SELECT COUNT(*) count
|
|
707
|
+
FROM skill_executions execution
|
|
708
|
+
LEFT JOIN sessions session ON session.id = execution.session_id
|
|
709
|
+
WHERE TRIM(COALESCE(execution.session_id, '')) = '' OR session.id IS NULL
|
|
710
|
+
`);
|
|
711
|
+
}
|
|
712
|
+
metrics.push({
|
|
713
|
+
key: "orphan_telemetry_events",
|
|
714
|
+
value: { toolCalls: orphanToolEvents, skillExecutions: orphanExecutions },
|
|
715
|
+
target: { toolCalls: 0, skillExecutions: 0 },
|
|
716
|
+
ok: orphanToolEvents === 0 && orphanExecutions === 0,
|
|
717
|
+
severity: "error",
|
|
718
|
+
});
|
|
719
|
+
}
|
|
577
720
|
if (doctorTableExists(db, "tool_call_events")) {
|
|
578
|
-
|
|
579
|
-
.map(column => column.name);
|
|
580
|
-
if (eventColumns.includes("failure_category")) {
|
|
721
|
+
if (toolEventColumns.has("failure_category")) {
|
|
581
722
|
const rows = db.prepare(`
|
|
582
723
|
SELECT failure_category category, COUNT(*) count
|
|
583
724
|
FROM tool_call_events
|
|
@@ -2575,7 +2716,8 @@ function marketplaceSourceRoot(sourcePath) {
|
|
|
2575
2716
|
join(sourcePath, "plugins", "claude-code"),
|
|
2576
2717
|
]) {
|
|
2577
2718
|
const root = canonicalDirectory(candidate);
|
|
2578
|
-
if (root && existsSync(join(root, "hooks.json"))
|
|
2719
|
+
if (root && (existsSync(join(root, "hooks", "hooks.json"))
|
|
2720
|
+
|| existsSync(join(root, "hooks.json"))))
|
|
2579
2721
|
return root;
|
|
2580
2722
|
}
|
|
2581
2723
|
return null;
|
|
@@ -3266,7 +3408,11 @@ function inspectLegacyRoot(project, homePath, target, inspection) {
|
|
|
3266
3408
|
enqueueCommand(command);
|
|
3267
3409
|
}
|
|
3268
3410
|
if (target.inspectPluginConfig || !target.commands) {
|
|
3269
|
-
const
|
|
3411
|
+
const conventionalConfigPath = join(root, "hooks", "hooks.json");
|
|
3412
|
+
const legacyConfigPath = join(root, "hooks.json");
|
|
3413
|
+
const configPath = existsSync(legacyConfigPath)
|
|
3414
|
+
? legacyConfigPath
|
|
3415
|
+
: conventionalConfigPath;
|
|
3270
3416
|
if (!existsSync(configPath)) {
|
|
3271
3417
|
inspection.missing.push(configPath);
|
|
3272
3418
|
enqueue(join(root, "hooks", "session-start"));
|
|
@@ -3395,6 +3541,111 @@ function inspectLegacyRoot(project, homePath, target, inspection) {
|
|
|
3395
3541
|
}
|
|
3396
3542
|
}
|
|
3397
3543
|
}
|
|
3544
|
+
function installedPluginVersionsByRoot(dependencies) {
|
|
3545
|
+
const installedPluginsPath = dependencies.installedPluginsPath
|
|
3546
|
+
?? join(dependencies.homePath ?? homedir(), ".claude", "plugins", "installed_plugins.json");
|
|
3547
|
+
const versions = new Map();
|
|
3548
|
+
if (!existsSync(installedPluginsPath))
|
|
3549
|
+
return new Map();
|
|
3550
|
+
try {
|
|
3551
|
+
const manifest = asRecord(parseJsonFile(installedPluginsPath));
|
|
3552
|
+
const plugins = asRecord(manifest?.plugins);
|
|
3553
|
+
if (!plugins)
|
|
3554
|
+
return new Map();
|
|
3555
|
+
for (const entries of Object.values(plugins)) {
|
|
3556
|
+
if (!Array.isArray(entries))
|
|
3557
|
+
continue;
|
|
3558
|
+
for (const entry of entries) {
|
|
3559
|
+
const record = asRecord(entry);
|
|
3560
|
+
if (typeof record?.installPath !== "string" || typeof record.version !== "string")
|
|
3561
|
+
continue;
|
|
3562
|
+
const root = canonicalDirectory(record.installPath);
|
|
3563
|
+
if (!root)
|
|
3564
|
+
continue;
|
|
3565
|
+
const rootVersions = versions.get(root) ?? new Set();
|
|
3566
|
+
rootVersions.add(record.version);
|
|
3567
|
+
versions.set(root, rootVersions);
|
|
3568
|
+
}
|
|
3569
|
+
}
|
|
3570
|
+
}
|
|
3571
|
+
catch {
|
|
3572
|
+
return new Map();
|
|
3573
|
+
}
|
|
3574
|
+
return new Map([...versions].map(([root, values]) => [root, [...values].sort()]));
|
|
3575
|
+
}
|
|
3576
|
+
function readPluginVersion(path) {
|
|
3577
|
+
try {
|
|
3578
|
+
const metadata = asRecord(parseJsonFile(path));
|
|
3579
|
+
return typeof metadata?.version === "string" ? metadata.version : undefined;
|
|
3580
|
+
}
|
|
3581
|
+
catch {
|
|
3582
|
+
return undefined;
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
function inspectPluginLifecycle(targets, dependencies) {
|
|
3586
|
+
const inspection = {
|
|
3587
|
+
layoutIssues: [],
|
|
3588
|
+
versionIssues: [],
|
|
3589
|
+
versionRecords: [],
|
|
3590
|
+
sessionStartIssues: [],
|
|
3591
|
+
daemonIssues: [],
|
|
3592
|
+
};
|
|
3593
|
+
const installedVersions = installedPluginVersionsByRoot(dependencies);
|
|
3594
|
+
for (const target of targets) {
|
|
3595
|
+
const root = target.root;
|
|
3596
|
+
const packagePath = join(root, "package.json");
|
|
3597
|
+
const manifestPath = join(root, ".claude-plugin", "plugin.json");
|
|
3598
|
+
const hasPackageMetadata = existsSync(packagePath) || existsSync(manifestPath);
|
|
3599
|
+
if (!hasPackageMetadata)
|
|
3600
|
+
continue;
|
|
3601
|
+
const conventionalConfigPath = join(root, "hooks", "hooks.json");
|
|
3602
|
+
const legacyConfigPath = join(root, "hooks.json");
|
|
3603
|
+
if (!existsSync(conventionalConfigPath)) {
|
|
3604
|
+
inspection.layoutIssues.push({ root, issue: "missing hooks/hooks.json" });
|
|
3605
|
+
}
|
|
3606
|
+
if (existsSync(legacyConfigPath)) {
|
|
3607
|
+
inspection.layoutIssues.push({ root, issue: "retired root hooks.json is present" });
|
|
3608
|
+
}
|
|
3609
|
+
const activeConfigPath = existsSync(conventionalConfigPath)
|
|
3610
|
+
? conventionalConfigPath
|
|
3611
|
+
: legacyConfigPath;
|
|
3612
|
+
try {
|
|
3613
|
+
const config = asRecord(parseJsonFile(activeConfigPath));
|
|
3614
|
+
const hooks = asRecord(config?.hooks);
|
|
3615
|
+
const commands = [];
|
|
3616
|
+
collectCommandStrings(hooks?.SessionStart, commands);
|
|
3617
|
+
if (!commands.some((command) => /(?:^|[/\\])session-start(?:\s|$|["'])/.test(command))) {
|
|
3618
|
+
inspection.sessionStartIssues.push({ root, issue: "SessionStart wrapper is not registered" });
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3621
|
+
catch {
|
|
3622
|
+
inspection.sessionStartIssues.push({ root, issue: "hook configuration is unreadable" });
|
|
3623
|
+
}
|
|
3624
|
+
const packageVersion = readPluginVersion(packagePath);
|
|
3625
|
+
const manifestVersion = readPluginVersion(manifestPath);
|
|
3626
|
+
const registryVersions = installedVersions.get(root) ?? [];
|
|
3627
|
+
const versionRecord = { root, packageVersion, manifestVersion, registryVersions };
|
|
3628
|
+
inspection.versionRecords.push(versionRecord);
|
|
3629
|
+
if (!packageVersion
|
|
3630
|
+
|| !manifestVersion
|
|
3631
|
+
|| packageVersion !== manifestVersion
|
|
3632
|
+
|| registryVersions.some((version) => version !== packageVersion)) {
|
|
3633
|
+
inspection.versionIssues.push(versionRecord);
|
|
3634
|
+
}
|
|
3635
|
+
const sessionStartPath = join(root, "hooks", "session-start");
|
|
3636
|
+
const daemonPath = join(root, "dist", "hooks", "hook-daemon.js");
|
|
3637
|
+
try {
|
|
3638
|
+
const sessionStart = readFileSync(sessionStartPath, "utf8");
|
|
3639
|
+
if (!existsSync(daemonPath) || !sessionStart.includes("hook-daemon.js")) {
|
|
3640
|
+
inspection.daemonIssues.push({ root, issue: "SessionStart does not register the packaged hook daemon" });
|
|
3641
|
+
}
|
|
3642
|
+
}
|
|
3643
|
+
catch {
|
|
3644
|
+
inspection.daemonIssues.push({ root, issue: "SessionStart wrapper or hook daemon is missing" });
|
|
3645
|
+
}
|
|
3646
|
+
}
|
|
3647
|
+
return inspection;
|
|
3648
|
+
}
|
|
3398
3649
|
function checkLegacyPaths(project, dependencies) {
|
|
3399
3650
|
const selection = selectActiveLegacyTargets(project, dependencies);
|
|
3400
3651
|
const inspection = {
|
|
@@ -3438,6 +3689,40 @@ function checkLegacyPaths(project, dependencies) {
|
|
|
3438
3689
|
const resolutionMethod = artifactResolutionFailed && !selection.method.endsWith("-unresolved")
|
|
3439
3690
|
? `${selection.method}-unresolved`
|
|
3440
3691
|
: selection.method;
|
|
3692
|
+
const lifecycle = inspectPluginLifecycle(selection.targets, dependencies);
|
|
3693
|
+
const injectedSettingsHome = dependencies.claudeSettingsPath
|
|
3694
|
+
? dirname(dependencies.claudeSettingsPath)
|
|
3695
|
+
: null;
|
|
3696
|
+
const skillHome = dependencies.homePath
|
|
3697
|
+
?? (injectedSettingsHome && basename(injectedSettingsHome) === ".claude"
|
|
3698
|
+
? dirname(injectedSettingsHome)
|
|
3699
|
+
: injectedSettingsHome)
|
|
3700
|
+
?? homedir();
|
|
3701
|
+
const selectedSkillRoot = selection.targets
|
|
3702
|
+
.map((target) => target.root)
|
|
3703
|
+
.find((root) => existsSync(join(root, "skills"))) ?? null;
|
|
3704
|
+
const activePlugin = dependencies.activePluginRoot
|
|
3705
|
+
? {
|
|
3706
|
+
root: existsSync(dependencies.activePluginRoot) ? dependencies.activePluginRoot : null,
|
|
3707
|
+
version: null,
|
|
3708
|
+
}
|
|
3709
|
+
: selectedSkillRoot
|
|
3710
|
+
? { root: selectedSkillRoot, version: null }
|
|
3711
|
+
: resolveActivePlugin(skillHome, {
|
|
3712
|
+
installedPluginsPath: dependencies.installedPluginsPath,
|
|
3713
|
+
pluginCachePath: dependencies.pluginCachePath,
|
|
3714
|
+
});
|
|
3715
|
+
const activeSkillRoot = activePlugin.root;
|
|
3716
|
+
const shadowedSkills = activeSkillRoot
|
|
3717
|
+
? scanClaudeSkillShadowing(skillHome, activeSkillRoot)
|
|
3718
|
+
: [];
|
|
3719
|
+
const remediation = shadowedSkills
|
|
3720
|
+
.filter((skill) => !skill.isKnownDevFlowArtifact)
|
|
3721
|
+
.map((skill) => ({
|
|
3722
|
+
skillName: skill.skillName,
|
|
3723
|
+
path: skill.globalPath,
|
|
3724
|
+
action: "inspect manually; file is modified or not a known DevFlow artifact",
|
|
3725
|
+
}));
|
|
3441
3726
|
const metrics = [
|
|
3442
3727
|
{
|
|
3443
3728
|
key: "active_plugin_resolution",
|
|
@@ -3458,6 +3743,34 @@ function checkLegacyPaths(project, dependencies) {
|
|
|
3458
3743
|
ok: true,
|
|
3459
3744
|
severity: "info",
|
|
3460
3745
|
},
|
|
3746
|
+
{
|
|
3747
|
+
key: "plugin_hook_layout",
|
|
3748
|
+
value: lifecycle.layoutIssues,
|
|
3749
|
+
target: "hooks/hooks.json present and root hooks.json absent",
|
|
3750
|
+
ok: lifecycle.layoutIssues.length === 0,
|
|
3751
|
+
severity: "error",
|
|
3752
|
+
},
|
|
3753
|
+
{
|
|
3754
|
+
key: "plugin_version_parity",
|
|
3755
|
+
value: lifecycle.versionRecords,
|
|
3756
|
+
target: "manifest, package, and installed registry versions match",
|
|
3757
|
+
ok: lifecycle.versionIssues.length === 0,
|
|
3758
|
+
severity: "error",
|
|
3759
|
+
},
|
|
3760
|
+
{
|
|
3761
|
+
key: "session_start_hook_registration",
|
|
3762
|
+
value: lifecycle.sessionStartIssues,
|
|
3763
|
+
target: [],
|
|
3764
|
+
ok: lifecycle.sessionStartIssues.length === 0,
|
|
3765
|
+
severity: "error",
|
|
3766
|
+
},
|
|
3767
|
+
{
|
|
3768
|
+
key: "hook_daemon_artifact_registration",
|
|
3769
|
+
value: lifecycle.daemonIssues,
|
|
3770
|
+
target: [],
|
|
3771
|
+
ok: lifecycle.daemonIssues.length === 0,
|
|
3772
|
+
severity: "error",
|
|
3773
|
+
},
|
|
3461
3774
|
{
|
|
3462
3775
|
key: "malformed_active_settings",
|
|
3463
3776
|
value: selection.malformedSettings,
|
|
@@ -3541,7 +3854,40 @@ function checkLegacyPaths(project, dependencies) {
|
|
|
3541
3854
|
ok: true,
|
|
3542
3855
|
severity: "info",
|
|
3543
3856
|
},
|
|
3857
|
+
{
|
|
3858
|
+
key: "skill_shadowing_count",
|
|
3859
|
+
value: shadowedSkills.length,
|
|
3860
|
+
target: 0,
|
|
3861
|
+
ok: shadowedSkills.length === 0,
|
|
3862
|
+
severity: "error",
|
|
3863
|
+
},
|
|
3864
|
+
{
|
|
3865
|
+
key: "shadowed_skills",
|
|
3866
|
+
value: shadowedSkills,
|
|
3867
|
+
target: [],
|
|
3868
|
+
ok: shadowedSkills.length === 0,
|
|
3869
|
+
severity: "error",
|
|
3870
|
+
},
|
|
3871
|
+
{
|
|
3872
|
+
key: "active_skill_source",
|
|
3873
|
+
value: activeSkillRoot,
|
|
3874
|
+
target: "existing plugin cache path",
|
|
3875
|
+
ok: true,
|
|
3876
|
+
severity: "info",
|
|
3877
|
+
},
|
|
3878
|
+
{
|
|
3879
|
+
key: "skill_shadowing_remediation",
|
|
3880
|
+
value: remediation,
|
|
3881
|
+
target: [],
|
|
3882
|
+
ok: true,
|
|
3883
|
+
severity: "info",
|
|
3884
|
+
},
|
|
3544
3885
|
];
|
|
3886
|
+
const activePluginVersion = activePlugin.version
|
|
3887
|
+
?? lifecycle.versionRecords.find((record) => record.root === activeSkillRoot)?.packageVersion;
|
|
3888
|
+
if (activePluginVersion) {
|
|
3889
|
+
metrics.push({ key: "active_plugin_version", value: activePluginVersion, ok: true, severity: "info" });
|
|
3890
|
+
}
|
|
3545
3891
|
return { name: "Legacy paths", status: sectionStatus(metrics), metrics };
|
|
3546
3892
|
}
|
|
3547
3893
|
function loadAcceptanceManifest(dependencies) {
|
|
@@ -3839,7 +4185,7 @@ export async function runStructuredDoctor(options = {}, dependencies = {}) {
|
|
|
3839
4185
|
const exitCode = options.strict && sections.some((section) => section.status !== "green")
|
|
3840
4186
|
? 1
|
|
3841
4187
|
: 0;
|
|
3842
|
-
return { sections, exitCode };
|
|
4188
|
+
return { schemaVersion: DOCTOR_SCHEMA_VERSION, sections, exitCode };
|
|
3843
4189
|
}
|
|
3844
4190
|
export function runDoctor(options, dependencies = {}) {
|
|
3845
4191
|
return options === undefined
|