safeword 0.33.0 → 0.35.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 (69) hide show
  1. package/dist/{check-OCPEU75B.js → check-U766L3RA.js} +2 -2
  2. package/dist/{chunk-X3QSXFLN.js → chunk-6O7YRM2H.js} +2 -1
  3. package/dist/chunk-6O7YRM2H.js.map +1 -0
  4. package/dist/chunk-A4M46ZQN.js +312 -0
  5. package/dist/chunk-A4M46ZQN.js.map +1 -0
  6. package/dist/{chunk-UOUUMDSB.js → chunk-RVVXMIEB.js} +48 -3
  7. package/dist/chunk-RVVXMIEB.js.map +1 -0
  8. package/dist/{chunk-G4RIZGNH.js → chunk-UPZWDR6G.js} +8 -2
  9. package/dist/chunk-UPZWDR6G.js.map +1 -0
  10. package/dist/cli.js +21 -10
  11. package/dist/cli.js.map +1 -1
  12. package/dist/{diff-5EESXIAP.js → diff-S7DGZ72M.js} +2 -2
  13. package/dist/index.d.ts +1 -0
  14. package/dist/index.js +1 -1
  15. package/dist/presets/typescript/index.d.ts +19 -1
  16. package/dist/presets/typescript/index.js +3 -1
  17. package/dist/{reset-MOIC7UJX.js → reset-MLFMCCSO.js} +2 -2
  18. package/dist/{setup-7UMALSAE.js → setup-E5T74T34.js} +13 -6
  19. package/dist/setup-E5T74T34.js.map +1 -0
  20. package/dist/{sync-config-D77T4OIV.js → sync-config-3GZZ6CEX.js} +2 -2
  21. package/dist/ticket-new-FMXU2P4A.js +180 -0
  22. package/dist/ticket-new-FMXU2P4A.js.map +1 -0
  23. package/dist/{upgrade-I3KC4OHM.js → upgrade-7PPTN67U.js} +24 -6
  24. package/dist/upgrade-7PPTN67U.js.map +1 -0
  25. package/package.json +10 -2
  26. package/templates/SAFEWORD.md +18 -7
  27. package/templates/cursor/rules/safeword-figure-it-out.mdc +6 -0
  28. package/templates/doc-templates/test-definitions-feature.md +24 -2
  29. package/templates/guides/learning-extraction.md +9 -0
  30. package/templates/guides/planning-guide.md +54 -126
  31. package/templates/hooks/cursor/stop.ts +4 -2
  32. package/templates/hooks/lib/active-ticket.ts +32 -6
  33. package/templates/hooks/lib/learning-verification-stamps.ts +62 -0
  34. package/templates/hooks/lib/ledger-validation.ts +173 -0
  35. package/templates/hooks/lib/parse-annotation.ts +49 -0
  36. package/templates/hooks/lib/quality-state.ts +20 -3
  37. package/templates/hooks/lib/quality.ts +7 -3
  38. package/templates/hooks/lib/test-runner.ts +9 -4
  39. package/templates/hooks/post-tool-quality.ts +12 -2
  40. package/templates/hooks/post-tool-sync-learnings.ts +29 -2
  41. package/templates/hooks/pre-tool-config-guard.ts +2 -5
  42. package/templates/hooks/pre-tool-quality.ts +205 -3
  43. package/templates/hooks/prompt-questions.ts +16 -6
  44. package/templates/hooks/session-start-reentry.ts +105 -0
  45. package/templates/hooks/stop-quality.ts +32 -1
  46. package/templates/hooks/stop-reentry.ts +120 -0
  47. package/templates/skills/audit/SKILL.md +6 -0
  48. package/templates/skills/bdd/DECOMPOSITION.md +4 -0
  49. package/templates/skills/bdd/DISCOVERY.md +6 -0
  50. package/templates/skills/bdd/SCENARIOS.md +34 -6
  51. package/templates/skills/bdd/TDD.md +17 -3
  52. package/templates/skills/bdd/VERIFY.md +1 -1
  53. package/templates/skills/brainstorm/SKILL.md +10 -0
  54. package/templates/skills/debug/SKILL.md +11 -0
  55. package/templates/skills/figure-it-out/SKILL.md +88 -0
  56. package/templates/skills/quality-review/SKILL.md +6 -0
  57. package/templates/skills/tdd-review/SKILL.md +16 -1
  58. package/templates/skills/ticket-system/SKILL.md +13 -9
  59. package/dist/chunk-G4RIZGNH.js.map +0 -1
  60. package/dist/chunk-RE5O6MEQ.js +0 -76
  61. package/dist/chunk-RE5O6MEQ.js.map +0 -1
  62. package/dist/chunk-UOUUMDSB.js.map +0 -1
  63. package/dist/chunk-X3QSXFLN.js.map +0 -1
  64. package/dist/setup-7UMALSAE.js.map +0 -1
  65. package/dist/upgrade-I3KC4OHM.js.map +0 -1
  66. /package/dist/{check-OCPEU75B.js.map → check-U766L3RA.js.map} +0 -0
  67. /package/dist/{diff-5EESXIAP.js.map → diff-S7DGZ72M.js.map} +0 -0
  68. /package/dist/{reset-MOIC7UJX.js.map → reset-MLFMCCSO.js.map} +0 -0
  69. /package/dist/{sync-config-D77T4OIV.js.map → sync-config-3GZZ6CEX.js.map} +0 -0
@@ -6,7 +6,7 @@ import {
6
6
  createProjectContext,
7
7
  getMissingPacks,
8
8
  reconcile
9
- } from "./chunk-UOUUMDSB.js";
9
+ } from "./chunk-RVVXMIEB.js";
10
10
  import "./chunk-YVZL7WO5.js";
