safeword 0.12.2 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{check-3X75X2JL.js → check-X7NR4WAM.js} +5 -4
- package/dist/check-X7NR4WAM.js.map +1 -0
- package/dist/{chunk-3R26BJXN.js → chunk-TJOHD7CV.js} +56 -5
- package/dist/{chunk-3R26BJXN.js.map → chunk-TJOHD7CV.js.map} +1 -1
- package/dist/{chunk-O4LAXZK3.js → chunk-XLOXGDJG.js} +103 -41
- package/dist/chunk-XLOXGDJG.js.map +1 -0
- package/dist/cli.js +6 -6
- package/dist/{diff-6HFT7BLG.js → diff-3USPMFT2.js} +2 -2
- package/dist/{reset-XFXLQXOC.js → reset-CM3BNT5S.js} +2 -2
- package/dist/{setup-C6NF3YJ5.js → setup-C6MJRBGY.js} +5 -18
- package/dist/setup-C6MJRBGY.js.map +1 -0
- package/dist/{sync-config-PPTR3JPA.js → sync-config-KZE4R47T.js} +2 -2
- package/dist/{upgrade-C2I22FAB.js → upgrade-BIMRJENC.js} +12 -6
- package/dist/upgrade-BIMRJENC.js.map +1 -0
- package/package.json +2 -2
- package/templates/hooks/cursor/after-file-edit.ts +47 -0
- package/templates/hooks/cursor/stop.ts +73 -0
- package/templates/hooks/lib/lint.ts +49 -0
- package/templates/hooks/lib/quality.ts +18 -0
- package/templates/hooks/post-tool-lint.ts +33 -0
- package/templates/hooks/prompt-questions.ts +17 -0
- package/templates/hooks/prompt-timestamp.ts +30 -0
- package/templates/hooks/session-lint-check.ts +62 -0
- package/templates/hooks/session-verify-agents.ts +32 -0
- package/templates/hooks/session-version.ts +18 -0
- package/templates/hooks/stop-quality.ts +168 -0
- package/dist/check-3X75X2JL.js.map +0 -1
- package/dist/chunk-O4LAXZK3.js.map +0 -1
- package/dist/setup-C6NF3YJ5.js.map +0 -1
- package/dist/upgrade-C2I22FAB.js.map +0 -1
- package/templates/hooks/cursor/after-file-edit.sh +0 -58
- package/templates/hooks/cursor/stop.sh +0 -50
- package/templates/hooks/post-tool-lint.sh +0 -51
- package/templates/hooks/prompt-questions.sh +0 -27
- package/templates/hooks/prompt-timestamp.sh +0 -13
- package/templates/hooks/session-lint-check.sh +0 -42
- package/templates/hooks/session-verify-agents.sh +0 -31
- package/templates/hooks/session-version.sh +0 -17
- package/templates/hooks/stop-quality.sh +0 -91
- package/templates/lib/common.sh +0 -26
- package/templates/lib/jq-fallback.sh +0 -20
- /package/dist/{diff-6HFT7BLG.js.map → diff-3USPMFT2.js.map} +0 -0
- /package/dist/{reset-XFXLQXOC.js.map → reset-CM3BNT5S.js.map} +0 -0
- /package/dist/{sync-config-PPTR3JPA.js.map → sync-config-KZE4R47T.js.map} +0 -0
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
SAFEWORD_SCHEMA,
|
|
6
6
|
createProjectContext,
|
|
7
7
|
reconcile
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-XLOXGDJG.js";
|
|
9
9
|
import {
|
|
10
10
|
VERSION
|
|
11
11
|
} from "./chunk-ORQHKDT2.js";
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
header,
|
|
15
15
|
info,
|
|
16
16
|
keyValue,
|
|
17
|
+
listItem,
|
|
17
18
|
readFileSafe,
|
|
18
19
|
success,
|
|
19
20
|
warn
|
|
@@ -140,8 +141,8 @@ function reportHealthSummary(health) {
|
|
|
140
141
|
}
|
|
141
142
|
if (health.missingPackages.length > 0) {
|
|
142
143
|
header("Missing Packages");
|
|
143
|
-
|
|
144
|
-
info("
|
|
144
|
+
for (const pkg of health.missingPackages) listItem(pkg);
|
|
145
|
+
info("\nRun `safeword upgrade` to install missing packages");
|
|
145
146
|
return;
|
|
146
147
|
}
|
|
147
148
|
success("\nConfiguration is healthy");
|
|
@@ -167,4 +168,4 @@ async function check(options) {
|
|
|
167
168
|
export {
|
|
168
169
|
check
|
|
169
170
|
};
|
|
170
|
-
//# sourceMappingURL=check-
|
|
171
|
+
//# sourceMappingURL=check-X7NR4WAM.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/check.ts"],"sourcesContent":["/**\n * Check command - Verify project health and configuration\n *\n * Uses reconcile() with dryRun to detect missing files and configuration issues.\n */\n\nimport nodePath from 'node:path';\n\nimport { reconcile } from '../reconcile.js';\nimport { SAFEWORD_SCHEMA } from '../schema.js';\nimport { createProjectContext } from '../utils/context.js';\nimport { exists, readFileSafe } from '../utils/fs.js';\nimport { header, info, keyValue, listItem, success, warn } from '../utils/output.js';\nimport { isNewerVersion } from '../utils/version.js';\nimport { VERSION } from '../version.js';\n\nexport interface CheckOptions {\n offline?: boolean;\n}\n\n/**\n * Check for missing files from write actions\n * @param cwd\n * @param actions\n */\nfunction findMissingFiles(cwd: string, actions: { type: string; path: string }[]): string[] {\n const issues: string[] = [];\n for (const action of actions) {\n if (action.type === 'write' && !exists(nodePath.join(cwd, action.path))) {\n issues.push(`Missing: ${action.path}`);\n }\n }\n return issues;\n}\n\n/**\n * Check for missing text patch markers\n * @param cwd\n * @param actions\n */\nfunction findMissingPatches(\n cwd: string,\n actions: { type: string; path: string; definition?: { marker: string } }[],\n): string[] {\n const issues: string[] = [];\n for (const action of actions) {\n if (action.type !== 'text-patch') continue;\n\n const fullPath = nodePath.join(cwd, action.path);\n if (exists(fullPath)) {\n const content = readFileSafe(fullPath) ?? '';\n if (action.definition && !content.includes(action.definition.marker)) {\n issues.push(`${action.path} missing safeword link`);\n }\n } else {\n issues.push(`${action.path} file missing`);\n }\n }\n return issues;\n}\n\ninterface HealthStatus {\n configured: boolean;\n projectVersion: string | undefined;\n cliVersion: string;\n updateAvailable: boolean;\n latestVersion: string | undefined;\n issues: string[];\n missingPackages: string[];\n}\n\n/**\n * Check for latest version from npm (with timeout)\n * @param timeout\n */\nasync function checkLatestVersion(timeout = 3000): Promise<string | undefined> {\n try {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => {\n controller.abort();\n }, timeout);\n\n const response = await fetch('https://registry.npmjs.org/safeword/latest', {\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) return undefined;\n\n const data = (await response.json()) as { version?: string };\n return data.version ?? undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Check project configuration health using reconcile dryRun\n * @param cwd\n */\nasync function checkHealth(cwd: string): Promise<HealthStatus> {\n const safewordDirectory = nodePath.join(cwd, '.safeword');\n\n // Check if configured\n if (!exists(safewordDirectory)) {\n return {\n configured: false,\n projectVersion: undefined,\n cliVersion: VERSION,\n updateAvailable: false,\n latestVersion: undefined,\n issues: [],\n missingPackages: [],\n };\n }\n\n // Read project version\n const versionPath = nodePath.join(safewordDirectory, 'version');\n const projectVersion = readFileSafe(versionPath)?.trim() ?? undefined;\n\n // Use reconcile with dryRun to detect issues\n const ctx = createProjectContext(cwd);\n const result = await reconcile(SAFEWORD_SCHEMA, 'upgrade', ctx, { dryRun: true });\n\n // Collect issues from write actions and text patches\n // Filter out chmod (paths[] instead of path) and json-merge/unmerge (incompatible definition)\n const actionsWithPath = result.actions.filter(\n (\n a,\n ): a is Exclude<\n (typeof result.actions)[number],\n { type: 'chmod' } | { type: 'json-merge' } | { type: 'json-unmerge' }\n > => a.type !== 'chmod' && a.type !== 'json-merge' && a.type !== 'json-unmerge',\n );\n const issues: string[] = [\n ...findMissingFiles(cwd, actionsWithPath),\n ...findMissingPatches(cwd, actionsWithPath),\n ];\n\n // Check for missing .claude/settings.json\n if (!exists(nodePath.join(cwd, '.claude', 'settings.json'))) {\n issues.push('Missing: .claude/settings.json');\n }\n\n return {\n configured: true,\n projectVersion,\n cliVersion: VERSION,\n updateAvailable: false,\n latestVersion: undefined,\n issues,\n missingPackages: result.packagesToInstall,\n };\n}\n\n/**\n * Check for CLI updates and report status\n * @param health\n */\nasync function reportUpdateStatus(health: HealthStatus): Promise<void> {\n info('\\nChecking for updates...');\n const latestVersion = await checkLatestVersion();\n\n if (!latestVersion) {\n warn(\"Couldn't check for updates (offline?)\");\n return;\n }\n\n health.latestVersion = latestVersion;\n health.updateAvailable = isNewerVersion(health.cliVersion, latestVersion);\n\n if (health.updateAvailable) {\n warn(`Update available: v${latestVersion}`);\n info('Run `npm install -g safeword` to upgrade');\n } else {\n success('CLI is up to date');\n }\n}\n\n/**\n * Compare project version vs CLI version and report\n * @param health\n */\nfunction reportVersionMismatch(health: HealthStatus): void {\n if (!health.projectVersion) return;\n\n if (isNewerVersion(health.cliVersion, health.projectVersion)) {\n warn(`Project config (v${health.projectVersion}) is newer than CLI (v${health.cliVersion})`);\n info('Consider upgrading the CLI');\n } else if (isNewerVersion(health.projectVersion, health.cliVersion)) {\n info(`\\nUpgrade available for project config`);\n info(\n `Run \\`safeword upgrade\\` to update from v${health.projectVersion} to v${health.cliVersion}`,\n );\n }\n}\n\n/**\n * Report issues or success\n * @param health\n */\nfunction reportHealthSummary(health: HealthStatus): void {\n if (health.issues.length > 0) {\n header('Issues Found');\n for (const issue of health.issues) {\n warn(issue);\n }\n info('\\nRun `safeword upgrade` to repair configuration');\n return;\n }\n\n if (health.missingPackages.length > 0) {\n header('Missing Packages');\n for (const pkg of health.missingPackages) listItem(pkg);\n info('\\nRun `safeword upgrade` to install missing packages');\n return;\n }\n\n success('\\nConfiguration is healthy');\n}\n\n/**\n *\n * @param options\n */\nexport async function check(options: CheckOptions): Promise<void> {\n const cwd = process.cwd();\n\n header('Safeword Health Check');\n\n const health = await checkHealth(cwd);\n\n // Not configured\n if (!health.configured) {\n info('Not configured. Run `safeword setup` to initialize.');\n return;\n }\n\n // Show versions\n keyValue('Safeword CLI', `v${health.cliVersion}`);\n keyValue('Project config', health.projectVersion ? `v${health.projectVersion}` : 'unknown');\n\n // Check for updates (unless offline)\n if (options.offline) {\n info('\\nSkipped update check (offline mode)');\n } else {\n await reportUpdateStatus(health);\n }\n\n reportVersionMismatch(health);\n reportHealthSummary(health);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -130,26 +130,77 @@ function generateDepCruiseConfigFile(arch) {
|
|
|
130
130
|
const hasMonorepoRules = monorepoRules.length > 0;
|
|
131
131
|
return String.raw`module.exports = {
|
|
132
132
|
forbidden: [
|
|
133
|
+
// =========================================================================
|
|
133
134
|
// ERROR RULES (block on violations)
|
|
135
|
+
// =========================================================================
|
|
134
136
|
{
|
|
135
137
|
name: 'no-circular',
|
|
138
|
+
comment: 'Circular dependencies cause runtime issues and make code hard to reason about',
|
|
136
139
|
severity: 'error',
|
|
137
140
|
from: {},
|
|
138
141
|
to: { circular: true },
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: 'no-deprecated-deps',
|
|
145
|
+
comment: 'Deprecated npm packages should be replaced - they may have security issues or be unmaintained',
|
|
146
|
+
severity: 'error',
|
|
147
|
+
from: {},
|
|
148
|
+
to: { dependencyTypes: ['deprecated'] },
|
|
139
149
|
},${hasMonorepoRules ? `
|
|
140
150
|
${monorepoRules},` : ""}
|
|
141
|
-
|
|
151
|
+
|
|
152
|
+
// =========================================================================
|
|
153
|
+
// WARNING RULES (flag issues but don't block)
|
|
154
|
+
// =========================================================================
|
|
155
|
+
{
|
|
156
|
+
name: 'no-dev-deps-in-src',
|
|
157
|
+
comment: 'Production code should not import devDependencies - may cause runtime failures',
|
|
158
|
+
severity: 'warn',
|
|
159
|
+
from: {
|
|
160
|
+
path: '^(packages/[^/]+/)?src',
|
|
161
|
+
pathNot: '\\.test\\.[tj]sx?$',
|
|
162
|
+
},
|
|
163
|
+
to: { dependencyTypes: ['npm-dev'] },
|
|
164
|
+
},
|
|
142
165
|
{
|
|
143
166
|
name: 'no-orphans',
|
|
144
|
-
|
|
145
|
-
|
|
167
|
+
comment: 'Orphan modules are not imported anywhere - may be dead code',
|
|
168
|
+
severity: 'warn',
|
|
169
|
+
from: {
|
|
170
|
+
orphan: true,
|
|
171
|
+
pathNot: [
|
|
172
|
+
// Entry points
|
|
173
|
+
'(^|/)index\\.[tj]sx?$',
|
|
174
|
+
'(^|/)main\\.[tj]sx?$',
|
|
175
|
+
'(^|/)cli\\.[tj]s$',
|
|
176
|
+
'\\.config\\.[tj]s$',
|
|
177
|
+
'\\.config\\.mjs$',
|
|
178
|
+
// Test files
|
|
179
|
+
'\\.test\\.[tj]sx?$',
|
|
180
|
+
'\\.spec\\.[tj]sx?$',
|
|
181
|
+
'/tests/',
|
|
182
|
+
'/__tests__/',
|
|
183
|
+
// Astro/Next.js pages and content
|
|
184
|
+
'/src/content/',
|
|
185
|
+
'/src/pages/',
|
|
186
|
+
'/app/',
|
|
187
|
+
],
|
|
188
|
+
},
|
|
146
189
|
to: {},
|
|
147
190
|
},
|
|
148
191
|
],
|
|
149
192
|
options: {
|
|
150
193
|
doNotFollow: { path: ['node_modules', '.safeword'] },
|
|
194
|
+
exclude: {
|
|
195
|
+
path: ['node_modules', 'dist', 'build', 'coverage', '\\.d\\.ts$'],
|
|
196
|
+
},
|
|
197
|
+
tsPreCompilationDeps: true,
|
|
151
198
|
tsConfig: { fileName: 'tsconfig.json' },
|
|
152
|
-
enhancedResolveOptions: {
|
|
199
|
+
enhancedResolveOptions: {
|
|
200
|
+
extensions: ['.ts', '.tsx', '.js', '.jsx'],
|
|
201
|
+
exportsFields: ['exports'],
|
|
202
|
+
conditionNames: ['import', 'require', 'node', 'default'],
|
|
203
|
+
},
|
|
153
204
|
},
|
|
154
205
|
};
|
|
155
206
|
`;
|
|
@@ -226,4 +277,4 @@ export {
|
|
|
226
277
|
hasArchitectureDetected,
|
|
227
278
|
syncConfig
|
|
228
279
|
};
|
|
229
|
-
//# sourceMappingURL=chunk-
|
|
280
|
+
//# sourceMappingURL=chunk-TJOHD7CV.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\nexport interface 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 = { generatedConfig: false, createdMainConfig: false };\n\n // Generate and write .safeword/depcruise-config.js\n const generatedConfigPath = nodePath.join(safewordDirectory, 'depcruise-config.js');\n const generatedConfig = generateDepCruiseConfigFile(arch);\n writeFileSync(generatedConfigPath, generatedConfig);\n result.generatedConfig = true;\n\n // Create main config if not exists (self-healing)\n const mainConfigPath = nodePath.join(cwd, '.dependency-cruiser.js');\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.js');\n }\n if (result.createdMainConfig) {\n info('Created .dependency-cruiser.js');\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\nexport interface 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 // ERROR RULES (block on violations)\n {\n name: 'no-circular',\n severity: 'error',\n from: {},\n to: { circular: true },\n },${hasMonorepoRules ? `\\n${monorepoRules},` : ''}\n // INFO RULES (reported in /audit, not errors)\n {\n name: 'no-orphans',\n severity: 'info',\n from: { orphan: true, pathNot: ['\\\\.test\\\\.', 'index\\\\.ts$', 'main\\\\.ts$'] },\n to: {},\n },\n ],\n options: {\n doNotFollow: { path: ['node_modules', '.safeword'] },\n tsConfig: { fileName: 'tsconfig.json' },\n enhancedResolveOptions: { extensions: ['.ts', '.tsx', '.js', '.jsx'] },\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.js');\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\nexport interface 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 = { generatedConfig: false, createdMainConfig: false };\n\n // Generate and write .safeword/depcruise-config.js\n const generatedConfigPath = nodePath.join(safewordDirectory, 'depcruise-config.js');\n const generatedConfig = generateDepCruiseConfigFile(arch);\n writeFileSync(generatedConfigPath, generatedConfig);\n result.generatedConfig = true;\n\n // Create main config if not exists (self-healing)\n const mainConfigPath = nodePath.join(cwd, '.dependency-cruiser.js');\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.js');\n }\n if (result.createdMainConfig) {\n info('Created .dependency-cruiser.js');\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\nexport interface 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: '^(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.js');\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"]}
|
|
@@ -5,12 +5,16 @@ import {
|
|
|
5
5
|
ensureDirectory,
|
|
6
6
|
exists,
|
|
7
7
|
getTemplatesDirectory,
|
|
8
|
+
info,
|
|
9
|
+
listItem,
|
|
8
10
|
makeScriptsExecutable,
|
|
9
11
|
readFile,
|
|
10
12
|
readFileSafe,
|
|
11
13
|
readJson,
|
|
12
14
|
remove,
|
|
13
15
|
removeIfEmpty,
|
|
16
|
+
success,
|
|
17
|
+
warn,
|
|
14
18
|
writeFile,
|
|
15
19
|
writeJson
|
|
16
20
|
} from "./chunk-DYLHQBW3.js";
|
|
@@ -484,6 +488,62 @@ function executeTextUnpatch(cwd, path, definition) {
|
|
|
484
488
|
writeFile(fullPath, unpatched);
|
|
485
489
|
}
|
|
486
490
|
|
|
491
|
+
// src/utils/install.ts
|
|
492
|
+
import { execFileSync } from "child_process";
|
|
493
|
+
import { existsSync } from "fs";
|
|
494
|
+
import { join } from "path";
|
|
495
|
+
function detectPackageManager(cwd) {
|
|
496
|
+
if (existsSync(join(cwd, "bun.lockb")) || existsSync(join(cwd, "bun.lock"))) return "bun";
|
|
497
|
+
if (existsSync(join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
498
|
+
if (existsSync(join(cwd, "yarn.lock"))) return "yarn";
|
|
499
|
+
if (existsSync(join(cwd, "package-lock.json"))) return "npm";
|
|
500
|
+
return "npm";
|
|
501
|
+
}
|
|
502
|
+
function getInstallArguments(pm) {
|
|
503
|
+
const args = {
|
|
504
|
+
npm: ["install", "-D"],
|
|
505
|
+
yarn: ["add", "-D"],
|
|
506
|
+
pnpm: ["add", "-D"],
|
|
507
|
+
bun: ["add", "-D"]
|
|
508
|
+
};
|
|
509
|
+
return args[pm];
|
|
510
|
+
}
|
|
511
|
+
function getInstallCommand(pm, packages) {
|
|
512
|
+
const cmds = {
|
|
513
|
+
npm: `npm install -D ${packages.join(" ")}`,
|
|
514
|
+
yarn: `yarn add -D ${packages.join(" ")}`,
|
|
515
|
+
pnpm: `pnpm add -D ${packages.join(" ")}`,
|
|
516
|
+
bun: `bun add -D ${packages.join(" ")}`
|
|
517
|
+
};
|
|
518
|
+
return cmds[pm];
|
|
519
|
+
}
|
|
520
|
+
function installDependencies(cwd, packages, label = "packages") {
|
|
521
|
+
if (packages.length === 0) return;
|
|
522
|
+
const pm = detectPackageManager(cwd);
|
|
523
|
+
const displayCmd = getInstallCommand(pm, packages);
|
|
524
|
+
info(`
|
|
525
|
+
Installing ${label}...`);
|
|
526
|
+
info(`Running: ${displayCmd}`);
|
|
527
|
+
try {
|
|
528
|
+
const args = [...getInstallArguments(pm), ...packages];
|
|
529
|
+
execFileSync(pm, args, { cwd, stdio: "inherit" });
|
|
530
|
+
success(`Installed ${label}`);
|
|
531
|
+
} catch {
|
|
532
|
+
warn(`Failed to install ${label}. Run manually:`);
|
|
533
|
+
listItem(displayCmd);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
var MCP_SERVERS = {
|
|
537
|
+
context7: {
|
|
538
|
+
command: "npx",
|
|
539
|
+
args: ["-y", "@upstash/context7-mcp@latest"]
|
|
540
|
+
},
|
|
541
|
+
playwright: {
|
|
542
|
+
command: "npx",
|
|
543
|
+
args: ["@playwright/mcp@latest"]
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
|
|
487
547
|
// src/templates/config.ts
|
|
488
548
|
function getEslintConfig(hasExistingFormatter2 = false) {
|
|
489
549
|
if (hasExistingFormatter2) {
|
|
@@ -556,8 +616,8 @@ export default [
|
|
|
556
616
|
`;
|
|
557
617
|
}
|
|
558
618
|
var CURSOR_HOOKS = {
|
|
559
|
-
afterFileEdit: [{ command: "./.safeword/hooks/cursor/after-file-edit.
|
|
560
|
-
stop: [{ command: "./.safeword/hooks/cursor/stop.
|
|
619
|
+
afterFileEdit: [{ command: "bun ./.safeword/hooks/cursor/after-file-edit.ts" }],
|
|
620
|
+
stop: [{ command: "bun ./.safeword/hooks/cursor/stop.ts" }]
|
|
561
621
|
};
|
|
562
622
|
var SETTINGS_HOOKS = {
|
|
563
623
|
SessionStart: [
|
|
@@ -565,7 +625,7 @@ var SETTINGS_HOOKS = {
|
|
|
565
625
|
hooks: [
|
|
566
626
|
{
|
|
567
627
|
type: "command",
|
|
568
|
-
command: '"$CLAUDE_PROJECT_DIR"/.safeword/hooks/session-verify-agents.
|
|
628
|
+
command: 'bun "$CLAUDE_PROJECT_DIR"/.safeword/hooks/session-verify-agents.ts'
|
|
569
629
|
}
|
|
570
630
|
]
|
|
571
631
|
},
|
|
@@ -573,7 +633,7 @@ var SETTINGS_HOOKS = {
|
|
|
573
633
|
hooks: [
|
|
574
634
|
{
|
|
575
635
|
type: "command",
|
|
576
|
-
command: '"$CLAUDE_PROJECT_DIR"/.safeword/hooks/session-version.
|
|
636
|
+
command: 'bun "$CLAUDE_PROJECT_DIR"/.safeword/hooks/session-version.ts'
|
|
577
637
|
}
|
|
578
638
|
]
|
|
579
639
|
},
|
|
@@ -581,7 +641,7 @@ var SETTINGS_HOOKS = {
|
|
|
581
641
|
hooks: [
|
|
582
642
|
{
|
|
583
643
|
type: "command",
|
|
584
|
-
command: '"$CLAUDE_PROJECT_DIR"/.safeword/hooks/session-lint-check.
|
|
644
|
+
command: 'bun "$CLAUDE_PROJECT_DIR"/.safeword/hooks/session-lint-check.ts'
|
|
585
645
|
}
|
|
586
646
|
]
|
|
587
647
|
}
|
|
@@ -591,7 +651,7 @@ var SETTINGS_HOOKS = {
|
|
|
591
651
|
hooks: [
|
|
592
652
|
{
|
|
593
653
|
type: "command",
|
|
594
|
-
command: '"$CLAUDE_PROJECT_DIR"/.safeword/hooks/prompt-timestamp.
|
|
654
|
+
command: 'bun "$CLAUDE_PROJECT_DIR"/.safeword/hooks/prompt-timestamp.ts'
|
|
595
655
|
}
|
|
596
656
|
]
|
|
597
657
|
},
|
|
@@ -599,7 +659,7 @@ var SETTINGS_HOOKS = {
|
|
|
599
659
|
hooks: [
|
|
600
660
|
{
|
|
601
661
|
type: "command",
|
|
602
|
-
command: '"$CLAUDE_PROJECT_DIR"/.safeword/hooks/prompt-questions.
|
|
662
|
+
command: 'bun "$CLAUDE_PROJECT_DIR"/.safeword/hooks/prompt-questions.ts'
|
|
603
663
|
}
|
|
604
664
|
]
|
|
605
665
|
}
|
|
@@ -609,7 +669,7 @@ var SETTINGS_HOOKS = {
|
|
|
609
669
|
hooks: [
|
|
610
670
|
{
|
|
611
671
|
type: "command",
|
|
612
|
-
command: '"$CLAUDE_PROJECT_DIR"/.safeword/hooks/stop-quality.
|
|
672
|
+
command: 'bun "$CLAUDE_PROJECT_DIR"/.safeword/hooks/stop-quality.ts'
|
|
613
673
|
}
|
|
614
674
|
]
|
|
615
675
|
}
|
|
@@ -620,7 +680,7 @@ var SETTINGS_HOOKS = {
|
|
|
620
680
|
hooks: [
|
|
621
681
|
{
|
|
622
682
|
type: "command",
|
|
623
|
-
command: '"$CLAUDE_PROJECT_DIR"/.safeword/hooks/post-tool-lint.
|
|
683
|
+
command: 'bun "$CLAUDE_PROJECT_DIR"/.safeword/hooks/post-tool-lint.ts'
|
|
624
684
|
}
|
|
625
685
|
]
|
|
626
686
|
}
|
|
@@ -647,18 +707,6 @@ function filterOutSafewordHooks(hooks) {
|
|
|
647
707
|
return hooks.filter((h) => !isSafewordHook(h));
|
|
648
708
|
}
|
|
649
709
|
|
|
650
|
-
// src/utils/install.ts
|
|
651
|
-
var MCP_SERVERS = {
|
|
652
|
-
context7: {
|
|
653
|
-
command: "npx",
|
|
654
|
-
args: ["-y", "@upstash/context7-mcp@latest"]
|
|
655
|
-
},
|
|
656
|
-
playwright: {
|
|
657
|
-
command: "npx",
|
|
658
|
-
args: ["@playwright/mcp@latest"]
|
|
659
|
-
}
|
|
660
|
-
};
|
|
661
|
-
|
|
662
710
|
// src/schema.ts
|
|
663
711
|
var BIOME_JSON_MERGE = {
|
|
664
712
|
keys: ["files.includes"],
|
|
@@ -711,7 +759,7 @@ var SAFEWORD_SCHEMA = {
|
|
|
711
759
|
".safeword",
|
|
712
760
|
".safeword/hooks",
|
|
713
761
|
".safeword/hooks/cursor",
|
|
714
|
-
".safeword/lib",
|
|
762
|
+
".safeword/hooks/lib",
|
|
715
763
|
".safeword/guides",
|
|
716
764
|
".safeword/templates",
|
|
717
765
|
".safeword/prompts",
|
|
@@ -745,9 +793,19 @@ var SAFEWORD_SCHEMA = {
|
|
|
745
793
|
".safeword/guides/test-definitions-guide.md",
|
|
746
794
|
// Boundaries config now project-specific (v0.9.0)
|
|
747
795
|
".safeword/eslint-boundaries.config.mjs",
|
|
748
|
-
//
|
|
749
|
-
".
|
|
750
|
-
".safeword/
|
|
796
|
+
// Shell hooks replaced with TypeScript/Bun (v0.13.0)
|
|
797
|
+
".safeword/hooks/session-verify-agents.sh",
|
|
798
|
+
".safeword/hooks/session-version.sh",
|
|
799
|
+
".safeword/hooks/session-lint-check.sh",
|
|
800
|
+
".safeword/hooks/prompt-timestamp.sh",
|
|
801
|
+
".safeword/hooks/prompt-questions.sh",
|
|
802
|
+
".safeword/hooks/post-tool-lint.sh",
|
|
803
|
+
".safeword/hooks/stop-quality.sh",
|
|
804
|
+
".safeword/hooks/cursor/after-file-edit.sh",
|
|
805
|
+
".safeword/hooks/cursor/stop.sh",
|
|
806
|
+
// Shell libraries no longer needed with Bun
|
|
807
|
+
".safeword/lib/common.sh",
|
|
808
|
+
".safeword/lib/jq-fallback.sh"
|
|
751
809
|
],
|
|
752
810
|
// Packages to uninstall on upgrade (now bundled in eslint-plugin-safeword)
|
|
753
811
|
deprecatedPackages: [
|
|
@@ -778,25 +836,27 @@ var SAFEWORD_SCHEMA = {
|
|
|
778
836
|
],
|
|
779
837
|
// Directories to delete on upgrade (no longer managed by safeword)
|
|
780
838
|
deprecatedDirs: [
|
|
781
|
-
".husky"
|
|
839
|
+
".husky",
|
|
782
840
|
// Pre-commit hooks no longer managed by safeword
|
|
841
|
+
".safeword/lib"
|
|
842
|
+
// Shell libraries no longer needed with Bun (v0.13.0)
|
|
783
843
|
],
|
|
784
844
|
// Files owned by safeword (overwritten on upgrade if content changed)
|
|
785
845
|
ownedFiles: {
|
|
786
846
|
// Core files
|
|
787
847
|
".safeword/SAFEWORD.md": { template: "SAFEWORD.md" },
|
|
788
848
|
".safeword/version": { content: () => VERSION },
|
|
789
|
-
// Hooks (
|
|
790
|
-
".safeword/hooks/
|
|
791
|
-
".safeword/hooks/
|
|
792
|
-
|
|
793
|
-
".safeword/hooks/
|
|
794
|
-
".safeword/hooks/
|
|
795
|
-
".safeword/hooks/
|
|
796
|
-
".safeword/hooks/
|
|
797
|
-
|
|
798
|
-
".safeword/
|
|
799
|
-
".safeword/
|
|
849
|
+
// Hooks shared library (2 files) - TypeScript with Bun runtime
|
|
850
|
+
".safeword/hooks/lib/lint.ts": { template: "hooks/lib/lint.ts" },
|
|
851
|
+
".safeword/hooks/lib/quality.ts": { template: "hooks/lib/quality.ts" },
|
|
852
|
+
// Hooks (7 files) - TypeScript with Bun runtime
|
|
853
|
+
".safeword/hooks/session-verify-agents.ts": { template: "hooks/session-verify-agents.ts" },
|
|
854
|
+
".safeword/hooks/session-version.ts": { template: "hooks/session-version.ts" },
|
|
855
|
+
".safeword/hooks/session-lint-check.ts": { template: "hooks/session-lint-check.ts" },
|
|
856
|
+
".safeword/hooks/prompt-timestamp.ts": { template: "hooks/prompt-timestamp.ts" },
|
|
857
|
+
".safeword/hooks/prompt-questions.ts": { template: "hooks/prompt-questions.ts" },
|
|
858
|
+
".safeword/hooks/post-tool-lint.ts": { template: "hooks/post-tool-lint.ts" },
|
|
859
|
+
".safeword/hooks/stop-quality.ts": { template: "hooks/stop-quality.ts" },
|
|
800
860
|
// Guides (11 files)
|
|
801
861
|
".safeword/guides/architecture-guide.md": { template: "guides/architecture-guide.md" },
|
|
802
862
|
".safeword/guides/cli-reference.md": { template: "guides/cli-reference.md" },
|
|
@@ -888,9 +948,9 @@ var SAFEWORD_SCHEMA = {
|
|
|
888
948
|
".cursor/commands/cleanup-zombies.md": { template: "commands/cleanup-zombies.md" },
|
|
889
949
|
".cursor/commands/lint.md": { template: "commands/lint.md" },
|
|
890
950
|
".cursor/commands/quality-review.md": { template: "commands/quality-review.md" },
|
|
891
|
-
// Cursor hooks adapters (2 files)
|
|
892
|
-
".safeword/hooks/cursor/after-file-edit.
|
|
893
|
-
".safeword/hooks/cursor/stop.
|
|
951
|
+
// Cursor hooks adapters (2 files) - TypeScript with Bun runtime
|
|
952
|
+
".safeword/hooks/cursor/after-file-edit.ts": { template: "hooks/cursor/after-file-edit.ts" },
|
|
953
|
+
".safeword/hooks/cursor/stop.ts": { template: "hooks/cursor/stop.ts" }
|
|
894
954
|
},
|
|
895
955
|
// Files created if missing, updated only if content matches current template
|
|
896
956
|
managedFiles: {
|
|
@@ -1263,8 +1323,10 @@ function createProjectContext(cwd) {
|
|
|
1263
1323
|
|
|
1264
1324
|
export {
|
|
1265
1325
|
reconcile,
|
|
1326
|
+
detectPackageManager,
|
|
1327
|
+
installDependencies,
|
|
1266
1328
|
SAFEWORD_SCHEMA,
|
|
1267
1329
|
isGitRepo,
|
|
1268
1330
|
createProjectContext
|
|
1269
1331
|
};
|
|
1270
|
-
//# sourceMappingURL=chunk-
|
|
1332
|
+
//# sourceMappingURL=chunk-XLOXGDJG.js.map
|