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.
Files changed (107) hide show
  1. package/dist/{check-OD4QHC2P.js → check-6IHMMAS6.js} +41 -27
  2. package/dist/check-6IHMMAS6.js.map +1 -0
  3. package/dist/{chunk-64TSNY2M.js → chunk-DIJ35ZXH.js} +2 -2
  4. package/dist/{chunk-EUSXT3MN.js → chunk-HTDMZQKA.js} +37 -6
  5. package/dist/chunk-HTDMZQKA.js.map +1 -0
  6. package/dist/chunk-LRYWFRPD.js +46 -0
  7. package/dist/chunk-LRYWFRPD.js.map +1 -0
  8. package/dist/{chunk-VLK2DXJ7.js → chunk-MT4WBU2P.js} +76 -43
  9. package/dist/chunk-MT4WBU2P.js.map +1 -0
  10. package/dist/{chunk-FZVYR37T.js → chunk-Q7ALQS7T.js} +3 -3
  11. package/dist/{chunk-RQBSXETC.js → chunk-UYTOLZT2.js} +185 -31
  12. package/dist/chunk-UYTOLZT2.js.map +1 -0
  13. package/dist/chunk-ZESHX2BU.js +14 -0
  14. package/dist/chunk-ZESHX2BU.js.map +1 -0
  15. package/dist/cli.js +10 -10
  16. package/dist/cli.js.map +1 -1
  17. package/dist/{diff-L7DED7WY.js → diff-5YTHPOAV.js} +36 -5
  18. package/dist/diff-5YTHPOAV.js.map +1 -0
  19. package/dist/index.js +1 -1
  20. package/dist/presets/typescript/index.d.ts +0 -42
  21. package/dist/presets/typescript/index.js +1 -1
  22. package/dist/{reset-LE6GYEYU.js → reset-OBM2VJQT.js} +2 -2
  23. package/dist/{setup-ZJCYBXA6.js → setup-ZFQDI5QT.js} +5 -5
  24. package/dist/{sync-tickets-53DTTY7B.js → sync-tickets-35ZSEKIE.js} +8 -2
  25. package/dist/sync-tickets-35ZSEKIE.js.map +1 -0
  26. package/dist/{sync-tracker-MP6BTZXX.js → sync-tracker-YUXD7QKS.js} +144 -11
  27. package/dist/sync-tracker-YUXD7QKS.js.map +1 -0
  28. package/dist/{test-plan-HWRDWG2X.js → test-plan-MS6HRVBR.js} +9 -3
  29. package/dist/test-plan-MS6HRVBR.js.map +1 -0
  30. package/dist/{ticket-new-GXYCW5ML.js → ticket-new-S6IMADXY.js} +6 -2
  31. package/dist/{ticket-new-GXYCW5ML.js.map → ticket-new-S6IMADXY.js.map} +1 -1
  32. package/dist/{upgrade-7JTIBI3I.js → upgrade-N3WTG3KL.js} +7 -10
  33. package/dist/upgrade-N3WTG3KL.js.map +1 -0
  34. package/package.json +15 -14
  35. package/templates/codex/config.toml +3 -3
  36. package/templates/commands/audit.md +3 -3
  37. package/templates/commands/refactor.md +1 -1
  38. package/templates/commands/self-review.md +1 -1
  39. package/templates/commands/verify.md +10 -4
  40. package/templates/cursor/rules/bdd-tdd.mdc +1 -1
  41. package/templates/cursor/rules/safeword-refactoring.mdc +1 -1
  42. package/templates/cursor/rules/safeword-tdd-review.mdc +1 -1
  43. package/templates/guides/llm-evals-guide.md +485 -0
  44. package/templates/guides/testing-guide.md +35 -13
  45. package/templates/guides/verification-lanes-guide.md +574 -0
  46. package/templates/hooks/codex/pre-tool-quality.ts +11 -2
  47. package/templates/hooks/cursor/after-file-edit.ts +5 -2
  48. package/templates/hooks/cursor/before-shell-execution.ts +75 -0
  49. package/templates/hooks/cursor/gate-adapter.ts +278 -0
  50. package/templates/hooks/cursor/post-tool-quality.ts +70 -0
  51. package/templates/hooks/cursor/pre-tool-quality.ts +107 -0
  52. package/templates/hooks/cursor/stop.ts +22 -2
  53. package/templates/hooks/lib/active-ticket.ts +166 -0
  54. package/templates/hooks/lib/architecture-staged-scope.ts +130 -0
  55. package/templates/hooks/lib/auto-upgrade-lock.ts +89 -0
  56. package/templates/hooks/lib/auto-upgrade.ts +417 -0
  57. package/templates/hooks/lib/branch-staleness.ts +49 -0
  58. package/templates/hooks/lib/checkbox-transitions.ts +2 -2
  59. package/templates/hooks/lib/cursor-run-identity.ts +216 -0
  60. package/templates/hooks/lib/dependency-readiness.ts +83 -0
  61. package/templates/hooks/lib/done-gate.ts +175 -0
  62. package/templates/hooks/lib/ledger-git.ts +92 -0
  63. package/templates/hooks/lib/ledger-validation.ts +31 -24
  64. package/templates/hooks/lib/quality-state.ts +65 -17
  65. package/templates/hooks/lib/review-trigger.ts +4 -31
  66. package/templates/hooks/lib/run-identity.ts +150 -0
  67. package/templates/hooks/lib/safeword-context.ts +83 -0
  68. package/templates/hooks/post-tool-dependency-readiness.ts +84 -0
  69. package/templates/hooks/post-tool-quality.ts +6 -18
  70. package/templates/hooks/pre-tool-architecture-stage.ts +9 -0
  71. package/templates/hooks/pre-tool-quality.ts +71 -5
  72. package/templates/hooks/pre-tool-stale-main.ts +83 -0
  73. package/templates/hooks/prompt-questions.ts +13 -1
  74. package/templates/hooks/record-skill-invocation.ts +49 -16
  75. package/templates/hooks/session-auto-upgrade.ts +13 -300
  76. package/templates/hooks/session-codex-start.ts +39 -0
  77. package/templates/hooks/session-cursor-auto-upgrade.ts +35 -0
  78. package/templates/hooks/session-dependency-readiness.ts +36 -0
  79. package/templates/hooks/session-safeword-context.ts +19 -81
  80. package/templates/hooks/stop-quality.ts +15 -60
  81. package/templates/hooks/write-review-stamp.ts +4 -2
  82. package/templates/skills/audit/SKILL.md +3 -3
  83. package/templates/skills/bdd/SCENARIOS.md +1 -1
  84. package/templates/skills/bdd/TDD.md +3 -1
  85. package/templates/skills/quality-review/SKILL.md +8 -3
  86. package/templates/skills/refactor/SKILL.md +34 -11
  87. package/templates/skills/review-spec/SKILL.md +2 -1
  88. package/templates/skills/self-review/SKILL.md +1 -1
  89. package/templates/skills/tdd-review/SKILL.md +4 -2
  90. package/templates/skills/testing/SKILL.md +33 -0
  91. package/templates/skills/ticket-system/SKILL.md +14 -2
  92. package/templates/skills/verify/SKILL.md +10 -4
  93. package/dist/check-OD4QHC2P.js.map +0 -1
  94. package/dist/chunk-EUSXT3MN.js.map +0 -1
  95. package/dist/chunk-FJYRWU2V.js +0 -21
  96. package/dist/chunk-FJYRWU2V.js.map +0 -1
  97. package/dist/chunk-RQBSXETC.js.map +0 -1
  98. package/dist/chunk-VLK2DXJ7.js.map +0 -1
  99. package/dist/diff-L7DED7WY.js.map +0 -1
  100. package/dist/sync-tickets-53DTTY7B.js.map +0 -1
  101. package/dist/sync-tracker-MP6BTZXX.js.map +0 -1
  102. package/dist/test-plan-HWRDWG2X.js.map +0 -1
  103. package/dist/upgrade-7JTIBI3I.js.map +0 -1
  104. /package/dist/{chunk-64TSNY2M.js.map → chunk-DIJ35ZXH.js.map} +0 -0
  105. /package/dist/{chunk-FZVYR37T.js.map → chunk-Q7ALQS7T.js.map} +0 -0
  106. /package/dist/{reset-LE6GYEYU.js.map → reset-OBM2VJQT.js.map} +0 -0
  107. /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-FJYRWU2V.js";