11
11
  import {
12
12
  VERSION
@@ -189,4 +189,4 @@ async function check(options) {
189
189
  export {
190
190
  check
191
191
  };
192
- //# sourceMappingURL=check-OCPEU75B.js.map
192
+ //# sourceMappingURL=check-U766L3RA.js.map
@@ -263,6 +263,7 @@ function hasArchitectureDetected(arch) {
263
263
  return arch.elements.length > 0 || arch.isMonorepo || (arch.workspaces?.length ?? 0) > 0;
264
264
  }
265
265
  async function syncConfig() {
266
+ await Promise.resolve();
266
267
  const cwd = process.cwd();
267
268
  const safewordDirectory = nodePath3.join(cwd, ".safeword");
268
269
  if (!exists(safewordDirectory)) {
@@ -286,4 +287,4 @@ export {
286
287
  hasArchitectureDetected,
287
288
  syncConfig
288
289
  };
289
- //# sourceMappingURL=chunk-X3QSXFLN.js.map
290
+ //# sourceMappingURL=chunk-6O7YRM2H.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/sync-config.ts","../src/utils/boundaries.ts","../src/utils/depcruise-config.ts"],"sourcesContent":["/**\n * Sync Config command - Regenerate depcruise config from current project structure\n *\n * Used by `/audit` slash command to refresh config before running checks.\n */\n\nimport { writeFileSync } from 'node:fs';\nimport nodePath from 'node:path';\n\nimport { detectArchitecture } from '../utils/boundaries.js';\nimport {\n type DepCruiseArchitecture,\n detectWorkspaces,\n generateDepCruiseConfigFile,\n generateDepCruiseMainConfig,\n} from '../utils/depcruise-config.js';\nimport { exists } from '../utils/fs.js';\nimport { error, info, success } from '../utils/output.js';\n\ninterface SyncConfigResult {\n generatedConfig: boolean;\n createdMainConfig: boolean;\n}\n\n/**\n * Core sync logic - writes depcruise configs to disk\n * Can be called from setup or as standalone command\n */\nexport function syncConfigCore(cwd: string, arch: DepCruiseArchitecture): SyncConfigResult {\n const safewordDirectory = nodePath.join(cwd, '.safeword');\n const result: SyncConfigResult = {\n generatedConfig: false,\n createdMainConfig: false,\n };\n\n // Generate and write .safeword/depcruise-config.cjs (CJS for compatibility)\n const generatedConfigPath = nodePath.join(safewordDirectory, 'depcruise-config.cjs');\n const generatedConfig = generateDepCruiseConfigFile(arch);\n writeFileSync(generatedConfigPath, generatedConfig);\n result.generatedConfig = true;\n\n // Create main config if not exists (self-healing)\n // Use .cjs extension to work in ESM projects (type: \"module\")\n const mainConfigPath = nodePath.join(cwd, '.dependency-cruiser.cjs');\n if (!exists(mainConfigPath)) {\n const mainConfig = generateDepCruiseMainConfig();\n writeFileSync(mainConfigPath, mainConfig);\n result.createdMainConfig = true;\n }\n\n return result;\n}\n\n/**\n * Build full architecture info by combining detected layers with workspaces\n */\nexport function buildArchitecture(cwd: string): DepCruiseArchitecture {\n const arch = detectArchitecture(cwd);\n const workspaces = detectWorkspaces(cwd);\n return { ...arch, workspaces };\n}\n\n/**\n * Check if architecture was detected (layers, monorepo structure, or workspaces)\n */\nexport function hasArchitectureDetected(arch: DepCruiseArchitecture): boolean {\n return arch.elements.length > 0 || arch.isMonorepo || (arch.workspaces?.length ?? 0) > 0;\n}\n\n/**\n * CLI command: Sync depcruise config with current project structure\n */\n\nexport async function syncConfig(): Promise<void> {\n // Public CLI command contract is Promise<void>; body is sync today but the\n // signature reserves room for async I/O. Token await keeps the contract honest.\n await Promise.resolve();\n const cwd = process.cwd();\n const safewordDirectory = nodePath.join(cwd, '.safeword');\n\n // Check if .safeword exists\n if (!exists(safewordDirectory)) {\n error('Not configured. Run `safeword setup` first.');\n process.exit(1);\n }\n\n // Detect current architecture and workspaces\n const arch = buildArchitecture(cwd);\n const result = syncConfigCore(cwd, arch);\n\n if (result.generatedConfig) {\n info('Generated .safeword/depcruise-config.cjs');\n }\n if (result.createdMainConfig) {\n info('Created .dependency-cruiser.cjs');\n }\n\n success('Config synced');\n}\n","/**\n * Architecture boundaries detection\n *\n * Auto-detects common architecture directories for use by\n * dependency-cruiser layer enforcement.\n *\n * Supports:\n * - Standard projects (src/utils, utils/)\n * - Monorepos (packages/*, apps/*)\n * - Various naming conventions (helpers, shared, core, etc.)\n */\n\nimport { readdirSync } from 'node:fs';\nimport nodePath from 'node:path';\n\nimport { exists } from './fs.js';\n\n/**\n * Architecture layer definitions with alternative names.\n * Each layer maps to equivalent directory names.\n * Order defines hierarchy: earlier = lower layer.\n */\nconst ARCHITECTURE_LAYERS = [\n // Layer 0: Pure types (no imports)\n { layer: 'types', dirs: ['types', 'interfaces', 'schemas'] },\n // Layer 1: Utilities (only types)\n { layer: 'utils', dirs: ['utils', 'helpers', 'shared', 'common', 'core'] },\n // Layer 2: Libraries (types, utils)\n { layer: 'lib', dirs: ['lib', 'libraries'] },\n // Layer 3: State & logic (types, utils, lib)\n { layer: 'hooks', dirs: ['hooks', 'composables'] },\n { layer: 'services', dirs: ['services', 'api', 'stores', 'state'] },\n // Layer 4: UI components (all above)\n { layer: 'components', dirs: ['components', 'ui'] },\n // Layer 5: Features (all above)\n { layer: 'features', dirs: ['features', 'modules', 'domains'] },\n // Layer 6: Entry points (can import everything)\n { layer: 'app', dirs: ['app', 'pages', 'views', 'routes', 'commands'] },\n] as const;\n\ntype Layer = (typeof ARCHITECTURE_LAYERS)[number]['layer'];\n\ninterface DetectedElement {\n layer: Layer;\n pattern: string; // glob pattern for boundaries config\n location: string; // human-readable location\n}\n\nexport interface DetectedArchitecture {\n elements: DetectedElement[];\n isMonorepo: boolean;\n}\n\n/**\n * Find monorepo package directories\n * @param projectDirectory\n */\nfunction findMonorepoPackages(projectDirectory: string): string[] {\n const packages: string[] = [];\n\n // Check common monorepo patterns\n const monorepoRoots = ['packages', 'apps', 'libs', 'modules'];\n\n for (const root of monorepoRoots) {\n const rootPath = nodePath.join(projectDirectory, root);\n if (!exists(rootPath)) continue;\n\n try {\n const entries = readdirSync(rootPath, { withFileTypes: true });\n for (const entry of entries) {\n if (entry.isDirectory() && !entry.name.startsWith('.')) {\n packages.push(nodePath.join(root, entry.name));\n }\n }\n } catch {\n // Directory not readable, skip\n }\n }\n\n return packages;\n}\n\n/**\n * Check if a layer already exists for this path prefix\n * @param elements\n * @param layer\n * @param pathPrefix\n */\nfunction hasLayerForPrefix(elements: DetectedElement[], layer: Layer, pathPrefix: string): boolean {\n return elements.some(\n element => element.layer === layer && element.pattern.startsWith(pathPrefix),\n );\n}\n\n/**\n * Scan a single search path for architecture layers\n * @param projectDirectory\n * @param searchPath\n * @param pathPrefix\n * @param elements\n */\nfunction scanSearchPath(\n projectDirectory: string,\n searchPath: string,\n pathPrefix: string,\n elements: DetectedElement[],\n): void {\n for (const layerDefinition of ARCHITECTURE_LAYERS) {\n for (const dirName of layerDefinition.dirs) {\n const fullPath = nodePath.join(projectDirectory, searchPath, dirName);\n if (exists(fullPath) && !hasLayerForPrefix(elements, layerDefinition.layer, pathPrefix)) {\n elements.push({\n layer: layerDefinition.layer,\n pattern: `${pathPrefix}${dirName}/**`,\n location: `${pathPrefix}${dirName}`,\n });\n }\n }\n }\n}\n\n/**\n * Scan a directory for architecture layers\n * @param projectDirectory\n * @param basePath\n */\nfunction scanForLayers(projectDirectory: string, basePath: string): DetectedElement[] {\n const elements: DetectedElement[] = [];\n const prefix = basePath ? `${basePath}/` : '';\n\n // Check src/ and root level\n scanSearchPath(projectDirectory, nodePath.join(basePath, 'src'), `${prefix}src/`, elements);\n scanSearchPath(projectDirectory, basePath, prefix, elements);\n\n return elements;\n}\n\n/**\n * Detects architecture directories in the project\n * Handles both standard projects and monorepos\n * @param projectDirectory\n */\nexport function detectArchitecture(projectDirectory: string): DetectedArchitecture {\n const elements: DetectedElement[] = [];\n\n // First, check for monorepo packages\n const packages = findMonorepoPackages(projectDirectory);\n const isMonorepo = packages.length > 0;\n\n if (isMonorepo) {\n // Scan each package\n for (const pkg of packages) {\n elements.push(...scanForLayers(projectDirectory, pkg));\n }\n }\n\n // Also scan root level (works for both monorepo root and standard projects)\n elements.push(...scanForLayers(projectDirectory, ''));\n\n // Deduplicate by pattern\n const seen = new Set<string>();\n const uniqueElements = elements.filter(element => {\n if (seen.has(element.pattern)) return false;\n seen.add(element.pattern);\n return true;\n });\n\n return { elements: uniqueElements, isMonorepo };\n}\n","/**\n * Dependency-cruiser config generator\n *\n * Generates dependency-cruiser configuration from detected architecture.\n * Used by `safeword sync-config` command and `/audit` slash command.\n */\n\nimport nodePath from 'node:path';\n\nimport type { DetectedArchitecture } from './boundaries.js';\nimport { readJson } from './fs.js';\n\nexport interface DepCruiseArchitecture extends DetectedArchitecture {\n workspaces?: string[];\n}\n\ninterface PackageJson {\n workspaces?: string[] | { packages?: string[] };\n}\n\n/**\n * Detect workspaces from package.json\n * Supports both array format and object format (yarn workspaces)\n */\nexport function detectWorkspaces(cwd: string): string[] | undefined {\n const packageJsonPath = nodePath.join(cwd, 'package.json');\n const packageJson = readJson(packageJsonPath) as PackageJson | undefined;\n\n if (!packageJson?.workspaces) return undefined;\n\n // Handle both formats: string[] or { packages: string[] }\n const workspaces = Array.isArray(packageJson.workspaces)\n ? packageJson.workspaces\n : packageJson.workspaces.packages;\n\n return workspaces && workspaces.length > 0 ? workspaces : undefined;\n}\n\n/**\n * Generate monorepo hierarchy rules based on workspace patterns\n */\nfunction generateMonorepoRules(workspaces: string[]): string {\n const rules: string[] = [];\n\n const hasLibs = workspaces.some(w => w.startsWith('libs'));\n const hasPackages = workspaces.some(w => w.startsWith('packages'));\n const hasApps = workspaces.some(w => w.startsWith('apps'));\n\n // libs cannot import packages or apps\n if (hasLibs && (hasPackages || hasApps)) {\n rules.push(` {\n name: 'libs-cannot-import-packages-or-apps',\n severity: 'error',\n from: { path: '^libs/' },\n to: { path: '^(packages|apps)/' },\n }`);\n }\n\n // packages cannot import apps\n if (hasPackages && hasApps) {\n rules.push(` {\n name: 'packages-cannot-import-apps',\n severity: 'error',\n from: { path: '^packages/' },\n to: { path: '^apps/' },\n }`);\n }\n\n return rules.join(',\\n');\n}\n\n/**\n * Generate .safeword/depcruise-config.cjs content (forbidden rules + options)\n */\nexport function generateDepCruiseConfigFile(arch: DepCruiseArchitecture): string {\n const monorepoRules = arch.workspaces ? generateMonorepoRules(arch.workspaces) : '';\n const hasMonorepoRules = monorepoRules.length > 0;\n\n return String.raw`module.exports = {\n forbidden: [\n // =========================================================================\n // ERROR RULES (block on violations)\n // =========================================================================\n {\n name: 'no-circular',\n // Runtime cycles cause initialization-order bugs and make code hard to reason about.\n // Type-only edges (import type) are erased at compile time and cannot cause runtime\n // cycles — TypeScript designed import type for exactly this case, and depcruise\n // documents viaOnly + dependencyTypesNot: ['type-only'] as the canonical opt-in.\n comment: 'Circular dependencies cause runtime issues and make code hard to reason about',\n severity: 'error',\n from: {},\n to: { circular: true, viaOnly: { dependencyTypesNot: ['type-only'] } },\n },\n {\n name: 'no-deprecated-deps',\n comment: 'Deprecated npm packages should be replaced - they may have security issues or be unmaintained',\n severity: 'error',\n from: {},\n to: { dependencyTypes: ['deprecated'] },\n },${hasMonorepoRules ? `\\n${monorepoRules},` : ''}\n\n // =========================================================================\n // WARNING RULES (flag issues but don't block)\n // =========================================================================\n {\n name: 'no-dev-deps-in-src',\n comment: 'Production code should not import devDependencies - may cause runtime failures',\n severity: 'warn',\n from: {\n path: ['^src', '^packages/[^/]+/src'],\n pathNot: '\\\\.test\\\\.[tj]sx?$',\n },\n to: { dependencyTypes: ['npm-dev'] },\n },\n {\n name: 'no-orphans',\n comment: 'Orphan modules are not imported anywhere - may be dead code',\n severity: 'warn',\n from: {\n orphan: true,\n pathNot: [\n // Entry points\n '(^|/)index\\\\.[tj]sx?$',\n '(^|/)main\\\\.[tj]sx?$',\n '(^|/)cli\\\\.[tj]s$',\n '\\\\.config\\\\.[tj]s$',\n '\\\\.config\\\\.mjs$',\n // Test files\n '\\\\.test\\\\.[tj]sx?$',\n '\\\\.spec\\\\.[tj]sx?$',\n '/tests/',\n '/__tests__/',\n // Astro/Next.js pages and content\n '/src/content/',\n '/src/pages/',\n '/app/',\n ],\n },\n to: {},\n },\n ],\n options: {\n doNotFollow: { path: ['node_modules', '.safeword'] },\n exclude: {\n path: ['node_modules', 'dist', 'build', 'coverage', '\\\\.d\\\\.ts$'],\n },\n tsPreCompilationDeps: true,\n tsConfig: { fileName: 'tsconfig.json' },\n enhancedResolveOptions: {\n extensions: ['.ts', '.tsx', '.js', '.jsx'],\n exportsFields: ['exports'],\n conditionNames: ['import', 'require', 'node', 'default'],\n },\n },\n};\n`;\n}\n\n/**\n * Generate .dependency-cruiser.js (main config that imports generated)\n */\nexport function generateDepCruiseMainConfig(): string {\n return `/**\n * Dependency Cruiser Configuration\n *\n * Imports auto-generated rules from .safeword/depcruise-config.cjs\n * ADD YOUR CUSTOM RULES BELOW the spread operator.\n */\n\nconst generated = require('./.safeword/depcruise-config.cjs');\n\nmodule.exports = {\n forbidden: [\n ...generated.forbidden,\n // ADD YOUR CUSTOM RULES BELOW:\n // { name: 'no-legacy', from: { path: 'legacy/' }, to: { path: 'new/' } },\n ],\n options: {\n ...generated.options,\n // Your overrides here\n },\n};\n`;\n}\n"],"mappings":"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","names":["nodePath","nodePath","nodePath","nodePath"]}
@@ -0,0 +1,312 @@
1
+ import {
2
+ LANGUAGE_PACKS,
3
+ addInstalledPack,
4
+ isGitRepo,
5
+ isPackInstalled
6
+ } from "./chunk-RVVXMIEB.js";
7
+ import {
8
+ SAFEWORD_PEER_DEPENDENCIES
9
+ } from "./chunk-HSC7TELY.js";
10
+ import {
11
+ exists,
12
+ readJson
13
+ } from "./chunk-QARISSCT.js";
14
+ import {
15
+ info,
16
+ listItem
17
+ } from "./chunk-VZ2E2QRM.js";
18
+
19
+ // src/packs/install.ts
20
+ function installPack(packId, cwd) {
21
+ if (isPackInstalled(cwd, packId)) {
22
+ return;
23
+ }
24
+ const pack = LANGUAGE_PACKS[packId];
25
+ if (!pack) {
26
+ throw new Error(`Unknown pack: ${packId}`);
27
+ }
28
+ pack.setup(cwd, { isGitRepo: isGitRepo(cwd) });
29
+ addInstalledPack(cwd, packId);
30
+ }
31
+
32
+ // src/utils/eslint-peer-check.ts
33
+ import nodePath from "path";
34
+ function getSupportedEslintMajors() {
35
+ const range = SAFEWORD_PEER_DEPENDENCIES.eslint;
36
+ if (!range) return [];
37
+ return extractMajors(range);
38
+ }
39
+ function extractMajorFromComparator(comparator) {
40
+ const cleaned = comparator.trim().replace(/^[\^~>=<]+/, "");
41
+ const match = /^(\d+)\./.exec(cleaned) ?? /^(\d+)(?:\.x|\.\*|$)/.exec(cleaned);
42
+ const majorString = match?.[1];
43
+ if (majorString === void 0) return void 0;
44
+ const major = Number.parseInt(majorString, 10);
45
+ return Number.isNaN(major) ? void 0 : major;
46
+ }
47
+ function extractMajors(range) {
48
+ const majors = /* @__PURE__ */ new Set();
49
+ for (const part of range.split("||")) {
50
+ const major = extractMajorFromComparator(part);
51
+ if (major !== void 0) majors.add(major);
52
+ }
53
+ return [...majors].toSorted((a, b) => a - b);
54
+ }
55
+ function getEslintPeerMismatchWarning(cwd) {
56
+ const packageJsonPath = nodePath.join(cwd, "package.json");
57
+ if (!exists(packageJsonPath)) return void 0;
58
+ const pkg = readJson(packageJsonPath);
59
+ if (!pkg) return void 0;
60
+ const declared = pkg.dependencies?.eslint ?? pkg.devDependencies?.eslint;
61
+ if (!declared) return void 0;
62
+ const installedMajor = extractMajorFromComparator(declared);
63
+ if (installedMajor === void 0) return void 0;
64
+ const supportedMajors = getSupportedEslintMajors();
65
+ if (supportedMajors.length === 0) return void 0;
66
+ if (supportedMajors.includes(installedMajor)) return void 0;
67
+ const supportedDisplay = supportedMajors.map((major) => `${major}.x`).join(" or ");
68
+ return [
69
+ `Project declares eslint@${declared} (major ${installedMajor}), but safeword`,
70
+ `supports eslint ${supportedDisplay}. Safeword's bundled plugins are tested`,
71
+ `against the supported range; lint may crash on other majors (e.g. plugin`,
72
+ `transitives that reference removed ESLint APIs).`
73
+ ].join("\n");
74
+ }
75
+
76
+ // src/utils/vendored-ignores-nudge.ts
77
+ import { readFileSync as readFileSync2 } from "fs";
78
+ import nodePath3 from "path";
79
+
80
+ // src/utils/eslint-auto-patch.ts
81
+ import { execSync } from "child_process";
82
+ import { copyFileSync, readFileSync, writeFileSync } from "fs";
83
+ import nodePath2 from "path";
84
+ var VENDORED_MARKER = "vendoredIgnores";
85
+ var ESM_IMPORT = "import safeword from 'safeword/eslint';";
86
+ var CJS_IMPORT = "const safeword = require('safeword/eslint');";
87
+ var SPREAD = "...safeword.configs.vendoredIgnores,";
88
+ var BACKUP_SUFFIX = ".safeword-bak";
89
+ var TS_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".mts", ".cts"]);
90
+ var CJS_EXTENSIONS = /* @__PURE__ */ new Set([".cjs"]);
91
+ function findMatchingBracket(source, openIndex) {
92
+ if (source[openIndex] !== "[") return -1;
93
+ let depth = 1;
94
+ let i = openIndex + 1;
95
+ while (i < source.length) {
96
+ const after = skipNonContent(source, i);
97
+ if (after === -1) return -1;
98
+ if (after > i) {
99
+ i = after;
100
+ continue;
101
+ }
102
+ const ch = source[i];
103
+ if (ch === "[") depth += 1;
104
+ else if (ch === "]") {
105
+ depth -= 1;
106
+ if (depth === 0) return i;
107
+ }
108
+ i += 1;
109
+ }
110
+ return -1;
111
+ }
112
+ function skipNonContent(source, i) {
113
+ const ch = source[i];
114
+ const next = source[i + 1];
115
+ if (ch === "/" && next === "/") {
116
+ const nl = source.indexOf("\n", i);
117
+ return nl === -1 ? -1 : nl + 1;
118
+ }
119
+ if (ch === "/" && next === "*") {
120
+ const close = source.indexOf("*/", i + 2);
121
+ return close === -1 ? -1 : close + 2;
122
+ }
123
+ if (ch === "'" || ch === '"' || ch === "`") {
124
+ return skipStringLiteral(source, i);
125
+ }
126
+ return i;
127
+ }
128
+ function skipStringLiteral(source, startIndex) {
129
+ const quote = source[startIndex];
130
+ let i = startIndex + 1;
131
+ while (i < source.length) {
132
+ const ch = source[i];
133
+ if (ch === "\\") {
134
+ i += 2;
135
+ continue;
136
+ }
137
+ if (ch === quote) return i + 1;
138
+ i += 1;
139
+ }
140
+ return -1;
141
+ }
142
+ function findDefaultExportArrayClose(source) {
143
+ const esmResult = findArrayCloseAfter(source, "export default", false);
144
+ if (esmResult !== -1) return esmResult;
145
+ return findArrayCloseAfter(source, "module.exports", true);
146
+ }
147
+ function skipWhitespace(source, start) {
148
+ let i = start;
149
+ while (i < source.length && /\s/.test(source[i] ?? "")) i += 1;
150
+ return i;
151
+ }
152
+ function lastNonWhitespaceIndex(source, start) {
153
+ let i = start;
154
+ while (i >= 0 && /\s/.test(source[i] ?? "")) i -= 1;
155
+ return i;
156
+ }
157
+ function findArrayCloseAfter(source, marker, requireEquals) {
158
+ const markerIndex = source.indexOf(marker);
159
+ if (markerIndex === -1) return -1;
160
+ let cursor = skipWhitespace(source, markerIndex + marker.length);
161
+ if (requireEquals) {
162
+ if (source[cursor] !== "=") return -1;
163
+ cursor = skipWhitespace(source, cursor + 1);
164
+ }
165
+ if (source[cursor] === "[") return findMatchingBracket(source, cursor);
166
+ if (source.startsWith("defineConfig", cursor)) {
167
+ return findDefineConfigArrayClose(source, cursor);
168
+ }
169
+ return -1;
170
+ }
171
+ function findDefineConfigArrayClose(source, defineStart) {
172
+ const paren = skipWhitespace(source, defineStart + "defineConfig".length);
173
+ if (source[paren] !== "(") return -1;
174
+ const inner = skipWhitespace(source, paren + 1);
175
+ if (source[inner] !== "[") return -1;
176
+ return findMatchingBracket(source, inner);
177
+ }
178
+ function ensureSafewordImport(source, eol, isCjs) {
179
+ const importLine = isCjs ? CJS_IMPORT : ESM_IMPORT;
180
+ if (source.includes(importLine) || source.includes("from 'safeword/eslint'")) return source;
181
+ const importLineRegex = isCjs ? /^(?:const|let|var)\s.+?=\s*require\s*\(.+?\);[ \t]*\r?$/gm : /^import\s.+?;[ \t]*\r?$/gm;
182
+ let lastImportEnd = -1;
183
+ for (const match of source.matchAll(importLineRegex)) {
184
+ lastImportEnd = (match.index ?? 0) + match[0].length;
185
+ }
186
+ if (lastImportEnd === -1) {
187
+ return importLine + eol + source;
188
+ }
189
+ return source.slice(0, lastImportEnd) + eol + importLine + source.slice(lastImportEnd);
190
+ }
191
+ function detectEol(source) {
192
+ return source.includes("\r\n") ? "\r\n" : "\n";
193
+ }
194
+ function isTypeScriptConfig(configPath) {
195
+ return TS_EXTENSIONS.has(nodePath2.extname(configPath));
196
+ }
197
+ function isCjsConfig(configPath) {
198
+ return CJS_EXTENSIONS.has(nodePath2.extname(configPath));
199
+ }
200
+ function autoPatchEslintConfig(options) {
201
+ const { configPath } = options;
202
+ let source;
203
+ try {
204
+ source = readFileSync(configPath, "utf8");
205
+ } catch {
206
+ return { kind: "bailed", reason: "read-failed" };
207
+ }
208
+ if (source.includes(VENDORED_MARKER)) {
209
+ return { kind: "idempotent-skip" };
210
+ }
211
+ const closeIndex = findDefaultExportArrayClose(source);
212
+ if (closeIndex === -1) {
213
+ return { kind: "bailed", reason: "unrecognized-shape" };
214
+ }
215
+ const eol = detectEol(source);
216
+ const sourceWithImport = ensureSafewordImport(source, eol, isCjsConfig(configPath));
217
+ const finalClose = findDefaultExportArrayClose(sourceWithImport);
218
+ if (finalClose === -1) {
219
+ return { kind: "bailed", reason: "unrecognized-shape" };
220
+ }
221
+ const probe = lastNonWhitespaceIndex(sourceWithImport, finalClose - 1);
222
+ const charBefore = sourceWithImport[probe];
223
+ const needsLeadingComma = charBefore !== "[" && charBefore !== ",";
224
+ const before = sourceWithImport.slice(0, finalClose);
225
+ const after = sourceWithImport.slice(finalClose);
226
+ const prefix = `${needsLeadingComma ? "," : ""}${eol} `;
227
+ const patched = `${before}${prefix}${SPREAD}${eol}${after}`;
228
+ const writeResult = writeAndValidate(configPath, patched, !isTypeScriptConfig(configPath));
229
+ if (!writeResult.ok) return { kind: "bailed", reason: writeResult.reason };
230
+ return { kind: "patched", configPath, backupPath: writeResult.backupPath };
231
+ }
232
+ function writeAndValidate(configPath, patched, runSyntaxCheck) {
233
+ const backupPath = `${configPath}${BACKUP_SUFFIX}`;
234
+ try {
235
+ copyFileSync(configPath, backupPath);
236
+ writeFileSync(configPath, patched, "utf8");
237
+ } catch {
238
+ return { ok: false, reason: "write-failed" };
239
+ }
240
+ if (!runSyntaxCheck) return { ok: true, backupPath };
241
+ try {
242
+ execSync(`node --check "${configPath}"`, { stdio: "pipe" });
243
+ return { ok: true, backupPath };
244
+ } catch {
245
+ try {
246
+ copyFileSync(backupPath, configPath);
247
+ } catch {
248
+ }
249
+ return { ok: false, reason: "syntax-check-failed" };
250
+ }
251
+ }
252
+
253
+ // src/utils/vendored-ignores-nudge.ts
254
+ function shouldEmitVendoredIgnoresNudge(options) {
255
+ const { cwd, existingEslintConfig, hasJavaScript } = options;
256
+ if (!hasJavaScript) return false;
257
+ if (!existingEslintConfig) return false;
258
+ const fullPath = nodePath3.join(cwd, existingEslintConfig);
259
+ let text;
260
+ try {
261
+ text = readFileSync2(fullPath, "utf8");
262
+ } catch {
263
+ return true;
264
+ }
265
+ return !text.includes(".safeword/") && !text.includes("vendoredIgnores");
266
+ }
267
+ function printVendoredIgnoresNudge() {
268
+ info(
269
+ "\nSafeword vendors hook scripts under .safeword/. Add this line to your existing ESLint config so your lint doesn't flag them:"
270
+ );
271
+ listItem("import safeword from 'safeword/eslint';");
272
+ listItem("// ... your existing configs");
273
+ listItem("...safeword.configs.vendoredIgnores,");
274
+ }
275
+ function maybeAutoPatchOrNudge(options) {
276
+ if (!shouldEmitVendoredIgnoresNudge(options)) return;
277
+ if (isOptOut(options.noModify ?? false)) {
278
+ printVendoredIgnoresNudge();
279
+ return;
280
+ }
281
+ if (!options.existingEslintConfig) return;
282
+ const configPath = nodePath3.join(options.cwd, options.existingEslintConfig);
283
+ const result = autoPatchEslintConfig({ configPath });
284
+ if (result.kind === "patched") {
285
+ printAutoPatchConfirmation(result.configPath, result.backupPath);
286
+ return;
287
+ }
288
+ if (result.kind === "idempotent-skip") return;
289
+ printAutoPatchBailLine();
290
+ printVendoredIgnoresNudge();
291
+ }
292
+ function isOptOut(noModifyFlag) {
293
+ if (noModifyFlag) return true;
294
+ const envValue = process.env.SAFEWORD_NO_MODIFY;
295
+ return envValue !== void 0 && envValue !== "";
296
+ }
297
+ function printAutoPatchConfirmation(configPath, backupPath) {
298
+ info(`
299
+ Added vendoredIgnores to ${configPath}; backup at ${backupPath}`);
300
+ }
301
+ function printAutoPatchBailLine() {
302
+ info(
303
+ "\nCouldn't auto-patch your eslint config (unrecognized export shape or syntax check failed). Add it manually:"
304
+ );
305
+ }
306
+
307
+ export {
308
+ installPack,
309
+ getEslintPeerMismatchWarning,
310
+ maybeAutoPatchOrNudge
311
+ };
312
+ //# sourceMappingURL=chunk-A4M46ZQN.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/packs/install.ts","../src/utils/eslint-peer-check.ts","../src/utils/vendored-ignores-nudge.ts","../src/utils/eslint-auto-patch.ts"],"sourcesContent":["/**\n * Pack Installation\n *\n * Install language packs and update config.\n */\n\nimport { isGitRepo } from '../utils/git.js';\nimport { addInstalledPack, isPackInstalled } from './config.js';\nimport { LANGUAGE_PACKS } from './registry.js';\n\n/**\n * Install a language pack.\n *\n * Runs the pack's setup function and updates config.json.\n * Idempotent - does nothing if pack is already installed.\n *\n * @param packId - Pack ID to install (e.g., 'python')\n * @param cwd - Project root directory\n * @throws Error if pack ID is unknown\n */\nexport function installPack(packId: string, cwd: string): void {\n // Idempotent - skip if already installed\n if (isPackInstalled(cwd, packId)) {\n return;\n }\n\n const pack = LANGUAGE_PACKS[packId];\n if (!pack) {\n throw new Error(`Unknown pack: ${packId}`);\n }\n\n pack.setup(cwd, { isGitRepo: isGitRepo(cwd) });\n addInstalledPack(cwd, packId);\n}\n","/**\n * ESLint peer-dep mismatch detection for safeword install/upgrade flows.\n *\n * Reads the project's declared `eslint` version (dependencies or devDependencies)\n * and compares its major against safeword's own peerDependencies.eslint range.\n * Returns a human-readable warning when the customer's major is outside the\n * supported set; returns undefined otherwise (including when nothing is\n * declared or the range can't be parsed — only positively-mismatched majors\n * warn).\n *\n * Background: safeword's ESLint preset bundles plugins whose internals can\n * break on ESLint majors safeword hasn't tested against. The vitest plugin's\n * transitive @typescript-eslint/utils@7.x crashed on ESLint 10 LegacyESLint\n * removal; that motivated this guard so the next collision surfaces at\n * install time rather than the first lint run.\n */\n\nimport nodePath from 'node:path';\n\nimport { SAFEWORD_PEER_DEPENDENCIES } from '../version.js';\nimport { exists, readJson } from './fs.js';\n\ninterface ProjectPackageJson {\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n}\n\n/**\n * Pull the supported eslint major versions from safeword's own peerDependencies.\n * `^9.22.0` → [9]; `^9.22.0 || ^10.0.0` → [9, 10].\n *\n * Reads from version.ts rather than calling require() locally — version.ts sits\n * at src/ depth (same as the bundled dist/) so its `require('../package.json')`\n * resolves correctly in both contexts. A direct require() here would resolve\n * `../../package.json` to a nonexistent path post-bundling.\n */\nfunction getSupportedEslintMajors(): number[] {\n const range = SAFEWORD_PEER_DEPENDENCIES.eslint;\n if (!range) return [];\n return extractMajors(range);\n}\n\n/**\n * Extract the leading numeric major from a single version comparator\n * (e.g. `^9.22.0`, `>=9.0.0`, `9.x`, `9.0.0`). Returns undefined for ranges\n * that can't be coerced (workspace:*, file:, git+, *, latest).\n */\nfunction extractMajorFromComparator(comparator: string): number | undefined {\n const cleaned = comparator.trim().replace(/^[\\^~>=<]+/, '');\n const match = /^(\\d+)\\./.exec(cleaned) ?? /^(\\d+)(?:\\.x|\\.\\*|$)/.exec(cleaned);\n const majorString = match?.[1];\n if (majorString === undefined) return undefined;\n const major = Number.parseInt(majorString, 10);\n return Number.isNaN(major) ? undefined : major;\n}\n\n/**\n * Parse a possibly-disjunctive range (`^9.22.0 || ^10.0.0`) into the set of\n * majors it covers. Comparators that don't yield a major are dropped.\n */\nfunction extractMajors(range: string): number[] {\n const majors = new Set<number>();\n for (const part of range.split('||')) {\n const major = extractMajorFromComparator(part);\n if (major !== undefined) majors.add(major);\n }\n return [...majors].toSorted((a, b) => a - b);\n}\n\n/**\n * Returns a warning string when the project's declared eslint major is\n * outside safeword's supported peer range; undefined otherwise.\n */\nexport function getEslintPeerMismatchWarning(cwd: string): string | undefined {\n const packageJsonPath = nodePath.join(cwd, 'package.json');\n if (!exists(packageJsonPath)) return undefined;\n\n const pkg = readJson(packageJsonPath) as ProjectPackageJson | undefined;\n if (!pkg) return undefined;\n\n const declared = pkg.dependencies?.eslint ?? pkg.devDependencies?.eslint;\n if (!declared) return undefined;\n\n const installedMajor = extractMajorFromComparator(declared);\n if (installedMajor === undefined) return undefined;\n\n const supportedMajors = getSupportedEslintMajors();\n if (supportedMajors.length === 0) return undefined;\n if (supportedMajors.includes(installedMajor)) return undefined;\n\n const supportedDisplay = supportedMajors.map(major => `${major}.x`).join(' or ');\n return [\n `Project declares eslint@${declared} (major ${installedMajor}), but safeword`,\n `supports eslint ${supportedDisplay}. Safeword's bundled plugins are tested`,\n `against the supported range; lint may crash on other majors (e.g. plugin`,\n `transitives that reference removed ESLint APIs).`,\n ].join('\\n');\n}\n","import { readFileSync } from 'node:fs';\nimport nodePath from 'node:path';\n\nimport { autoPatchEslintConfig } from './eslint-auto-patch.js';\nimport { info, listItem } from './output.js';\n\nexport interface VendoredIgnoresNudgeOptions {\n cwd: string;\n /** Path (relative to cwd) of the consumer's existing ESLint config, or undefined if none. */\n existingEslintConfig: string | undefined;\n /** True if the project has JavaScript/TypeScript sources safeword can lint. */\n hasJavaScript: boolean;\n}\n\n/**\n * Decide whether to print the install-time nudge telling the user to add\n * `...safeword.configs.vendoredIgnores` to their existing eslint config.\n *\n * Emits iff:\n * 1. an existing eslint config is detected, AND\n * 2. the project has JavaScript, AND\n * 3. the config file's text does NOT already mention `.safeword/` or\n * `vendoredIgnores`\n *\n * The substring check makes the nudge self-quiescing: once the user applies\n * the snippet (manually following the 153 nudge or via 154's auto-patch),\n * repeat `setup`/`upgrade` runs go quiet.\n */\nexport function shouldEmitVendoredIgnoresNudge(options: VendoredIgnoresNudgeOptions): boolean {\n const { cwd, existingEslintConfig, hasJavaScript } = options;\n if (!hasJavaScript) return false;\n if (!existingEslintConfig) return false;\n const fullPath = nodePath.join(cwd, existingEslintConfig);\n let text: string;\n try {\n text = readFileSync(fullPath, 'utf8');\n } catch {\n return true;\n }\n return !text.includes('.safeword/') && !text.includes('vendoredIgnores');\n}\n\n/**\n * Print the nudge using safeword's standard output helpers.\n *\n * Module-internal — callers should use {@link maybePrintVendoredIgnoresNudge}\n * which wraps the decide-then-print sequence.\n */\nfunction printVendoredIgnoresNudge(): void {\n info(\n \"\\nSafeword vendors hook scripts under .safeword/. Add this line to your existing ESLint config so your lint doesn't flag them:\",\n );\n listItem(\"import safeword from 'safeword/eslint';\");\n listItem('// ... your existing configs');\n listItem('...safeword.configs.vendoredIgnores,');\n}\n\nexport interface AutoPatchOrNudgeOptions extends VendoredIgnoresNudgeOptions {\n /** True if the user passed `--no-modify` to setup/upgrade. Defaults to false. */\n noModify?: boolean;\n}\n\n/**\n * Orchestrator for ticket 154: try to auto-patch the consumer's eslint\n * config; on opt-out or bail, fall through to the 153 print-only nudge.\n *\n * Decision tree:\n * - If `shouldEmitVendoredIgnoresNudge` returns false (no existing\n * config / non-JS / already handled) → silent.\n * - Else if `--no-modify` flag OR `SAFEWORD_NO_MODIFY` env var → print\n * the 153 nudge only (no edit attempted).\n * - Else attempt auto-patch:\n * - `patched` → print confirmation with paths\n * - `idempotent-skip` → silent (defensive — the predicate above\n * already caught this case)\n * - `bailed` → print bail line + 153 nudge (caller sees both, the\n * user knows safeword tried and gets the manual snippet)\n */\nexport function maybeAutoPatchOrNudge(options: AutoPatchOrNudgeOptions): void {\n if (!shouldEmitVendoredIgnoresNudge(options)) return;\n if (isOptOut(options.noModify ?? false)) {\n printVendoredIgnoresNudge();\n return;\n }\n if (!options.existingEslintConfig) return;\n const configPath = nodePath.join(options.cwd, options.existingEslintConfig);\n const result = autoPatchEslintConfig({ configPath });\n if (result.kind === 'patched') {\n printAutoPatchConfirmation(result.configPath, result.backupPath);\n return;\n }\n if (result.kind === 'idempotent-skip') return;\n printAutoPatchBailLine();\n printVendoredIgnoresNudge();\n}\n\nfunction isOptOut(noModifyFlag: boolean): boolean {\n if (noModifyFlag) return true;\n const envValue = process.env.SAFEWORD_NO_MODIFY;\n return envValue !== undefined && envValue !== '';\n}\n\nfunction printAutoPatchConfirmation(configPath: string, backupPath: string): void {\n info(`\\nAdded vendoredIgnores to ${configPath}; backup at ${backupPath}`);\n}\n\nfunction printAutoPatchBailLine(): void {\n info(\n \"\\nCouldn't auto-patch your eslint config (unrecognized export shape or syntax check failed). Add it manually:\",\n );\n}\n","/**\n * Auto-patch a downstream project's flat ESLint config to spread\n * `safeword.configs.vendoredIgnores`. Used by `safeword setup` and\n * `safeword upgrade` (ticket 154).\n *\n * Textual insertion, not AST — the heuristic handles the common shapes\n * (bare array literal + `defineConfig(...)` wrapper). Anything else\n * (function-returning-config, single-imported-config, unrecognized\n * wrapper) bails out so the caller can fall back to ticket 153's\n * print-only nudge.\n *\n * Safety:\n * - `.safeword-bak` written before any edit\n * - `node --check` validates JS variants (.mjs/.js/.cjs); failure\n * restores the backup and bails\n * - TS variants (.ts/.mts/.cts) skip syntax check — node can't parse\n * TS, and pulling in a TS-aware checker would balloon dep weight for\n * a one-line insert\n *\n * Idempotency by substring: any config text containing\n * `vendoredIgnores` is treated as already-patched (covers both prior\n * auto-patch and manual application of the 153 print-nudge).\n */\n\nimport { execSync } from 'node:child_process';\nimport { copyFileSync, readFileSync, writeFileSync } from 'node:fs';\nimport nodePath from 'node:path';\n\nconst VENDORED_MARKER = 'vendoredIgnores';\nconst ESM_IMPORT = \"import safeword from 'safeword/eslint';\";\nconst CJS_IMPORT = \"const safeword = require('safeword/eslint');\";\nconst SPREAD = '...safeword.configs.vendoredIgnores,';\nconst BACKUP_SUFFIX = '.safeword-bak';\nconst TS_EXTENSIONS = new Set(['.ts', '.mts', '.cts']);\nconst CJS_EXTENSIONS = new Set(['.cjs']);\n\nexport interface AutoPatchOptions {\n /** Path (absolute or relative to cwd) of the flat ESLint config to patch. */\n configPath: string;\n}\n\nexport type AutoPatchResult =\n | { kind: 'patched'; configPath: string; backupPath: string }\n | { kind: 'idempotent-skip' }\n | { kind: 'bailed'; reason: BailReason };\n\ntype BailReason = 'read-failed' | 'unrecognized-shape' | 'syntax-check-failed' | 'write-failed';\n\n/**\n * Walk a string starting at an opening `[` and return the index of the\n * matching `]`. Returns -1 if no match (unbalanced) or if any string /\n * comment is unterminated. Tracks single/double/backtick strings and\n * `//` + `/* * /` comments so brackets inside literals don't fool the\n * walker.\n */\nfunction findMatchingBracket(source: string, openIndex: number): number {\n if (source[openIndex] !== '[') return -1;\n let depth = 1;\n let i = openIndex + 1;\n while (i < source.length) {\n const after = skipNonContent(source, i);\n if (after === -1) return -1;\n if (after > i) {\n i = after;\n continue;\n }\n const ch = source[i];\n if (ch === '[') depth += 1;\n else if (ch === ']') {\n depth -= 1;\n if (depth === 0) return i;\n }\n i += 1;\n }\n return -1;\n}\n\n/**\n * If `source[i]` starts a comment or string literal, return the index just\n * past its end. Otherwise return `i` unchanged. Returns -1 if a comment or\n * string is unterminated.\n */\nfunction skipNonContent(source: string, i: number): number {\n const ch = source[i];\n const next = source[i + 1];\n if (ch === '/' && next === '/') {\n const nl = source.indexOf('\\n', i);\n return nl === -1 ? -1 : nl + 1;\n }\n if (ch === '/' && next === '*') {\n const close = source.indexOf('*/', i + 2);\n return close === -1 ? -1 : close + 2;\n }\n if (ch === \"'\" || ch === '\"' || ch === '`') {\n return skipStringLiteral(source, i);\n }\n return i;\n}\n\nfunction skipStringLiteral(source: string, startIndex: number): number {\n const quote = source[startIndex];\n let i = startIndex + 1;\n while (i < source.length) {\n const ch = source[i];\n if (ch === '\\\\') {\n i += 2;\n continue;\n }\n if (ch === quote) return i + 1;\n i += 1;\n }\n return -1;\n}\n\n/**\n * Find the position of the closing `]` of the default-export config array.\n *\n * Recognized shapes:\n * - `export default [...]` (ESM, bare array)\n * - `export default defineConfig([...])` (ESM, wrapper)\n * - `module.exports = [...]` (CJS, bare array)\n * - `module.exports = defineConfig([...])` (CJS, wrapper)\n *\n * Returns -1 on any unrecognized shape (function-returning-config,\n * single-imported-config, unknown wrapper).\n */\nfunction findDefaultExportArrayClose(source: string): number {\n const esmResult = findArrayCloseAfter(source, 'export default', false);\n if (esmResult !== -1) return esmResult;\n return findArrayCloseAfter(source, 'module.exports', true);\n}\n\nfunction skipWhitespace(source: string, start: number): number {\n let i = start;\n while (i < source.length && /\\s/.test(source[i] ?? '')) i += 1;\n return i;\n}\n\n/**\n * Walk backward from `start` over whitespace, returning the index of the\n * last non-whitespace char. -1 if the entire prefix is whitespace.\n */\nfunction lastNonWhitespaceIndex(source: string, start: number): number {\n let i = start;\n while (i >= 0 && /\\s/.test(source[i] ?? '')) i -= 1;\n return i;\n}\n\nfunction findArrayCloseAfter(source: string, marker: string, requireEquals: boolean): number {\n const markerIndex = source.indexOf(marker);\n if (markerIndex === -1) return -1;\n let cursor = skipWhitespace(source, markerIndex + marker.length);\n if (requireEquals) {\n if (source[cursor] !== '=') return -1;\n cursor = skipWhitespace(source, cursor + 1);\n }\n if (source[cursor] === '[') return findMatchingBracket(source, cursor);\n if (source.startsWith('defineConfig', cursor)) {\n return findDefineConfigArrayClose(source, cursor);\n }\n return -1;\n}\n\nfunction findDefineConfigArrayClose(source: string, defineStart: number): number {\n const paren = skipWhitespace(source, defineStart + 'defineConfig'.length);\n if (source[paren] !== '(') return -1;\n const inner = skipWhitespace(source, paren + 1);\n if (source[inner] !== '[') return -1;\n return findMatchingBracket(source, inner);\n}\n\n/**\n * Insert the safeword import after the last existing import/require line,\n * or at the very top if there are none. Uses ESM `import` syntax for\n * `.mjs/.js/.ts/.mts/.cts`, CJS `require` syntax for `.cjs`.\n */\nfunction ensureSafewordImport(source: string, eol: string, isCjs: boolean): string {\n const importLine = isCjs ? CJS_IMPORT : ESM_IMPORT;\n if (source.includes(importLine) || source.includes(\"from 'safeword/eslint'\")) return source;\n const importLineRegex = isCjs\n ? /^(?:const|let|var)\\s.+?=\\s*require\\s*\\(.+?\\);[ \\t]*\\r?$/gm\n : /^import\\s.+?;[ \\t]*\\r?$/gm;\n let lastImportEnd = -1;\n for (const match of source.matchAll(importLineRegex)) {\n lastImportEnd = (match.index ?? 0) + match[0].length;\n }\n if (lastImportEnd === -1) {\n return importLine + eol + source;\n }\n return source.slice(0, lastImportEnd) + eol + importLine + source.slice(lastImportEnd);\n}\n\nfunction detectEol(source: string): string {\n return source.includes('\\r\\n') ? '\\r\\n' : '\\n';\n}\n\nfunction isTypeScriptConfig(configPath: string): boolean {\n return TS_EXTENSIONS.has(nodePath.extname(configPath));\n}\n\nfunction isCjsConfig(configPath: string): boolean {\n return CJS_EXTENSIONS.has(nodePath.extname(configPath));\n}\n\n/**\n * Main entry point. See module docstring for behavior contract.\n */\nexport function autoPatchEslintConfig(options: AutoPatchOptions): AutoPatchResult {\n const { configPath } = options;\n\n let source: string;\n try {\n source = readFileSync(configPath, 'utf8');\n } catch {\n return { kind: 'bailed', reason: 'read-failed' };\n }\n\n if (source.includes(VENDORED_MARKER)) {\n return { kind: 'idempotent-skip' };\n }\n\n const closeIndex = findDefaultExportArrayClose(source);\n if (closeIndex === -1) {\n return { kind: 'bailed', reason: 'unrecognized-shape' };\n }\n\n const eol = detectEol(source);\n const sourceWithImport = ensureSafewordImport(source, eol, isCjsConfig(configPath));\n // closeIndex was computed on `source`; if `ensureSafewordImport` prepended\n // content, recompute on the new text. Either way it's idempotent and cheap.\n const finalClose = findDefaultExportArrayClose(sourceWithImport);\n if (finalClose === -1) {\n // Shouldn't happen — we just verified the shape — but bail defensively.\n return { kind: 'bailed', reason: 'unrecognized-shape' };\n }\n\n // Decide whether we need a leading comma. Walk backward from the\n // closing `]`, skipping whitespace; if the last non-whitespace char is\n // `[` (empty array) or `,` (trailing-comma style) no comma is needed.\n // Otherwise the last element lacks a trailing comma and we must add\n // one before our spread.\n const probe = lastNonWhitespaceIndex(sourceWithImport, finalClose - 1);\n const charBefore = sourceWithImport[probe];\n const needsLeadingComma = charBefore !== '[' && charBefore !== ',';\n\n const before = sourceWithImport.slice(0, finalClose);\n const after = sourceWithImport.slice(finalClose);\n const prefix = `${needsLeadingComma ? ',' : ''}${eol} `;\n const patched = `${before}${prefix}${SPREAD}${eol}${after}`;\n\n const writeResult = writeAndValidate(configPath, patched, !isTypeScriptConfig(configPath));\n if (!writeResult.ok) return { kind: 'bailed', reason: writeResult.reason };\n return { kind: 'patched', configPath, backupPath: writeResult.backupPath };\n}\n\nfunction writeAndValidate(\n configPath: string,\n patched: string,\n runSyntaxCheck: boolean,\n):\n | { ok: true; backupPath: string }\n | { ok: false; reason: 'write-failed' | 'syntax-check-failed' } {\n const backupPath = `${configPath}${BACKUP_SUFFIX}`;\n try {\n copyFileSync(configPath, backupPath);\n writeFileSync(configPath, patched, 'utf8');\n } catch {\n return { ok: false, reason: 'write-failed' };\n }\n if (!runSyntaxCheck) return { ok: true, backupPath };\n try {\n execSync(`node --check \"${configPath}\"`, { stdio: 'pipe' });\n return { ok: true, backupPath };\n } catch {\n // Revert so the user's config is byte-identical to its pre-command state.\n try {\n copyFileSync(backupPath, configPath);\n } catch {\n // Best-effort revert; if this fails the backup is still on disk.\n }\n return { ok: false, reason: 'syntax-check-failed' };\n }\n}\n"],"mappings":"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","names":["readFileSync","nodePath","nodePath","nodePath","readFileSync"]}
@@ -61,7 +61,7 @@ function migratePackId(cwd, oldId, newId) {
61
61
  writeConfig(cwd, config);
62
62
  }
63
63
  function addInstalledPack(cwd, packId) {
64
- const config = readConfig(cwd) ?? { version: VERSION, installedPacks: [] };
64
+ const config = readConfig(cwd) ?? { installedPacks: [] };
65
65
  if (!config.installedPacks.includes(packId)) {
66
66
  config.installedPacks.push(packId);
67
67
  writeConfig(cwd, config);
@@ -565,6 +565,7 @@ function getClaudeParentDirectoryForCleanup(filePath) {
565
565
  return parentDirectory;
566
566
  }
567
567
  async function reconcile(schema, mode, ctx, options) {
568
+ await Promise.resolve();
568
569
  const dryRun = options?.dryRun ?? false;
569
570
  const plan = computePlan(schema, mode, ctx);
570
571
  if (dryRun) {
@@ -1941,6 +1942,7 @@ var SETTINGS_HOOKS = {
1941
1942
  hook(`bun ${HOOKS_DIR}/session-verify-agents.ts`),
1942
1943
  hook(`bun ${HOOKS_DIR}/session-version.ts`),
1943
1944
  hook(`bun ${HOOKS_DIR}/session-lint-check.ts`),
1945
+ hook(`bun ${HOOKS_DIR}/session-start-reentry.ts`),
1944
1946
  matchedHook("compact", `bun ${HOOKS_DIR}/session-compact-context.ts`),
1945
1947
  asyncHook(`bun ${HOOKS_DIR}/session-update-check.ts`)
1946
1948
  ],
@@ -1948,7 +1950,7 @@ var SETTINGS_HOOKS = {
1948
1950
  hook(`bun ${HOOKS_DIR}/prompt-timestamp.ts`),
1949
1951
  hook(`bun ${HOOKS_DIR}/prompt-questions.ts`)
1950
1952
  ],
1951
- Stop: [hook(`bun ${HOOKS_DIR}/stop-quality.ts`)],
1953
+ Stop: [hook(`bun ${HOOKS_DIR}/stop-quality.ts`), hook(`bun ${HOOKS_DIR}/stop-reentry.ts`)],
1952
1954
  PreToolUse: [
1953
1955
  matchedHook(EDIT_TOOLS, `bun ${HOOKS_DIR}/pre-tool-quality.ts`),
1954
1956
  matchedHook(EDIT_TOOLS, `bun ${HOOKS_DIR}/pre-tool-config-guard.ts`),
@@ -2561,10 +2563,15 @@ var SAFEWORD_SCHEMA = {
2561
2563
  ".safeword/hooks/lib/skill-invocation-log.ts": {
2562
2564
  template: "hooks/lib/skill-invocation-log.ts"
2563
2565
  },
2566
+ ".safeword/hooks/lib/parse-annotation.ts": { template: "hooks/lib/parse-annotation.ts" },
2567
+ ".safeword/hooks/lib/ledger-validation.ts": { template: "hooks/lib/ledger-validation.ts" },
2564
2568
  ".safeword/hooks/lib/scenario-format.ts": { template: "hooks/lib/scenario-format.ts" },
2565
2569
  ".safeword/hooks/lib/test-runner.ts": { template: "hooks/lib/test-runner.ts" },
2566
2570
  ".safeword/hooks/lib/update-cache.ts": { template: "hooks/lib/update-cache.ts" },
2567
2571
  ".safeword/hooks/lib/version.ts": { template: "hooks/lib/version.ts" },
2572
+ ".safeword/hooks/lib/learning-verification-stamps.ts": {
2573
+ template: "hooks/lib/learning-verification-stamps.ts"
2574
+ },
2568
2575
  // Generated at setup/upgrade from SAFEWORD_SCHEMA itself — the prefix list
2569
2576
  // the auto-upgrade hook uses to decide which files to stage. See owned-paths.ts.
2570
2577
  ".safeword/hooks/lib/owned-paths.ts": {
@@ -2617,6 +2624,10 @@ var SAFEWORD_SCHEMA = {
2617
2624
  template: "hooks/session-cleanup-quality.ts"
2618
2625
  },
2619
2626
  ".safeword/hooks/stop-quality.ts": { template: "hooks/stop-quality.ts" },
2627
+ ".safeword/hooks/stop-reentry.ts": { template: "hooks/stop-reentry.ts" },
2628
+ ".safeword/hooks/session-start-reentry.ts": {
2629
+ template: "hooks/session-start-reentry.ts"
2630
+ },
2620
2631
  ".safeword/hooks/post-tool-sync-learnings.ts": {
2621
2632
  template: "hooks/post-tool-sync-learnings.ts"
2622
2633
  },
@@ -2742,6 +2753,9 @@ var SAFEWORD_SCHEMA = {
2742
2753
  ".claude/skills/tdd-review/SKILL.md": {
2743
2754
  template: "skills/tdd-review/SKILL.md"
2744
2755
  },
2756
+ ".claude/skills/figure-it-out/SKILL.md": {
2757
+ template: "skills/figure-it-out/SKILL.md"
2758
+ },
2745
2759
  // Cursor rules
2746
2760
  ".cursor/rules/safeword-core.mdc": {
2747
2761
  template: "cursor/rules/safeword-core.mdc"
@@ -2755,6 +2769,9 @@ var SAFEWORD_SCHEMA = {
2755
2769
  ".cursor/rules/safeword-elicitation.mdc": {
2756
2770
  template: "cursor/rules/safeword-elicitation.mdc"
2757
2771
  },
2772
+ ".cursor/rules/safeword-figure-it-out.mdc": {
2773
+ template: "cursor/rules/safeword-figure-it-out.mdc"
2774
+ },
2758
2775
  ".cursor/rules/safeword-quality-reviewing.mdc": {
2759
2776
  template: "cursor/rules/safeword-quality-reviewing.mdc"
2760
2777
  },
@@ -2930,6 +2947,34 @@ var SAFEWORD_SCHEMA = {
2930
2947
  // Removing any of them would silently regress the prompt back to legacy
2931
2948
  // free-form review.
2932
2949
  requires: ["QUALITY_REVIEW_MESSAGE", "CONFIDENT", "BLOCKED", "Tried:", "Need:"]
2950
+ },
2951
+ "packages/cli/templates/doc-templates/test-definitions-feature.md": {
2952
+ // Canonical test-definitions.md format. Rule grouping (Gherkin 6+
2953
+ // Rule: keyword, Example Mapping alignment) + nested Scenario with
2954
+ // Given/When/Then + per-scenario RED/GREEN/REFACTOR sub-checkboxes.
2955
+ // The R/G/R checkboxes are load-bearing: parseTddStep in
2956
+ // hooks/lib/active-ticket.ts depends on them to inject TDD-step
2957
+ // guidance during Phase 6 implement. Removing any marker silently
2958
+ // regresses the format the BDD skill teaches.
2959
+ requires: [
2960
+ "## Rule:",
2961
+ "### Scenario:",
2962
+ "Given",
2963
+ "When",
2964
+ "Then",
2965
+ "- [ ] RED",
2966
+ "- [ ] GREEN",
2967
+ "- [ ] REFACTOR"
2968
+ ]
2969
+ },
2970
+ "packages/cli/templates/hooks/lib/scenario-format.ts": {
2971
+ // Runtime gate that powers done-phase scenario-completeness checks
2972
+ // (stop-quality.ts) and progress reporting. analyzeScenarioFormat is
2973
+ // imported by the stop hook; isUnrecognized distinguishes "no
2974
+ // scenarios yet" from "scenarios in legacy/malformed format" so the
2975
+ // done gate can hard-block the latter. Removing either silently
2976
+ // regresses scenario-completeness enforcement.
2977
+ requires: ["analyzeScenarioFormat", "isUnrecognized", "export function"]
2933
2978
  }
2934
2979
  },
2935
2980
  // NPM packages to install (JS/TS specific packages from typescript pack)
@@ -3133,4 +3178,4 @@ export {
3133
3178
  detectLanguages2,
3134
3179
  createProjectContext
3135
3180
  };
3136
- //# sourceMappingURL=chunk-UOUUMDSB.js.map
3181
+ //# sourceMappingURL=chunk-RVVXMIEB.js.map