safeword 0.15.9 → 0.15.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{check-LOVNOEUC.js → check-KVWOWINW.js} +4 -4
- package/dist/{chunk-JLOQFDEL.js → chunk-4QVNEEYP.js} +2 -2
- package/dist/{chunk-JLOQFDEL.js.map → chunk-4QVNEEYP.js.map} +1 -1
- package/dist/{chunk-EGI2VVJ4.js → chunk-CBJLUXZ4.js} +135 -70
- package/dist/chunk-CBJLUXZ4.js.map +1 -0
- package/dist/{chunk-NUKQPPCM.js → chunk-R6ZGEH4J.js} +11 -5
- package/dist/chunk-R6ZGEH4J.js.map +1 -0
- package/dist/{chunk-ZBTABXIC.js → chunk-UIURPBCD.js} +2 -2
- package/dist/{chunk-OYBKTOVF.js → chunk-VWOO5PJN.js} +49 -43
- package/dist/chunk-VWOO5PJN.js.map +1 -0
- package/dist/cli.js +7 -7
- package/dist/cli.js.map +1 -1
- package/dist/{diff-P66MAGJQ.js → diff-WEDHTHFK.js} +3 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/presets/typescript/index.d.ts +29 -7
- package/dist/presets/typescript/index.js +4 -2
- package/dist/{reset-FD5WGVGB.js → reset-GG5AST3L.js} +3 -3
- package/dist/{setup-I63R6JD2.js → setup-CM7WY4GS.js} +5 -5
- package/dist/{sync-config-SJ2HBZM6.js → sync-config-GPYJLOW5.js} +2 -2
- package/dist/{upgrade-HG7TZBLZ.js → upgrade-JKAXT46Q.js} +4 -4
- package/package.json +6 -1
- package/templates/SAFEWORD.md +4 -30
- package/templates/cursor/rules/bdd-core.mdc +52 -0
- package/templates/cursor/rules/bdd-decomposition.mdc +43 -0
- package/templates/cursor/rules/bdd-discovery.mdc +46 -0
- package/templates/cursor/rules/bdd-done.mdc +44 -0
- package/templates/cursor/rules/bdd-scenarios.mdc +50 -0
- package/templates/cursor/rules/bdd-splitting.mdc +51 -0
- package/templates/cursor/rules/bdd-tdd.mdc +55 -0
- package/templates/cursor/rules/safeword-debugging.mdc +7 -0
- package/templates/cursor/rules/safeword-quality-reviewing.mdc +0 -1
- package/templates/guides/architecture-guide.md +0 -38
- package/templates/guides/context-files-guide.md +1 -14
- package/templates/guides/design-doc-guide.md +0 -12
- package/templates/guides/planning-guide.md +1 -18
- package/templates/guides/testing-guide.md +13 -44
- package/templates/hooks/stop-quality.ts +97 -34
- package/templates/skills/safeword-bdd-orchestrating/DECOMPOSITION.md +44 -0
- package/templates/skills/safeword-bdd-orchestrating/DISCOVERY.md +59 -0
- package/templates/skills/safeword-bdd-orchestrating/DONE.md +44 -0
- package/templates/skills/safeword-bdd-orchestrating/SCENARIOS.md +59 -0
- package/templates/skills/safeword-bdd-orchestrating/SKILL.md +26 -548
- package/templates/skills/safeword-bdd-orchestrating/SPLITTING.md +56 -0
- package/templates/skills/safeword-bdd-orchestrating/TDD.md +69 -0
- package/templates/skills/safeword-debugging/SKILL.md +19 -0
- package/templates/skills/safeword-quality-reviewing/SKILL.md +34 -116
- package/dist/chunk-EGI2VVJ4.js.map +0 -1
- package/dist/chunk-NUKQPPCM.js.map +0 -1
- package/dist/chunk-OYBKTOVF.js.map +0 -1
- package/templates/cursor/rules/safeword-bdd-orchestrating.mdc +0 -628
- package/templates/guides/code-philosophy.md +0 -207
- package/templates/prompts/quality-review.md +0 -10
- /package/dist/{check-LOVNOEUC.js.map → check-KVWOWINW.js.map} +0 -0
- /package/dist/{chunk-ZBTABXIC.js.map → chunk-UIURPBCD.js.map} +0 -0
- /package/dist/{diff-P66MAGJQ.js.map → diff-WEDHTHFK.js.map} +0 -0
- /package/dist/{reset-FD5WGVGB.js.map → reset-GG5AST3L.js.map} +0 -0
- /package/dist/{setup-I63R6JD2.js.map → setup-CM7WY4GS.js.map} +0 -0
- /package/dist/{sync-config-SJ2HBZM6.js.map → sync-config-GPYJLOW5.js.map} +0 -0
- /package/dist/{upgrade-HG7TZBLZ.js.map → upgrade-JKAXT46Q.js.map} +0 -0
|
@@ -3,13 +3,13 @@ import {
|
|
|
3
3
|
} from "./chunk-FJYRWU2V.js";
|
|
4
4
|
import {
|
|
5
5
|
getMissingPacks
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-UIURPBCD.js";
|
|
7
7
|
import {
|
|
8
8
|
SAFEWORD_SCHEMA,
|
|
9
9
|
createProjectContext,
|
|
10
10
|
reconcile
|
|
11
|
-
} from "./chunk-
|
|
12
|
-
import "./chunk-
|
|
11
|
+
} from "./chunk-CBJLUXZ4.js";
|
|
12
|
+
import "./chunk-VWOO5PJN.js";
|
|
13
13
|
import {
|
|
14
14
|
VERSION
|
|
15
15
|
} from "./chunk-ORQHKDT2.js";
|
|
@@ -189,4 +189,4 @@ async function check(options) {
|
|
|
189
189
|
export {
|
|
190
190
|
check
|
|
191
191
|
};
|
|
192
|
-
//# sourceMappingURL=check-
|
|
192
|
+
//# sourceMappingURL=check-KVWOWINW.js.map
|
|
@@ -209,7 +209,7 @@ function generateDepCruiseMainConfig() {
|
|
|
209
209
|
return `/**
|
|
210
210
|
* Dependency Cruiser Configuration
|
|
211
211
|
*
|
|
212
|
-
* Imports auto-generated rules from .safeword/depcruise-config.
|
|
212
|
+
* Imports auto-generated rules from .safeword/depcruise-config.cjs
|
|
213
213
|
* ADD YOUR CUSTOM RULES BELOW the spread operator.
|
|
214
214
|
*/
|
|
215
215
|
|
|
@@ -280,4 +280,4 @@ export {
|
|
|
280
280
|
hasArchitectureDetected,
|
|
281
281
|
syncConfig
|
|
282
282
|
};
|
|
283
|
-
//# sourceMappingURL=chunk-
|
|
283
|
+
//# sourceMappingURL=chunk-4QVNEEYP.js.map
|
|
@@ -1 +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 */\nexport async function syncConfig(): Promise<void> {\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 and config generation\n *\n * Auto-detects common architecture directories and generates\n * eslint-plugin-boundaries config with sensible hierarchy rules.\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\n/**\n * Hierarchy rules: what each layer can import\n * Lower layers have fewer import permissions\n */\nconst HIERARCHY: Record<Layer, Layer[]> = {\n types: [],\n utils: ['types'],\n lib: ['utils', 'types'],\n hooks: ['lib', 'utils', 'types'],\n services: ['lib', 'utils', 'types'],\n components: ['hooks', 'services', 'lib', 'utils', 'types'],\n features: ['components', 'hooks', 'services', 'lib', 'utils', 'types'],\n app: ['features', 'components', 'hooks', 'services', 'lib', 'utils', 'types'],\n};\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/**\n * Format a single element for the config\n * @param el\n */\nfunction formatElement(element: DetectedElement): string {\n return ` { type: '${element.layer}', pattern: '${element.pattern}', mode: 'full' }`;\n}\n\n/**\n * Format allowed imports for a rule\n * @param allowed\n */\nfunction formatAllowedImports(allowed: Layer[]): string {\n return allowed.map(d => `'${d}'`).join(', ');\n}\n\n/**\n * Generate a single rule for what a layer can import\n * @param layer\n * @param detectedLayers\n */\nfunction generateRule(layer: Layer, detectedLayers: Set<Layer>): string | undefined {\n const allowedLayers = HIERARCHY[layer];\n if (allowedLayers.length === 0) return undefined;\n\n const allowed = allowedLayers.filter(dep => detectedLayers.has(dep));\n if (allowed.length === 0) return undefined;\n\n return ` { from: ['${layer}'], allow: [${formatAllowedImports(allowed)}] }`;\n}\n\n/**\n * Build description of what was detected\n * @param arch\n */\nfunction buildDetectedInfo(arch: DetectedArchitecture): string {\n if (arch.elements.length === 0) {\n return 'No architecture directories detected yet - add types/, utils/, components/, etc.';\n }\n const locations = arch.elements.map(element => element.location).join(', ');\n const monorepoNote = arch.isMonorepo ? ' (monorepo)' : '';\n return `Detected: ${locations}${monorepoNote}`;\n}\n\n/**\n *\n * @param arch\n */\nexport function generateBoundariesConfig(arch: DetectedArchitecture): string {\n const hasElements = arch.elements.length > 0;\n\n // Generate element definitions\n const elementsContent = arch.elements.map(element => formatElement(element)).join(',\\n');\n\n // Generate rules (what each layer can import)\n const detectedLayers = new Set(arch.elements.map(element => element.layer));\n const rules = [...detectedLayers]\n .map(layer => generateRule(layer, detectedLayers))\n .filter((rule): rule is string => rule !== undefined);\n const rulesContent = rules.join(',\\n');\n\n const detectedInfo = buildDetectedInfo(arch);\n\n return `/**\n * Architecture Boundaries Configuration (AUTO-GENERATED)\n *\n * ${detectedInfo}\n *\n * This enforces import boundaries between architectural layers:\n * - Lower layers (types, utils) cannot import from higher layers (components, features)\n * - Uses 'error' severity - LLMs ignore warnings, errors force compliance\n *\n * Recognized directories (in hierarchy order):\n * types → utils → lib → hooks/services → components → features/modules → app\n *\n * To customize, override in your eslint.config.mjs:\n * rules: { 'boundaries/element-types': ['error', { ... }] }\n */\n\nimport boundaries from 'eslint-plugin-boundaries';\n\nexport default {\n plugins: { boundaries },\n settings: {\n 'boundaries/elements': [\n${elementsContent}\n ],\n },\n rules: {${\n hasElements\n ? `\n 'boundaries/element-types': ['error', {\n default: 'disallow',\n rules: [\n${rulesContent}\n ],\n }],`\n : ''\n }\n 'boundaries/no-unknown': 'off', // Allow files outside defined elements\n 'boundaries/no-unknown-files': 'off', // Allow non-matching files\n },\n};\n`;\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.js 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 comment: 'Circular dependencies cause runtime issues and make code hard to reason about',\n severity: 'error',\n from: {},\n to: { circular: true },\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.js\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"]}
|
|
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 */\nexport async function syncConfig(): Promise<void> {\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 and config generation\n *\n * Auto-detects common architecture directories and generates\n * eslint-plugin-boundaries config with sensible hierarchy rules.\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\n/**\n * Hierarchy rules: what each layer can import\n * Lower layers have fewer import permissions\n */\nconst HIERARCHY: Record<Layer, Layer[]> = {\n types: [],\n utils: ['types'],\n lib: ['utils', 'types'],\n hooks: ['lib', 'utils', 'types'],\n services: ['lib', 'utils', 'types'],\n components: ['hooks', 'services', 'lib', 'utils', 'types'],\n features: ['components', 'hooks', 'services', 'lib', 'utils', 'types'],\n app: ['features', 'components', 'hooks', 'services', 'lib', 'utils', 'types'],\n};\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/**\n * Format a single element for the config\n * @param el\n */\nfunction formatElement(element: DetectedElement): string {\n return ` { type: '${element.layer}', pattern: '${element.pattern}', mode: 'full' }`;\n}\n\n/**\n * Format allowed imports for a rule\n * @param allowed\n */\nfunction formatAllowedImports(allowed: Layer[]): string {\n return allowed.map(d => `'${d}'`).join(', ');\n}\n\n/**\n * Generate a single rule for what a layer can import\n * @param layer\n * @param detectedLayers\n */\nfunction generateRule(layer: Layer, detectedLayers: Set<Layer>): string | undefined {\n const allowedLayers = HIERARCHY[layer];\n if (allowedLayers.length === 0) return undefined;\n\n const allowed = allowedLayers.filter(dep => detectedLayers.has(dep));\n if (allowed.length === 0) return undefined;\n\n return ` { from: ['${layer}'], allow: [${formatAllowedImports(allowed)}] }`;\n}\n\n/**\n * Build description of what was detected\n * @param arch\n */\nfunction buildDetectedInfo(arch: DetectedArchitecture): string {\n if (arch.elements.length === 0) {\n return 'No architecture directories detected yet - add types/, utils/, components/, etc.';\n }\n const locations = arch.elements.map(element => element.location).join(', ');\n const monorepoNote = arch.isMonorepo ? ' (monorepo)' : '';\n return `Detected: ${locations}${monorepoNote}`;\n}\n\n/**\n *\n * @param arch\n */\nexport function generateBoundariesConfig(arch: DetectedArchitecture): string {\n const hasElements = arch.elements.length > 0;\n\n // Generate element definitions\n const elementsContent = arch.elements.map(element => formatElement(element)).join(',\\n');\n\n // Generate rules (what each layer can import)\n const detectedLayers = new Set(arch.elements.map(element => element.layer));\n const rules = [...detectedLayers]\n .map(layer => generateRule(layer, detectedLayers))\n .filter((rule): rule is string => rule !== undefined);\n const rulesContent = rules.join(',\\n');\n\n const detectedInfo = buildDetectedInfo(arch);\n\n return `/**\n * Architecture Boundaries Configuration (AUTO-GENERATED)\n *\n * ${detectedInfo}\n *\n * This enforces import boundaries between architectural layers:\n * - Lower layers (types, utils) cannot import from higher layers (components, features)\n * - Uses 'error' severity - LLMs ignore warnings, errors force compliance\n *\n * Recognized directories (in hierarchy order):\n * types → utils → lib → hooks/services → components → features/modules → app\n *\n * To customize, override in your eslint.config.mjs:\n * rules: { 'boundaries/element-types': ['error', { ... }] }\n */\n\nimport boundaries from 'eslint-plugin-boundaries';\n\nexport default {\n plugins: { boundaries },\n settings: {\n 'boundaries/elements': [\n${elementsContent}\n ],\n },\n rules: {${\n hasElements\n ? `\n 'boundaries/element-types': ['error', {\n default: 'disallow',\n rules: [\n${rulesContent}\n ],\n }],`\n : ''\n }\n 'boundaries/no-unknown': 'off', // Allow files outside defined elements\n 'boundaries/no-unknown-files': 'off', // Allow non-matching files\n },\n};\n`;\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 comment: 'Circular dependencies cause runtime issues and make code hard to reason about',\n severity: 'error',\n from: {},\n to: { circular: true },\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"]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
detect
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-VWOO5PJN.js";
|
|
4
4
|
import {
|
|
5
5
|
VERSION
|
|
6
6
|
} from "./chunk-ORQHKDT2.js";
|
|
@@ -482,7 +482,7 @@ function executeAction(action, ctx, result) {
|
|
|
482
482
|
break;
|
|
483
483
|
}
|
|
484
484
|
case "json-unmerge": {
|
|
485
|
-
executeJsonUnmerge(ctx.cwd, action.path, action.definition);
|
|
485
|
+
executeJsonUnmerge(ctx.cwd, action.path, action.definition, ctx);
|
|
486
486
|
break;
|
|
487
487
|
}
|
|
488
488
|
case "text-patch": {
|
|
@@ -542,12 +542,12 @@ function executeJsonMerge(cwd, path2, definition, ctx) {
|
|
|
542
542
|
if (JSON.stringify(existing) === JSON.stringify(merged)) return;
|
|
543
543
|
writeJson(fullPath, merged);
|
|
544
544
|
}
|
|
545
|
-
function executeJsonUnmerge(cwd, path2, definition) {
|
|
545
|
+
function executeJsonUnmerge(cwd, path2, definition, ctx) {
|
|
546
546
|
const fullPath = nodePath2.join(cwd, path2);
|
|
547
547
|
if (!exists(fullPath)) return;
|
|
548
548
|
const existing = readJson(fullPath);
|
|
549
549
|
if (!existing) return;
|
|
550
|
-
const unmerged = definition.unmerge(existing);
|
|
550
|
+
const unmerged = definition.unmerge(existing, ctx);
|
|
551
551
|
if (definition.removeFileIfEmpty) {
|
|
552
552
|
const remainingKeys = Object.keys(unmerged).filter((k) => unmerged[k] !== void 0);
|
|
553
553
|
if (remainingKeys.length === 0) {
|
|
@@ -787,14 +787,14 @@ ignore = [
|
|
|
787
787
|
|
|
788
788
|
[lint.mccabe]
|
|
789
789
|
max-complexity = 10`;
|
|
790
|
-
function generateRuffBaseConfig(existingRuffConfig
|
|
790
|
+
function generateRuffBaseConfig(existingRuffConfig) {
|
|
791
791
|
if (existingRuffConfig) {
|
|
792
792
|
return `# Safeword Ruff config - extends project config with stricter rules
|
|
793
793
|
# Used by hooks for LLM enforcement. Human pre-commits use project config.
|
|
794
794
|
# Re-run \`safeword upgrade\` to regenerate after project config changes.
|
|
795
795
|
|
|
796
|
-
# Inherit from project's
|
|
797
|
-
extend = "
|
|
796
|
+
# Inherit from project's ${existingRuffConfig}
|
|
797
|
+
extend = "../${existingRuffConfig}"
|
|
798
798
|
|
|
799
799
|
# Safeword overrides (stricter than project defaults)
|
|
800
800
|
line-length = 100
|
|
@@ -889,6 +889,30 @@ function getPrettierConfig(hasExistingFormatter2) {
|
|
|
889
889
|
configEntry: " eslintConfigPrettier,"
|
|
890
890
|
};
|
|
891
891
|
}
|
|
892
|
+
function getMonorepoSnippet(directoryVariable) {
|
|
893
|
+
return `// Monorepo support: detect Next.js apps to scope Next.js-only rules
|
|
894
|
+
// - Returns undefined for single-app Next.js projects (use full Next config)
|
|
895
|
+
// - Returns string[] of glob patterns for monorepos (scope Next.js rules)
|
|
896
|
+
const nextPaths = detect.findNextConfigPaths(${directoryVariable});
|
|
897
|
+
|
|
898
|
+
// Map framework to base config
|
|
899
|
+
// Note: Astro config only lints .astro files, so we combine it with TypeScript config
|
|
900
|
+
// to also lint .ts files in Astro projects
|
|
901
|
+
// Note: In monorepos, Next.js uses React config + scoped Next.js rules
|
|
902
|
+
const baseConfigs = {
|
|
903
|
+
next: nextPaths ? configs.recommendedTypeScriptReact : configs.recommendedTypeScriptNext,
|
|
904
|
+
react: configs.recommendedTypeScriptReact,
|
|
905
|
+
astro: [...configs.recommendedTypeScript, ...configs.astro],
|
|
906
|
+
typescript: configs.recommendedTypeScript,
|
|
907
|
+
javascript: configs.recommended,
|
|
908
|
+
};
|
|
909
|
+
|
|
910
|
+
// Build scoped Next.js rules for monorepos
|
|
911
|
+
// Each Next.js app gets its own scoped config with files: pattern
|
|
912
|
+
const scopedNextConfigs = nextPaths?.flatMap((filePath) =>
|
|
913
|
+
configs.nextOnlyRules.map((config) => ({ ...config, files: [filePath] }))
|
|
914
|
+
) ?? [];`;
|
|
915
|
+
}
|
|
892
916
|
var SAFEWORD_STRICT_RULES_FULL = `// Safeword strict rules - applied after project rules (win on conflict)
|
|
893
917
|
const safewordStrictRules = {
|
|
894
918
|
rules: {
|
|
@@ -936,24 +960,19 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
936
960
|
const deps = detect.collectAllDeps(__dirname);
|
|
937
961
|
const framework = detect.detectFramework(deps);
|
|
938
962
|
|
|
939
|
-
|
|
940
|
-
// Note: Astro config only lints .astro files, so we combine it with TypeScript config
|
|
941
|
-
// to also lint .ts files in Astro projects
|
|
942
|
-
const baseConfigs = {
|
|
943
|
-
next: configs.recommendedTypeScriptNext,
|
|
944
|
-
react: configs.recommendedTypeScriptReact,
|
|
945
|
-
astro: [...configs.recommendedTypeScript, ...configs.astro],
|
|
946
|
-
typescript: configs.recommendedTypeScript,
|
|
947
|
-
javascript: configs.recommended,
|
|
948
|
-
};
|
|
963
|
+
${getMonorepoSnippet("__dirname")}
|
|
949
964
|
|
|
950
965
|
export default [
|
|
951
966
|
{ ignores: detect.getIgnores(deps) },
|
|
952
967
|
...baseConfigs[framework],
|
|
953
|
-
...
|
|
954
|
-
|
|
968
|
+
...scopedNextConfigs,
|
|
969
|
+
// Testing configs - always included (file-scoped to *.test.* and *.e2e.*)
|
|
970
|
+
...configs.vitest,
|
|
971
|
+
...configs.playwright,
|
|
972
|
+
// TanStack Query - always included (rules only match useQuery/useMutation patterns)
|
|
973
|
+
...configs.tanstackQuery,
|
|
974
|
+
// Tailwind - only if detected (plugin needs tailwind config to validate classes)
|
|
955
975
|
...(detect.hasTailwind(deps) ? configs.tailwind : []),
|
|
956
|
-
...(detect.hasTanstackQuery(deps) ? configs.tanstackQuery : []),
|
|
957
976
|
eslintConfigPrettier,
|
|
958
977
|
];
|
|
959
978
|
`;
|
|
@@ -968,24 +987,19 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
968
987
|
const deps = detect.collectAllDeps(__dirname);
|
|
969
988
|
const framework = detect.detectFramework(deps);
|
|
970
989
|
|
|
971
|
-
|
|
972
|
-
// Note: Astro config only lints .astro files, so we combine it with TypeScript config
|
|
973
|
-
// to also lint .ts files in Astro projects
|
|
974
|
-
const baseConfigs = {
|
|
975
|
-
next: configs.recommendedTypeScriptNext,
|
|
976
|
-
react: configs.recommendedTypeScriptReact,
|
|
977
|
-
astro: [...configs.recommendedTypeScript, ...configs.astro],
|
|
978
|
-
typescript: configs.recommendedTypeScript,
|
|
979
|
-
javascript: configs.recommended,
|
|
980
|
-
};
|
|
990
|
+
${getMonorepoSnippet("__dirname")}
|
|
981
991
|
|
|
982
992
|
export default [
|
|
983
993
|
{ ignores: detect.getIgnores(deps) },
|
|
984
994
|
...baseConfigs[framework],
|
|
985
|
-
...
|
|
986
|
-
|
|
995
|
+
...scopedNextConfigs,
|
|
996
|
+
// Testing configs - always included (file-scoped to *.test.* and *.e2e.*)
|
|
997
|
+
...configs.vitest,
|
|
998
|
+
...configs.playwright,
|
|
999
|
+
// TanStack Query - always included (rules only match useQuery/useMutation patterns)
|
|
1000
|
+
...configs.tanstackQuery,
|
|
1001
|
+
// Tailwind - only if detected (plugin needs tailwind config to validate classes)
|
|
987
1002
|
...(detect.hasTailwind(deps) ? configs.tailwind : []),
|
|
988
|
-
...(detect.hasTanstackQuery(deps) ? configs.tanstackQuery : []),
|
|
989
1003
|
];
|
|
990
1004
|
`;
|
|
991
1005
|
}
|
|
@@ -1069,34 +1083,33 @@ ${prettier.import}
|
|
|
1069
1083
|
const { detect, configs } = safeword;
|
|
1070
1084
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
1071
1085
|
// Look in parent directory for deps (this file is in .safeword/)
|
|
1072
|
-
const
|
|
1086
|
+
const projectDir = dirname(__dirname);
|
|
1087
|
+
const deps = detect.collectAllDeps(projectDir);
|
|
1073
1088
|
const framework = detect.detectFramework(deps);
|
|
1074
1089
|
|
|
1075
|
-
|
|
1076
|
-
next: configs.recommendedTypeScriptNext,
|
|
1077
|
-
react: configs.recommendedTypeScriptReact,
|
|
1078
|
-
astro: [...configs.recommendedTypeScript, ...configs.astro],
|
|
1079
|
-
typescript: configs.recommendedTypeScript,
|
|
1080
|
-
javascript: configs.recommended,
|
|
1081
|
-
};
|
|
1090
|
+
${getMonorepoSnippet("projectDir")}
|
|
1082
1091
|
|
|
1083
1092
|
${SAFEWORD_STRICT_RULES_FULL}
|
|
1084
1093
|
|
|
1085
1094
|
export default [
|
|
1086
1095
|
{ ignores: detect.getIgnores(deps) },
|
|
1087
1096
|
...baseConfigs[framework],
|
|
1088
|
-
...
|
|
1089
|
-
|
|
1097
|
+
...scopedNextConfigs,
|
|
1098
|
+
// Testing configs - always included (file-scoped to *.test.* and *.e2e.*)
|
|
1099
|
+
...configs.vitest,
|
|
1100
|
+
...configs.playwright,
|
|
1101
|
+
// TanStack Query - always included (rules only match useQuery/useMutation patterns)
|
|
1102
|
+
...configs.tanstackQuery,
|
|
1103
|
+
// Tailwind - only if detected (plugin needs tailwind config to validate classes)
|
|
1090
1104
|
...(detect.hasTailwind(deps) ? configs.tailwind : []),
|
|
1091
|
-
...(detect.hasTanstackQuery(deps) ? configs.tanstackQuery : []),
|
|
1092
1105
|
safewordStrictRules,
|
|
1093
1106
|
${prettier.configEntry}
|
|
1094
1107
|
];
|
|
1095
1108
|
`;
|
|
1096
1109
|
}
|
|
1097
1110
|
var CURSOR_HOOKS = {
|
|
1098
|
-
afterFileEdit: [{ command: "bun
|
|
1099
|
-
stop: [{ command: "bun
|
|
1111
|
+
afterFileEdit: [{ command: "bun ../.safeword/hooks/cursor/after-file-edit.ts" }],
|
|
1112
|
+
stop: [{ command: "bun ../.safeword/hooks/cursor/stop.ts" }]
|
|
1100
1113
|
};
|
|
1101
1114
|
var SETTINGS_HOOKS = {
|
|
1102
1115
|
SessionStart: [
|
|
@@ -1207,7 +1220,7 @@ var BIOME_JSON_MERGE = {
|
|
|
1207
1220
|
}
|
|
1208
1221
|
};
|
|
1209
1222
|
},
|
|
1210
|
-
unmerge: (existing) => {
|
|
1223
|
+
unmerge: (existing, _ctx) => {
|
|
1211
1224
|
const files = existing.files ?? {};
|
|
1212
1225
|
const existingIncludes = Array.isArray(files.includes) ? files.includes : [];
|
|
1213
1226
|
const safewordExcludes = /* @__PURE__ */ new Set(["!eslint.config.mjs", "!.safeword", "!.safeword/**"]);
|
|
@@ -1352,7 +1365,7 @@ var typescriptJsonMerges = {
|
|
|
1352
1365
|
result.scripts = scripts;
|
|
1353
1366
|
return result;
|
|
1354
1367
|
},
|
|
1355
|
-
unmerge: (existing) => {
|
|
1368
|
+
unmerge: (existing, _ctx) => {
|
|
1356
1369
|
const result = { ...existing };
|
|
1357
1370
|
const scripts = { ...existing.scripts };
|
|
1358
1371
|
delete scripts["lint:eslint"];
|
|
@@ -1379,17 +1392,37 @@ var typescriptJsonMerges = {
|
|
|
1379
1392
|
result[key] = value;
|
|
1380
1393
|
}
|
|
1381
1394
|
}
|
|
1382
|
-
const
|
|
1383
|
-
|
|
1384
|
-
|
|
1395
|
+
const safewordPlugins = getPrettierPlugins(ctx.projectType);
|
|
1396
|
+
const existingPlugins = Array.isArray(result.plugins) ? result.plugins : [];
|
|
1397
|
+
const allPlugins = [...existingPlugins];
|
|
1398
|
+
for (const plugin of safewordPlugins) {
|
|
1399
|
+
if (!allPlugins.includes(plugin)) {
|
|
1400
|
+
allPlugins.push(plugin);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
const tailwindIndex = allPlugins.indexOf("prettier-plugin-tailwindcss");
|
|
1404
|
+
if (tailwindIndex !== -1 && tailwindIndex !== allPlugins.length - 1) {
|
|
1405
|
+
allPlugins.splice(tailwindIndex, 1);
|
|
1406
|
+
allPlugins.push("prettier-plugin-tailwindcss");
|
|
1407
|
+
}
|
|
1408
|
+
if (allPlugins.length > 0) {
|
|
1409
|
+
result.plugins = allPlugins;
|
|
1385
1410
|
} else {
|
|
1386
1411
|
delete result.plugins;
|
|
1387
1412
|
}
|
|
1388
1413
|
return result;
|
|
1389
1414
|
},
|
|
1390
|
-
unmerge: (existing) => {
|
|
1415
|
+
unmerge: (existing, ctx) => {
|
|
1391
1416
|
const result = { ...existing };
|
|
1392
|
-
|
|
1417
|
+
const safewordPlugins = new Set(getPrettierPlugins(ctx.projectType));
|
|
1418
|
+
if (Array.isArray(result.plugins)) {
|
|
1419
|
+
const remaining = result.plugins.filter((p) => !safewordPlugins.has(p));
|
|
1420
|
+
if (remaining.length > 0) {
|
|
1421
|
+
result.plugins = remaining;
|
|
1422
|
+
} else {
|
|
1423
|
+
delete result.plugins;
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1393
1426
|
return result;
|
|
1394
1427
|
}
|
|
1395
1428
|
},
|
|
@@ -1531,6 +1564,8 @@ var SAFEWORD_SCHEMA = {
|
|
|
1531
1564
|
".cursor/rules/safeword-tdd-enforcing.mdc",
|
|
1532
1565
|
".claude/commands/tdd.md",
|
|
1533
1566
|
".cursor/commands/tdd.md",
|
|
1567
|
+
// BDD skill split into phase files (v0.16.0)
|
|
1568
|
+
".cursor/rules/safeword-bdd-orchestrating.mdc",
|
|
1534
1569
|
".safeword/commands/tdd.md",
|
|
1535
1570
|
// Brainstorming skill removed - never used, BDD discovery phase covers this (v0.16.0)
|
|
1536
1571
|
".claude/skills/safeword-brainstorming/SKILL.md",
|
|
@@ -1619,16 +1654,13 @@ var SAFEWORD_SCHEMA = {
|
|
|
1619
1654
|
template: "hooks/post-tool-lint.ts"
|
|
1620
1655
|
},
|
|
1621
1656
|
".safeword/hooks/stop-quality.ts": { template: "hooks/stop-quality.ts" },
|
|
1622
|
-
// Guides (
|
|
1657
|
+
// Guides (10 files)
|
|
1623
1658
|
".safeword/guides/architecture-guide.md": {
|
|
1624
1659
|
template: "guides/architecture-guide.md"
|
|
1625
1660
|
},
|
|
1626
1661
|
".safeword/guides/cli-reference.md": {
|
|
1627
1662
|
template: "guides/cli-reference.md"
|
|
1628
1663
|
},
|
|
1629
|
-
".safeword/guides/code-philosophy.md": {
|
|
1630
|
-
template: "guides/code-philosophy.md"
|
|
1631
|
-
},
|
|
1632
1664
|
".safeword/guides/context-files-guide.md": {
|
|
1633
1665
|
template: "guides/context-files-guide.md"
|
|
1634
1666
|
},
|
|
@@ -1675,13 +1707,10 @@ var SAFEWORD_SCHEMA = {
|
|
|
1675
1707
|
".safeword/templates/work-log-template.md": {
|
|
1676
1708
|
template: "doc-templates/work-log-template.md"
|
|
1677
1709
|
},
|
|
1678
|
-
// Prompts (
|
|
1710
|
+
// Prompts (1 file)
|
|
1679
1711
|
".safeword/prompts/architecture.md": {
|
|
1680
1712
|
template: "prompts/architecture.md"
|
|
1681
1713
|
},
|
|
1682
|
-
".safeword/prompts/quality-review.md": {
|
|
1683
|
-
template: "prompts/quality-review.md"
|
|
1684
|
-
},
|
|
1685
1714
|
// Scripts (3 files)
|
|
1686
1715
|
".safeword/scripts/bisect-test-pollution.sh": {
|
|
1687
1716
|
template: "scripts/bisect-test-pollution.sh"
|
|
@@ -1705,6 +1734,24 @@ var SAFEWORD_SCHEMA = {
|
|
|
1705
1734
|
".claude/skills/safeword-bdd-orchestrating/SKILL.md": {
|
|
1706
1735
|
template: "skills/safeword-bdd-orchestrating/SKILL.md"
|
|
1707
1736
|
},
|
|
1737
|
+
".claude/skills/safeword-bdd-orchestrating/DISCOVERY.md": {
|
|
1738
|
+
template: "skills/safeword-bdd-orchestrating/DISCOVERY.md"
|
|
1739
|
+
},
|
|
1740
|
+
".claude/skills/safeword-bdd-orchestrating/SCENARIOS.md": {
|
|
1741
|
+
template: "skills/safeword-bdd-orchestrating/SCENARIOS.md"
|
|
1742
|
+
},
|
|
1743
|
+
".claude/skills/safeword-bdd-orchestrating/DECOMPOSITION.md": {
|
|
1744
|
+
template: "skills/safeword-bdd-orchestrating/DECOMPOSITION.md"
|
|
1745
|
+
},
|
|
1746
|
+
".claude/skills/safeword-bdd-orchestrating/TDD.md": {
|
|
1747
|
+
template: "skills/safeword-bdd-orchestrating/TDD.md"
|
|
1748
|
+
},
|
|
1749
|
+
".claude/skills/safeword-bdd-orchestrating/DONE.md": {
|
|
1750
|
+
template: "skills/safeword-bdd-orchestrating/DONE.md"
|
|
1751
|
+
},
|
|
1752
|
+
".claude/skills/safeword-bdd-orchestrating/SPLITTING.md": {
|
|
1753
|
+
template: "skills/safeword-bdd-orchestrating/SPLITTING.md"
|
|
1754
|
+
},
|
|
1708
1755
|
".claude/commands/bdd.md": { template: "commands/bdd.md" },
|
|
1709
1756
|
".claude/commands/done.md": { template: "commands/done.md" },
|
|
1710
1757
|
".claude/commands/audit.md": { template: "commands/audit.md" },
|
|
@@ -1729,8 +1776,26 @@ var SAFEWORD_SCHEMA = {
|
|
|
1729
1776
|
".cursor/rules/safeword-refactoring.mdc": {
|
|
1730
1777
|
template: "cursor/rules/safeword-refactoring.mdc"
|
|
1731
1778
|
},
|
|
1732
|
-
".cursor/rules/
|
|
1733
|
-
template: "cursor/rules/
|
|
1779
|
+
".cursor/rules/bdd-core.mdc": {
|
|
1780
|
+
template: "cursor/rules/bdd-core.mdc"
|
|
1781
|
+
},
|
|
1782
|
+
".cursor/rules/bdd-discovery.mdc": {
|
|
1783
|
+
template: "cursor/rules/bdd-discovery.mdc"
|
|
1784
|
+
},
|
|
1785
|
+
".cursor/rules/bdd-scenarios.mdc": {
|
|
1786
|
+
template: "cursor/rules/bdd-scenarios.mdc"
|
|
1787
|
+
},
|
|
1788
|
+
".cursor/rules/bdd-decomposition.mdc": {
|
|
1789
|
+
template: "cursor/rules/bdd-decomposition.mdc"
|
|
1790
|
+
},
|
|
1791
|
+
".cursor/rules/bdd-tdd.mdc": {
|
|
1792
|
+
template: "cursor/rules/bdd-tdd.mdc"
|
|
1793
|
+
},
|
|
1794
|
+
".cursor/rules/bdd-done.mdc": {
|
|
1795
|
+
template: "cursor/rules/bdd-done.mdc"
|
|
1796
|
+
},
|
|
1797
|
+
".cursor/rules/bdd-splitting.mdc": {
|
|
1798
|
+
template: "cursor/rules/bdd-splitting.mdc"
|
|
1734
1799
|
},
|
|
1735
1800
|
// Cursor commands (8 files - same as Claude)
|
|
1736
1801
|
".cursor/commands/bdd.md": { template: "commands/bdd.md" },
|
|
@@ -1913,15 +1978,15 @@ function findExistingEslintConfig(cwd) {
|
|
|
1913
1978
|
}
|
|
1914
1979
|
return void 0;
|
|
1915
1980
|
}
|
|
1916
|
-
function
|
|
1917
|
-
if (existsSync3(nodePath4.join(cwd, "ruff.toml"))) return
|
|
1981
|
+
function findExistingRuffConfig(cwd) {
|
|
1982
|
+
if (existsSync3(nodePath4.join(cwd, "ruff.toml"))) return "ruff.toml";
|
|
1918
1983
|
const pyprojectPath = nodePath4.join(cwd, PYPROJECT_TOML);
|
|
1919
|
-
if (!existsSync3(pyprojectPath)) return
|
|
1984
|
+
if (!existsSync3(pyprojectPath)) return void 0;
|
|
1920
1985
|
try {
|
|
1921
1986
|
const content = readFileSync2(pyprojectPath, "utf8");
|
|
1922
|
-
return content.includes("[tool.ruff]");
|
|
1987
|
+
return content.includes("[tool.ruff]") ? "pyproject.toml" : void 0;
|
|
1923
1988
|
} catch {
|
|
1924
|
-
return
|
|
1989
|
+
return void 0;
|
|
1925
1990
|
}
|
|
1926
1991
|
}
|
|
1927
1992
|
function hasExistingMypyConfig(cwd) {
|
|
@@ -1979,7 +2044,7 @@ function detectExistingTooling(cwd, scripts) {
|
|
|
1979
2044
|
existingFormatter: cwd ? hasExistingFormatter(cwd, scripts) : "format" in scripts,
|
|
1980
2045
|
existingEslintConfig: eslintConfig,
|
|
1981
2046
|
legacyEslint: eslintConfig?.startsWith(".eslintrc") ?? false,
|
|
1982
|
-
existingRuffConfig: cwd ?
|
|
2047
|
+
existingRuffConfig: cwd ? findExistingRuffConfig(cwd) : void 0,
|
|
1983
2048
|
existingMypyConfig: cwd ? hasExistingMypyConfig(cwd) : false,
|
|
1984
2049
|
existingImportLinterConfig: cwd ? hasExistingImportLinterConfig(cwd) : false,
|
|
1985
2050
|
existingGolangciConfig: cwd ? findExistingGolangciConfig(cwd) : void 0
|
|
@@ -2035,4 +2100,4 @@ export {
|
|
|
2035
2100
|
detectLanguages,
|
|
2036
2101
|
createProjectContext
|
|
2037
2102
|
};
|
|
2038
|
-
//# sourceMappingURL=chunk-
|
|
2103
|
+
//# sourceMappingURL=chunk-CBJLUXZ4.js.map
|