safeword 0.56.0 → 0.58.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-OD4QHC2P.js → check-6IHMMAS6.js} +41 -27
- package/dist/check-6IHMMAS6.js.map +1 -0
- package/dist/{chunk-64TSNY2M.js → chunk-DIJ35ZXH.js} +2 -2
- package/dist/{chunk-EUSXT3MN.js → chunk-HTDMZQKA.js} +37 -6
- package/dist/chunk-HTDMZQKA.js.map +1 -0
- package/dist/chunk-LRYWFRPD.js +46 -0
- package/dist/chunk-LRYWFRPD.js.map +1 -0
- package/dist/{chunk-VLK2DXJ7.js → chunk-MT4WBU2P.js} +76 -43
- package/dist/chunk-MT4WBU2P.js.map +1 -0
- package/dist/{chunk-FZVYR37T.js → chunk-Q7ALQS7T.js} +3 -3
- package/dist/{chunk-RQBSXETC.js → chunk-UYTOLZT2.js} +185 -31
- package/dist/chunk-UYTOLZT2.js.map +1 -0
- package/dist/chunk-ZESHX2BU.js +14 -0
- package/dist/chunk-ZESHX2BU.js.map +1 -0
- package/dist/cli.js +10 -10
- package/dist/cli.js.map +1 -1
- package/dist/{diff-L7DED7WY.js → diff-5YTHPOAV.js} +36 -5
- package/dist/diff-5YTHPOAV.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/presets/typescript/index.d.ts +0 -42
- package/dist/presets/typescript/index.js +1 -1
- package/dist/{reset-LE6GYEYU.js → reset-OBM2VJQT.js} +2 -2
- package/dist/{setup-ZJCYBXA6.js → setup-ZFQDI5QT.js} +5 -5
- package/dist/{sync-tickets-53DTTY7B.js → sync-tickets-35ZSEKIE.js} +8 -2
- package/dist/sync-tickets-35ZSEKIE.js.map +1 -0
- package/dist/{sync-tracker-MP6BTZXX.js → sync-tracker-YUXD7QKS.js} +144 -11
- package/dist/sync-tracker-YUXD7QKS.js.map +1 -0
- package/dist/{test-plan-HWRDWG2X.js → test-plan-MS6HRVBR.js} +9 -3
- package/dist/test-plan-MS6HRVBR.js.map +1 -0
- package/dist/{ticket-new-GXYCW5ML.js → ticket-new-S6IMADXY.js} +6 -2
- package/dist/{ticket-new-GXYCW5ML.js.map → ticket-new-S6IMADXY.js.map} +1 -1
- package/dist/{upgrade-7JTIBI3I.js → upgrade-N3WTG3KL.js} +7 -10
- package/dist/upgrade-N3WTG3KL.js.map +1 -0
- package/package.json +15 -14
- package/templates/codex/config.toml +3 -3
- package/templates/commands/audit.md +3 -3
- package/templates/commands/refactor.md +1 -1
- package/templates/commands/self-review.md +1 -1
- package/templates/commands/verify.md +10 -4
- package/templates/cursor/rules/bdd-tdd.mdc +1 -1
- package/templates/cursor/rules/safeword-refactoring.mdc +1 -1
- package/templates/cursor/rules/safeword-tdd-review.mdc +1 -1
- package/templates/guides/llm-evals-guide.md +485 -0
- package/templates/guides/testing-guide.md +35 -13
- package/templates/guides/verification-lanes-guide.md +574 -0
- package/templates/hooks/codex/pre-tool-quality.ts +11 -2
- package/templates/hooks/cursor/after-file-edit.ts +5 -2
- package/templates/hooks/cursor/before-shell-execution.ts +75 -0
- package/templates/hooks/cursor/gate-adapter.ts +278 -0
- package/templates/hooks/cursor/post-tool-quality.ts +70 -0
- package/templates/hooks/cursor/pre-tool-quality.ts +107 -0
- package/templates/hooks/cursor/stop.ts +22 -2
- package/templates/hooks/lib/active-ticket.ts +166 -0
- package/templates/hooks/lib/architecture-staged-scope.ts +130 -0
- package/templates/hooks/lib/auto-upgrade-lock.ts +89 -0
- package/templates/hooks/lib/auto-upgrade.ts +417 -0
- package/templates/hooks/lib/branch-staleness.ts +49 -0
- package/templates/hooks/lib/checkbox-transitions.ts +2 -2
- package/templates/hooks/lib/cursor-run-identity.ts +216 -0
- package/templates/hooks/lib/dependency-readiness.ts +83 -0
- package/templates/hooks/lib/done-gate.ts +175 -0
- package/templates/hooks/lib/ledger-git.ts +92 -0
- package/templates/hooks/lib/ledger-validation.ts +31 -24
- package/templates/hooks/lib/quality-state.ts +65 -17
- package/templates/hooks/lib/review-trigger.ts +4 -31
- package/templates/hooks/lib/run-identity.ts +150 -0
- package/templates/hooks/lib/safeword-context.ts +83 -0
- package/templates/hooks/post-tool-dependency-readiness.ts +84 -0
- package/templates/hooks/post-tool-quality.ts +6 -18
- package/templates/hooks/pre-tool-architecture-stage.ts +9 -0
- package/templates/hooks/pre-tool-quality.ts +71 -5
- package/templates/hooks/pre-tool-stale-main.ts +83 -0
- package/templates/hooks/prompt-questions.ts +13 -1
- package/templates/hooks/record-skill-invocation.ts +49 -16
- package/templates/hooks/session-auto-upgrade.ts +13 -300
- package/templates/hooks/session-codex-start.ts +39 -0
- package/templates/hooks/session-cursor-auto-upgrade.ts +35 -0
- package/templates/hooks/session-dependency-readiness.ts +36 -0
- package/templates/hooks/session-safeword-context.ts +19 -81
- package/templates/hooks/stop-quality.ts +15 -60
- package/templates/hooks/write-review-stamp.ts +4 -2
- package/templates/skills/audit/SKILL.md +3 -3
- package/templates/skills/bdd/SCENARIOS.md +1 -1
- package/templates/skills/bdd/TDD.md +3 -1
- package/templates/skills/quality-review/SKILL.md +8 -3
- package/templates/skills/refactor/SKILL.md +34 -11
- package/templates/skills/review-spec/SKILL.md +2 -1
- package/templates/skills/self-review/SKILL.md +1 -1
- package/templates/skills/tdd-review/SKILL.md +4 -2
- package/templates/skills/testing/SKILL.md +33 -0
- package/templates/skills/ticket-system/SKILL.md +14 -2
- package/templates/skills/verify/SKILL.md +10 -4
- package/dist/check-OD4QHC2P.js.map +0 -1
- package/dist/chunk-EUSXT3MN.js.map +0 -1
- package/dist/chunk-FJYRWU2V.js +0 -21
- package/dist/chunk-FJYRWU2V.js.map +0 -1
- package/dist/chunk-RQBSXETC.js.map +0 -1
- package/dist/chunk-VLK2DXJ7.js.map +0 -1
- package/dist/diff-L7DED7WY.js.map +0 -1
- package/dist/sync-tickets-53DTTY7B.js.map +0 -1
- package/dist/sync-tracker-MP6BTZXX.js.map +0 -1
- package/dist/test-plan-HWRDWG2X.js.map +0 -1
- package/dist/upgrade-7JTIBI3I.js.map +0 -1
- /package/dist/{chunk-64TSNY2M.js.map → chunk-DIJ35ZXH.js.map} +0 -0
- /package/dist/{chunk-FZVYR37T.js.map → chunk-Q7ALQS7T.js.map} +0 -0
- /package/dist/{reset-LE6GYEYU.js.map → reset-OBM2VJQT.js.map} +0 -0
- /package/dist/{setup-ZJCYBXA6.js.map → setup-ZFQDI5QT.js.map} +0 -0
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
import {
|
|
2
|
+
buildIndexConflictListMessage
|
|
3
|
+
} from "./chunk-ZESHX2BU.js";
|
|
4
|
+
import {
|
|
5
|
+
fetchRegistryLatestVersion,
|
|
2
6
|
isNewerVersion
|
|
3
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-LRYWFRPD.js";
|
|
4
8
|
import {
|
|
5
9
|
checkHealth,
|
|
6
10
|
reportHealthSummary
|
|
7
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-Q7ALQS7T.js";
|
|
8
12
|
import "./chunk-YXNI7W5D.js";
|
|
9
13
|
import "./chunk-XTLCJKGE.js";
|
|
10
|
-
import "./chunk-
|
|
11
|
-
import
|
|
14
|
+
import "./chunk-UYTOLZT2.js";
|
|
15
|
+
import {
|
|
16
|
+
detectPackageManager
|
|
17
|
+
} from "./chunk-GS3TBFXU.js";
|
|
12
18
|
import {
|
|
13
19
|
syncTickets
|
|
14
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-HTDMZQKA.js";
|
|
15
21
|
import "./chunk-NHXVS5FL.js";
|
|
16
22
|
import "./chunk-P2IC575P.js";
|
|
17
23
|
import "./chunk-PHR2K2Y3.js";
|
|
@@ -27,26 +33,9 @@ import "./chunk-KIZYVSME.js";
|
|
|
27
33
|
|
|
28
34
|
// src/commands/check.ts
|
|
29
35
|
import process from "process";
|
|
30
|
-
async function checkLatestVersion(timeout = 3e3) {
|
|
31
|
-
try {
|
|
32
|
-
const controller = new AbortController();
|
|
33
|
-
const timeoutId = setTimeout(() => {
|
|
34
|
-
controller.abort();
|
|
35
|
-
}, timeout);
|
|
36
|
-
const response = await fetch("https://registry.npmjs.org/safeword/latest", {
|
|
37
|
-
signal: controller.signal
|
|
38
|
-
});
|
|
39
|
-
clearTimeout(timeoutId);
|
|
40
|
-
if (!response.ok) return void 0;
|
|
41
|
-
const data = await response.json();
|
|
42
|
-
return data.version ?? void 0;
|
|
43
|
-
} catch {
|
|
44
|
-
return void 0;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
36
|
async function reportUpdateStatus(health) {
|
|
48
37
|
info("\nChecking for updates...");
|
|
49
|
-
const latestVersion = await
|
|
38
|
+
const latestVersion = await fetchRegistryLatestVersion();
|
|
50
39
|
if (!latestVersion) {
|
|
51
40
|
warn("Couldn't check for updates (offline?)");
|
|
52
41
|
return;
|
|
@@ -60,11 +49,33 @@ async function reportUpdateStatus(health) {
|
|
|
60
49
|
success("CLI is up to date");
|
|
61
50
|
}
|
|
62
51
|
}
|
|
63
|
-
function
|
|
52
|
+
function isDigit(char) {
|
|
53
|
+
return char >= "0" && char <= "9";
|
|
54
|
+
}
|
|
55
|
+
function isPackageVersionSpecChar(char) {
|
|
56
|
+
return char >= "a" && char <= "z" || char >= "A" && char <= "Z" || isDigit(char) || char === "." || char === "-" || char === "+";
|
|
57
|
+
}
|
|
58
|
+
function isSafePackageVersion(version) {
|
|
59
|
+
if (version.length === 0 || !version.includes(".")) return false;
|
|
60
|
+
for (const char of version) {
|
|
61
|
+
if (!isPackageVersionSpecChar(char)) return false;
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
function reportVersionMismatch(health, cwd) {
|
|
64
66
|
if (!health.projectVersion) return;
|
|
65
67
|
if (isNewerVersion(health.cliVersion, health.projectVersion)) {
|
|
66
68
|
warn(`Project config (v${health.projectVersion}) is newer than CLI (v${health.cliVersion})`);
|
|
67
|
-
|
|
69
|
+
if (!isSafePackageVersion(health.projectVersion)) {
|
|
70
|
+
warn(
|
|
71
|
+
"Project version is not safe to use in a package install command; inspect .safeword/version."
|
|
72
|
+
);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const pm = detectPackageManager(cwd);
|
|
76
|
+
const runSafewordUpgrade = pm === "bun" || pm === "yarn" ? `${pm} run safeword upgrade` : `${pm} exec safeword upgrade`;
|
|
77
|
+
info("Update the project-local CLI first:");
|
|
78
|
+
info(`${pm} add -D safeword@${health.projectVersion} && ${runSafewordUpgrade}`);
|
|
68
79
|
} else if (isNewerVersion(health.projectVersion, health.cliVersion)) {
|
|
69
80
|
info(`
|
|
70
81
|
Upgrade available for project config`);
|
|
@@ -79,6 +90,9 @@ function regenerateTicketIndex(cwd) {
|
|
|
79
90
|
if (result.wrote) {
|
|
80
91
|
info("Regenerated ticket index (INDEX.md / INDEX-completed.md)");
|
|
81
92
|
}
|
|
93
|
+
if (result.indexConflicts.length > 0) {
|
|
94
|
+
warn(buildIndexConflictListMessage(result.indexConflicts));
|
|
95
|
+
}
|
|
82
96
|
} catch (error) {
|
|
83
97
|
if (process.env.DEBUG) {
|
|
84
98
|
console.error("[check] ticket index regen failed:", error);
|
|
@@ -102,7 +116,7 @@ async function check(options) {
|
|
|
102
116
|
} else {
|
|
103
117
|
await reportUpdateStatus(health);
|
|
104
118
|
}
|
|
105
|
-
reportVersionMismatch(health);
|
|
119
|
+
reportVersionMismatch(health, cwd);
|
|
106
120
|
const hasIssues = reportHealthSummary(health);
|
|
107
121
|
if (hasIssues) {
|
|
108
122
|
process.exit(1);
|
|
@@ -111,4 +125,4 @@ async function check(options) {
|
|
|
111
125
|
export {
|
|
112
126
|
check
|
|
113
127
|
};
|
|
114
|
-
//# sourceMappingURL=check-
|
|
128
|
+
//# sourceMappingURL=check-6IHMMAS6.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 { detectPackageManager } from '../utils/install.js';\nimport { header, info, keyValue, success, warn } from '../utils/output.js';\nimport { buildIndexConflictListMessage } from '../utils/ticket-index-warnings.js';\nimport { fetchRegistryLatestVersion, isNewerVersion } from '../utils/version.js';\n\ninterface CheckOptions {\n offline?: boolean;\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 fetchRegistryLatestVersion();\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\nfunction isDigit(char: string): boolean {\n return char >= '0' && char <= '9';\n}\n\nfunction isPackageVersionSpecChar(char: string): boolean {\n return (\n (char >= 'a' && char <= 'z') ||\n (char >= 'A' && char <= 'Z') ||\n isDigit(char) ||\n char === '.' ||\n char === '-' ||\n char === '+'\n );\n}\n\nfunction isSafePackageVersion(version: string): boolean {\n if (version.length === 0 || !version.includes('.')) return false;\n for (const char of version) {\n if (!isPackageVersionSpecChar(char)) return false;\n }\n return true;\n}\n\n/**\n * Compare project version vs CLI version and report\n * @param health\n */\nfunction reportVersionMismatch(health: HealthStatus, cwd: string): 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\n if (!isSafePackageVersion(health.projectVersion)) {\n warn(\n 'Project version is not safe to use in a package install command; inspect .safeword/version.',\n );\n return;\n }\n\n const pm = detectPackageManager(cwd);\n const runSafewordUpgrade =\n pm === 'bun' || pm === 'yarn' ? `${pm} run safeword upgrade` : `${pm} exec safeword upgrade`;\n info('Update the project-local CLI first:');\n info(`${pm} add -D safeword@${health.projectVersion} && ${runSafewordUpgrade}`);\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 if (result.indexConflicts.length > 0) {\n warn(buildIndexConflictListMessage(result.indexConflicts));\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, cwd);\n const hasIssues = reportHealthSummary(health);\n\n if (hasIssues) {\n process.exit(1);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAQA,OAAO,aAAa;AAiBpB,eAAe,mBAAmB,QAAqC;AACrE,OAAK,2BAA2B;AAChC,QAAM,gBAAgB,MAAM,2BAA2B;AAEvD,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;AAEA,SAAS,QAAQ,MAAuB;AACtC,SAAO,QAAQ,OAAO,QAAQ;AAChC;AAEA,SAAS,yBAAyB,MAAuB;AACvD,SACG,QAAQ,OAAO,QAAQ,OACvB,QAAQ,OAAO,QAAQ,OACxB,QAAQ,IAAI,KACZ,SAAS,OACT,SAAS,OACT,SAAS;AAEb;AAEA,SAAS,qBAAqB,SAA0B;AACtD,MAAI,QAAQ,WAAW,KAAK,CAAC,QAAQ,SAAS,GAAG,EAAG,QAAO;AAC3D,aAAW,QAAQ,SAAS;AAC1B,QAAI,CAAC,yBAAyB,IAAI,EAAG,QAAO;AAAA,EAC9C;AACA,SAAO;AACT;AAMA,SAAS,sBAAsB,QAAsB,KAAmB;AACtE,MAAI,CAAC,OAAO,eAAgB;AAE5B,MAAI,eAAe,OAAO,YAAY,OAAO,cAAc,GAAG;AAC5D,SAAK,oBAAoB,OAAO,cAAc,yBAAyB,OAAO,UAAU,GAAG;AAE3F,QAAI,CAAC,qBAAqB,OAAO,cAAc,GAAG;AAChD;AAAA,QACE;AAAA,MACF;AACA;AAAA,IACF;AAEA,UAAM,KAAK,qBAAqB,GAAG;AACnC,UAAM,qBACJ,OAAO,SAAS,OAAO,SAAS,GAAG,EAAE,0BAA0B,GAAG,EAAE;AACtE,SAAK,qCAAqC;AAC1C,SAAK,GAAG,EAAE,oBAAoB,OAAO,cAAc,OAAO,kBAAkB,EAAE;AAAA,EAChF,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;AACA,QAAI,OAAO,eAAe,SAAS,GAAG;AACpC,WAAK,8BAA8B,OAAO,cAAc,CAAC;AAAA,IAC3D;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,QAAQ,GAAG;AACjC,QAAM,YAAY,oBAAoB,MAAM;AAE5C,MAAI,WAAW;AACb,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF;","names":[]}
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
addInstalledPack,
|
|
4
4
|
isGitRepo,
|
|
5
5
|
isPackInstalled
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-UYTOLZT2.js";
|
|
7
7
|
import {
|
|
8
8
|
SAFEWORD_PEER_DEPENDENCIES
|
|
9
9
|
} from "./chunk-HSC7TELY.js";
|
|
@@ -392,4 +392,4 @@ export {
|
|
|
392
392
|
getEslintPeerMismatchWarning,
|
|
393
393
|
maybeAutoPatchOrNudge
|
|
394
394
|
};
|
|
395
|
-
//# sourceMappingURL=chunk-
|
|
395
|
+
//# sourceMappingURL=chunk-DIJ35ZXH.js.map
|
|
@@ -69,6 +69,15 @@ var COMPLETED_INDEX_FILENAME = "INDEX-completed.md";
|
|
|
69
69
|
var COMPLETED_DIRNAME = "completed";
|
|
70
70
|
var NO_EPIC_GROUP = "(no epic)";
|
|
71
71
|
var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([COMPLETED_DIRNAME, "tmp"]);
|
|
72
|
+
var MERGE_CONFLICT_MARKER_PATTERN = /^(?:<{7}|={7}|>{7})(?:\s|$)/m;
|
|
73
|
+
function hasMergeConflictMarkers(content) {
|
|
74
|
+
return MERGE_CONFLICT_MARKER_PATTERN.test(content);
|
|
75
|
+
}
|
|
76
|
+
function detectConflictedIndex(indexPath) {
|
|
77
|
+
if (!existsSync(indexPath)) return void 0;
|
|
78
|
+
const content = readFileSync(indexPath, "utf8");
|
|
79
|
+
return hasMergeConflictMarkers(content) ? indexPath : void 0;
|
|
80
|
+
}
|
|
72
81
|
function stripQuotes(value) {
|
|
73
82
|
if (value.length >= 2 && (value.startsWith("'") && value.endsWith("'") || value.startsWith('"') && value.endsWith('"'))) {
|
|
74
83
|
return value.slice(1, -1);
|
|
@@ -211,24 +220,33 @@ function groupByEpic(entries) {
|
|
|
211
220
|
}
|
|
212
221
|
function buildIndexContent(entries, options) {
|
|
213
222
|
const isActive = options.variant === "active";
|
|
214
|
-
const
|
|
223
|
+
const headerLines = [
|
|
215
224
|
isActive ? "# Project Tickets \u2014 Index" : "# Project Tickets \u2014 Completed Archive",
|
|
216
225
|
"",
|
|
217
226
|
"<!-- Auto-generated by `safeword sync-tickets`. Do not edit by hand. -->",
|
|
218
227
|
isActive ? "<!-- Active tickets, grouped by epic. Completed tickets live in INDEX-completed.md. -->" : "<!-- Completed tickets (the completed/ archive), grouped by epic. -->",
|
|
228
|
+
"",
|
|
229
|
+
"<!-- prettier-ignore-start -->",
|
|
219
230
|
""
|
|
220
231
|
];
|
|
232
|
+
const footerLines = ["<!-- prettier-ignore-end -->", ""];
|
|
221
233
|
if (entries.length === 0) {
|
|
222
|
-
return [
|
|
234
|
+
return [
|
|
235
|
+
...headerLines,
|
|
236
|
+
isActive ? "No active tickets." : "No completed tickets.",
|
|
237
|
+
"",
|
|
238
|
+
...footerLines
|
|
239
|
+
].join("\n");
|
|
223
240
|
}
|
|
224
241
|
const blocks = deriveBlocks(entries);
|
|
225
242
|
const labelById = new Map(entries.map((entry) => [entry.id, entry.title]));
|
|
226
|
-
const lines = [...
|
|
243
|
+
const lines = [...headerLines, `## Tickets (${entries.length})`, ""];
|
|
227
244
|
for (const [epic, group] of groupByEpic(entries)) {
|
|
228
245
|
lines.push(`### ${epic}`, "");
|
|
229
246
|
for (const entry of group) lines.push(...renderEntry(entry, blocks, labelById));
|
|
230
247
|
lines.push("");
|
|
231
248
|
}
|
|
249
|
+
lines.push(...footerLines);
|
|
232
250
|
return lines.join("\n");
|
|
233
251
|
}
|
|
234
252
|
function writeIfChanged(path, content) {
|
|
@@ -242,8 +260,20 @@ function syncTickets(cwd) {
|
|
|
242
260
|
const relativeLabel = nodePath.relative(cwd, ticketsDirectory) || TICKETS_RELATIVE_PATH;
|
|
243
261
|
const indexPath = nodePath.join(ticketsDirectory, INDEX_FILENAME);
|
|
244
262
|
const completedIndexPath = nodePath.join(ticketsDirectory, COMPLETED_INDEX_FILENAME);
|
|
263
|
+
const indexConflicts = [
|
|
264
|
+
detectConflictedIndex(indexPath),
|
|
265
|
+
detectConflictedIndex(completedIndexPath)
|
|
266
|
+
].filter((path) => path !== void 0);
|
|
245
267
|
if (!existsSync(ticketsDirectory)) {
|
|
246
|
-
return {
|
|
268
|
+
return {
|
|
269
|
+
wrote: false,
|
|
270
|
+
active: [],
|
|
271
|
+
completed: [],
|
|
272
|
+
skipped: [],
|
|
273
|
+
indexPath,
|
|
274
|
+
completedIndexPath,
|
|
275
|
+
indexConflicts: []
|
|
276
|
+
};
|
|
247
277
|
}
|
|
248
278
|
const { active, completed, skipped } = readTickets(ticketsDirectory, relativeLabel);
|
|
249
279
|
const isWroteActive = writeIfChanged(indexPath, buildIndexContent(active, { variant: "active" }));
|
|
@@ -255,7 +285,8 @@ function syncTickets(cwd) {
|
|
|
255
285
|
completed,
|
|
256
286
|
skipped,
|
|
257
287
|
indexPath,
|
|
258
|
-
completedIndexPath
|
|
288
|
+
completedIndexPath,
|
|
289
|
+
indexConflicts
|
|
259
290
|
};
|
|
260
291
|
}
|
|
261
292
|
|
|
@@ -266,4 +297,4 @@ export {
|
|
|
266
297
|
readTickets,
|
|
267
298
|
syncTickets
|
|
268
299
|
};
|
|
269
|
-
//# sourceMappingURL=chunk-
|
|
300
|
+
//# sourceMappingURL=chunk-HTDMZQKA.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']);\nconst MERGE_CONFLICT_MARKER_PATTERN = /^(?:<{7}|={7}|>{7})(?:\\s|$)/m;\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 externalIssue: string | undefined; // canonical tracker issue link (optional)\n externalPullRequests: string[]; // active/relevant PR links (optional)\n dependsOn: string[]; // ticket ids this one depends on (directed edge); [] when none\n blockedOn: string[]; // ticket ids this one is hard-blocked on (gates phase advance); [] when none\n blockedOnOverride: string | undefined; // reason recorded to advance past a non-done blocker; undefined 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 indexConflicts: string[];\n}\n\n/** Detect Git merge-conflict markers in generated artifact content. */\nfunction hasMergeConflictMarkers(content: string): boolean {\n return MERGE_CONFLICT_MARKER_PATTERN.test(content);\n}\n\nfunction detectConflictedIndex(indexPath: string): string | undefined {\n if (!existsSync(indexPath)) return undefined;\n const content = readFileSync(indexPath, 'utf8');\n return hasMergeConflictMarkers(content) ? indexPath : undefined;\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/** Parse comma/YAML-list fields into normalized string arrays. */\nfunction parseStringList(raw: string | undefined): string[] {\n if (raw === undefined) return [];\n const normalized = raw.trim();\n if (normalized === '') return [];\n\n const listBody =\n normalized.startsWith('[') && normalized.endsWith(']') ? normalized.slice(1, -1) : normalized;\n\n return listBody\n .split(',')\n .map(item => stripQuotes(item.trim()))\n .map(item => item.trim())\n .filter(Boolean);\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 externalIssue: fields.get('external_issue') ?? fields.get('external'),\n externalPullRequests: parseStringList(fields.get('external_prs')),\n goal: goalLine(bodyLines),\n dependsOn: parseTicketIdList(fields.get('depends_on')),\n blockedOn: parseTicketIdList(fields.get('blocked_on')),\n blockedOnOverride: fields.get('blocked_on_override'),\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 if (entry.blockedOnOverride !== undefined) lines.push(` override: ${entry.blockedOnOverride}`);\n if (entry.externalIssue !== undefined && entry.externalIssue.length > 0) {\n lines.push(` external issue: ${entry.externalIssue}`);\n }\n if (entry.externalPullRequests.length > 0) {\n lines.push(` external PRs: ${entry.externalPullRequests.join(', ')}`);\n }\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].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 headerLines = [\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 '<!-- prettier-ignore-start -->',\n '',\n ];\n const footerLines = ['<!-- prettier-ignore-end -->', ''];\n\n if (entries.length === 0) {\n return [\n ...headerLines,\n isActive ? 'No active tickets.' : 'No completed tickets.',\n '',\n ...footerLines,\n ].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 = [...headerLines, `## 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 lines.push(...footerLines);\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 const indexConflicts = [\n detectConflictedIndex(indexPath),\n detectConflictedIndex(completedIndexPath),\n ].filter((path): path is string => path !== undefined);\n\n if (!existsSync(ticketsDirectory)) {\n return {\n wrote: false,\n active: [],\n completed: [],\n skipped: [],\n indexPath,\n completedIndexPath,\n indexConflicts: [],\n };\n }\n\n const { active, completed, skipped } = readTickets(ticketsDirectory, relativeLabel);\n\n const isWroteActive = writeIfChanged(indexPath, buildIndexContent(active, { variant: 'active' }));\n\n const completedDirectory = nodePath.join(ticketsDirectory, COMPLETED_DIRNAME);\n const isWroteCompleted =\n completed.length > 0 || existsSync(completedDirectory)\n ? writeIfChanged(completedIndexPath, buildIndexContent(completed, { variant: 'completed' }))\n : false;\n\n return {\n wrote: isWroteActive || isWroteCompleted,\n active,\n completed,\n skipped,\n indexPath,\n completedIndexPath,\n indexConflicts,\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 if (reachesSelf(start, edges)) inCycle.add(start);\n }\n\n return [...inCycle].toSorted((a, b) => a.localeCompare(b));\n}\n\n/**\n * DFS along depends_on edges from `start`; returns true when `start` is\n * reachable from itself (it lies on a cycle, including via a self-edge).\n */\nfunction reachesSelf(start: string, edges: Map<string, string[]>): boolean {\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 return true;\n }\n if (seen.has(next)) continue;\n seen.add(next);\n stack.push(...(edges.get(next) ?? []));\n }\n return false;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// src/utils/version.ts
|
|
2
|
+
function compareVersions(a, b) {
|
|
3
|
+
const aParts = a.split(".").map(Number);
|
|
4
|
+
const bParts = b.split(".").map(Number);
|
|
5
|
+
for (let i = 0; i < 3; i++) {
|
|
6
|
+
const aValue = aParts[i] ?? 0;
|
|
7
|
+
const bValue = bParts[i] ?? 0;
|
|
8
|
+
if (aValue < bValue) return -1;
|
|
9
|
+
if (aValue > bValue) return 1;
|
|
10
|
+
}
|
|
11
|
+
return 0;
|
|
12
|
+
}
|
|
13
|
+
function isNewerVersion(current, latest) {
|
|
14
|
+
return compareVersions(current, latest) === -1;
|
|
15
|
+
}
|
|
16
|
+
var REGISTRY_TIMEOUT_MS = 3e3;
|
|
17
|
+
var VERSION_PATTERN = /^\d+\.\d+\.\d+$/;
|
|
18
|
+
function isComparableVersion(value) {
|
|
19
|
+
return typeof value === "string" && VERSION_PATTERN.test(value);
|
|
20
|
+
}
|
|
21
|
+
async function fetchRegistryLatestVersion(timeout = REGISTRY_TIMEOUT_MS) {
|
|
22
|
+
const controller = new AbortController();
|
|
23
|
+
const timeoutId = setTimeout(() => {
|
|
24
|
+
controller.abort();
|
|
25
|
+
}, timeout);
|
|
26
|
+
try {
|
|
27
|
+
const response = await fetch("https://registry.npmjs.org/safeword/latest", {
|
|
28
|
+
signal: controller.signal
|
|
29
|
+
});
|
|
30
|
+
if (!response.ok) return void 0;
|
|
31
|
+
const data = await response.json();
|
|
32
|
+
return isComparableVersion(data.version) ? data.version : void 0;
|
|
33
|
+
} catch {
|
|
34
|
+
return void 0;
|
|
35
|
+
} finally {
|
|
36
|
+
clearTimeout(timeoutId);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export {
|
|
41
|
+
compareVersions,
|
|
42
|
+
isNewerVersion,
|
|
43
|
+
isComparableVersion,
|
|
44
|
+
fetchRegistryLatestVersion
|
|
45
|
+
};
|
|
46
|
+
//# sourceMappingURL=chunk-LRYWFRPD.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils/version.ts"],"sourcesContent":["/**\n * Version comparison utilities\n */\n\n/**\n * Compare two semver versions\n * @param a\n * @param b\n * @returns -1 if a < b, 0 if a == b, 1 if a > b\n */\nexport function compareVersions(a: string, b: string): -1 | 0 | 1 {\n const aParts = a.split('.').map(Number);\n const bParts = b.split('.').map(Number);\n\n for (let i = 0; i < 3; i++) {\n const aValue = aParts[i] ?? 0;\n const bValue = bParts[i] ?? 0;\n if (aValue < bValue) return -1;\n if (aValue > bValue) return 1;\n }\n\n return 0;\n}\n\n/**\n * Check if latest version is newer than current\n * @param current\n * @param latest\n */\nexport function isNewerVersion(current: string, latest: string): boolean {\n return compareVersions(current, latest) === -1;\n}\n\nconst REGISTRY_TIMEOUT_MS = 3000;\nconst VERSION_PATTERN = /^\\d+\\.\\d+\\.\\d+$/;\n\n/**\n * Type guard for a comparable semver string (e.g. `\"1.2.3\"`). Use before\n * passing a registry- or cache-sourced value into {@link compareVersions},\n * which silently produces `NaN` comparisons on malformed input.\n * @param value\n */\nexport function isComparableVersion(value: unknown): value is string {\n return typeof value === 'string' && VERSION_PATTERN.test(value);\n}\n\n/**\n * Fetch the latest published safeword version from the npm registry.\n * Returns undefined on network error, timeout, non-OK response, or an\n * unparseable/invalid version — callers treat undefined as \"unknown\".\n * @param timeout milliseconds before the request is aborted\n */\nexport async function fetchRegistryLatestVersion(\n timeout = REGISTRY_TIMEOUT_MS,\n): Promise<string | undefined> {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => {\n controller.abort();\n }, timeout);\n\n try {\n const response = await fetch('https://registry.npmjs.org/safeword/latest', {\n signal: controller.signal,\n });\n if (!response.ok) return undefined;\n\n const data = (await response.json()) as { version?: unknown };\n return isComparableVersion(data.version) ? data.version : undefined;\n } catch {\n return undefined;\n } finally {\n clearTimeout(timeoutId);\n }\n}\n"],"mappings":";AAUO,SAAS,gBAAgB,GAAW,GAAuB;AAChE,QAAM,SAAS,EAAE,MAAM,GAAG,EAAE,IAAI,MAAM;AACtC,QAAM,SAAS,EAAE,MAAM,GAAG,EAAE,IAAI,MAAM;AAEtC,WAAS,IAAI,GAAG,IAAI,GAAG,KAAK;AAC1B,UAAM,SAAS,OAAO,CAAC,KAAK;AAC5B,UAAM,SAAS,OAAO,CAAC,KAAK;AAC5B,QAAI,SAAS,OAAQ,QAAO;AAC5B,QAAI,SAAS,OAAQ,QAAO;AAAA,EAC9B;AAEA,SAAO;AACT;AAOO,SAAS,eAAe,SAAiB,QAAyB;AACvE,SAAO,gBAAgB,SAAS,MAAM,MAAM;AAC9C;AAEA,IAAM,sBAAsB;AAC5B,IAAM,kBAAkB;AAQjB,SAAS,oBAAoB,OAAiC;AACnE,SAAO,OAAO,UAAU,YAAY,gBAAgB,KAAK,KAAK;AAChE;AAQA,eAAsB,2BACpB,UAAU,qBACmB;AAC7B,QAAM,aAAa,IAAI,gBAAgB;AACvC,QAAM,YAAY,WAAW,MAAM;AACjC,eAAW,MAAM;AAAA,EACnB,GAAG,OAAO;AAEV,MAAI;AACF,UAAM,WAAW,MAAM,MAAM,8CAA8C;AAAA,MACzE,QAAQ,WAAW;AAAA,IACrB,CAAC;AACD,QAAI,CAAC,SAAS,GAAI,QAAO;AAEzB,UAAM,OAAQ,MAAM,SAAS,KAAK;AAClC,WAAO,oBAAoB,KAAK,OAAO,IAAI,KAAK,UAAU;AAAA,EAC5D,QAAQ;AACN,WAAO;AAAA,EACT,UAAE;AACA,iBAAa,SAAS;AAAA,EACxB;AACF;","names":[]}
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
} from "./chunk-HSC7TELY.js";
|
|
7
7
|
|
|
8
8
|
// src/presets/typescript/eslint-configs/astro.ts
|
|
9
|
-
import
|
|
9
|
+
import eslintPluginAstro from "eslint-plugin-astro";
|
|
10
10
|
|
|
11
11
|
// src/presets/typescript/eslint-configs/lazy.ts
|
|
12
12
|
function lazyConfigArray(builder) {
|
|
@@ -28,15 +28,36 @@ function lazyConfigArray(builder) {
|
|
|
28
28
|
});
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
// src/presets/typescript/eslint-configs/
|
|
31
|
+
// src/presets/typescript/eslint-configs/optional-dependency.ts
|
|
32
|
+
import { createRequire } from "module";
|
|
33
|
+
import nodePath from "path";
|
|
32
34
|
var requireFromHere = createRequire(import.meta.url);
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
function optionalRequire(packageName) {
|
|
36
|
+
try {
|
|
37
|
+
const requireFromCwd = createRequire(nodePath.join(process.cwd(), "__placeholder__.js"));
|
|
38
|
+
return requireFromCwd(packageName);
|
|
39
|
+
} catch {
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
return requireFromHere(packageName);
|
|
43
|
+
} catch {
|
|
44
|
+
return void 0;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function hasOptionalDependency(packageName) {
|
|
48
|
+
return optionalRequire(packageName) !== void 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/presets/typescript/eslint-configs/astro.ts
|
|
52
|
+
function buildAstroConfig({
|
|
53
|
+
astroPlugin = eslintPluginAstro,
|
|
54
|
+
hasJsxA11y = hasOptionalDependency("eslint-plugin-jsx-a11y")
|
|
55
|
+
} = {}) {
|
|
35
56
|
return [
|
|
36
57
|
// Spread flat/recommended (5 config objects: plugin setup, file patterns, prettier overrides, rules)
|
|
37
|
-
...astroPlugin.configs["flat/recommended"],
|
|
38
|
-
// Accessibility rules adapted for Astro
|
|
39
|
-
...astroPlugin.configs["flat/jsx-a11y-strict"],
|
|
58
|
+
...astroPlugin.configs["flat/recommended"] ?? [],
|
|
59
|
+
// Accessibility rules adapted for Astro when eslint-plugin-jsx-a11y is installed.
|
|
60
|
+
...hasJsxA11y ? astroPlugin.configs["flat/jsx-a11y-strict"] ?? [] : [],
|
|
40
61
|
// Add LLM-critical rules
|
|
41
62
|
{
|
|
42
63
|
name: "safeword/astro",
|
|
@@ -50,7 +71,8 @@ var astroConfig = lazyConfigArray(() => {
|
|
|
50
71
|
}
|
|
51
72
|
}
|
|
52
73
|
];
|
|
53
|
-
}
|
|
74
|
+
}
|
|
75
|
+
var astroConfig = lazyConfigArray(() => buildAstroConfig());
|
|
54
76
|
|
|
55
77
|
// src/presets/typescript/eslint-configs/base.ts
|
|
56
78
|
import js from "@eslint/js";
|
|
@@ -756,7 +778,6 @@ import { createRequire as createRequire3 } from "module";
|
|
|
756
778
|
|
|
757
779
|
// src/presets/typescript/eslint-configs/recommended-react.ts
|
|
758
780
|
import eslintReactPlugin from "@eslint-react/eslint-plugin";
|
|
759
|
-
import jsxA11y from "eslint-plugin-jsx-a11y";
|
|
760
781
|
import reactHooksPluginImport from "eslint-plugin-react-hooks";
|
|
761
782
|
|
|
762
783
|
// src/presets/typescript/eslint-configs/recommended-typescript.ts
|
|
@@ -924,40 +945,52 @@ var eslintReactRuleOverrides = {
|
|
|
924
945
|
"@eslint-react/dom-no-unknown-property": "error"
|
|
925
946
|
// class -> className, has autofix
|
|
926
947
|
};
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
rules
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
948
|
+
function loadJsxA11yStrictConfig() {
|
|
949
|
+
const jsxA11y = optionalRequire("eslint-plugin-jsx-a11y");
|
|
950
|
+
return jsxA11y?.flatConfigs?.strict;
|
|
951
|
+
}
|
|
952
|
+
function buildRecommendedTypeScriptReact({
|
|
953
|
+
loadJsxA11yStrictConfig: loadOptionalJsxA11yStrictConfig = loadJsxA11yStrictConfig
|
|
954
|
+
} = {}) {
|
|
955
|
+
const jsxA11yStrictConfig = loadOptionalJsxA11yStrictConfig();
|
|
956
|
+
return [
|
|
957
|
+
// All TypeScript rules (includes base plugins)
|
|
958
|
+
...recommendedTypeScript,
|
|
959
|
+
// React, JSX, DOM, RSC, and Web API rules
|
|
960
|
+
eslintReactRecommendedTypeScriptConfig,
|
|
961
|
+
// React Hooks + Compiler rules (v7.x flat config)
|
|
962
|
+
// Using recommended-latest which includes void-use-memo
|
|
963
|
+
reactHooksConfig,
|
|
964
|
+
// Accessibility rules - strict preset (all at error level) when installed.
|
|
965
|
+
...jsxA11yStrictConfig ? [jsxA11yStrictConfig] : [],
|
|
966
|
+
// Escalate warn rules to error + add LLM-critical rules
|
|
967
|
+
{
|
|
968
|
+
name: "safeword/react-hooks-rules",
|
|
969
|
+
rules: {
|
|
970
|
+
// Escalate default warns to error (LLMs ignore warnings)
|
|
971
|
+
"react-hooks/exhaustive-deps": "error",
|
|
972
|
+
// Default: warn
|
|
973
|
+
"react-hooks/incompatible-library": "error",
|
|
974
|
+
// Default: warn
|
|
975
|
+
"react-hooks/unsupported-syntax": "error",
|
|
976
|
+
// Default: warn
|
|
977
|
+
// LLM-critical rules NOT in recommended-latest preset
|
|
978
|
+
"react-hooks/memoized-effect-dependencies": "error",
|
|
979
|
+
// LLMs create unstable refs as deps
|
|
980
|
+
"react-hooks/no-deriving-state-in-effects": "error"
|
|
981
|
+
// LLMs derive state in useEffect
|
|
982
|
+
}
|
|
983
|
+
},
|
|
984
|
+
// React rule overrides for TypeScript projects
|
|
985
|
+
{
|
|
986
|
+
name: "safeword/react-rules",
|
|
987
|
+
rules: eslintReactRuleOverrides
|
|
953
988
|
}
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
}
|
|
960
|
-
];
|
|
989
|
+
];
|
|
990
|
+
}
|
|
991
|
+
var recommendedTypeScriptReact = lazyConfigArray(
|
|
992
|
+
() => buildRecommendedTypeScriptReact()
|
|
993
|
+
);
|
|
961
994
|
|
|
962
995
|
// src/presets/typescript/eslint-configs/recommended-nextjs.ts
|
|
963
996
|
var requireFromHere3 = createRequire3(import.meta.url);
|
|
@@ -1259,4 +1292,4 @@ export {
|
|
|
1259
1292
|
eslintPlugin,
|
|
1260
1293
|
typescript_default
|
|
1261
1294
|
};
|
|
1262
|
-
//# sourceMappingURL=chunk-
|
|
1295
|
+
//# sourceMappingURL=chunk-MT4WBU2P.js.map
|