safeword 0.46.2 → 0.48.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/dist/check-OFMUQD3Q.js +112 -0
- package/dist/check-OFMUQD3Q.js.map +1 -0
- package/dist/{chunk-G2UYJDPN.js → chunk-2HB6H4G5.js} +6 -4
- package/dist/chunk-2HB6H4G5.js.map +1 -0
- package/dist/{check-2RU6E3DW.js → chunk-42IGUF5V.js} +116 -132
- package/dist/chunk-42IGUF5V.js.map +1 -0
- package/dist/{chunk-2EH6Z7P3.js → chunk-4J3GYDJF.js} +330 -145
- package/dist/chunk-4J3GYDJF.js.map +1 -0
- package/dist/chunk-54KAYQTY.js +120 -0
- package/dist/chunk-54KAYQTY.js.map +1 -0
- package/dist/{chunk-PZ6ZWEF6.js → chunk-IGULTNHR.js} +19 -6
- package/dist/chunk-IGULTNHR.js.map +1 -0
- package/dist/chunk-QLXFPFIC.js +464 -0
- package/dist/chunk-QLXFPFIC.js.map +1 -0
- package/dist/{chunk-U5XQME4I.js → chunk-SD4UB7HJ.js} +54 -28
- package/dist/chunk-SD4UB7HJ.js.map +1 -0
- package/dist/{chunk-HN3ITK4W.js → chunk-UMPMYZ4F.js} +86 -3
- package/dist/chunk-UMPMYZ4F.js.map +1 -0
- package/dist/{chunk-AFJEWSWF.js → chunk-ZNIJO52Z.js} +3 -2
- package/dist/{chunk-AFJEWSWF.js.map → chunk-ZNIJO52Z.js.map} +1 -1
- package/dist/cli.js +18 -11
- package/dist/cli.js.map +1 -1
- package/dist/{codify-D6WZ5AS4.js → codify-HIWCNPCY.js} +64 -18
- package/dist/codify-HIWCNPCY.js.map +1 -0
- package/dist/{diff-ODVLDTIF.js → diff-SACXR7ES.js} +3 -3
- package/dist/index.js +1 -1
- package/dist/lint-gherkin-KJH3GNJQ.js +49 -0
- package/dist/lint-gherkin-KJH3GNJQ.js.map +1 -0
- package/dist/presets/typescript/index.d.ts +4 -1
- package/dist/presets/typescript/index.js +1 -1
- package/dist/{reset-I6G7C33F.js → reset-AJ4B74Y2.js} +3 -3
- package/dist/{setup-FXQTOYBA.js → setup-IV3O3CKO.js} +25 -9
- package/dist/setup-IV3O3CKO.js.map +1 -0
- package/dist/{sync-config-JA5ASYNG.js → sync-config-ELXIXIY3.js} +2 -2
- package/dist/{sync-learnings-3RSSZUYD.js → sync-learnings-D2JYMRKZ.js} +2 -2
- package/dist/{sync-tickets-MVFO7377.js → sync-tickets-YDFPSIVS.js} +3 -3
- package/dist/{ticket-new-J3XJOQVP.js → ticket-new-BATYGHWL.js} +2 -2
- package/dist/{upgrade-GEVFSMCL.js → upgrade-FYGFT3CJ.js} +85 -5
- package/dist/upgrade-FYGFT3CJ.js.map +1 -0
- package/package.json +12 -10
- package/templates/SAFEWORD.md +3 -1
- package/templates/codex/config.toml +33 -0
- package/templates/commands/audit.md +21 -3
- package/templates/commands/verify.md +21 -5
- package/templates/cucumber/cucumber.mjs +48 -4
- package/templates/cucumber/safeword-lane.feature +3 -3
- package/templates/cucumber/shared.steps.ts +3 -3
- package/templates/cursor/rules/safeword-quality-reviewing.mdc +3 -1
- package/templates/doc-templates/test-definitions-feature.md +8 -18
- package/templates/guides/context-files-guide.md +7 -28
- package/templates/guides/planning-guide.md +33 -18
- package/templates/hooks/codex/pre-tool-quality.ts +181 -0
- package/templates/hooks/lib/dependency-readiness.ts +789 -0
- package/templates/hooks/lib/lint.ts +21 -0
- package/templates/hooks/lib/quality-state.ts +9 -0
- package/templates/hooks/lib/replan-relevance.ts +13 -2
- package/templates/hooks/lib/skill-invocation-log.ts +9 -5
- package/templates/hooks/lib/test-runner.ts +81 -28
- package/templates/hooks/pre-tool-dependency-readiness.ts +72 -0
- package/templates/hooks/pre-tool-quality.ts +2 -1
- package/templates/hooks/record-skill-invocation.ts +48 -0
- package/templates/hooks/resolve-namespace-root.ts +9 -0
- package/templates/hooks/session-compact-context.ts +2 -3
- package/templates/hooks/session-dependency-readiness.ts +91 -0
- package/templates/hooks/session-safeword-context.ts +89 -0
- package/templates/hooks/stop-quality.ts +7 -5
- package/templates/skills/audit/SKILL.md +22 -4
- package/templates/skills/bdd/SCENARIOS.md +53 -45
- package/templates/skills/bdd/SKILL.md +1 -1
- package/templates/skills/bdd/TDD.md +9 -0
- package/templates/skills/explain/SKILL.md +1 -1
- package/templates/skills/quality-review/SKILL.md +2 -1
- package/templates/skills/review-spec/SKILL.md +2 -2
- package/templates/skills/verify/SKILL.md +21 -5
- package/dist/check-2RU6E3DW.js.map +0 -1
- package/dist/chunk-2EH6Z7P3.js.map +0 -1
- package/dist/chunk-3BMVTFFM.js +0 -65
- package/dist/chunk-3BMVTFFM.js.map +0 -1
- package/dist/chunk-G2UYJDPN.js.map +0 -1
- package/dist/chunk-HN3ITK4W.js.map +0 -1
- package/dist/chunk-PZ6ZWEF6.js.map +0 -1
- package/dist/chunk-U5XQME4I.js.map +0 -1
- package/dist/codify-D6WZ5AS4.js.map +0 -1
- package/dist/setup-FXQTOYBA.js.map +0 -1
- package/dist/upgrade-GEVFSMCL.js.map +0 -1
- package/templates/hooks/session-verify-agents.ts +0 -32
- /package/dist/{diff-ODVLDTIF.js.map → diff-SACXR7ES.js.map} +0 -0
- /package/dist/{reset-I6G7C33F.js.map → reset-AJ4B74Y2.js.map} +0 -0
- /package/dist/{sync-config-JA5ASYNG.js.map → sync-config-ELXIXIY3.js.map} +0 -0
- /package/dist/{sync-learnings-3RSSZUYD.js.map → sync-learnings-D2JYMRKZ.js.map} +0 -0
- /package/dist/{sync-tickets-MVFO7377.js.map → sync-tickets-YDFPSIVS.js.map} +0 -0
- /package/dist/{ticket-new-J3XJOQVP.js.map → ticket-new-BATYGHWL.js.map} +0 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isNewerVersion
|
|
3
|
+
} from "./chunk-FJYRWU2V.js";
|
|
4
|
+
import {
|
|
5
|
+
checkHealth,
|
|
6
|
+
reportHealthSummary
|
|
7
|
+
} from "./chunk-42IGUF5V.js";
|
|
8
|
+
import {
|
|
9
|
+
syncTickets
|
|
10
|
+
} from "./chunk-2HB6H4G5.js";
|
|
11
|
+
import "./chunk-NHXVS5FL.js";
|
|
12
|
+
import "./chunk-IGULTNHR.js";
|
|
13
|
+
import "./chunk-QLXFPFIC.js";
|
|
14
|
+
import "./chunk-4J3GYDJF.js";
|
|
15
|
+
import "./chunk-54KAYQTY.js";
|
|
16
|
+
import "./chunk-LODQOJEK.js";
|
|
17
|
+
import "./chunk-HSC7TELY.js";
|
|
18
|
+
import {
|
|
19
|
+
header,
|
|
20
|
+
info,
|
|
21
|
+
keyValue,
|
|
22
|
+
success,
|
|
23
|
+
warn
|
|
24
|
+
} from "./chunk-445LAX4Y.js";
|
|
25
|
+
|
|
26
|
+
// src/commands/check.ts
|
|
27
|
+
import process from "process";
|
|
28
|
+
async function checkLatestVersion(timeout = 3e3) {
|
|
29
|
+
try {
|
|
30
|
+
const controller = new AbortController();
|
|
31
|
+
const timeoutId = setTimeout(() => {
|
|
32
|
+
controller.abort();
|
|
33
|
+
}, timeout);
|
|
34
|
+
const response = await fetch("https://registry.npmjs.org/safeword/latest", {
|
|
35
|
+
signal: controller.signal
|
|
36
|
+
});
|
|
37
|
+
clearTimeout(timeoutId);
|
|
38
|
+
if (!response.ok) return void 0;
|
|
39
|
+
const data = await response.json();
|
|
40
|
+
return data.version ?? void 0;
|
|
41
|
+
} catch {
|
|
42
|
+
return void 0;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function reportUpdateStatus(health) {
|
|
46
|
+
info("\nChecking for updates...");
|
|
47
|
+
const latestVersion = await checkLatestVersion();
|
|
48
|
+
if (!latestVersion) {
|
|
49
|
+
warn("Couldn't check for updates (offline?)");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
health.latestVersion = latestVersion;
|
|
53
|
+
health.updateAvailable = isNewerVersion(health.cliVersion, latestVersion);
|
|
54
|
+
if (health.updateAvailable) {
|
|
55
|
+
warn(`Update available: v${latestVersion}`);
|
|
56
|
+
info("Run `bunx safeword@latest upgrade` to upgrade");
|
|
57
|
+
} else {
|
|
58
|
+
success("CLI is up to date");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function reportVersionMismatch(health) {
|
|
62
|
+
if (!health.projectVersion) return;
|
|
63
|
+
if (isNewerVersion(health.cliVersion, health.projectVersion)) {
|
|
64
|
+
warn(`Project config (v${health.projectVersion}) is newer than CLI (v${health.cliVersion})`);
|
|
65
|
+
info("Consider upgrading the CLI");
|
|
66
|
+
} else if (isNewerVersion(health.projectVersion, health.cliVersion)) {
|
|
67
|
+
info(`
|
|
68
|
+
Upgrade available for project config`);
|
|
69
|
+
info(
|
|
70
|
+
`Run \`safeword upgrade\` to update from v${health.projectVersion} to v${health.cliVersion}`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function regenerateTicketIndex(cwd) {
|
|
75
|
+
try {
|
|
76
|
+
const result = syncTickets(cwd);
|
|
77
|
+
if (result.wrote) {
|
|
78
|
+
info("Regenerated ticket index (INDEX.md / INDEX-completed.md)");
|
|
79
|
+
}
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (process.env.DEBUG) {
|
|
82
|
+
console.error("[check] ticket index regen failed:", error);
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async function check(options) {
|
|
88
|
+
const cwd = process.cwd();
|
|
89
|
+
header("Safeword Health Check");
|
|
90
|
+
const health = await checkHealth(cwd);
|
|
91
|
+
if (!health.configured) {
|
|
92
|
+
info("Not configured. Run `safeword setup` to initialize.");
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
regenerateTicketIndex(cwd);
|
|
96
|
+
keyValue("Safeword CLI", `v${health.cliVersion}`);
|
|
97
|
+
keyValue("Project config", health.projectVersion ? `v${health.projectVersion}` : "unknown");
|
|
98
|
+
if (options.offline) {
|
|
99
|
+
info("\nSkipped update check (offline mode)");
|
|
100
|
+
} else {
|
|
101
|
+
await reportUpdateStatus(health);
|
|
102
|
+
}
|
|
103
|
+
reportVersionMismatch(health);
|
|
104
|
+
const hasIssues = reportHealthSummary(health);
|
|
105
|
+
if (hasIssues) {
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
export {
|
|
110
|
+
check
|
|
111
|
+
};
|
|
112
|
+
//# sourceMappingURL=check-OFMUQD3Q.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/check.ts"],"sourcesContent":["/**\n * Check command - Verify project health and configuration\n *\n * The config-health core lives in ../health.ts (shared with the setup/upgrade\n * self-verify, ticket 3293WH). This command adds the standalone-only\n * surfaces: npm update-check, version display, and ticket-index refresh.\n */\n\nimport process from 'node:process';\n\nimport { checkHealth, type HealthStatus, reportHealthSummary } from '../health.js';\nimport { syncTickets } from '../ticket-sync/index.js';\nimport { header, info, keyValue, success, warn } from '../utils/output.js';\nimport { isNewerVersion } from '../utils/version.js';\n\ninterface CheckOptions {\n offline?: boolean;\n}\n\n/**\n * Check for latest version from npm (with timeout)\n * @param timeout\n */\nasync function checkLatestVersion(timeout = 3000): Promise<string | undefined> {\n try {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => {\n controller.abort();\n }, timeout);\n\n const response = await fetch('https://registry.npmjs.org/safeword/latest', {\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) return undefined;\n\n const data = (await response.json()) as { version?: string };\n return data.version ?? undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Check for CLI updates and report status\n * @param health\n */\nasync function reportUpdateStatus(health: HealthStatus): Promise<void> {\n info('\\nChecking for updates...');\n const latestVersion = await checkLatestVersion();\n\n if (!latestVersion) {\n warn(\"Couldn't check for updates (offline?)\");\n return;\n }\n\n health.latestVersion = latestVersion;\n health.updateAvailable = isNewerVersion(health.cliVersion, latestVersion);\n\n if (health.updateAvailable) {\n warn(`Update available: v${latestVersion}`);\n info('Run `bunx safeword@latest upgrade` to upgrade');\n } else {\n success('CLI is up to date');\n }\n}\n\n/**\n * Compare project version vs CLI version and report\n * @param health\n */\nfunction reportVersionMismatch(health: HealthStatus): void {\n if (!health.projectVersion) return;\n\n if (isNewerVersion(health.cliVersion, health.projectVersion)) {\n warn(`Project config (v${health.projectVersion}) is newer than CLI (v${health.cliVersion})`);\n info('Consider upgrading the CLI');\n } else if (isNewerVersion(health.projectVersion, health.cliVersion)) {\n info(`\\nUpgrade available for project config`);\n info(\n `Run \\`safeword upgrade\\` to update from v${health.projectVersion} to v${health.cliVersion}`,\n );\n }\n}\n\n/**\n * Regenerate the ticket discovery index, swallowing any error — index\n * freshness must never block or fail a health check. Reports only when it\n * actually rewrote a file.\n * @param cwd\n */\nfunction regenerateTicketIndex(cwd: string): void {\n try {\n const result = syncTickets(cwd);\n if (result.wrote) {\n info('Regenerated ticket index (INDEX.md / INDEX-completed.md)');\n }\n } catch (error: unknown) {\n // Best-effort: index freshness must never fail the health check. Surface\n // under DEBUG, then return — the deliberate swallow point.\n if (process.env.DEBUG) {\n console.error('[check] ticket index regen failed:', error);\n }\n return;\n }\n}\n\n/**\n *\n * @param options\n */\nexport async function check(options: CheckOptions): Promise<void> {\n const cwd = process.cwd();\n\n header('Safeword Health Check');\n\n const health = await checkHealth(cwd);\n\n // Not configured\n if (!health.configured) {\n info('Not configured. Run `safeword setup` to initialize.');\n return;\n }\n\n // Keep the ticket discovery index fresh at this checkpoint (best-effort —\n // never fail the health check on index regen). Ticket 1GGD28.\n regenerateTicketIndex(cwd);\n\n // Show versions\n keyValue('Safeword CLI', `v${health.cliVersion}`);\n keyValue('Project config', health.projectVersion ? `v${health.projectVersion}` : 'unknown');\n\n // Check for updates (unless offline)\n if (options.offline) {\n info('\\nSkipped update check (offline mode)');\n } else {\n await reportUpdateStatus(health);\n }\n\n reportVersionMismatch(health);\n const hasIssues = reportHealthSummary(health);\n\n if (hasIssues) {\n process.exit(1);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAQA,OAAO,aAAa;AAepB,eAAe,mBAAmB,UAAU,KAAmC;AAC7E,MAAI;AACF,UAAM,aAAa,IAAI,gBAAgB;AACvC,UAAM,YAAY,WAAW,MAAM;AACjC,iBAAW,MAAM;AAAA,IACnB,GAAG,OAAO;AAEV,UAAM,WAAW,MAAM,MAAM,8CAA8C;AAAA,MACzE,QAAQ,WAAW;AAAA,IACrB,CAAC;AAED,iBAAa,SAAS;AAEtB,QAAI,CAAC,SAAS,GAAI,QAAO;AAEzB,UAAM,OAAQ,MAAM,SAAS,KAAK;AAClC,WAAO,KAAK,WAAW;AAAA,EACzB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAMA,eAAe,mBAAmB,QAAqC;AACrE,OAAK,2BAA2B;AAChC,QAAM,gBAAgB,MAAM,mBAAmB;AAE/C,MAAI,CAAC,eAAe;AAClB,SAAK,uCAAuC;AAC5C;AAAA,EACF;AAEA,SAAO,gBAAgB;AACvB,SAAO,kBAAkB,eAAe,OAAO,YAAY,aAAa;AAExE,MAAI,OAAO,iBAAiB;AAC1B,SAAK,sBAAsB,aAAa,EAAE;AAC1C,SAAK,+CAA+C;AAAA,EACtD,OAAO;AACL,YAAQ,mBAAmB;AAAA,EAC7B;AACF;AAMA,SAAS,sBAAsB,QAA4B;AACzD,MAAI,CAAC,OAAO,eAAgB;AAE5B,MAAI,eAAe,OAAO,YAAY,OAAO,cAAc,GAAG;AAC5D,SAAK,oBAAoB,OAAO,cAAc,yBAAyB,OAAO,UAAU,GAAG;AAC3F,SAAK,4BAA4B;AAAA,EACnC,WAAW,eAAe,OAAO,gBAAgB,OAAO,UAAU,GAAG;AACnE,SAAK;AAAA,qCAAwC;AAC7C;AAAA,MACE,4CAA4C,OAAO,cAAc,QAAQ,OAAO,UAAU;AAAA,IAC5F;AAAA,EACF;AACF;AAQA,SAAS,sBAAsB,KAAmB;AAChD,MAAI;AACF,UAAM,SAAS,YAAY,GAAG;AAC9B,QAAI,OAAO,OAAO;AAChB,WAAK,0DAA0D;AAAA,IACjE;AAAA,EACF,SAAS,OAAgB;AAGvB,QAAI,QAAQ,IAAI,OAAO;AACrB,cAAQ,MAAM,sCAAsC,KAAK;AAAA,IAC3D;AACA;AAAA,EACF;AACF;AAMA,eAAsB,MAAM,SAAsC;AAChE,QAAM,MAAM,QAAQ,IAAI;AAExB,SAAO,uBAAuB;AAE9B,QAAM,SAAS,MAAM,YAAY,GAAG;AAGpC,MAAI,CAAC,OAAO,YAAY;AACtB,SAAK,qDAAqD;AAC1D;AAAA,EACF;AAIA,wBAAsB,GAAG;AAGzB,WAAS,gBAAgB,IAAI,OAAO,UAAU,EAAE;AAChD,WAAS,kBAAkB,OAAO,iBAAiB,IAAI,OAAO,cAAc,KAAK,SAAS;AAG1F,MAAI,QAAQ,SAAS;AACnB,SAAK,uCAAuC;AAAA,EAC9C,OAAO;AACL,UAAM,mBAAmB,MAAM;AAAA,EACjC;AAEA,wBAAsB,MAAM;AAC5B,QAAM,YAAY,oBAAoB,MAAM;AAE5C,MAAI,WAAW;AACb,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF;","names":[]}
|
|
@@ -3,7 +3,11 @@ import {
|
|
|
3
3
|
} from "./chunk-NHXVS5FL.js";
|
|
4
4
|
import {
|
|
5
5
|
resolveTicketsDirectory
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-54KAYQTY.js";
|
|
7
|
+
|
|
8
|
+
// src/ticket-sync/index.ts
|
|
9
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from "fs";
|
|
10
|
+
import nodePath from "path";
|
|
7
11
|
|
|
8
12
|
// src/utils/ticket-relations.ts
|
|
9
13
|
function parseTicketIdList(raw) {
|
|
@@ -56,8 +60,6 @@ function findTicketsInCycles(nodes) {
|
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
// src/ticket-sync/index.ts
|
|
59
|
-
import { existsSync, readdirSync, readFileSync, writeFileSync } from "fs";
|
|
60
|
-
import nodePath from "path";
|
|
61
63
|
var TICKETS_RELATIVE_PATH = "<namespace-root>/tickets";
|
|
62
64
|
var INDEX_FILENAME = "INDEX.md";
|
|
63
65
|
var COMPLETED_INDEX_FILENAME = "INDEX-completed.md";
|
|
@@ -242,4 +244,4 @@ export {
|
|
|
242
244
|
readTickets,
|
|
243
245
|
syncTickets
|
|
244
246
|
};
|
|
245
|
-
//# sourceMappingURL=chunk-
|
|
247
|
+
//# sourceMappingURL=chunk-2HB6H4G5.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/ticket-sync/index.ts","../src/utils/ticket-relations.ts"],"sourcesContent":["/**\n * Ticket sync — generates capability-discovery indexes over the ticket corpus:\n * `<namespace-root>/tickets/INDEX.md` (active tickets, grouped by epic) and\n * `INDEX-completed.md` (the `completed/` archive). Mirrors `learning-sync`\n * (plain markdown + grep, no skill-description char cap) so \"is there already\n * a ticket for X?\" is one grep instead of a hundreds-of-folders hunt.\n *\n * Fired manually via `safeword sync-tickets`, as a `safeword check` step, and\n * after `ticket new`.\n *\n * Ticket 1GGD28.\n */\n\nimport { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport nodePath from 'node:path';\n\nimport { resolveTicketsDirectory } from '../utils/configured-paths.js';\nimport { formatTicketReference } from '../utils/ticket-reference.js';\nimport { deriveBlocks, parseTicketIdList } from '../utils/ticket-relations.js';\n\n/** Placeholder label for callers that read a directory without a project cwd. */\nexport const TICKETS_RELATIVE_PATH = '<namespace-root>/tickets';\nexport const INDEX_FILENAME = 'INDEX.md';\nexport const COMPLETED_INDEX_FILENAME = 'INDEX-completed.md';\nexport const COMPLETED_DIRNAME = 'completed';\n\nconst NO_EPIC_GROUP = '(no epic)';\nconst SKIP_DIRECTORIES = new Set([COMPLETED_DIRNAME, 'tmp']);\n\nexport interface TicketEntry {\n id: string;\n folder: string; // folder name, e.g. 1GGD28-ticket-discovery-index\n relativePath: string; // e.g. <namespace-root>/tickets/1GGD28-ticket-discovery-index\n title: string;\n status: string;\n epic: string | undefined; // undefined → grouped under \"(no epic)\"\n goal: string | undefined; // the **Goal:** one-liner, when present\n dependsOn: string[]; // ticket ids this one depends on (directed edge); [] when none\n}\n\nexport interface TicketSyncResult {\n wrote: boolean;\n active: TicketEntry[];\n completed: TicketEntry[];\n skipped: { folder: string; reason: string }[];\n indexPath: string;\n completedIndexPath: string;\n}\n\n/** Strip a single layer of matching surrounding quotes. */\nfunction stripQuotes(value: string): string {\n if (\n value.length >= 2 &&\n ((value.startsWith(\"'\") && value.endsWith(\"'\")) ||\n (value.startsWith('\"') && value.endsWith('\"')))\n ) {\n return value.slice(1, -1);\n }\n return value;\n}\n\n/** Parse the leading `--- … ---` frontmatter block into a key→value map. */\nfunction parseFrontmatter(content: string): { fields: Map<string, string>; bodyStart: number } {\n const lines = content.split('\\n');\n const fields = new Map<string, string>();\n if (lines[0]?.trim() !== '---') return { fields, bodyStart: 0 };\n\n for (let index = 1; index < lines.length; index += 1) {\n const line = lines[index] ?? '';\n if (line.trim() === '---') return { fields, bodyStart: index + 1 };\n const match = /^([a-z_][\\w-]*):(.*)$/i.exec(line);\n if (match?.[1] !== undefined) fields.set(match[1], stripQuotes((match[2] ?? '').trim()));\n }\n return { fields, bodyStart: 0 };\n}\n\n/** First `# H1` heading text in the body, if any. */\nfunction firstHeading(bodyLines: string[]): string | undefined {\n for (const line of bodyLines) {\n if (line.startsWith('# ')) return line.slice(2).trim();\n }\n return undefined;\n}\n\n/** The `**Goal:**` one-liner from the body, label stripped, if present. */\nfunction goalLine(bodyLines: string[]): string | undefined {\n for (const line of bodyLines) {\n const match = /^\\*\\*Goal:\\*\\*(.*)$/.exec(line.trim());\n if (match?.[1] !== undefined) {\n const goal = match[1].trim();\n if (goal.length > 0) return goal;\n }\n }\n return undefined;\n}\n\n/**\n * Parse a single ticket.md. Returns the entry (minus relativePath) when it has\n * an `id:`, or a skip reason. Title resolves frontmatter `title` → first H1 →\n * frontmatter `slug` → folder name.\n */\nfunction parseTicket(\n filePath: string,\n folder: string,\n): { ok: true; entry: Omit<TicketEntry, 'relativePath'> } | { ok: false; reason: string } {\n const content = readFileSync(filePath, 'utf8');\n const { fields, bodyStart } = parseFrontmatter(content);\n\n const id = fields.get('id');\n if (id === undefined || id.length === 0) {\n return { ok: false, reason: 'missing id: in frontmatter' };\n }\n\n const bodyLines = content.split('\\n').slice(bodyStart);\n const title = fields.get('title') ?? firstHeading(bodyLines) ?? fields.get('slug') ?? folder;\n const status = fields.get('status') ?? '—';\n const epic = fields.get('epic');\n\n return {\n ok: true,\n entry: {\n id,\n folder,\n title,\n status,\n epic,\n goal: goalLine(bodyLines),\n dependsOn: parseTicketIdList(fields.get('depends_on')),\n },\n };\n}\n\n/** Parse every ticket folder directly under `directory`, returning entries +\n * skip reasons. Folders without a ticket.md are silently ignored (not skipped).\n * `pathPrefix` is prepended to the folder for the entry's relativePath. */\nfunction readTicketFolders(\n directory: string,\n pathPrefix: string,\n): { entries: TicketEntry[]; skipped: { folder: string; reason: string }[] } {\n if (!existsSync(directory)) return { entries: [], skipped: [] };\n\n const entries: TicketEntry[] = [];\n const skipped: { folder: string; reason: string }[] = [];\n\n const folders = readdirSync(directory, { withFileTypes: true })\n .filter(dirent => dirent.isDirectory() && !SKIP_DIRECTORIES.has(dirent.name))\n .map(dirent => dirent.name)\n .toSorted((a, b) => a.localeCompare(b));\n\n for (const folder of folders) {\n const ticketPath = nodePath.join(directory, folder, 'ticket.md');\n if (!existsSync(ticketPath)) continue; // not a ticket folder — ignore\n const parsed = parseTicket(ticketPath, folder);\n if (parsed.ok) {\n entries.push({ ...parsed.entry, relativePath: `${pathPrefix}/${folder}` });\n } else {\n skipped.push({ folder, reason: parsed.reason });\n }\n }\n\n return { entries, skipped };\n}\n\n/**\n * Read the corpus into active (top-level) and completed (`completed/`) entries,\n * each sorted by id, plus any skipped folders. INDEX*.md are files, so the\n * directory filter excludes them from being parsed as tickets.\n */\nexport function readTickets(\n ticketsDirectory: string,\n relativeLabel: string = TICKETS_RELATIVE_PATH,\n): {\n active: TicketEntry[];\n completed: TicketEntry[];\n skipped: { folder: string; reason: string }[];\n} {\n const active = readTicketFolders(ticketsDirectory, relativeLabel);\n const completed = readTicketFolders(\n nodePath.join(ticketsDirectory, COMPLETED_DIRNAME),\n `${relativeLabel}/${COMPLETED_DIRNAME}`,\n );\n\n const byId = (a: TicketEntry, b: TicketEntry) => a.id.localeCompare(b.id);\n return {\n active: active.entries.toSorted(byId),\n completed: completed.entries.toSorted(byId),\n skipped: [...active.skipped, ...completed.skipped],\n };\n}\n\n/** Render a list of related ticket ids slug-first, falling back to the bare id\n * for targets outside this index (cross-variant or not-yet-created). */\nfunction renderRelation(ids: string[], labelById: Map<string, string>): string {\n return ids\n .map(id => {\n const title = labelById.get(id);\n return title === undefined ? id : formatTicketReference(id, title);\n })\n .join(', ');\n}\n\n/** Render one entry as a block: header, optional goal, relation edges, path. */\nfunction renderEntry(\n entry: TicketEntry,\n blocks: Map<string, string[]>,\n labelById: Map<string, string>,\n): string[] {\n const epic = entry.epic ?? '—';\n const lines = [\n `- **${formatTicketReference(entry.id, entry.title)}** (${entry.status}, epic: ${epic})`,\n ];\n if (entry.goal !== undefined) lines.push(` ${entry.goal}`);\n if (entry.dependsOn.length > 0) {\n lines.push(` blocked by: ${renderRelation(entry.dependsOn, labelById)}`);\n }\n const blocking = blocks.get(entry.id) ?? [];\n if (blocking.length > 0) lines.push(` blocks: ${renderRelation(blocking, labelById)}`);\n lines.push(` → \\`${entry.relativePath}\\``);\n return lines;\n}\n\n/** Group entries by epic; \"(no epic)\" sorts last, every other group alphabetical. */\nfunction groupByEpic(entries: TicketEntry[]): [string, TicketEntry[]][] {\n const groups = new Map<string, TicketEntry[]>();\n for (const entry of entries) {\n const key = entry.epic ?? NO_EPIC_GROUP;\n const bucket = groups.get(key);\n if (bucket) bucket.push(entry);\n else groups.set(key, [entry]);\n }\n return [...groups.entries()].toSorted(([a], [b]) => {\n if (a === NO_EPIC_GROUP) return 1;\n if (b === NO_EPIC_GROUP) return -1;\n return a.localeCompare(b);\n });\n}\n\n/**\n * Render the full index for one variant. Deterministic: same entries → same\n * bytes. No size cap — agents Read or grep the file.\n */\nexport function buildIndexContent(\n entries: TicketEntry[],\n options: { variant: 'active' | 'completed' },\n): string {\n const isActive = options.variant === 'active';\n const header = [\n isActive ? '# Project Tickets — Index' : '# Project Tickets — Completed Archive',\n '',\n '<!-- Auto-generated by `safeword sync-tickets`. Do not edit by hand. -->',\n isActive\n ? '<!-- Active tickets, grouped by epic. Completed tickets live in INDEX-completed.md. -->'\n : '<!-- Completed tickets (the completed/ archive), grouped by epic. -->',\n '',\n ];\n\n if (entries.length === 0) {\n return [...header, isActive ? 'No active tickets.' : 'No completed tickets.', ''].join('\\n');\n }\n\n const blocks = deriveBlocks(entries);\n const labelById = new Map(entries.map(entry => [entry.id, entry.title]));\n\n const lines = [...header, `## Tickets (${entries.length})`, ''];\n for (const [epic, group] of groupByEpic(entries)) {\n lines.push(`### ${epic}`, '');\n for (const entry of group) lines.push(...renderEntry(entry, blocks, labelById));\n lines.push('');\n }\n return lines.join('\\n');\n}\n\n/** Write `content` to `path` only when it differs; report whether it wrote. */\nfunction writeIfChanged(path: string, content: string): boolean {\n const previous = existsSync(path) ? readFileSync(path, 'utf8') : undefined;\n if (previous === content) return false;\n writeFileSync(path, content);\n return true;\n}\n\n/**\n * Generate/update both ticket indexes from the corpus. No-op (creates nothing)\n * when the tickets directory is absent. The completed archive is written when\n * a `completed/` directory exists or completed entries are present.\n */\nexport function syncTickets(cwd: string): TicketSyncResult {\n const ticketsDirectory = resolveTicketsDirectory(cwd);\n const relativeLabel = nodePath.relative(cwd, ticketsDirectory) || TICKETS_RELATIVE_PATH;\n const indexPath = nodePath.join(ticketsDirectory, INDEX_FILENAME);\n const completedIndexPath = nodePath.join(ticketsDirectory, COMPLETED_INDEX_FILENAME);\n\n if (!existsSync(ticketsDirectory)) {\n return { wrote: false, active: [], completed: [], skipped: [], indexPath, completedIndexPath };\n }\n\n const { active, completed, skipped } = readTickets(ticketsDirectory, relativeLabel);\n\n const wroteActive = writeIfChanged(indexPath, buildIndexContent(active, { variant: 'active' }));\n\n const completedDirectory = nodePath.join(ticketsDirectory, COMPLETED_DIRNAME);\n const wroteCompleted =\n completed.length > 0 || existsSync(completedDirectory)\n ? writeIfChanged(completedIndexPath, buildIndexContent(completed, { variant: 'completed' }))\n : false;\n\n return {\n wrote: wroteActive || wroteCompleted,\n active,\n completed,\n skipped,\n indexPath,\n completedIndexPath,\n };\n}\n","/**\n * Structured ticket relations (ticket AKZJXC).\n *\n * One canonical directed edge — `depends_on` — stored as an inline-array scalar\n * the hand-rolled frontmatter parser can hold. The inverse (`blocks`) is always\n * derived across the corpus; cycles and dangling refs surface as warnings, never\n * errors (mirrors safeword's tolerant ID resolution).\n */\n\n/** A ticket reduced to its id and its outgoing `depends_on` edges. */\nexport interface TicketNode {\n id: string;\n dependsOn: string[];\n}\n\n/**\n * Parse a `depends_on` frontmatter scalar into ticket ids. Accepts the inline\n * array form (`[A, B]`) or a bare comma list (`A, B`); trims each id and drops\n * empties. Missing/empty input → `[]`.\n * @param raw the raw frontmatter value, or undefined when the key is absent\n */\nexport function parseTicketIdList(raw?: string): string[] {\n if (raw === undefined) return [];\n const inner = raw.trim().replace(/^\\[/, '').replace(/\\]$/, '');\n return inner\n .split(',')\n .map(id => id.trim())\n .filter(id => id.length > 0);\n}\n\n/**\n * Invert the `depends_on` graph into `id → ids that depend on it` (the derived\n * `blocks` edges). Only ids that block something appear as keys; each value\n * preserves corpus order.\n * @param nodes every ticket's id + depends_on edges\n */\nexport function deriveBlocks(nodes: TicketNode[]): Map<string, string[]> {\n const blocks = new Map<string, string[]>();\n for (const node of nodes) {\n for (const target of node.dependsOn) {\n const blockers = blocks.get(target) ?? [];\n blockers.push(node.id);\n blocks.set(target, blockers);\n }\n }\n return blocks;\n}\n\n/**\n * `depends_on` targets absent from the corpus, as `{from, missing}` pairs sorted\n * by from then missing. Warn-only — a target may live on another branch or in\n * completed/.\n * @param nodes every ticket's id + depends_on edges\n */\nexport function findDanglingDependencies(nodes: TicketNode[]): { from: string; missing: string }[] {\n const known = new Set(nodes.map(node => node.id));\n const dangling: { from: string; missing: string }[] = [];\n for (const node of nodes) {\n for (const target of node.dependsOn) {\n if (!known.has(target)) dangling.push({ from: node.id, missing: target });\n }\n }\n return dangling.toSorted(\n (a, b) => a.from.localeCompare(b.from) || a.missing.localeCompare(b.missing),\n );\n}\n\n/**\n * Sorted ids of tickets that participate in any `depends_on` cycle (a node\n * reachable from itself, including a self-edge). Warn-only. Dangling targets are\n * inert — they have no outgoing edges, so they can't form a cycle.\n * @param nodes every ticket's id + depends_on edges\n */\nexport function findTicketsInCycles(nodes: TicketNode[]): string[] {\n const edges = new Map(nodes.map(node => [node.id, node.dependsOn]));\n const inCycle = new Set<string>();\n\n for (const start of edges.keys()) {\n // DFS along depends_on edges; reaching `start` again means it's on a cycle.\n const stack = [...(edges.get(start) ?? [])];\n const seen = new Set<string>();\n while (stack.length > 0) {\n const next = stack.pop();\n if (next === undefined) continue;\n if (next === start) {\n inCycle.add(start);\n break;\n }\n if (seen.has(next)) continue;\n seen.add(next);\n stack.push(...(edges.get(next) ?? []));\n }\n }\n\n return [...inCycle].toSorted((a, b) => a.localeCompare(b));\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,33 +1,35 @@
|
|
|
1
1
|
import {
|
|
2
2
|
findDanglingDependencies,
|
|
3
3
|
findTicketsInCycles,
|
|
4
|
-
readTickets
|
|
5
|
-
|
|
6
|
-
} from "./chunk-G2UYJDPN.js";
|
|
4
|
+
readTickets
|
|
5
|
+
} from "./chunk-2HB6H4G5.js";
|
|
7
6
|
import {
|
|
8
7
|
formatTicketReference
|
|
9
8
|
} from "./chunk-NHXVS5FL.js";
|
|
10
9
|
import {
|
|
11
10
|
buildCoverageReport,
|
|
11
|
+
buildCoverageReportFromFeature,
|
|
12
12
|
computeSkipMask,
|
|
13
13
|
stripInlineComments
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-IGULTNHR.js";
|
|
15
15
|
import {
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
FeatureParseError,
|
|
17
|
+
findFeatureLineageIssues,
|
|
18
|
+
findFeatureSourcePath
|
|
19
|
+
} from "./chunk-QLXFPFIC.js";
|
|
18
20
|
import {
|
|
19
21
|
SAFEWORD_SCHEMA,
|
|
20
22
|
createProjectContext,
|
|
21
23
|
getMissingPacks,
|
|
22
24
|
reconcile
|
|
23
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-4J3GYDJF.js";
|
|
24
26
|
import {
|
|
25
27
|
defaultConfiguredPath,
|
|
28
|
+
readConfiguredDocumentationSources,
|
|
26
29
|
readConfiguredPath,
|
|
27
30
|
resolveConfiguredPath,
|
|
28
31
|
resolveTicketsDirectory
|
|
29
|
-
} from "./chunk-
|
|
30
|
-
import "./chunk-LODQOJEK.js";
|
|
32
|
+
} from "./chunk-54KAYQTY.js";
|
|
31
33
|
import {
|
|
32
34
|
VERSION
|
|
33
35
|
} from "./chunk-HSC7TELY.js";
|
|
@@ -36,14 +38,13 @@ import {
|
|
|
36
38
|
header,
|
|
37
39
|
info,
|
|
38
40
|
isDirectory,
|
|
39
|
-
keyValue,
|
|
40
41
|
listItem,
|
|
41
42
|
readFileSafe,
|
|
42
43
|
success,
|
|
43
44
|
warn
|
|
44
45
|
} from "./chunk-445LAX4Y.js";
|
|
45
46
|
|
|
46
|
-
// src/
|
|
47
|
+
// src/health.ts
|
|
47
48
|
import { readdirSync as readdirSync2 } from "fs";
|
|
48
49
|
import nodePath2 from "path";
|
|
49
50
|
|
|
@@ -365,7 +366,7 @@ function validatePersonas(parsed) {
|
|
|
365
366
|
];
|
|
366
367
|
}
|
|
367
368
|
|
|
368
|
-
// src/
|
|
369
|
+
// src/health.ts
|
|
369
370
|
function findMissingFiles(cwd, actions) {
|
|
370
371
|
const issues = [];
|
|
371
372
|
for (const action of actions) {
|
|
@@ -427,6 +428,12 @@ function findGlossaryAdvisories(cwd) {
|
|
|
427
428
|
`${nodePath2.relative(cwd, defaultPath)} exists but paths.glossary points to ${override} \u2014 legacy file is orphaned. Consider removing.`
|
|
428
429
|
];
|
|
429
430
|
}
|
|
431
|
+
function findDocumentationSourceIssues(cwd) {
|
|
432
|
+
return readConfiguredDocumentationSources(cwd).flatMap((source) => {
|
|
433
|
+
if (source.type !== "local") return [];
|
|
434
|
+
return exists(source.resolvedPath) ? [] : [`docs-source: ${source.path}: file or directory not found`];
|
|
435
|
+
});
|
|
436
|
+
}
|
|
430
437
|
function listTicketIds(ticketsRoot) {
|
|
431
438
|
try {
|
|
432
439
|
return readdirSync2(ticketsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name !== "completed").map((entry) => entry.name);
|
|
@@ -465,22 +472,57 @@ function archAlignmentHasContent(implPlanContent) {
|
|
|
465
472
|
if (body.length === 0) return false;
|
|
466
473
|
return !(body.length === 1 && (body[0] ?? "").toLowerCase().startsWith("skip:"));
|
|
467
474
|
}
|
|
468
|
-
function
|
|
475
|
+
function emptyCoverageDiagnostics() {
|
|
476
|
+
return { issues: [], advisories: [] };
|
|
477
|
+
}
|
|
478
|
+
function findCoverageDiagnostics(cwd) {
|
|
469
479
|
const ticketsRoot = resolveTicketsDirectory(cwd);
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
480
|
+
const all = emptyCoverageDiagnostics();
|
|
481
|
+
for (const ticketId of listTicketIds(ticketsRoot)) {
|
|
482
|
+
const ticketDiagnostics = coverageDiagnosticsForTicket(cwd, ticketsRoot, ticketId);
|
|
483
|
+
all.issues.push(...ticketDiagnostics.issues);
|
|
484
|
+
all.advisories.push(...ticketDiagnostics.advisories);
|
|
485
|
+
}
|
|
486
|
+
return all;
|
|
473
487
|
}
|
|
474
|
-
function
|
|
488
|
+
function coverageDiagnosticsForTicket(cwd, ticketsRoot, ticketId) {
|
|
475
489
|
const ticketDirectory = nodePath2.join(ticketsRoot, ticketId);
|
|
476
490
|
const ticketContent = readFileSafe(nodePath2.join(ticketDirectory, "ticket.md"));
|
|
477
|
-
if (ticketContent === void 0 || !isInProgress(ticketContent))
|
|
491
|
+
if (ticketContent === void 0 || !isInProgress(ticketContent))
|
|
492
|
+
return emptyCoverageDiagnostics();
|
|
478
493
|
const specContent = readFileSafe(nodePath2.join(ticketDirectory, "spec.md"));
|
|
479
|
-
if (specContent === void 0) return
|
|
480
|
-
const
|
|
481
|
-
|
|
494
|
+
if (specContent === void 0) return emptyCoverageDiagnostics();
|
|
495
|
+
const featureSource = readFeatureSource(cwd, ticketId);
|
|
496
|
+
try {
|
|
497
|
+
const report = featureSource === void 0 ? buildCoverageReport(
|
|
498
|
+
specContent,
|
|
499
|
+
readFileSafe(nodePath2.join(ticketDirectory, "test-definitions.md"))
|
|
500
|
+
) : buildCoverageReportFromFeature(specContent, featureSource.content);
|
|
501
|
+
const lineageIssues = featureSource === void 0 ? [] : formatFeatureLineageIssues(cwd, ticketId, featureSource);
|
|
502
|
+
return { issues: lineageIssues, advisories: formatCoverageReport(ticketId, report) };
|
|
503
|
+
} catch (parseError) {
|
|
504
|
+
if (parseError instanceof FeatureParseError && featureSource !== void 0) {
|
|
505
|
+
return {
|
|
506
|
+
issues: [
|
|
507
|
+
`${formatCoverageTicketLabel(ticketId)}: ${nodePath2.relative(cwd, featureSource.path)}: invalid Gherkin feature: ${parseError.message}`
|
|
508
|
+
],
|
|
509
|
+
advisories: []
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
throw parseError;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
function formatFeatureLineageIssues(cwd, ticketId, featureSource) {
|
|
516
|
+
const label = formatCoverageTicketLabel(ticketId);
|
|
517
|
+
const relativePath = nodePath2.relative(cwd, featureSource.path);
|
|
518
|
+
return findFeatureLineageIssues(featureSource.content).map(
|
|
519
|
+
(issue) => `${label}: ${relativePath}: ${issue}`
|
|
482
520
|
);
|
|
483
|
-
|
|
521
|
+
}
|
|
522
|
+
function readFeatureSource(cwd, ticketFolder) {
|
|
523
|
+
const featurePath = findFeatureSourcePath(cwd, ticketFolder);
|
|
524
|
+
const content = featurePath === void 0 ? void 0 : readFileSafe(featurePath);
|
|
525
|
+
return featurePath === void 0 || content === void 0 ? void 0 : { path: featurePath, content };
|
|
484
526
|
}
|
|
485
527
|
function isInProgress(ticketContent) {
|
|
486
528
|
const lines = ticketContent.split("\n");
|
|
@@ -493,8 +535,7 @@ function isInProgress(ticketContent) {
|
|
|
493
535
|
return false;
|
|
494
536
|
}
|
|
495
537
|
function formatCoverageReport(ticketId, report) {
|
|
496
|
-
const
|
|
497
|
-
const ticketLabel = dashIndex === -1 ? ticketId : formatTicketReference(ticketId.slice(0, dashIndex), ticketId.slice(dashIndex + 1));
|
|
538
|
+
const ticketLabel = formatCoverageTicketLabel(ticketId);
|
|
498
539
|
return [
|
|
499
540
|
...report.uncovered.map(
|
|
500
541
|
(acId) => `${ticketLabel}: acceptance criterion ${acId} has no scenario (uncovered)`
|
|
@@ -507,6 +548,10 @@ function formatCoverageReport(ticketId, report) {
|
|
|
507
548
|
)
|
|
508
549
|
];
|
|
509
550
|
}
|
|
551
|
+
function formatCoverageTicketLabel(ticketId) {
|
|
552
|
+
const dashIndex = ticketId.indexOf("-");
|
|
553
|
+
return dashIndex === -1 ? ticketId : formatTicketReference(ticketId.slice(0, dashIndex), ticketId.slice(dashIndex + 1));
|
|
554
|
+
}
|
|
510
555
|
function findRelationAdvisories(cwd) {
|
|
511
556
|
const ticketsDirectory = resolveTicketsDirectory(cwd);
|
|
512
557
|
let entries;
|
|
@@ -545,24 +590,7 @@ function findMissingPatches(cwd, actions) {
|
|
|
545
590
|
}
|
|
546
591
|
return issues;
|
|
547
592
|
}
|
|
548
|
-
async function
|
|
549
|
-
try {
|
|
550
|
-
const controller = new AbortController();
|
|
551
|
-
const timeoutId = setTimeout(() => {
|
|
552
|
-
controller.abort();
|
|
553
|
-
}, timeout);
|
|
554
|
-
const response = await fetch("https://registry.npmjs.org/safeword/latest", {
|
|
555
|
-
signal: controller.signal
|
|
556
|
-
});
|
|
557
|
-
clearTimeout(timeoutId);
|
|
558
|
-
if (!response.ok) return void 0;
|
|
559
|
-
const data = await response.json();
|
|
560
|
-
return data.version ?? void 0;
|
|
561
|
-
} catch {
|
|
562
|
-
return void 0;
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
async function checkHealth(cwd) {
|
|
593
|
+
async function checkHealth(cwd, options = {}) {
|
|
566
594
|
const safewordDirectory = nodePath2.join(cwd, ".safeword");
|
|
567
595
|
if (!exists(safewordDirectory)) {
|
|
568
596
|
return {
|
|
@@ -590,12 +618,15 @@ async function checkHealth(cwd) {
|
|
|
590
618
|
...findMissingFiles(cwd, actionsWithPath),
|
|
591
619
|
...findMissingPatches(cwd, actionsWithPath),
|
|
592
620
|
...findPersonaIssues(cwd),
|
|
593
|
-
...findGlossaryIssues(cwd)
|
|
621
|
+
...findGlossaryIssues(cwd),
|
|
622
|
+
...findDocumentationSourceIssues(cwd)
|
|
594
623
|
];
|
|
595
624
|
if (!exists(nodePath2.join(cwd, ".claude", "settings.json"))) {
|
|
596
625
|
issues.push("Missing: .claude/settings.json");
|
|
597
626
|
}
|
|
598
|
-
const missingPacks = getMissingPacks(cwd);
|
|
627
|
+
const missingPacks = options.skipPackageChecks ? [] : getMissingPacks(cwd);
|
|
628
|
+
const coverageDiagnostics = findCoverageDiagnostics(cwd);
|
|
629
|
+
issues.push(...coverageDiagnostics.issues);
|
|
599
630
|
return {
|
|
600
631
|
configured: true,
|
|
601
632
|
projectVersion,
|
|
@@ -607,111 +638,64 @@ async function checkHealth(cwd) {
|
|
|
607
638
|
...findNamespaceAdvisories(cwd),
|
|
608
639
|
...findPersonaAdvisories(cwd),
|
|
609
640
|
...findGlossaryAdvisories(cwd),
|
|
610
|
-
...
|
|
641
|
+
...coverageDiagnostics.advisories,
|
|
611
642
|
...findRelationAdvisories(cwd),
|
|
612
643
|
...findArchitectureAdvisories(cwd)
|
|
613
644
|
],
|
|
614
|
-
missingPackages: result.packagesToInstall,
|
|
645
|
+
missingPackages: options.skipPackageChecks ? [] : result.packagesToInstall,
|
|
615
646
|
missingPacks
|
|
616
647
|
};
|
|
617
648
|
}
|
|
618
|
-
|
|
619
|
-
info("\nChecking for updates...");
|
|
620
|
-
const latestVersion = await checkLatestVersion();
|
|
621
|
-
if (!latestVersion) {
|
|
622
|
-
warn("Couldn't check for updates (offline?)");
|
|
623
|
-
return;
|
|
624
|
-
}
|
|
625
|
-
health.latestVersion = latestVersion;
|
|
626
|
-
health.updateAvailable = isNewerVersion(health.cliVersion, latestVersion);
|
|
627
|
-
if (health.updateAvailable) {
|
|
628
|
-
warn(`Update available: v${latestVersion}`);
|
|
629
|
-
info("Run `bunx safeword@latest upgrade` to upgrade");
|
|
630
|
-
} else {
|
|
631
|
-
success("CLI is up to date");
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
function reportVersionMismatch(health) {
|
|
635
|
-
if (!health.projectVersion) return;
|
|
636
|
-
if (isNewerVersion(health.cliVersion, health.projectVersion)) {
|
|
637
|
-
warn(`Project config (v${health.projectVersion}) is newer than CLI (v${health.cliVersion})`);
|
|
638
|
-
info("Consider upgrading the CLI");
|
|
639
|
-
} else if (isNewerVersion(health.projectVersion, health.cliVersion)) {
|
|
640
|
-
info(`
|
|
641
|
-
Upgrade available for project config`);
|
|
642
|
-
info(
|
|
643
|
-
`Run \`safeword upgrade\` to update from v${health.projectVersion} to v${health.cliVersion}`
|
|
644
|
-
);
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
function reportHealthSummary(health) {
|
|
648
|
-
if (health.advisories.length > 0) {
|
|
649
|
-
header("Advisories");
|
|
650
|
-
for (const advisory of health.advisories) {
|
|
651
|
-
warn(advisory);
|
|
652
|
-
}
|
|
653
|
-
}
|
|
649
|
+
function firstFailureSection(health) {
|
|
654
650
|
if (health.missingPacks.length > 0) {
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
651
|
+
return {
|
|
652
|
+
title: "Missing Language Packs",
|
|
653
|
+
lines: health.missingPacks.map((pack) => `${pack} pack not installed`),
|
|
654
|
+
render: listItem,
|
|
655
|
+
defaultHint: "Run `safeword upgrade` to install missing packs"
|
|
656
|
+
};
|
|
661
657
|
}
|
|
662
658
|
if (health.missingPackages.length > 0) {
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
659
|
+
return {
|
|
660
|
+
title: "Missing Packages",
|
|
661
|
+
lines: health.missingPackages,
|
|
662
|
+
render: listItem,
|
|
663
|
+
defaultHint: "Run `safeword upgrade` to install missing packages"
|
|
664
|
+
};
|
|
667
665
|
}
|
|
668
666
|
if (health.issues.length > 0) {
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
667
|
+
return {
|
|
668
|
+
title: "Issues Found",
|
|
669
|
+
lines: health.issues,
|
|
670
|
+
render: warn,
|
|
671
|
+
defaultHint: "Run `safeword upgrade` to repair configuration"
|
|
672
|
+
};
|
|
675
673
|
}
|
|
676
|
-
|
|
677
|
-
return false;
|
|
674
|
+
return void 0;
|
|
678
675
|
}
|
|
679
|
-
function
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
}
|
|
685
|
-
} catch (error) {
|
|
686
|
-
if (process.env.DEBUG) {
|
|
687
|
-
console.error("[check] ticket index regen failed:", error);
|
|
676
|
+
function reportHealthSummary(health, options = {}) {
|
|
677
|
+
if (health.advisories.length > 0) {
|
|
678
|
+
header("Advisories");
|
|
679
|
+
for (const advisory of health.advisories) {
|
|
680
|
+
warn(advisory);
|
|
688
681
|
}
|
|
689
|
-
return;
|
|
690
682
|
}
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
const health = await checkHealth(cwd);
|
|
696
|
-
if (!health.configured) {
|
|
697
|
-
info("Not configured. Run `safeword setup` to initialize.");
|
|
698
|
-
return;
|
|
683
|
+
const failure = firstFailureSection(health);
|
|
684
|
+
if (failure === void 0) {
|
|
685
|
+
success("\nConfiguration is healthy");
|
|
686
|
+
return false;
|
|
699
687
|
}
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
if (options.offline) {
|
|
704
|
-
info("\nSkipped update check (offline mode)");
|
|
705
|
-
} else {
|
|
706
|
-
await reportUpdateStatus(health);
|
|
707
|
-
}
|
|
708
|
-
reportVersionMismatch(health);
|
|
709
|
-
const hasIssues = reportHealthSummary(health);
|
|
710
|
-
if (hasIssues) {
|
|
711
|
-
process.exit(1);
|
|
688
|
+
header(failure.title);
|
|
689
|
+
for (const line of failure.lines) {
|
|
690
|
+
failure.render(line);
|
|
712
691
|
}
|
|
692
|
+
info(`
|
|
693
|
+
${options.repairHint ?? failure.defaultHint}`);
|
|
694
|
+
return true;
|
|
713
695
|
}
|
|
696
|
+
|
|
714
697
|
export {
|
|
715
|
-
|
|
698
|
+
checkHealth,
|
|
699
|
+
reportHealthSummary
|
|
716
700
|
};
|
|
717
|
-
//# sourceMappingURL=
|
|
701
|
+
//# sourceMappingURL=chunk-42IGUF5V.js.map
|