7
+ } from "./chunk-LRYWFRPD.js";
4
8
  import {
5
9
  checkHealth,
6
10
  reportHealthSummary
7
- } from "./chunk-FZVYR37T.js";
11
+ } from "./chunk-Q7ALQS7T.js";
8
12
  import "./chunk-YXNI7W5D.js";
9
13
  import "./chunk-XTLCJKGE.js";
10
- import "./chunk-RQBSXETC.js";
11
- import "./chunk-GS3TBFXU.js";
14
+ import "./chunk-UYTOLZT2.js";
15
+ import {
16
+ detectPackageManager
17
+ } from "./chunk-GS3TBFXU.js";
12
18
  import {
13
19
  syncTickets
14
- } from "./chunk-EUSXT3MN.js";
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 checkLatestVersion();
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 reportVersionMismatch(health) {
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
- info("Consider upgrading the CLI");
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-OD4QHC2P.js.map
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-RQBSXETC.js";
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-64TSNY2M.js.map
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 header = [
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 [...header, isActive ? "No active tickets." : "No completed tickets.", ""].join("\n");
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 = [...header, `## Tickets (${entries.length})`, ""];
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 { wrote: false, active: [], completed: [], skipped: [], indexPath, completedIndexPath };
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-EUSXT3MN.js.map
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 { createRequire } from "module";
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/astro.ts
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
- var astroConfig = lazyConfigArray(() => {
34
- const astroPlugin = requireFromHere("eslint-plugin-astro");
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 (requires eslint-plugin-jsx-a11y installed)
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
- var recommendedTypeScriptReact = [
928
- // All TypeScript rules (includes base plugins)
929
- ...recommendedTypeScript,
930
- // React, JSX, DOM, RSC, and Web API rules
931
- eslintReactRecommendedTypeScriptConfig,
932
- // React Hooks + Compiler rules (v7.x flat config)
933
- // Using recommended-latest which includes void-use-memo
934
- reactHooksConfig,
935
- // Accessibility rules - strict preset (all at error level)
936
- jsxA11y.flatConfigs.strict,
937
- // Escalate warn rules to error + add LLM-critical rules
938
- {
939
- name: "safeword/react-hooks-rules",
940
- rules: {
941
- // Escalate default warns to error (LLMs ignore warnings)
942
- "react-hooks/exhaustive-deps": "error",
943
- // Default: warn
944
- "react-hooks/incompatible-library": "error",
945
- // Default: warn
946
- "react-hooks/unsupported-syntax": "error",
947
- // Default: warn
948
- // LLM-critical rules NOT in recommended-latest preset
949
- "react-hooks/memoized-effect-dependencies": "error",
950
- // LLMs create unstable refs as deps
951
- "react-hooks/no-deriving-state-in-effects": "error"
952
- // LLMs derive state in useEffect
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
- // React rule overrides for TypeScript projects
956
- {
957
- name: "safeword/react-rules",
958
- rules: eslintReactRuleOverrides
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-VLK2DXJ7.js.map
1295
+ //# sourceMappingURL=chunk-MT4WBU2P.js.map