safeword 0.52.0 → 0.54.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-2DK5F4OO.js → check-IZ42MYCT.js} +10 -10
- package/dist/{chunk-MPEK5NNA.js → chunk-G3BDQLVU.js} +7 -7
- package/dist/chunk-G3BDQLVU.js.map +1 -0
- package/dist/{chunk-WE7ZQLCT.js → chunk-GT7KMCFG.js} +23 -8
- package/dist/chunk-GT7KMCFG.js.map +1 -0
- package/dist/{chunk-2WUL76K5.js → chunk-HN3ZZETN.js} +2 -2
- package/dist/{chunk-MPYFFJBF.js → chunk-I2GV5QKO.js} +10 -3
- package/dist/{chunk-MPYFFJBF.js.map → chunk-I2GV5QKO.js.map} +1 -1
- package/dist/{chunk-BAJDI467.js → chunk-N74UNUZ6.js} +22 -19
- package/dist/chunk-N74UNUZ6.js.map +1 -0
- package/dist/{chunk-LODQOJEK.js → chunk-PHR2K2Y3.js} +41 -29
- package/dist/chunk-PHR2K2Y3.js.map +1 -0
- package/dist/{chunk-DFATZL6F.js → chunk-SW3D7PN7.js} +470 -283
- package/dist/chunk-SW3D7PN7.js.map +1 -0
- package/dist/{chunk-JLFYAVLP.js → chunk-VVWHQBTU.js} +22 -19
- package/dist/chunk-VVWHQBTU.js.map +1 -0
- package/dist/{chunk-SD4UB7HJ.js → chunk-VW6VSC5R.js} +55 -33
- package/dist/chunk-VW6VSC5R.js.map +1 -0
- package/dist/{chunk-QLXFPFIC.js → chunk-XTLCJKGE.js} +5 -5
- package/dist/chunk-XTLCJKGE.js.map +1 -0
- package/dist/{chunk-5WF73MP2.js → chunk-YXEKBWNB.js} +12 -12
- package/dist/chunk-YXEKBWNB.js.map +1 -0
- package/dist/{chunk-IGULTNHR.js → chunk-YXNI7W5D.js} +14 -12
- package/dist/chunk-YXNI7W5D.js.map +1 -0
- package/dist/cli.js +12 -12
- package/dist/{codify-YUGZVCR4.js → codify-2HGAGRPT.js} +14 -12
- package/dist/codify-2HGAGRPT.js.map +1 -0
- package/dist/{diff-ZAYGIAQF.js → diff-ZCRAOPAC.js} +33 -32
- package/dist/diff-ZCRAOPAC.js.map +1 -0
- package/dist/index.js +2 -2
- package/dist/{lint-gherkin-KJH3GNJQ.js → lint-gherkin-FLJEH3EJ.js} +2 -2
- package/dist/presets/typescript/index.d.ts +11 -11
- package/dist/presets/typescript/index.js +2 -2
- package/dist/{reset-TKMSK7DX.js → reset-ETS3FAAG.js} +15 -17
- package/dist/reset-ETS3FAAG.js.map +1 -0
- package/dist/{setup-4FE4FWIA.js → setup-MCRTMRBD.js} +29 -26
- package/dist/setup-MCRTMRBD.js.map +1 -0
- package/dist/{sync-config-5FCJLGMW.js → sync-config-IDVS7DTC.js} +3 -3
- package/dist/{sync-learnings-C7GXSMKZ.js → sync-learnings-PALEDNJ2.js} +3 -3
- package/dist/{sync-tickets-SDX4XPVS.js → sync-tickets-O6KTBGNT.js} +4 -4
- package/dist/{test-plan-4D3WNSQP.js → test-plan-PV6C5IUO.js} +18 -10
- package/dist/test-plan-PV6C5IUO.js.map +1 -0
- package/dist/{ticket-new-CVBD3MVR.js → ticket-new-W3YOFGQU.js} +4 -4
- package/dist/{ticket-new-CVBD3MVR.js.map → ticket-new-W3YOFGQU.js.map} +1 -1
- package/dist/{upgrade-2TVZ6C3T.js → upgrade-RRO7PLMT.js} +48 -29
- package/dist/upgrade-RRO7PLMT.js.map +1 -0
- package/package.json +14 -16
- package/templates/codex/config.toml +7 -0
- package/templates/commands/audit.md +2 -2
- package/templates/commands/verify.md +2 -2
- package/templates/cursor/rules/safeword-debugging.mdc +2 -205
- package/templates/cursor/rules/safeword-quality-reviewing.mdc +2 -182
- package/templates/cursor/rules/safeword-refactoring.mdc +2 -171
- package/templates/cursor/rules/safeword-testing.mdc +2 -272
- package/templates/doc-templates/test-definitions-feature.md +1 -1
- package/templates/hooks/lib/ledger-validation.ts +31 -7
- package/templates/hooks/lib/lint-config.ts +55 -0
- package/templates/hooks/lib/lint.ts +11 -1
- package/templates/hooks/lib/quality-state.ts +22 -1
- package/templates/hooks/lib/skill-invocation-log.ts +12 -4
- package/templates/hooks/lib/update-cache.ts +98 -2
- package/templates/hooks/pre-tool-quality.ts +5 -21
- package/templates/hooks/record-skill-invocation.ts +7 -1
- package/templates/hooks/session-auto-upgrade.ts +193 -48
- package/templates/hooks/session-compact-context.ts +4 -15
- package/templates/hooks/session-lint-check.ts +11 -3
- package/templates/hooks/stop-quality.ts +59 -64
- package/templates/hooks/stop-reentry.ts +24 -19
- package/templates/skills/audit/SKILL.md +2 -2
- package/templates/skills/bdd/SKILL.md +6 -2
- package/templates/skills/bdd/TDD.md +13 -3
- package/templates/skills/bdd/VERIFY.md +1 -1
- package/templates/skills/debug/SKILL.md +29 -15
- package/templates/skills/elicit/SKILL.md +4 -0
- package/templates/skills/figure-it-out/SKILL.md +6 -0
- package/templates/skills/quality-review/SKILL.md +41 -9
- package/templates/skills/refactor/SKILL.md +26 -11
- package/templates/skills/verify/SKILL.md +2 -2
- package/dist/chunk-5WF73MP2.js.map +0 -1
- package/dist/chunk-BAJDI467.js.map +0 -1
- package/dist/chunk-DFATZL6F.js.map +0 -1
- package/dist/chunk-IGULTNHR.js.map +0 -1
- package/dist/chunk-JLFYAVLP.js.map +0 -1
- package/dist/chunk-LODQOJEK.js.map +0 -1
- package/dist/chunk-MPEK5NNA.js.map +0 -1
- package/dist/chunk-QLXFPFIC.js.map +0 -1
- package/dist/chunk-SD4UB7HJ.js.map +0 -1
- package/dist/chunk-WE7ZQLCT.js.map +0 -1
- package/dist/codify-YUGZVCR4.js.map +0 -1
- package/dist/diff-ZAYGIAQF.js.map +0 -1
- package/dist/reset-TKMSK7DX.js.map +0 -1
- package/dist/setup-4FE4FWIA.js.map +0 -1
- package/dist/test-plan-4D3WNSQP.js.map +0 -1
- package/dist/upgrade-2TVZ6C3T.js.map +0 -1
- package/templates/hooks/session-update-check.ts +0 -81
- /package/dist/{check-2DK5F4OO.js.map → check-IZ42MYCT.js.map} +0 -0
- /package/dist/{chunk-2WUL76K5.js.map → chunk-HN3ZZETN.js.map} +0 -0
- /package/dist/{lint-gherkin-KJH3GNJQ.js.map → lint-gherkin-FLJEH3EJ.js.map} +0 -0
- /package/dist/{sync-config-5FCJLGMW.js.map → sync-config-IDVS7DTC.js.map} +0 -0
- /package/dist/{sync-learnings-C7GXSMKZ.js.map → sync-learnings-PALEDNJ2.js.map} +0 -0
- /package/dist/{sync-tickets-SDX4XPVS.js.map → sync-tickets-O6KTBGNT.js.map} +0 -0
|
@@ -4,17 +4,17 @@ import {
|
|
|
4
4
|
import {
|
|
5
5
|
checkHealth,
|
|
6
6
|
reportHealthSummary
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-N74UNUZ6.js";
|
|
8
8
|
import {
|
|
9
9
|
syncTickets
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-VVWHQBTU.js";
|
|
11
11
|
import "./chunk-NHXVS5FL.js";
|
|
12
|
-
import "./chunk-
|
|
13
|
-
import "./chunk-
|
|
14
|
-
import "./chunk-
|
|
15
|
-
import "./chunk-
|
|
16
|
-
import "./chunk-
|
|
17
|
-
import "./chunk-
|
|
12
|
+
import "./chunk-YXNI7W5D.js";
|
|
13
|
+
import "./chunk-XTLCJKGE.js";
|
|
14
|
+
import "./chunk-SW3D7PN7.js";
|
|
15
|
+
import "./chunk-HN3ZZETN.js";
|
|
16
|
+
import "./chunk-GT7KMCFG.js";
|
|
17
|
+
import "./chunk-PHR2K2Y3.js";
|
|
18
18
|
import "./chunk-HSC7TELY.js";
|
|
19
19
|
import {
|
|
20
20
|
header,
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
keyValue,
|
|
23
23
|
success,
|
|
24
24
|
warn
|
|
25
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-I2GV5QKO.js";
|
|
26
26
|
|
|
27
27
|
// src/commands/check.ts
|
|
28
28
|
import process from "process";
|
|
@@ -110,4 +110,4 @@ async function check(options) {
|
|
|
110
110
|
export {
|
|
111
111
|
check
|
|
112
112
|
};
|
|
113
|
-
//# sourceMappingURL=check-
|
|
113
|
+
//# sourceMappingURL=check-IZ42MYCT.js.map
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
info,
|
|
5
5
|
readJson,
|
|
6
6
|
success
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-I2GV5QKO.js";
|
|
8
8
|
|
|
9
9
|
// src/commands/sync-config.ts
|
|
10
10
|
import { readFileSync, writeFileSync } from "fs";
|
|
@@ -132,7 +132,7 @@ function generateMonorepoRules(workspaces) {
|
|
|
132
132
|
}
|
|
133
133
|
return rules.join(",\n");
|
|
134
134
|
}
|
|
135
|
-
function
|
|
135
|
+
function generateDependencyCruiseConfigFile(arch) {
|
|
136
136
|
const monorepoRules = arch.workspaces ? generateMonorepoRules(arch.workspaces) : "";
|
|
137
137
|
const hasMonorepoRules = monorepoRules.length > 0;
|
|
138
138
|
return String.raw`module.exports = {
|
|
@@ -217,7 +217,7 @@ ${monorepoRules},` : ""}
|
|
|
217
217
|
};
|
|
218
218
|
`;
|
|
219
219
|
}
|
|
220
|
-
function
|
|
220
|
+
function generateDependencyCruiseMainConfig() {
|
|
221
221
|
return `/**
|
|
222
222
|
* Dependency Cruiser Configuration
|
|
223
223
|
*
|
|
@@ -249,12 +249,12 @@ function syncConfigCore(cwd, arch) {
|
|
|
249
249
|
createdMainConfig: false
|
|
250
250
|
};
|
|
251
251
|
const generatedConfigPath = nodePath3.join(safewordDirectory, "depcruise-config.cjs");
|
|
252
|
-
const generatedConfig =
|
|
252
|
+
const generatedConfig = generateDependencyCruiseConfigFile(arch);
|
|
253
253
|
writeFileSync(generatedConfigPath, generatedConfig);
|
|
254
254
|
result.generatedConfig = true;
|
|
255
255
|
const mainConfigPath = nodePath3.join(cwd, ".dependency-cruiser.cjs");
|
|
256
256
|
if (!exists(mainConfigPath)) {
|
|
257
|
-
const mainConfig =
|
|
257
|
+
const mainConfig = generateDependencyCruiseMainConfig();
|
|
258
258
|
writeFileSync(mainConfigPath, mainConfig);
|
|
259
259
|
result.createdMainConfig = true;
|
|
260
260
|
}
|
|
@@ -273,7 +273,7 @@ function checkConfig(cwd, arch) {
|
|
|
273
273
|
if (!exists(generatedConfigPath)) {
|
|
274
274
|
return { matches: false, reason: "missing" };
|
|
275
275
|
}
|
|
276
|
-
const generated =
|
|
276
|
+
const generated = generateDependencyCruiseConfigFile(arch);
|
|
277
277
|
const onDisk = readFileSync(generatedConfigPath, "utf8");
|
|
278
278
|
return generated === onDisk ? { matches: true } : { matches: false, reason: "drifted" };
|
|
279
279
|
}
|
|
@@ -312,4 +312,4 @@ export {
|
|
|
312
312
|
hasArchitectureDetected,
|
|
313
313
|
syncConfig
|
|
314
314
|
};
|
|
315
|
-
//# sourceMappingURL=chunk-
|
|
315
|
+
//# sourceMappingURL=chunk-G3BDQLVU.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/sync-config.ts","../src/utils/boundaries.ts","../src/utils/depcruise-config.ts"],"sourcesContent":["/**\n * Sync Config command - Regenerate depcruise config from current project structure\n *\n * Default mode writes config to disk. `--check` mode reports drift without writing —\n * used by `/audit` to detect stale config without polluting the working tree.\n */\n\nimport { readFileSync, writeFileSync } from 'node:fs';\nimport nodePath from 'node:path';\n\nimport { detectArchitecture } from '../utils/boundaries.js';\nimport {\n type DependencyCruiseArchitecture,\n detectWorkspaces,\n generateDependencyCruiseConfigFile,\n generateDependencyCruiseMainConfig,\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: DependencyCruiseArchitecture): 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 = generateDependencyCruiseConfigFile(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 = generateDependencyCruiseMainConfig();\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): DependencyCruiseArchitecture {\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: DependencyCruiseArchitecture): boolean {\n return arch.elements.length > 0 || arch.isMonorepo || (arch.workspaces?.length ?? 0) > 0;\n}\n\n/**\n * Check if generated config matches on-disk content. No writes.\n * Returns { matches: true } when bytes are byte-equal.\n * Returns { matches: false, reason } when on-disk is missing or differs.\n */\nfunction checkConfig(\n cwd: string,\n arch: DependencyCruiseArchitecture,\n): { matches: true } | { matches: false; reason: 'missing' | 'drifted' } {\n const generatedConfigPath = nodePath.join(cwd, '.safeword', 'depcruise-config.cjs');\n if (!exists(generatedConfigPath)) {\n return { matches: false, reason: 'missing' };\n }\n const generated = generateDependencyCruiseConfigFile(arch);\n const onDisk = readFileSync(generatedConfigPath, 'utf8');\n return generated === onDisk ? { matches: true } : { matches: false, reason: 'drifted' };\n}\n\n/**\n * CLI command: Sync depcruise config with current project structure\n */\n\nexport async function syncConfig(options: { check?: boolean } = {}): Promise<void> {\n // Public CLI command contract is Promise<void>; body is sync today but the\n // signature reserves room for async I/O. Token await keeps the contract honest.\n await Promise.resolve();\n const cwd = process.cwd();\n const safewordDirectory = nodePath.join(cwd, '.safeword');\n\n // Check if .safeword exists\n if (!exists(safewordDirectory)) {\n error('Not configured. Run `safeword setup` first.');\n process.exit(1);\n }\n\n const arch = buildArchitecture(cwd);\n\n if (options.check) {\n const result = checkConfig(cwd, arch);\n if (result.matches) {\n success('Config in sync');\n return;\n }\n const message =\n result.reason === 'missing'\n ? 'Missing .safeword/depcruise-config.cjs — run `safeword sync-config` to generate it.'\n : 'Stale .safeword/depcruise-config.cjs — run `safeword sync-config` to refresh.';\n error(message);\n process.exit(1);\n }\n\n const result = syncConfigCore(cwd, arch);\n\n if (result.generatedConfig) {\n info('Generated .safeword/depcruise-config.cjs');\n }\n if (result.createdMainConfig) {\n info('Created .dependency-cruiser.cjs');\n }\n\n success('Config synced');\n}\n","/**\n * Architecture boundaries detection\n *\n * Auto-detects common architecture directories for use by\n * dependency-cruiser layer enforcement.\n *\n * Supports:\n * - Standard projects (src/utils, utils/)\n * - Monorepos (packages/*, apps/*)\n * - Various naming conventions (helpers, shared, core, etc.)\n */\n\nimport { readdirSync } from 'node:fs';\nimport nodePath from 'node:path';\n\nimport { exists } from './fs.js';\n\n/**\n * Architecture layer definitions with alternative names.\n * Each layer maps to equivalent directory names.\n * Order defines hierarchy: earlier = lower layer.\n */\nconst ARCHITECTURE_LAYERS = [\n // Layer 0: Pure types (no imports)\n { layer: 'types', dirs: ['types', 'interfaces', 'schemas'] },\n // Layer 1: Utilities (only types)\n { layer: 'utils', dirs: ['utils', 'helpers', 'shared', 'common', 'core'] },\n // Layer 2: Libraries (types, utils)\n { layer: 'lib', dirs: ['lib', 'libraries'] },\n // Layer 3: State & logic (types, utils, lib)\n { layer: 'hooks', dirs: ['hooks', 'composables'] },\n { layer: 'services', dirs: ['services', 'api', 'stores', 'state'] },\n // Layer 4: UI components (all above)\n { layer: 'components', dirs: ['components', 'ui'] },\n // Layer 5: Features (all above)\n { layer: 'features', dirs: ['features', 'modules', 'domains'] },\n // Layer 6: Entry points (can import everything)\n { layer: 'app', dirs: ['app', 'pages', 'views', 'routes', 'commands'] },\n] as const;\n\ntype Layer = (typeof ARCHITECTURE_LAYERS)[number]['layer'];\n\ninterface DetectedElement {\n layer: Layer;\n pattern: string; // glob pattern for boundaries config\n location: string; // human-readable location\n}\n\nexport interface DetectedArchitecture {\n elements: DetectedElement[];\n isMonorepo: boolean;\n}\n\n/**\n * Find monorepo package directories\n * @param projectDirectory\n */\nfunction findMonorepoPackages(projectDirectory: string): string[] {\n const packages: string[] = [];\n\n // Check common monorepo patterns\n const monorepoRoots = ['packages', 'apps', 'libs', 'modules'];\n\n for (const root of monorepoRoots) {\n const rootPath = nodePath.join(projectDirectory, root);\n if (!exists(rootPath)) continue;\n\n try {\n const entries = readdirSync(rootPath, { withFileTypes: true });\n for (const entry of entries) {\n if (entry.isDirectory() && !entry.name.startsWith('.')) {\n packages.push(nodePath.join(root, entry.name));\n }\n }\n } catch {\n // Directory not readable, skip\n }\n }\n\n return packages;\n}\n\n/**\n * Check if a layer already exists for this path prefix\n * @param elements\n * @param layer\n * @param pathPrefix\n */\nfunction hasLayerForPrefix(elements: DetectedElement[], layer: Layer, pathPrefix: string): boolean {\n return elements.some(\n element => element.layer === layer && element.pattern.startsWith(pathPrefix),\n );\n}\n\n/**\n * Scan a single search path for architecture layers\n * @param projectDirectory\n * @param searchPath\n * @param pathPrefix\n * @param elements\n */\n/**\n * A `features/` directory holding Gherkin `.feature` files is safeword's BDD\n * acceptance lane (scaffolded by setup, ticket 102b), not a feature-sliced\n * architecture layer — without this guard every safeword project would\n * \"detect\" architecture and get depcruise configs it doesn't need.\n */\nfunction isGherkinDirectory(fullPath: string): boolean {\n try {\n return readdirSync(fullPath).some(entry => entry.endsWith('.feature'));\n } catch {\n return false;\n }\n}\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 (\n exists(fullPath) &&\n !isGherkinDirectory(fullPath) &&\n !hasLayerForPrefix(elements, layerDefinition.layer, pathPrefix)\n ) {\n elements.push({\n layer: layerDefinition.layer,\n pattern: `${pathPrefix}${dirName}/**`,\n location: `${pathPrefix}${dirName}`,\n });\n }\n }\n }\n}\n\n/**\n * Scan a directory for architecture layers\n * @param projectDirectory\n * @param basePath\n */\nfunction scanForLayers(projectDirectory: string, basePath: string): DetectedElement[] {\n const elements: DetectedElement[] = [];\n const prefix = basePath ? `${basePath}/` : '';\n\n // Check src/ and root level\n scanSearchPath(projectDirectory, nodePath.join(basePath, 'src'), `${prefix}src/`, elements);\n scanSearchPath(projectDirectory, basePath, prefix, elements);\n\n return elements;\n}\n\n/**\n * Detects architecture directories in the project\n * Handles both standard projects and monorepos\n * @param projectDirectory\n */\nexport function detectArchitecture(projectDirectory: string): DetectedArchitecture {\n const elements: DetectedElement[] = [];\n\n // First, check for monorepo packages\n const packages = findMonorepoPackages(projectDirectory);\n const isMonorepo = packages.length > 0;\n\n if (isMonorepo) {\n // Scan each package\n for (const pkg of packages) {\n elements.push(...scanForLayers(projectDirectory, pkg));\n }\n }\n\n // Also scan root level (works for both monorepo root and standard projects)\n elements.push(...scanForLayers(projectDirectory, ''));\n\n // Deduplicate by pattern\n const seen = new Set<string>();\n const uniqueElements = elements.filter(element => {\n if (seen.has(element.pattern)) return false;\n seen.add(element.pattern);\n return true;\n });\n\n return { elements: uniqueElements, isMonorepo };\n}\n","/**\n * Dependency-cruiser config generator\n *\n * Generates dependency-cruiser configuration from detected architecture.\n * Used by `safeword sync-config` command and `/audit` slash command.\n */\n\nimport nodePath from 'node:path';\n\nimport type { DetectedArchitecture } from './boundaries.js';\nimport { readJson } from './fs.js';\n\nexport interface DependencyCruiseArchitecture 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 generateDependencyCruiseConfigFile(arch: DependencyCruiseArchitecture): string {\n const monorepoRules = arch.workspaces ? generateMonorepoRules(arch.workspaces) : '';\n const hasMonorepoRules = monorepoRules.length > 0;\n\n return String.raw`module.exports = {\n forbidden: [\n // =========================================================================\n // ERROR RULES (block on violations)\n // =========================================================================\n {\n name: 'no-circular',\n // Runtime cycles cause initialization-order bugs and make code hard to reason about.\n // Type-only edges (import type) are erased at compile time and cannot cause runtime\n // cycles - TypeScript designed import type for exactly this case, and depcruise\n // documents viaOnly + dependencyTypesNot: ['type-only'] as the canonical opt-in.\n comment: 'Circular dependencies cause runtime issues and make code hard to reason about',\n severity: 'error',\n from: {},\n to: { circular: true, viaOnly: { dependencyTypesNot: ['type-only'] } },\n },\n {\n name: 'no-deprecated-deps',\n comment: 'Deprecated npm packages should be replaced - they may have security issues or be unmaintained',\n severity: 'error',\n from: {},\n to: { dependencyTypes: ['deprecated'] },\n },${hasMonorepoRules ? `\\n${monorepoRules},` : ''}\n\n // =========================================================================\n // WARNING RULES (flag issues but don't block)\n // =========================================================================\n {\n name: 'no-dev-deps-in-src',\n comment: 'Production code should not import devDependencies - may cause runtime failures',\n severity: 'warn',\n from: {\n path: ['^src', '^packages/[^/]+/src'],\n pathNot: '\\\\.test\\\\.[tj]sx?$',\n },\n to: { dependencyTypes: ['npm-dev'] },\n },\n {\n name: 'no-orphans',\n comment: 'Orphan modules are not imported anywhere - may be dead code',\n severity: 'warn',\n from: {\n orphan: true,\n pathNot: [\n // Entry points\n '(^|/)index\\\\.[tj]sx?$',\n '(^|/)main\\\\.[tj]sx?$',\n '(^|/)cli\\\\.[tj]s$',\n '(^|/)cucumber\\\\.mjs$',\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 generateDependencyCruiseMainConfig(): 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","result"]}
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
listItem,
|
|
4
4
|
success,
|
|
5
5
|
warn
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-I2GV5QKO.js";
|
|
7
7
|
|
|
8
8
|
// src/utils/install.ts
|
|
9
9
|
import { execFileSync } from "child_process";
|
|
@@ -16,7 +16,14 @@ var PM_COMMANDS = {
|
|
|
16
16
|
pnpm: { install: "add", uninstall: "remove" },
|
|
17
17
|
bun: { install: "add", uninstall: "remove" }
|
|
18
18
|
};
|
|
19
|
+
function isPnpmWorkspace(cwd) {
|
|
20
|
+
return existsSync(path.join(cwd, "pnpm-workspace.yaml"));
|
|
21
|
+
}
|
|
22
|
+
function pnpmWorkspaceFlags(pm, cwd) {
|
|
23
|
+
return pm === "pnpm" && isPnpmWorkspace(cwd) ? ["-w"] : [];
|
|
24
|
+
}
|
|
19
25
|
function detectPackageManager(cwd) {
|
|
26
|
+
if (isPnpmWorkspace(cwd)) return "pnpm";
|
|
20
27
|
if (existsSync(path.join(cwd, "bun.lockb")) || existsSync(path.join(cwd, "bun.lock")))
|
|
21
28
|
return "bun";
|
|
22
29
|
if (existsSync(path.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
@@ -25,21 +32,30 @@ function detectPackageManager(cwd) {
|
|
|
25
32
|
if (process.versions.bun) return "bun";
|
|
26
33
|
return "npm";
|
|
27
34
|
}
|
|
28
|
-
function getUninstallCommand(
|
|
35
|
+
function getUninstallCommand(packages, cwd) {
|
|
36
|
+
const pm = detectPackageManager(cwd);
|
|
29
37
|
const { uninstall } = PM_COMMANDS[pm];
|
|
30
|
-
|
|
38
|
+
const extraFlags = pnpmWorkspaceFlags(pm, cwd);
|
|
39
|
+
const flagString = extraFlags.length > 0 ? ` ${extraFlags.join(" ")}` : "";
|
|
40
|
+
return `${pm} ${uninstall}${flagString} ${packages.join(" ")}`;
|
|
31
41
|
}
|
|
32
42
|
function installDependencies(cwd, packages, label = "packages") {
|
|
33
43
|
if (packages.length === 0) return;
|
|
34
44
|
if (process.env.SAFEWORD_SKIP_INSTALL) return;
|
|
35
45
|
const pm = detectPackageManager(cwd);
|
|
36
46
|
const { install } = PM_COMMANDS[pm];
|
|
37
|
-
const
|
|
47
|
+
const extraFlags = pnpmWorkspaceFlags(pm, cwd);
|
|
48
|
+
const flagString = extraFlags.length > 0 ? ` ${extraFlags.join(" ")}` : "";
|
|
49
|
+
const displayCommand = `${pm} ${install} ${DEV_FLAG}${flagString} ${packages.join(" ")}`;
|
|
38
50
|
info(`
|
|
39
51
|
Installing ${label}...`);
|
|
40
52
|
info(`Running: ${displayCommand}`);
|
|
41
53
|
try {
|
|
42
|
-
execFileSync(pm, [install, DEV_FLAG, ...packages], {
|
|
54
|
+
execFileSync(pm, [install, DEV_FLAG, ...extraFlags, ...packages], {
|
|
55
|
+
cwd,
|
|
56
|
+
stdio: "pipe",
|
|
57
|
+
timeout: 12e4
|
|
58
|
+
});
|
|
43
59
|
success(`Installed ${label}`);
|
|
44
60
|
} catch {
|
|
45
61
|
warn(`Failed to install ${label}. Run manually:`);
|
|
@@ -48,8 +64,7 @@ Installing ${label}...`);
|
|
|
48
64
|
}
|
|
49
65
|
var MCP_SERVERS = {
|
|
50
66
|
context7: {
|
|
51
|
-
|
|
52
|
-
args: ["@upstash/context7-mcp@latest"]
|
|
67
|
+
url: "https://mcp.context7.com/mcp"
|
|
53
68
|
},
|
|
54
69
|
playwright: {
|
|
55
70
|
command: "bunx",
|
|
@@ -63,4 +78,4 @@ export {
|
|
|
63
78
|
installDependencies,
|
|
64
79
|
MCP_SERVERS
|
|
65
80
|
};
|
|
66
|
-
//# sourceMappingURL=chunk-
|
|
81
|
+
//# sourceMappingURL=chunk-GT7KMCFG.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils/install.ts"],"sourcesContent":["/**\n * Shared installation utilities\n *\n * Package manager detection and MCP server constants.\n * Operations are handled by reconcile() in src/reconcile.ts.\n */\n\nimport { execFileSync } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport path from 'node:path';\n\nimport { info, listItem, success, warn } from './output.js';\n\ntype PackageManager = 'npm' | 'yarn' | 'pnpm' | 'bun';\n\n/** Dev-dependency flag, shared across all package managers. */\nconst DEV_FLAG = '-D';\n\n/**\n * Package manager command definitions.\n * Single source of truth for install/uninstall args across all managers.\n */\nconst PM_COMMANDS: Record<PackageManager, { install: string; uninstall: string }> = {\n npm: { install: 'install', uninstall: 'uninstall' },\n yarn: { install: 'add', uninstall: 'remove' },\n pnpm: { install: 'add', uninstall: 'remove' },\n bun: { install: 'add', uninstall: 'remove' },\n};\n\nfunction isPnpmWorkspace(cwd: string): boolean {\n return existsSync(path.join(cwd, 'pnpm-workspace.yaml'));\n}\n\nfunction pnpmWorkspaceFlags(pm: PackageManager, cwd: string): string[] {\n return pm === 'pnpm' && isPnpmWorkspace(cwd) ? ['-w'] : [];\n}\n\n/**\n * Detect package manager by lockfile and workspace config.\n * pnpm-workspace.yaml takes priority over bun.lockb — catalog: protocol\n * requires pnpm even when a bun lockfile also exists.\n */\nexport function detectPackageManager(cwd: string): PackageManager {\n if (isPnpmWorkspace(cwd)) return 'pnpm';\n if (existsSync(path.join(cwd, 'bun.lockb')) || existsSync(path.join(cwd, 'bun.lock')))\n return 'bun';\n if (existsSync(path.join(cwd, 'pnpm-lock.yaml'))) return 'pnpm';\n if (existsSync(path.join(cwd, 'yarn.lock'))) return 'yarn';\n if (existsSync(path.join(cwd, 'package-lock.json'))) return 'npm';\n // No lockfile found — fall back to current runtime (bun) or npm\n if (process.versions.bun) return 'bun';\n return 'npm';\n}\n\nexport function getUninstallCommand(packages: string[], cwd: string): string {\n const pm = detectPackageManager(cwd);\n const { uninstall } = PM_COMMANDS[pm];\n const extraFlags = pnpmWorkspaceFlags(pm, cwd);\n const flagString = extraFlags.length > 0 ? ` ${extraFlags.join(' ')}` : '';\n return `${pm} ${uninstall}${flagString} ${packages.join(' ')}`;\n}\n\nexport function installDependencies(cwd: string, packages: string[], label = 'packages'): void {\n if (packages.length === 0) return;\n if (process.env.SAFEWORD_SKIP_INSTALL) return;\n\n const pm = detectPackageManager(cwd);\n const { install } = PM_COMMANDS[pm];\n // pnpm workspaces require -w to install at the workspace root\n const extraFlags = pnpmWorkspaceFlags(pm, cwd);\n const flagString = extraFlags.length > 0 ? ` ${extraFlags.join(' ')}` : '';\n const displayCommand = `${pm} ${install} ${DEV_FLAG}${flagString} ${packages.join(' ')}`;\n\n info(`\\nInstalling ${label}...`);\n info(`Running: ${displayCommand}`);\n\n try {\n execFileSync(pm, [install, DEV_FLAG, ...extraFlags, ...packages], {\n cwd,\n stdio: 'pipe',\n timeout: 120_000,\n });\n success(`Installed ${label}`);\n } catch {\n warn(`Failed to install ${label}. Run manually:`);\n listItem(displayCommand);\n }\n}\n\n/**\n * MCP servers installed by safeword\n */\nexport const MCP_SERVERS = {\n context7: {\n url: 'https://mcp.context7.com/mcp',\n },\n playwright: {\n command: 'bunx',\n args: ['@playwright/mcp@latest'],\n },\n} as const;\n"],"mappings":";;;;;;;;AAOA,SAAS,oBAAoB;AAC7B,SAAS,kBAAkB;AAC3B,OAAO,UAAU;AAOjB,IAAM,WAAW;AAMjB,IAAM,cAA8E;AAAA,EAClF,KAAK,EAAE,SAAS,WAAW,WAAW,YAAY;AAAA,EAClD,MAAM,EAAE,SAAS,OAAO,WAAW,SAAS;AAAA,EAC5C,MAAM,EAAE,SAAS,OAAO,WAAW,SAAS;AAAA,EAC5C,KAAK,EAAE,SAAS,OAAO,WAAW,SAAS;AAC7C;AAEA,SAAS,gBAAgB,KAAsB;AAC7C,SAAO,WAAW,KAAK,KAAK,KAAK,qBAAqB,CAAC;AACzD;AAEA,SAAS,mBAAmB,IAAoB,KAAuB;AACrE,SAAO,OAAO,UAAU,gBAAgB,GAAG,IAAI,CAAC,IAAI,IAAI,CAAC;AAC3D;AAOO,SAAS,qBAAqB,KAA6B;AAChE,MAAI,gBAAgB,GAAG,EAAG,QAAO;AACjC,MAAI,WAAW,KAAK,KAAK,KAAK,WAAW,CAAC,KAAK,WAAW,KAAK,KAAK,KAAK,UAAU,CAAC;AAClF,WAAO;AACT,MAAI,WAAW,KAAK,KAAK,KAAK,gBAAgB,CAAC,EAAG,QAAO;AACzD,MAAI,WAAW,KAAK,KAAK,KAAK,WAAW,CAAC,EAAG,QAAO;AACpD,MAAI,WAAW,KAAK,KAAK,KAAK,mBAAmB,CAAC,EAAG,QAAO;AAE5D,MAAI,QAAQ,SAAS,IAAK,QAAO;AACjC,SAAO;AACT;AAEO,SAAS,oBAAoB,UAAoB,KAAqB;AAC3E,QAAM,KAAK,qBAAqB,GAAG;AACnC,QAAM,EAAE,UAAU,IAAI,YAAY,EAAE;AACpC,QAAM,aAAa,mBAAmB,IAAI,GAAG;AAC7C,QAAM,aAAa,WAAW,SAAS,IAAI,IAAI,WAAW,KAAK,GAAG,CAAC,KAAK;AACxE,SAAO,GAAG,EAAE,IAAI,SAAS,GAAG,UAAU,IAAI,SAAS,KAAK,GAAG,CAAC;AAC9D;AAEO,SAAS,oBAAoB,KAAa,UAAoB,QAAQ,YAAkB;AAC7F,MAAI,SAAS,WAAW,EAAG;AAC3B,MAAI,QAAQ,IAAI,sBAAuB;AAEvC,QAAM,KAAK,qBAAqB,GAAG;AACnC,QAAM,EAAE,QAAQ,IAAI,YAAY,EAAE;AAElC,QAAM,aAAa,mBAAmB,IAAI,GAAG;AAC7C,QAAM,aAAa,WAAW,SAAS,IAAI,IAAI,WAAW,KAAK,GAAG,CAAC,KAAK;AACxE,QAAM,iBAAiB,GAAG,EAAE,IAAI,OAAO,IAAI,QAAQ,GAAG,UAAU,IAAI,SAAS,KAAK,GAAG,CAAC;AAEtF,OAAK;AAAA,aAAgB,KAAK,KAAK;AAC/B,OAAK,YAAY,cAAc,EAAE;AAEjC,MAAI;AACF,iBAAa,IAAI,CAAC,SAAS,UAAU,GAAG,YAAY,GAAG,QAAQ,GAAG;AAAA,MAChE;AAAA,MACA,OAAO;AAAA,MACP,SAAS;AAAA,IACX,CAAC;AACD,YAAQ,aAAa,KAAK,EAAE;AAAA,EAC9B,QAAQ;AACN,SAAK,qBAAqB,KAAK,iBAAiB;AAChD,aAAS,cAAc;AAAA,EACzB;AACF;AAKO,IAAM,cAAc;AAAA,EACzB,UAAU;AAAA,IACR,KAAK;AAAA,EACP;AAAA,EACA,YAAY;AAAA,IACV,SAAS;AAAA,IACT,MAAM,CAAC,wBAAwB;AAAA,EACjC;AACF;","names":[]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
isDirectory,
|
|
3
3
|
readFileSafe
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-I2GV5QKO.js";
|
|
5
5
|
|
|
6
6
|
// src/utils/configured-paths.ts
|
|
7
7
|
import nodePath from "path";
|
|
@@ -117,4 +117,4 @@ export {
|
|
|
117
117
|
defaultConfiguredPath,
|
|
118
118
|
resolveConfiguredPath
|
|
119
119
|
};
|
|
120
|
-
//# sourceMappingURL=chunk-
|
|
120
|
+
//# sourceMappingURL=chunk-HN3ZZETN.js.map
|
|
@@ -150,9 +150,9 @@ function makeScriptsExecutable(dirPath) {
|
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
function readJson(path) {
|
|
153
|
-
const content = readFileSafe(path);
|
|
154
|
-
if (!content) return void 0;
|
|
155
153
|
try {
|
|
154
|
+
const content = readFileSafe(path);
|
|
155
|
+
if (!content) return void 0;
|
|
156
156
|
return JSON.parse(content);
|
|
157
157
|
} catch {
|
|
158
158
|
return void 0;
|
|
@@ -188,6 +188,12 @@ ${title}`);
|
|
|
188
188
|
function listItem(item, indent = 2) {
|
|
189
189
|
console.log(`${" ".repeat(indent)}\u2022 ${item}`);
|
|
190
190
|
}
|
|
191
|
+
function printReconcileWarnings(warnings) {
|
|
192
|
+
if (warnings.length === 0) return;
|
|
193
|
+
warn(`
|
|
194
|
+
${warnings.length} config(s) could not be updated:`);
|
|
195
|
+
for (const message of warnings) listItem(message);
|
|
196
|
+
}
|
|
191
197
|
function keyValue(key, value) {
|
|
192
198
|
console.log(` ${key}: ${value}`);
|
|
193
199
|
}
|
|
@@ -214,6 +220,7 @@ export {
|
|
|
214
220
|
error,
|
|
215
221
|
header,
|
|
216
222
|
listItem,
|
|
223
|
+
printReconcileWarnings,
|
|
217
224
|
keyValue
|
|
218
225
|
};
|
|
219
|
-
//# sourceMappingURL=chunk-
|
|
226
|
+
//# sourceMappingURL=chunk-I2GV5QKO.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/utils/fs.ts","../src/utils/output.ts"],"sourcesContent":["/**\n * File system utilities for CLI operations\n */\n\nimport {\n chmodSync,\n existsSync,\n mkdirSync,\n readdirSync,\n readFileSync,\n rmdirSync,\n rmSync,\n statSync,\n writeFileSync,\n} from 'node:fs';\nimport nodePath from 'node:path';\n\n// Get the directory of this module (for locating templates)\nconst __dirname = import.meta.dirname;\n\n/**\n * Get path to bundled templates directory.\n * Works in both development (src/) and production (dist/) contexts.\n *\n * Note: We check for SAFEWORD.md to distinguish from src/templates/ which\n * contains TypeScript source files (config.ts, content.ts).\n *\n * Path resolution (bundled with tsup):\n * - From dist/chunk-*.js: __dirname = packages/cli/dist/ → ../templates\n */\nexport function getTemplatesDirectory(): string {\n const knownTemplateFile = 'SAFEWORD.md';\n\n // Try different relative paths - the bundled code ends up in dist/ directly (flat)\n // while source is in src/utils/\n const candidates = [\n nodePath.join(__dirname, '..', 'templates'), // From dist/ (flat bundled)\n nodePath.join(__dirname, '..', '..', 'templates'), // From src/utils/ or dist/utils/\n nodePath.join(__dirname, 'templates'), // Direct sibling (unlikely but safe)\n ];\n\n for (const candidate of candidates) {\n if (existsSync(nodePath.join(candidate, knownTemplateFile))) {\n return candidate;\n }\n }\n\n throw new Error('Templates directory not found');\n}\n\n/**\n * Check if a path exists\n * @param path\n */\n/** True when `path` exists and is a directory (a file is not). */\nexport function isDirectory(path: string): boolean {\n try {\n return statSync(path).isDirectory();\n } catch {\n return false;\n }\n}\n\nexport function exists(path: string): boolean {\n return existsSync(path);\n}\n\n/**\n * Directories to exclude when scanning subdirectories for language manifests.\n * These contain vendored/generated files that would cause false positives.\n */\nconst SUBDIRECTORY_EXCLUDE = new Set([\n 'node_modules',\n '.git',\n '.safeword',\n 'vendor',\n 'dist',\n 'build',\n 'target',\n 'coverage',\n 'dbt_packages',\n 'out',\n '.next',\n '.nuxt',\n '.output',\n '__pycache__',\n '.venv',\n 'venv',\n]);\n\n/**\n * Check if a file exists anywhere in the project tree.\n * Recursively scans subdirectories, skipping excluded directories.\n *\n * @param cwd - Project root directory\n * @param filename - File to search for (e.g., 'pyproject.toml')\n * @returns true if found anywhere in the project tree\n */\nexport function existsInTree(cwd: string, filename: string): boolean {\n return findInTree(cwd, filename) !== undefined;\n}\n\n/**\n * Find a file anywhere in the project tree.\n * Recursively scans subdirectories, skipping excluded directories.\n * Root is checked first; deeper matches use depth-first traversal.\n *\n * @param cwd - Project root directory\n * @param filename - File to search for (e.g., 'pyproject.toml')\n * @param maxDepth - Maximum directory depth to scan (default: 10)\n * @returns Directory path where file was found, or undefined\n */\nexport function findInTree(cwd: string, filename: string, maxDepth = 10): string | undefined {\n // Check root first\n if (existsSync(nodePath.join(cwd, filename))) {\n return cwd;\n }\n\n return scanTreeForFile(cwd, filename, 1, maxDepth);\n}\n\n/** Return scannable subdirectory paths (excludes hidden dirs and SUBDIRECTORY_EXCLUDE). */\nfunction getScannableSubdirectories(directory: string): string[] {\n try {\n return readdirSync(directory, { withFileTypes: true })\n .filter(\n entry =>\n entry.isDirectory() &&\n !entry.name.startsWith('.') &&\n !SUBDIRECTORY_EXCLUDE.has(entry.name),\n )\n .map(entry => nodePath.join(directory, entry.name));\n } catch {\n return [];\n }\n}\n\nfunction scanTreeForFile(\n directory: string,\n filename: string,\n depth: number,\n maxDepth: number,\n): string | undefined {\n if (depth > maxDepth) return undefined;\n\n const subdirectories = getScannableSubdirectories(directory);\n\n // Check all children at this level first\n for (const subdirectory of subdirectories) {\n if (existsSync(nodePath.join(subdirectory, filename))) {\n return subdirectory;\n }\n }\n\n // Then recurse deeper\n for (const subdirectory of subdirectories) {\n const result = scanTreeForFile(subdirectory, filename, depth + 1, maxDepth);\n if (result !== undefined) return result;\n }\n\n return undefined;\n}\n\n/**\n * Index multiple files in a single tree walk — the multi-file analogue of\n * `findInTree`. Returns a Map from each requested filename to the directory of\n * its **shallowest** occurrence (root-first, level-order — same ordering as\n * `findInTree`), omitting names not found. One traversal regardless of how many\n * filenames are requested, so callers that probe many manifests stay O(one walk)\n * even on large/deep monorepos. Stops early once every name is located.\n */\nexport function indexFilesInTree(\n cwd: string,\n filenames: Iterable<string>,\n maxDepth = 10,\n): Map<string, string> {\n const wanted = [...filenames];\n const found = new Map<string, string>();\n // Cursor instead of Array.shift() so the BFS stays O(dirs), not O(dirs²) on\n // wide/deep monorepos — the very repos this single-walk index exists to serve.\n const queue: { directory: string; depth: number }[] = [{ directory: cwd, depth: 0 }];\n\n let head = 0;\n while (head < queue.length) {\n const item = queue[head];\n head += 1;\n if (item === undefined) break;\n for (const name of wanted) {\n if (!found.has(name) && existsSync(nodePath.join(item.directory, name))) {\n found.set(name, item.directory);\n }\n }\n if (found.size === wanted.length) break;\n if (item.depth >= maxDepth) continue;\n for (const subdirectory of getScannableSubdirectories(item.directory)) {\n queue.push({ directory: subdirectory, depth: item.depth + 1 });\n }\n }\n return found;\n}\n\n/**\n * Create directory recursively\n * @param path\n */\nexport function ensureDirectory(path: string): void {\n if (!existsSync(path)) {\n mkdirSync(path, { recursive: true });\n }\n}\n\n/**\n * Read file as string\n * @param path\n */\nexport function readFile(path: string): string {\n return readFileSync(path, 'utf8');\n}\n\n/**\n * Read file as string, return null if not exists\n * @param path\n */\nexport function readFileSafe(path: string): string | undefined {\n if (!existsSync(path)) return undefined;\n return readFileSync(path, 'utf8');\n}\n\n/**\n * Write file, creating parent directories if needed\n * @param path\n * @param content\n */\nexport function writeFile(path: string, content: string): void {\n ensureDirectory(nodePath.dirname(path));\n writeFileSync(path, content);\n}\n\n/**\n * Remove file or directory recursively\n * @param path\n */\nexport function remove(path: string): void {\n if (existsSync(path)) {\n rmSync(path, { recursive: true, force: true });\n }\n}\n\n/**\n * Remove directory only if empty, returns true if removed\n * @param path\n */\nexport function removeIfEmpty(path: string): boolean {\n if (!existsSync(path)) return false;\n try {\n rmdirSync(path); // Non-recursive, throws if not empty\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Make all shell scripts in a directory executable\n * @param dirPath\n */\nexport function makeScriptsExecutable(dirPath: string): void {\n if (!existsSync(dirPath)) return;\n for (const file of readdirSync(dirPath)) {\n if (file.endsWith('.sh')) {\n chmodSync(nodePath.join(dirPath, file), 0o755);\n }\n }\n}\n\n/**\n * Read JSON file\n * @param path\n */\nexport function readJson(path: string): unknown {\n const content = readFileSafe(path);\n if (!content) return undefined;\n try {\n return JSON.parse(content) as unknown;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Write JSON file with formatting\n * @param path\n * @param data\n */\nexport function writeJson(path: string, data: unknown): void {\n writeFile(path, `${JSON.stringify(data, undefined, 2)}\\n`);\n}\n","/**\n * Console output utilities for consistent CLI messaging\n */\n\n/**\n * Print info message\n * @param message\n */\nexport function info(message: string): void {\n console.log(message);\n}\n\n/**\n * Compose a glyph-prefixed line, hoisting any leading newlines ABOVE the glyph\n * so blank-line spacing renders before the marker instead of orphaning it on\n * its own line (ticket 469YSR). `('✓', '\\nFoo')` → `'\\n✓ Foo'`.\n * @param glyph the status glyph (✓ / ⚠ / ✗)\n * @param message the message, which may start with newline(s) for spacing\n */\nexport function formatGlyphLine(glyph: string, message: string): string {\n const leadingNewlines = /^\\n*/.exec(message)?.[0] ?? '';\n return `${leadingNewlines}${glyph} ${message.slice(leadingNewlines.length)}`;\n}\n\n/**\n * Print success message\n * @param message\n */\nexport function success(message: string): void {\n console.log(formatGlyphLine('✓', message));\n}\n\n/**\n * Print warning message\n * @param message\n */\nexport function warn(message: string): void {\n console.warn(formatGlyphLine('⚠', message));\n}\n\n/**\n * Print error message to stderr\n * @param message\n */\nexport function error(message: string): void {\n console.error(formatGlyphLine('✗', message));\n}\n\n/**\n * Print a section header\n * @param title\n */\nexport function header(title: string): void {\n console.log(`\\n${title}`);\n console.log('─'.repeat(title.length));\n}\n\n/**\n * Print a list item\n * @param item\n * @param indent\n */\nexport function listItem(item: string, indent = 2): void {\n console.log(`${' '.repeat(indent)}• ${item}`);\n}\n\n/**\n * Print key-value pair\n * @param key\n * @param value\n */\nexport function keyValue(key: string, value: string): void {\n console.log(` ${key}: ${value}`);\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/utils/fs.ts","../src/utils/output.ts"],"sourcesContent":["/**\n * File system utilities for CLI operations\n */\n\nimport {\n chmodSync,\n existsSync,\n mkdirSync,\n readdirSync,\n readFileSync,\n rmdirSync,\n rmSync,\n statSync,\n writeFileSync,\n} from 'node:fs';\nimport nodePath from 'node:path';\n\n// Get the directory of this module (for locating templates)\nconst __dirname = import.meta.dirname;\n\n/**\n * Get path to bundled templates directory.\n * Works in both development (src/) and production (dist/) contexts.\n *\n * Note: We check for SAFEWORD.md to distinguish from src/templates/ which\n * contains TypeScript source files (config.ts, content.ts).\n *\n * Path resolution (bundled with tsup):\n * - From dist/chunk-*.js: __dirname = packages/cli/dist/ → ../templates\n */\nexport function getTemplatesDirectory(): string {\n const knownTemplateFile = 'SAFEWORD.md';\n\n // Try different relative paths - the bundled code ends up in dist/ directly (flat)\n // while source is in src/utils/\n const candidates = [\n nodePath.join(__dirname, '..', 'templates'), // From dist/ (flat bundled)\n nodePath.join(__dirname, '..', '..', 'templates'), // From src/utils/ or dist/utils/\n nodePath.join(__dirname, 'templates'), // Direct sibling (unlikely but safe)\n ];\n\n for (const candidate of candidates) {\n if (existsSync(nodePath.join(candidate, knownTemplateFile))) {\n return candidate;\n }\n }\n\n throw new Error('Templates directory not found');\n}\n\n/**\n * Check if a path exists\n * @param path\n */\n/** True when `path` exists and is a directory (a file is not). */\nexport function isDirectory(path: string): boolean {\n try {\n return statSync(path).isDirectory();\n } catch {\n return false;\n }\n}\n\nexport function exists(path: string): boolean {\n return existsSync(path);\n}\n\n/**\n * Directories to exclude when scanning subdirectories for language manifests.\n * These contain vendored/generated files that would cause false positives.\n */\nconst SUBDIRECTORY_EXCLUDE = new Set([\n 'node_modules',\n '.git',\n '.safeword',\n 'vendor',\n 'dist',\n 'build',\n 'target',\n 'coverage',\n 'dbt_packages',\n 'out',\n '.next',\n '.nuxt',\n '.output',\n '__pycache__',\n '.venv',\n 'venv',\n]);\n\n/**\n * Check if a file exists anywhere in the project tree.\n * Recursively scans subdirectories, skipping excluded directories.\n *\n * @param cwd - Project root directory\n * @param filename - File to search for (e.g., 'pyproject.toml')\n * @returns true if found anywhere in the project tree\n */\nexport function existsInTree(cwd: string, filename: string): boolean {\n return findInTree(cwd, filename) !== undefined;\n}\n\n/**\n * Find a file anywhere in the project tree.\n * Recursively scans subdirectories, skipping excluded directories.\n * Root is checked first; deeper matches use depth-first traversal.\n *\n * @param cwd - Project root directory\n * @param filename - File to search for (e.g., 'pyproject.toml')\n * @param maxDepth - Maximum directory depth to scan (default: 10)\n * @returns Directory path where file was found, or undefined\n */\nexport function findInTree(cwd: string, filename: string, maxDepth = 10): string | undefined {\n // Check root first\n if (existsSync(nodePath.join(cwd, filename))) {\n return cwd;\n }\n\n return scanTreeForFile(cwd, filename, 1, maxDepth);\n}\n\n/** Return scannable subdirectory paths (excludes hidden dirs and SUBDIRECTORY_EXCLUDE). */\nfunction getScannableSubdirectories(directory: string): string[] {\n try {\n return readdirSync(directory, { withFileTypes: true })\n .filter(\n entry =>\n entry.isDirectory() &&\n !entry.name.startsWith('.') &&\n !SUBDIRECTORY_EXCLUDE.has(entry.name),\n )\n .map(entry => nodePath.join(directory, entry.name));\n } catch {\n return [];\n }\n}\n\nfunction scanTreeForFile(\n directory: string,\n filename: string,\n depth: number,\n maxDepth: number,\n): string | undefined {\n if (depth > maxDepth) return undefined;\n\n const subdirectories = getScannableSubdirectories(directory);\n\n // Check all children at this level first\n for (const subdirectory of subdirectories) {\n if (existsSync(nodePath.join(subdirectory, filename))) {\n return subdirectory;\n }\n }\n\n // Then recurse deeper\n for (const subdirectory of subdirectories) {\n const result = scanTreeForFile(subdirectory, filename, depth + 1, maxDepth);\n if (result !== undefined) return result;\n }\n\n return undefined;\n}\n\n/**\n * Index multiple files in a single tree walk — the multi-file analogue of\n * `findInTree`. Returns a Map from each requested filename to the directory of\n * its **shallowest** occurrence (root-first, level-order — same ordering as\n * `findInTree`), omitting names not found. One traversal regardless of how many\n * filenames are requested, so callers that probe many manifests stay O(one walk)\n * even on large/deep monorepos. Stops early once every name is located.\n */\nexport function indexFilesInTree(\n cwd: string,\n filenames: Iterable<string>,\n maxDepth = 10,\n): Map<string, string> {\n const wanted = [...filenames];\n const found = new Map<string, string>();\n // Cursor instead of Array.shift() so the BFS stays O(dirs), not O(dirs²) on\n // wide/deep monorepos — the very repos this single-walk index exists to serve.\n const queue: { directory: string; depth: number }[] = [{ directory: cwd, depth: 0 }];\n\n let head = 0;\n while (head < queue.length) {\n const item = queue[head];\n head += 1;\n if (item === undefined) break;\n for (const name of wanted) {\n if (!found.has(name) && existsSync(nodePath.join(item.directory, name))) {\n found.set(name, item.directory);\n }\n }\n if (found.size === wanted.length) break;\n if (item.depth >= maxDepth) continue;\n for (const subdirectory of getScannableSubdirectories(item.directory)) {\n queue.push({ directory: subdirectory, depth: item.depth + 1 });\n }\n }\n return found;\n}\n\n/**\n * Create directory recursively\n * @param path\n */\nexport function ensureDirectory(path: string): void {\n if (!existsSync(path)) {\n mkdirSync(path, { recursive: true });\n }\n}\n\n/**\n * Read file as string\n * @param path\n */\nexport function readFile(path: string): string {\n return readFileSync(path, 'utf8');\n}\n\n/**\n * Read file as string, return null if not exists\n * @param path\n */\nexport function readFileSafe(path: string): string | undefined {\n if (!existsSync(path)) return undefined;\n return readFileSync(path, 'utf8');\n}\n\n/**\n * Write file, creating parent directories if needed\n * @param path\n * @param content\n */\nexport function writeFile(path: string, content: string): void {\n ensureDirectory(nodePath.dirname(path));\n writeFileSync(path, content);\n}\n\n/**\n * Remove file or directory recursively\n * @param path\n */\nexport function remove(path: string): void {\n if (existsSync(path)) {\n rmSync(path, { recursive: true, force: true });\n }\n}\n\n/**\n * Remove directory only if empty, returns true if removed\n * @param path\n */\nexport function removeIfEmpty(path: string): boolean {\n if (!existsSync(path)) return false;\n try {\n rmdirSync(path); // Non-recursive, throws if not empty\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Make all shell scripts in a directory executable\n * @param dirPath\n */\nexport function makeScriptsExecutable(dirPath: string): void {\n if (!existsSync(dirPath)) return;\n for (const file of readdirSync(dirPath)) {\n if (file.endsWith('.sh')) {\n chmodSync(nodePath.join(dirPath, file), 0o755);\n }\n }\n}\n\n/**\n * Read and parse a JSON file, best-effort: returns `undefined` when no JSON\n * value can be obtained from the path — whether it is absent, empty, unparseable\n * (e.g. JSONC comments or malformed), or unreadable as a file at all. The read is\n * inside the `try` so a directory at the path (`EISDIR`) or a permission error\n * (`EACCES`) degrades to `undefined` like a parse failure, instead of throwing\n * uncaught and crashing callers (e.g. reconcile's jsonMerge). All callers already\n * treat the result as `T | undefined`.\n * @param path\n */\nexport function readJson(path: string): unknown {\n try {\n const content = readFileSafe(path);\n if (!content) return undefined;\n return JSON.parse(content) as unknown;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Write JSON file with formatting\n * @param path\n * @param data\n */\nexport function writeJson(path: string, data: unknown): void {\n writeFile(path, `${JSON.stringify(data, undefined, 2)}\\n`);\n}\n","/**\n * Console output utilities for consistent CLI messaging\n */\n\n/**\n * Print info message\n * @param message\n */\nexport function info(message: string): void {\n console.log(message);\n}\n\n/**\n * Compose a glyph-prefixed line, hoisting any leading newlines ABOVE the glyph\n * so blank-line spacing renders before the marker instead of orphaning it on\n * its own line (ticket 469YSR). `('✓', '\\nFoo')` → `'\\n✓ Foo'`.\n * @param glyph the status glyph (✓ / ⚠ / ✗)\n * @param message the message, which may start with newline(s) for spacing\n */\nexport function formatGlyphLine(glyph: string, message: string): string {\n const leadingNewlines = /^\\n*/.exec(message)?.[0] ?? '';\n return `${leadingNewlines}${glyph} ${message.slice(leadingNewlines.length)}`;\n}\n\n/**\n * Print success message\n * @param message\n */\nexport function success(message: string): void {\n console.log(formatGlyphLine('✓', message));\n}\n\n/**\n * Print warning message\n * @param message\n */\nexport function warn(message: string): void {\n console.warn(formatGlyphLine('⚠', message));\n}\n\n/**\n * Print error message to stderr\n * @param message\n */\nexport function error(message: string): void {\n console.error(formatGlyphLine('✗', message));\n}\n\n/**\n * Print a section header\n * @param title\n */\nexport function header(title: string): void {\n console.log(`\\n${title}`);\n console.log('─'.repeat(title.length));\n}\n\n/**\n * Print a list item\n * @param item\n * @param indent\n */\nexport function listItem(item: string, indent = 2): void {\n console.log(`${' '.repeat(indent)}• ${item}`);\n}\n\n/**\n * Print the non-fatal config-merge warnings collected during reconcile (e.g. a\n * jsonMerge target that exists but does not parse, so the merge was skipped).\n * No-op when empty. Shared by `setup` and `upgrade` so the heading stays in sync.\n * @param warnings\n */\nexport function printReconcileWarnings(warnings: string[]): void {\n if (warnings.length === 0) return;\n warn(`\\n${warnings.length} config(s) could not be updated:`);\n for (const message of warnings) listItem(message);\n}\n\n/**\n * Print key-value pair\n * @param key\n * @param value\n */\nexport function keyValue(key: string, value: string): void {\n console.log(` ${key}: ${value}`);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
findDanglingDependencies,
|
|
3
3
|
findTicketsInCycles,
|
|
4
4
|
readTickets
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-VVWHQBTU.js";
|
|
6
6
|
import {
|
|
7
7
|
formatTicketReference
|
|
8
8
|
} from "./chunk-NHXVS5FL.js";
|
|
@@ -11,25 +11,25 @@ import {
|
|
|
11
11
|
buildCoverageReportFromFeature,
|
|
12
12
|
computeSkipMask,
|
|
13
13
|
stripInlineComments
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-YXNI7W5D.js";
|
|
15
15
|
import {
|
|
16
16
|
FeatureParseError,
|
|
17
17
|
findFeatureLineageIssues,
|
|
18
18
|
findFeatureSourcePath
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-XTLCJKGE.js";
|
|
20
20
|
import {
|
|
21
21
|
SAFEWORD_SCHEMA,
|
|
22
22
|
createProjectContext,
|
|
23
23
|
getMissingPacks,
|
|
24
24
|
reconcile
|
|
25
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-SW3D7PN7.js";
|
|
26
26
|
import {
|
|
27
27
|
defaultConfiguredPath,
|
|
28
28
|
readConfiguredDocumentationSources,
|
|
29
29
|
readConfiguredPath,
|
|
30
30
|
resolveConfiguredPath,
|
|
31
31
|
resolveTicketsDirectory
|
|
32
|
-
} from "./chunk-
|
|
32
|
+
} from "./chunk-HN3ZZETN.js";
|
|
33
33
|
import {
|
|
34
34
|
VERSION
|
|
35
35
|
} from "./chunk-HSC7TELY.js";
|
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
readFileSafe,
|
|
43
43
|
success,
|
|
44
44
|
warn
|
|
45
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-I2GV5QKO.js";
|
|
46
46
|
|
|
47
47
|
// src/health.ts
|
|
48
48
|
import { readdirSync as readdirSync2 } from "fs";
|
|
@@ -85,7 +85,7 @@ function groupByLine(entries, pick) {
|
|
|
85
85
|
}
|
|
86
86
|
function findDuplicates(grouped, kind) {
|
|
87
87
|
const issues = [];
|
|
88
|
-
for (const [value, lines] of grouped
|
|
88
|
+
for (const [value, lines] of grouped) {
|
|
89
89
|
if (lines.length <= 1) continue;
|
|
90
90
|
for (const line of lines) {
|
|
91
91
|
const others = lines.filter((other) => other !== line).join(", ");
|
|
@@ -99,15 +99,18 @@ function findDuplicates(grouped, kind) {
|
|
|
99
99
|
function groupAliasesByLine(entries) {
|
|
100
100
|
const grouped = /* @__PURE__ */ new Map();
|
|
101
101
|
for (const entry of entries) {
|
|
102
|
-
|
|
103
|
-
if (alias.length === 0) continue;
|
|
104
|
-
const lines = grouped.get(alias) ?? [];
|
|
105
|
-
lines.push(entry.lineNumber);
|
|
106
|
-
grouped.set(alias, lines);
|
|
107
|
-
}
|
|
102
|
+
addEntryAliasesToGroup(entry, grouped);
|
|
108
103
|
}
|
|
109
104
|
return grouped;
|
|
110
105
|
}
|
|
106
|
+
function addEntryAliasesToGroup(entry, grouped) {
|
|
107
|
+
for (const alias of entry.aliases) {
|
|
108
|
+
if (alias.length === 0) continue;
|
|
109
|
+
const lines = grouped.get(alias) ?? [];
|
|
110
|
+
lines.push(entry.lineNumber);
|
|
111
|
+
grouped.set(alias, lines);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
111
114
|
function findAliasShadowingTerms(entries) {
|
|
112
115
|
const termLines = /* @__PURE__ */ new Map();
|
|
113
116
|
for (const entry of entries) {
|
|
@@ -220,7 +223,7 @@ function parseGlossary(content) {
|
|
|
220
223
|
let current;
|
|
221
224
|
let activeField;
|
|
222
225
|
for (const [index, line] of lines.entries()) {
|
|
223
|
-
if (skip[index]) continue;
|
|
226
|
+
if (skip[index] === true) continue;
|
|
224
227
|
const headerName = parseTermHeader(line);
|
|
225
228
|
if (headerName !== void 0) {
|
|
226
229
|
if (current) entries.push(current);
|
|
@@ -280,7 +283,7 @@ function parsePersonas(content) {
|
|
|
280
283
|
const personas = [];
|
|
281
284
|
let current;
|
|
282
285
|
for (const [index, line] of lines.entries()) {
|
|
283
|
-
if (skip[index]) continue;
|
|
286
|
+
if (skip[index] === true) continue;
|
|
284
287
|
const header2 = parseHeaderLine(line);
|
|
285
288
|
if (header2) {
|
|
286
289
|
if (current) personas.push(current);
|
|
@@ -459,15 +462,15 @@ function findArchitectureAdvisories(cwd) {
|
|
|
459
462
|
});
|
|
460
463
|
}
|
|
461
464
|
function archAlignmentHasContent(implPlanContent) {
|
|
462
|
-
let
|
|
465
|
+
let isInSection = false;
|
|
463
466
|
const body = [];
|
|
464
467
|
for (const raw of implPlanContent.split("\n")) {
|
|
465
468
|
const line = raw.trim();
|
|
466
469
|
if (line.startsWith("## ")) {
|
|
467
|
-
|
|
470
|
+
isInSection = line.slice(3).trim().toLowerCase() === "arch alignment";
|
|
468
471
|
continue;
|
|
469
472
|
}
|
|
470
|
-
if (
|
|
473
|
+
if (isInSection && line !== "") body.push(line);
|
|
471
474
|
}
|
|
472
475
|
if (body.length === 0) return false;
|
|
473
476
|
return !(body.length === 1 && (body[0] ?? "").toLowerCase().startsWith("skip:"));
|
|
@@ -698,4 +701,4 @@ export {
|
|
|
698
701
|
checkHealth,
|
|
699
702
|
reportHealthSummary
|
|
700
703
|
};
|
|
701
|
-
//# sourceMappingURL=chunk-
|
|
704
|
+
//# sourceMappingURL=chunk-N74UNUZ6.js.map
|