safeword 0.36.0 → 0.37.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/{chunk-6O7YRM2H.js → chunk-GR7LJEJ2.js} +22 -3
- package/dist/chunk-GR7LJEJ2.js.map +1 -0
- package/dist/cli.js +4 -4
- package/dist/cli.js.map +1 -1
- package/dist/{setup-LDUKJR47.js → setup-3VT6R5JB.js} +5 -2
- package/dist/setup-3VT6R5JB.js.map +1 -0
- package/dist/{sync-config-3GZZ6CEX.js → sync-config-AFQRCLFM.js} +2 -2
- package/package.json +1 -1
- package/templates/commands/audit.md +5 -3
- package/templates/skills/audit/SKILL.md +5 -3
- package/dist/chunk-6O7YRM2H.js.map +0 -1
- package/dist/setup-LDUKJR47.js.map +0 -1
- /package/dist/{sync-config-3GZZ6CEX.js.map → sync-config-AFQRCLFM.js.map} +0 -0
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from "./chunk-VZ2E2QRM.js";
|
|
10
10
|
|
|
11
11
|
// src/commands/sync-config.ts
|
|
12
|
-
import { writeFileSync } from "fs";
|
|
12
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
13
13
|
import nodePath3 from "path";
|
|
14
14
|
|
|
15
15
|
// src/utils/boundaries.ts
|
|
@@ -262,7 +262,16 @@ function buildArchitecture(cwd) {
|
|
|
262
262
|
function hasArchitectureDetected(arch) {
|
|
263
263
|
return arch.elements.length > 0 || arch.isMonorepo || (arch.workspaces?.length ?? 0) > 0;
|
|
264
264
|
}
|
|
265
|
-
|
|
265
|
+
function checkConfig(cwd, arch) {
|
|
266
|
+
const generatedConfigPath = nodePath3.join(cwd, ".safeword", "depcruise-config.cjs");
|
|
267
|
+
if (!exists(generatedConfigPath)) {
|
|
268
|
+
return { matches: false, reason: "missing" };
|
|
269
|
+
}
|
|
270
|
+
const generated = generateDepCruiseConfigFile(arch);
|
|
271
|
+
const onDisk = readFileSync(generatedConfigPath, "utf8");
|
|
272
|
+
return generated === onDisk ? { matches: true } : { matches: false, reason: "drifted" };
|
|
273
|
+
}
|
|
274
|
+
async function syncConfig(options = {}) {
|
|
266
275
|
await Promise.resolve();
|
|
267
276
|
const cwd = process.cwd();
|
|
268
277
|
const safewordDirectory = nodePath3.join(cwd, ".safeword");
|
|
@@ -271,6 +280,16 @@ async function syncConfig() {
|
|
|
271
280
|
process.exit(1);
|
|
272
281
|
}
|
|
273
282
|
const arch = buildArchitecture(cwd);
|
|
283
|
+
if (options.check) {
|
|
284
|
+
const result2 = checkConfig(cwd, arch);
|
|
285
|
+
if (result2.matches) {
|
|
286
|
+
success("Config in sync");
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const message = result2.reason === "missing" ? "Missing .safeword/depcruise-config.cjs \u2014 run `safeword sync-config` to generate it." : "Stale .safeword/depcruise-config.cjs \u2014 run `safeword sync-config` to refresh.";
|
|
290
|
+
error(message);
|
|
291
|
+
process.exit(1);
|
|
292
|
+
}
|
|
274
293
|
const result = syncConfigCore(cwd, arch);
|
|
275
294
|
if (result.generatedConfig) {
|
|
276
295
|
info("Generated .safeword/depcruise-config.cjs");
|
|
@@ -287,4 +306,4 @@ export {
|
|
|
287
306
|
hasArchitectureDetected,
|
|
288
307
|
syncConfig
|
|
289
308
|
};
|
|
290
|
-
//# sourceMappingURL=chunk-
|
|
309
|
+
//# sourceMappingURL=chunk-GR7LJEJ2.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 DepCruiseArchitecture,\n detectWorkspaces,\n generateDepCruiseConfigFile,\n generateDepCruiseMainConfig,\n} from '../utils/depcruise-config.js';\nimport { exists } from '../utils/fs.js';\nimport { error, info, success } from '../utils/output.js';\n\ninterface SyncConfigResult {\n generatedConfig: boolean;\n createdMainConfig: boolean;\n}\n\n/**\n * Core sync logic - writes depcruise configs to disk\n * Can be called from setup or as standalone command\n */\nexport function syncConfigCore(cwd: string, arch: DepCruiseArchitecture): SyncConfigResult {\n const safewordDirectory = nodePath.join(cwd, '.safeword');\n const result: SyncConfigResult = {\n generatedConfig: false,\n createdMainConfig: false,\n };\n\n // Generate and write .safeword/depcruise-config.cjs (CJS for compatibility)\n const generatedConfigPath = nodePath.join(safewordDirectory, 'depcruise-config.cjs');\n const generatedConfig = generateDepCruiseConfigFile(arch);\n writeFileSync(generatedConfigPath, generatedConfig);\n result.generatedConfig = true;\n\n // Create main config if not exists (self-healing)\n // Use .cjs extension to work in ESM projects (type: \"module\")\n const mainConfigPath = nodePath.join(cwd, '.dependency-cruiser.cjs');\n if (!exists(mainConfigPath)) {\n const mainConfig = generateDepCruiseMainConfig();\n writeFileSync(mainConfigPath, mainConfig);\n result.createdMainConfig = true;\n }\n\n return result;\n}\n\n/**\n * Build full architecture info by combining detected layers with workspaces\n */\nexport function buildArchitecture(cwd: string): DepCruiseArchitecture {\n const arch = detectArchitecture(cwd);\n const workspaces = detectWorkspaces(cwd);\n return { ...arch, workspaces };\n}\n\n/**\n * Check if architecture was detected (layers, monorepo structure, or workspaces)\n */\nexport function hasArchitectureDetected(arch: DepCruiseArchitecture): boolean {\n return arch.elements.length > 0 || arch.isMonorepo || (arch.workspaces?.length ?? 0) > 0;\n}\n\n/**\n * 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: DepCruiseArchitecture,\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 = generateDepCruiseConfigFile(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 */\nfunction scanSearchPath(\n projectDirectory: string,\n searchPath: string,\n pathPrefix: string,\n elements: DetectedElement[],\n): void {\n for (const layerDefinition of ARCHITECTURE_LAYERS) {\n for (const dirName of layerDefinition.dirs) {\n const fullPath = nodePath.join(projectDirectory, searchPath, dirName);\n if (exists(fullPath) && !hasLayerForPrefix(elements, layerDefinition.layer, pathPrefix)) {\n elements.push({\n layer: layerDefinition.layer,\n pattern: `${pathPrefix}${dirName}/**`,\n location: `${pathPrefix}${dirName}`,\n });\n }\n }\n }\n}\n\n/**\n * Scan a directory for architecture layers\n * @param projectDirectory\n * @param basePath\n */\nfunction scanForLayers(projectDirectory: string, basePath: string): DetectedElement[] {\n const elements: DetectedElement[] = [];\n const prefix = basePath ? `${basePath}/` : '';\n\n // Check src/ and root level\n scanSearchPath(projectDirectory, nodePath.join(basePath, 'src'), `${prefix}src/`, elements);\n scanSearchPath(projectDirectory, basePath, prefix, elements);\n\n return elements;\n}\n\n/**\n * Detects architecture directories in the project\n * Handles both standard projects and monorepos\n * @param projectDirectory\n */\nexport function detectArchitecture(projectDirectory: string): DetectedArchitecture {\n const elements: DetectedElement[] = [];\n\n // First, check for monorepo packages\n const packages = findMonorepoPackages(projectDirectory);\n const isMonorepo = packages.length > 0;\n\n if (isMonorepo) {\n // Scan each package\n for (const pkg of packages) {\n elements.push(...scanForLayers(projectDirectory, pkg));\n }\n }\n\n // Also scan root level (works for both monorepo root and standard projects)\n elements.push(...scanForLayers(projectDirectory, ''));\n\n // Deduplicate by pattern\n const seen = new Set<string>();\n const uniqueElements = elements.filter(element => {\n if (seen.has(element.pattern)) return false;\n seen.add(element.pattern);\n return true;\n });\n\n return { elements: uniqueElements, isMonorepo };\n}\n","/**\n * Dependency-cruiser config generator\n *\n * Generates dependency-cruiser configuration from detected architecture.\n * Used by `safeword sync-config` command and `/audit` slash command.\n */\n\nimport nodePath from 'node:path';\n\nimport type { DetectedArchitecture } from './boundaries.js';\nimport { readJson } from './fs.js';\n\nexport interface DepCruiseArchitecture extends DetectedArchitecture {\n workspaces?: string[];\n}\n\ninterface PackageJson {\n workspaces?: string[] | { packages?: string[] };\n}\n\n/**\n * Detect workspaces from package.json\n * Supports both array format and object format (yarn workspaces)\n */\nexport function detectWorkspaces(cwd: string): string[] | undefined {\n const packageJsonPath = nodePath.join(cwd, 'package.json');\n const packageJson = readJson(packageJsonPath) as PackageJson | undefined;\n\n if (!packageJson?.workspaces) return undefined;\n\n // Handle both formats: string[] or { packages: string[] }\n const workspaces = Array.isArray(packageJson.workspaces)\n ? packageJson.workspaces\n : packageJson.workspaces.packages;\n\n return workspaces && workspaces.length > 0 ? workspaces : undefined;\n}\n\n/**\n * Generate monorepo hierarchy rules based on workspace patterns\n */\nfunction generateMonorepoRules(workspaces: string[]): string {\n const rules: string[] = [];\n\n const hasLibs = workspaces.some(w => w.startsWith('libs'));\n const hasPackages = workspaces.some(w => w.startsWith('packages'));\n const hasApps = workspaces.some(w => w.startsWith('apps'));\n\n // libs cannot import packages or apps\n if (hasLibs && (hasPackages || hasApps)) {\n rules.push(` {\n name: 'libs-cannot-import-packages-or-apps',\n severity: 'error',\n from: { path: '^libs/' },\n to: { path: '^(packages|apps)/' },\n }`);\n }\n\n // packages cannot import apps\n if (hasPackages && hasApps) {\n rules.push(` {\n name: 'packages-cannot-import-apps',\n severity: 'error',\n from: { path: '^packages/' },\n to: { path: '^apps/' },\n }`);\n }\n\n return rules.join(',\\n');\n}\n\n/**\n * Generate .safeword/depcruise-config.cjs content (forbidden rules + options)\n */\nexport function generateDepCruiseConfigFile(arch: DepCruiseArchitecture): string {\n const monorepoRules = arch.workspaces ? generateMonorepoRules(arch.workspaces) : '';\n const hasMonorepoRules = monorepoRules.length > 0;\n\n return String.raw`module.exports = {\n forbidden: [\n // =========================================================================\n // ERROR RULES (block on violations)\n // =========================================================================\n {\n name: 'no-circular',\n // Runtime cycles cause initialization-order bugs and make code hard to reason about.\n // Type-only edges (import type) are erased at compile time and cannot cause runtime\n // cycles — TypeScript designed import type for exactly this case, and depcruise\n // documents viaOnly + dependencyTypesNot: ['type-only'] as the canonical opt-in.\n comment: 'Circular dependencies cause runtime issues and make code hard to reason about',\n severity: 'error',\n from: {},\n to: { circular: true, viaOnly: { dependencyTypesNot: ['type-only'] } },\n },\n {\n name: 'no-deprecated-deps',\n comment: 'Deprecated npm packages should be replaced - they may have security issues or be unmaintained',\n severity: 'error',\n from: {},\n to: { dependencyTypes: ['deprecated'] },\n },${hasMonorepoRules ? `\\n${monorepoRules},` : ''}\n\n // =========================================================================\n // WARNING RULES (flag issues but don't block)\n // =========================================================================\n {\n name: 'no-dev-deps-in-src',\n comment: 'Production code should not import devDependencies - may cause runtime failures',\n severity: 'warn',\n from: {\n path: ['^src', '^packages/[^/]+/src'],\n pathNot: '\\\\.test\\\\.[tj]sx?$',\n },\n to: { dependencyTypes: ['npm-dev'] },\n },\n {\n name: 'no-orphans',\n comment: 'Orphan modules are not imported anywhere - may be dead code',\n severity: 'warn',\n from: {\n orphan: true,\n pathNot: [\n // Entry points\n '(^|/)index\\\\.[tj]sx?$',\n '(^|/)main\\\\.[tj]sx?$',\n '(^|/)cli\\\\.[tj]s$',\n '\\\\.config\\\\.[tj]s$',\n '\\\\.config\\\\.mjs$',\n // Test files\n '\\\\.test\\\\.[tj]sx?$',\n '\\\\.spec\\\\.[tj]sx?$',\n '/tests/',\n '/__tests__/',\n // Astro/Next.js pages and content\n '/src/content/',\n '/src/pages/',\n '/app/',\n ],\n },\n to: {},\n },\n ],\n options: {\n doNotFollow: { path: ['node_modules', '.safeword'] },\n exclude: {\n path: ['node_modules', 'dist', 'build', 'coverage', '\\\\.d\\\\.ts$'],\n },\n tsPreCompilationDeps: true,\n tsConfig: { fileName: 'tsconfig.json' },\n enhancedResolveOptions: {\n extensions: ['.ts', '.tsx', '.js', '.jsx'],\n exportsFields: ['exports'],\n conditionNames: ['import', 'require', 'node', 'default'],\n },\n },\n};\n`;\n}\n\n/**\n * Generate .dependency-cruiser.js (main config that imports generated)\n */\nexport function generateDepCruiseMainConfig(): string {\n return `/**\n * Dependency Cruiser Configuration\n *\n * Imports auto-generated rules from .safeword/depcruise-config.cjs\n * ADD YOUR CUSTOM RULES BELOW the spread operator.\n */\n\nconst generated = require('./.safeword/depcruise-config.cjs');\n\nmodule.exports = {\n forbidden: [\n ...generated.forbidden,\n // ADD YOUR CUSTOM RULES BELOW:\n // { name: 'no-legacy', from: { path: 'legacy/' }, to: { path: 'new/' } },\n ],\n options: {\n ...generated.options,\n // Your overrides here\n },\n};\n`;\n}\n"],"mappings":";;;;;;;;;;;AAOA,SAAS,cAAc,qBAAqB;AAC5C,OAAOA,eAAc;;;ACIrB,SAAS,mBAAmB;AAC5B,OAAO,cAAc;AASrB,IAAM,sBAAsB;AAAA;AAAA,EAE1B,EAAE,OAAO,SAAS,MAAM,CAAC,SAAS,cAAc,SAAS,EAAE;AAAA;AAAA,EAE3D,EAAE,OAAO,SAAS,MAAM,CAAC,SAAS,WAAW,UAAU,UAAU,MAAM,EAAE;AAAA;AAAA,EAEzE,EAAE,OAAO,OAAO,MAAM,CAAC,OAAO,WAAW,EAAE;AAAA;AAAA,EAE3C,EAAE,OAAO,SAAS,MAAM,CAAC,SAAS,aAAa,EAAE;AAAA,EACjD,EAAE,OAAO,YAAY,MAAM,CAAC,YAAY,OAAO,UAAU,OAAO,EAAE;AAAA;AAAA,EAElE,EAAE,OAAO,cAAc,MAAM,CAAC,cAAc,IAAI,EAAE;AAAA;AAAA,EAElD,EAAE,OAAO,YAAY,MAAM,CAAC,YAAY,WAAW,SAAS,EAAE;AAAA;AAAA,EAE9D,EAAE,OAAO,OAAO,MAAM,CAAC,OAAO,SAAS,SAAS,UAAU,UAAU,EAAE;AACxE;AAmBA,SAAS,qBAAqB,kBAAoC;AAChE,QAAM,WAAqB,CAAC;AAG5B,QAAM,gBAAgB,CAAC,YAAY,QAAQ,QAAQ,SAAS;AAE5D,aAAW,QAAQ,eAAe;AAChC,UAAM,WAAW,SAAS,KAAK,kBAAkB,IAAI;AACrD,QAAI,CAAC,OAAO,QAAQ,EAAG;AAEvB,QAAI;AACF,YAAM,UAAU,YAAY,UAAU,EAAE,eAAe,KAAK,CAAC;AAC7D,iBAAW,SAAS,SAAS;AAC3B,YAAI,MAAM,YAAY,KAAK,CAAC,MAAM,KAAK,WAAW,GAAG,GAAG;AACtD,mBAAS,KAAK,SAAS,KAAK,MAAM,MAAM,IAAI,CAAC;AAAA,QAC/C;AAAA,MACF;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,SAAO;AACT;AAQA,SAAS,kBAAkB,UAA6B,OAAc,YAA6B;AACjG,SAAO,SAAS;AAAA,IACd,aAAW,QAAQ,UAAU,SAAS,QAAQ,QAAQ,WAAW,UAAU;AAAA,EAC7E;AACF;AASA,SAAS,eACP,kBACA,YACA,YACA,UACM;AACN,aAAW,mBAAmB,qBAAqB;AACjD,eAAW,WAAW,gBAAgB,MAAM;AAC1C,YAAM,WAAW,SAAS,KAAK,kBAAkB,YAAY,OAAO;AACpE,UAAI,OAAO,QAAQ,KAAK,CAAC,kBAAkB,UAAU,gBAAgB,OAAO,UAAU,GAAG;AACvF,iBAAS,KAAK;AAAA,UACZ,OAAO,gBAAgB;AAAA,UACvB,SAAS,GAAG,UAAU,GAAG,OAAO;AAAA,UAChC,UAAU,GAAG,UAAU,GAAG,OAAO;AAAA,QACnC,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AACF;AAOA,SAAS,cAAc,kBAA0B,UAAqC;AACpF,QAAM,WAA8B,CAAC;AACrC,QAAM,SAAS,WAAW,GAAG,QAAQ,MAAM;AAG3C,iBAAe,kBAAkB,SAAS,KAAK,UAAU,KAAK,GAAG,GAAG,MAAM,QAAQ,QAAQ;AAC1F,iBAAe,kBAAkB,UAAU,QAAQ,QAAQ;AAE3D,SAAO;AACT;AAOO,SAAS,mBAAmB,kBAAgD;AACjF,QAAM,WAA8B,CAAC;AAGrC,QAAM,WAAW,qBAAqB,gBAAgB;AACtD,QAAM,aAAa,SAAS,SAAS;AAErC,MAAI,YAAY;AAEd,eAAW,OAAO,UAAU;AAC1B,eAAS,KAAK,GAAG,cAAc,kBAAkB,GAAG,CAAC;AAAA,IACvD;AAAA,EACF;AAGA,WAAS,KAAK,GAAG,cAAc,kBAAkB,EAAE,CAAC;AAGpD,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,iBAAiB,SAAS,OAAO,aAAW;AAChD,QAAI,KAAK,IAAI,QAAQ,OAAO,EAAG,QAAO;AACtC,SAAK,IAAI,QAAQ,OAAO;AACxB,WAAO;AAAA,EACT,CAAC;AAED,SAAO,EAAE,UAAU,gBAAgB,WAAW;AAChD;;;ACjKA,OAAOC,eAAc;AAiBd,SAAS,iBAAiB,KAAmC;AAClE,QAAM,kBAAkBC,UAAS,KAAK,KAAK,cAAc;AACzD,QAAM,cAAc,SAAS,eAAe;AAE5C,MAAI,CAAC,aAAa,WAAY,QAAO;AAGrC,QAAM,aAAa,MAAM,QAAQ,YAAY,UAAU,IACnD,YAAY,aACZ,YAAY,WAAW;AAE3B,SAAO,cAAc,WAAW,SAAS,IAAI,aAAa;AAC5D;AAKA,SAAS,sBAAsB,YAA8B;AAC3D,QAAM,QAAkB,CAAC;AAEzB,QAAM,UAAU,WAAW,KAAK,OAAK,EAAE,WAAW,MAAM,CAAC;AACzD,QAAM,cAAc,WAAW,KAAK,OAAK,EAAE,WAAW,UAAU,CAAC;AACjE,QAAM,UAAU,WAAW,KAAK,OAAK,EAAE,WAAW,MAAM,CAAC;AAGzD,MAAI,YAAY,eAAe,UAAU;AACvC,UAAM,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA,MAKT;AAAA,EACJ;AAGA,MAAI,eAAe,SAAS;AAC1B,UAAM,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA,MAKT;AAAA,EACJ;AAEA,SAAO,MAAM,KAAK,KAAK;AACzB;AAKO,SAAS,4BAA4B,MAAqC;AAC/E,QAAM,gBAAgB,KAAK,aAAa,sBAAsB,KAAK,UAAU,IAAI;AACjF,QAAM,mBAAmB,cAAc,SAAS;AAEhD,SAAO,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAsBR,mBAAmB;AAAA,EAAK,aAAa,MAAM,EAAE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAyDrD;AAKO,SAAS,8BAAsC;AACpD,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAqBT;;;AF3JO,SAAS,eAAe,KAAa,MAA+C;AACzF,QAAM,oBAAoBC,UAAS,KAAK,KAAK,WAAW;AACxD,QAAM,SAA2B;AAAA,IAC/B,iBAAiB;AAAA,IACjB,mBAAmB;AAAA,EACrB;AAGA,QAAM,sBAAsBA,UAAS,KAAK,mBAAmB,sBAAsB;AACnF,QAAM,kBAAkB,4BAA4B,IAAI;AACxD,gBAAc,qBAAqB,eAAe;AAClD,SAAO,kBAAkB;AAIzB,QAAM,iBAAiBA,UAAS,KAAK,KAAK,yBAAyB;AACnE,MAAI,CAAC,OAAO,cAAc,GAAG;AAC3B,UAAM,aAAa,4BAA4B;AAC/C,kBAAc,gBAAgB,UAAU;AACxC,WAAO,oBAAoB;AAAA,EAC7B;AAEA,SAAO;AACT;AAKO,SAAS,kBAAkB,KAAoC;AACpE,QAAM,OAAO,mBAAmB,GAAG;AACnC,QAAM,aAAa,iBAAiB,GAAG;AACvC,SAAO,EAAE,GAAG,MAAM,WAAW;AAC/B;AAKO,SAAS,wBAAwB,MAAsC;AAC5E,SAAO,KAAK,SAAS,SAAS,KAAK,KAAK,eAAe,KAAK,YAAY,UAAU,KAAK;AACzF;AAOA,SAAS,YACP,KACA,MACuE;AACvE,QAAM,sBAAsBA,UAAS,KAAK,KAAK,aAAa,sBAAsB;AAClF,MAAI,CAAC,OAAO,mBAAmB,GAAG;AAChC,WAAO,EAAE,SAAS,OAAO,QAAQ,UAAU;AAAA,EAC7C;AACA,QAAM,YAAY,4BAA4B,IAAI;AAClD,QAAM,SAAS,aAAa,qBAAqB,MAAM;AACvD,SAAO,cAAc,SAAS,EAAE,SAAS,KAAK,IAAI,EAAE,SAAS,OAAO,QAAQ,UAAU;AACxF;AAMA,eAAsB,WAAW,UAA+B,CAAC,GAAkB;AAGjF,QAAM,QAAQ,QAAQ;AACtB,QAAM,MAAM,QAAQ,IAAI;AACxB,QAAM,oBAAoBA,UAAS,KAAK,KAAK,WAAW;AAGxD,MAAI,CAAC,OAAO,iBAAiB,GAAG;AAC9B,UAAM,6CAA6C;AACnD,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,QAAM,OAAO,kBAAkB,GAAG;AAElC,MAAI,QAAQ,OAAO;AACjB,UAAMC,UAAS,YAAY,KAAK,IAAI;AACpC,QAAIA,QAAO,SAAS;AAClB,cAAQ,gBAAgB;AACxB;AAAA,IACF;AACA,UAAM,UACJA,QAAO,WAAW,YACd,6FACA;AACN,UAAM,OAAO;AACb,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,QAAM,SAAS,eAAe,KAAK,IAAI;AAEvC,MAAI,OAAO,iBAAiB;AAC1B,SAAK,0CAA0C;AAAA,EACjD;AACA,MAAI,OAAO,mBAAmB;AAC5B,SAAK,iCAAiC;AAAA,EACxC;AAEA,UAAQ,eAAe;AACzB;","names":["nodePath","nodePath","nodePath","nodePath","result"]}
|
package/dist/cli.js
CHANGED
|
@@ -12,7 +12,7 @@ program.command("setup").description("Set up safeword in the current project").o
|
|
|
12
12
|
"--no-modify",
|
|
13
13
|
"Skip auto-editing the project ESLint config (prints the manual snippet instead). Also honored via SAFEWORD_NO_MODIFY env var."
|
|
14
14
|
).action(async (options) => {
|
|
15
|
-
const { setup } = await import("./setup-
|
|
15
|
+
const { setup } = await import("./setup-3VT6R5JB.js");
|
|
16
16
|
await setup({ noModify: options.modify === false });
|
|
17
17
|
});
|
|
18
18
|
program.command("check").description("Check project health and versions").option("--offline", "Skip remote version check").action(async (options) => {
|
|
@@ -34,9 +34,9 @@ program.command("reset").description("Remove safeword configuration from project
|
|
|
34
34
|
const { reset } = await import("./reset-2KZM4CMW.js");
|
|
35
35
|
await reset(options);
|
|
36
36
|
});
|
|
37
|
-
program.command("sync-config").description("Regenerate depcruise config from current project structure").action(async () => {
|
|
38
|
-
const { syncConfig } = await import("./sync-config-
|
|
39
|
-
await syncConfig();
|
|
37
|
+
program.command("sync-config").description("Regenerate depcruise config from current project structure").option("--check", "Report drift without writing (exits non-zero on drift)").action(async (options) => {
|
|
38
|
+
const { syncConfig } = await import("./sync-config-AFQRCLFM.js");
|
|
39
|
+
await syncConfig({ check: options.check });
|
|
40
40
|
});
|
|
41
41
|
var ticket = program.command("ticket").description("Ticket management");
|
|
42
42
|
ticket.command("new <slug>").description("Create a new ticket with a Crockford Base32 ID").option("--type <type>", "Ticket type: patch, task, or feature", "task").option("--title <title>", "Ticket title (defaults to slug)").action(async (slug, options) => {
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport process from 'node:process';\n\nimport { Command } from 'commander';\n\nimport { VERSION } from './version.js';\n\nconst program = new Command();\n\nprogram\n .name('safeword')\n .description('CLI for setting up and managing safeword development environments')\n .version(VERSION);\n\nprogram\n .command('setup')\n .description('Set up safeword in the current project')\n .option('-y, --yes', 'Skip confirmation prompts (for scripting)')\n .option(\n '--no-modify',\n 'Skip auto-editing the project ESLint config (prints the manual snippet instead). Also honored via SAFEWORD_NO_MODIFY env var.',\n )\n .action(async options => {\n const { setup } = await import('./commands/setup.js');\n await setup({ noModify: options.modify === false });\n });\n\nprogram\n .command('check')\n .description('Check project health and versions')\n .option('--offline', 'Skip remote version check')\n .action(async options => {\n const { check } = await import('./commands/check.js');\n await check(options);\n });\n\nprogram\n .command('upgrade')\n .description('Upgrade safeword configuration to latest version')\n .option(\n '--no-modify',\n 'Skip auto-editing the project ESLint config (prints the manual snippet instead). Also honored via SAFEWORD_NO_MODIFY env var.',\n )\n .action(async options => {\n const { upgrade } = await import('./commands/upgrade.js');\n await upgrade({ noModify: options.modify === false });\n });\n\nprogram\n .command('diff')\n .description('Preview changes that would be made by upgrade')\n .option('-v, --verbose', 'Show full diff output')\n .action(async options => {\n const { diff } = await import('./commands/diff.js');\n await diff(options);\n });\n\nprogram\n .command('reset')\n .description('Remove safeword configuration from project')\n .option('-y, --yes', 'Skip confirmation prompt')\n .option('--full', 'Also remove linting config and uninstall packages')\n .action(async options => {\n const { reset } = await import('./commands/reset.js');\n await reset(options);\n });\n\nprogram\n .command('sync-config')\n .description('Regenerate depcruise config from current project structure')\n .action(async () => {\n const { syncConfig } = await import('./commands/sync-config.js');\n await syncConfig();\n });\n\nconst ticket = program.command('ticket').description('Ticket management');\n\nticket\n .command('new <slug>')\n .description('Create a new ticket with a Crockford Base32 ID')\n .option('--type <type>', 'Ticket type: patch, task, or feature', 'task')\n .option('--title <title>', 'Ticket title (defaults to slug)')\n .action(async (slug: string, options: { type?: string; title?: string }) => {\n const { ticketNew } = await import('./commands/ticket-new.js');\n await ticketNew(slug, options);\n });\n\nprogram\n .command('sync-learnings')\n .description('Regenerate .safeword-project/learnings/INDEX.md')\n .option('-q, --quiet', 'Suppress success output (still prints skipped-file warnings to stderr)')\n .action(async (options: { quiet?: boolean }) => {\n const { syncLearningsCommand } = await import('./commands/sync-learnings.js');\n syncLearningsCommand({ quiet: options.quiet });\n });\n\n// Show help if no arguments provided\nif (process.argv.length === 2) {\n program.help();\n}\n\n// Parse arguments\nprogram.parse();\n"],"mappings":";;;;;;AAEA,OAAO,aAAa;AAEpB,SAAS,eAAe;AAIxB,IAAM,UAAU,IAAI,QAAQ;AAE5B,QACG,KAAK,UAAU,EACf,YAAY,mEAAmE,EAC/E,QAAQ,OAAO;AAElB,QACG,QAAQ,OAAO,EACf,YAAY,wCAAwC,EACpD,OAAO,aAAa,2CAA2C,EAC/D;AAAA,EACC;AAAA,EACA;AACF,EACC,OAAO,OAAM,YAAW;AACvB,QAAM,EAAE,MAAM,IAAI,MAAM,OAAO,qBAAqB;AACpD,QAAM,MAAM,EAAE,UAAU,QAAQ,WAAW,MAAM,CAAC;AACpD,CAAC;AAEH,QACG,QAAQ,OAAO,EACf,YAAY,mCAAmC,EAC/C,OAAO,aAAa,2BAA2B,EAC/C,OAAO,OAAM,YAAW;AACvB,QAAM,EAAE,MAAM,IAAI,MAAM,OAAO,qBAAqB;AACpD,QAAM,MAAM,OAAO;AACrB,CAAC;AAEH,QACG,QAAQ,SAAS,EACjB,YAAY,kDAAkD,EAC9D;AAAA,EACC;AAAA,EACA;AACF,EACC,OAAO,OAAM,YAAW;AACvB,QAAM,EAAE,QAAQ,IAAI,MAAM,OAAO,uBAAuB;AACxD,QAAM,QAAQ,EAAE,UAAU,QAAQ,WAAW,MAAM,CAAC;AACtD,CAAC;AAEH,QACG,QAAQ,MAAM,EACd,YAAY,+CAA+C,EAC3D,OAAO,iBAAiB,uBAAuB,EAC/C,OAAO,OAAM,YAAW;AACvB,QAAM,EAAE,KAAK,IAAI,MAAM,OAAO,oBAAoB;AAClD,QAAM,KAAK,OAAO;AACpB,CAAC;AAEH,QACG,QAAQ,OAAO,EACf,YAAY,4CAA4C,EACxD,OAAO,aAAa,0BAA0B,EAC9C,OAAO,UAAU,mDAAmD,EACpE,OAAO,OAAM,YAAW;AACvB,QAAM,EAAE,MAAM,IAAI,MAAM,OAAO,qBAAqB;AACpD,QAAM,MAAM,OAAO;AACrB,CAAC;AAEH,QACG,QAAQ,aAAa,EACrB,YAAY,4DAA4D,EACxE,OAAO,
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport process from 'node:process';\n\nimport { Command } from 'commander';\n\nimport { VERSION } from './version.js';\n\nconst program = new Command();\n\nprogram\n .name('safeword')\n .description('CLI for setting up and managing safeword development environments')\n .version(VERSION);\n\nprogram\n .command('setup')\n .description('Set up safeword in the current project')\n .option('-y, --yes', 'Skip confirmation prompts (for scripting)')\n .option(\n '--no-modify',\n 'Skip auto-editing the project ESLint config (prints the manual snippet instead). Also honored via SAFEWORD_NO_MODIFY env var.',\n )\n .action(async options => {\n const { setup } = await import('./commands/setup.js');\n await setup({ noModify: options.modify === false });\n });\n\nprogram\n .command('check')\n .description('Check project health and versions')\n .option('--offline', 'Skip remote version check')\n .action(async options => {\n const { check } = await import('./commands/check.js');\n await check(options);\n });\n\nprogram\n .command('upgrade')\n .description('Upgrade safeword configuration to latest version')\n .option(\n '--no-modify',\n 'Skip auto-editing the project ESLint config (prints the manual snippet instead). Also honored via SAFEWORD_NO_MODIFY env var.',\n )\n .action(async options => {\n const { upgrade } = await import('./commands/upgrade.js');\n await upgrade({ noModify: options.modify === false });\n });\n\nprogram\n .command('diff')\n .description('Preview changes that would be made by upgrade')\n .option('-v, --verbose', 'Show full diff output')\n .action(async options => {\n const { diff } = await import('./commands/diff.js');\n await diff(options);\n });\n\nprogram\n .command('reset')\n .description('Remove safeword configuration from project')\n .option('-y, --yes', 'Skip confirmation prompt')\n .option('--full', 'Also remove linting config and uninstall packages')\n .action(async options => {\n const { reset } = await import('./commands/reset.js');\n await reset(options);\n });\n\nprogram\n .command('sync-config')\n .description('Regenerate depcruise config from current project structure')\n .option('--check', 'Report drift without writing (exits non-zero on drift)')\n .action(async (options: { check?: boolean }) => {\n const { syncConfig } = await import('./commands/sync-config.js');\n await syncConfig({ check: options.check });\n });\n\nconst ticket = program.command('ticket').description('Ticket management');\n\nticket\n .command('new <slug>')\n .description('Create a new ticket with a Crockford Base32 ID')\n .option('--type <type>', 'Ticket type: patch, task, or feature', 'task')\n .option('--title <title>', 'Ticket title (defaults to slug)')\n .action(async (slug: string, options: { type?: string; title?: string }) => {\n const { ticketNew } = await import('./commands/ticket-new.js');\n await ticketNew(slug, options);\n });\n\nprogram\n .command('sync-learnings')\n .description('Regenerate .safeword-project/learnings/INDEX.md')\n .option('-q, --quiet', 'Suppress success output (still prints skipped-file warnings to stderr)')\n .action(async (options: { quiet?: boolean }) => {\n const { syncLearningsCommand } = await import('./commands/sync-learnings.js');\n syncLearningsCommand({ quiet: options.quiet });\n });\n\n// Show help if no arguments provided\nif (process.argv.length === 2) {\n program.help();\n}\n\n// Parse arguments\nprogram.parse();\n"],"mappings":";;;;;;AAEA,OAAO,aAAa;AAEpB,SAAS,eAAe;AAIxB,IAAM,UAAU,IAAI,QAAQ;AAE5B,QACG,KAAK,UAAU,EACf,YAAY,mEAAmE,EAC/E,QAAQ,OAAO;AAElB,QACG,QAAQ,OAAO,EACf,YAAY,wCAAwC,EACpD,OAAO,aAAa,2CAA2C,EAC/D;AAAA,EACC;AAAA,EACA;AACF,EACC,OAAO,OAAM,YAAW;AACvB,QAAM,EAAE,MAAM,IAAI,MAAM,OAAO,qBAAqB;AACpD,QAAM,MAAM,EAAE,UAAU,QAAQ,WAAW,MAAM,CAAC;AACpD,CAAC;AAEH,QACG,QAAQ,OAAO,EACf,YAAY,mCAAmC,EAC/C,OAAO,aAAa,2BAA2B,EAC/C,OAAO,OAAM,YAAW;AACvB,QAAM,EAAE,MAAM,IAAI,MAAM,OAAO,qBAAqB;AACpD,QAAM,MAAM,OAAO;AACrB,CAAC;AAEH,QACG,QAAQ,SAAS,EACjB,YAAY,kDAAkD,EAC9D;AAAA,EACC;AAAA,EACA;AACF,EACC,OAAO,OAAM,YAAW;AACvB,QAAM,EAAE,QAAQ,IAAI,MAAM,OAAO,uBAAuB;AACxD,QAAM,QAAQ,EAAE,UAAU,QAAQ,WAAW,MAAM,CAAC;AACtD,CAAC;AAEH,QACG,QAAQ,MAAM,EACd,YAAY,+CAA+C,EAC3D,OAAO,iBAAiB,uBAAuB,EAC/C,OAAO,OAAM,YAAW;AACvB,QAAM,EAAE,KAAK,IAAI,MAAM,OAAO,oBAAoB;AAClD,QAAM,KAAK,OAAO;AACpB,CAAC;AAEH,QACG,QAAQ,OAAO,EACf,YAAY,4CAA4C,EACxD,OAAO,aAAa,0BAA0B,EAC9C,OAAO,UAAU,mDAAmD,EACpE,OAAO,OAAM,YAAW;AACvB,QAAM,EAAE,MAAM,IAAI,MAAM,OAAO,qBAAqB;AACpD,QAAM,MAAM,OAAO;AACrB,CAAC;AAEH,QACG,QAAQ,aAAa,EACrB,YAAY,4DAA4D,EACxE,OAAO,WAAW,wDAAwD,EAC1E,OAAO,OAAO,YAAiC;AAC9C,QAAM,EAAE,WAAW,IAAI,MAAM,OAAO,2BAA2B;AAC/D,QAAM,WAAW,EAAE,OAAO,QAAQ,MAAM,CAAC;AAC3C,CAAC;AAEH,IAAM,SAAS,QAAQ,QAAQ,QAAQ,EAAE,YAAY,mBAAmB;AAExE,OACG,QAAQ,YAAY,EACpB,YAAY,gDAAgD,EAC5D,OAAO,iBAAiB,wCAAwC,MAAM,EACtE,OAAO,mBAAmB,iCAAiC,EAC3D,OAAO,OAAO,MAAc,YAA+C;AAC1E,QAAM,EAAE,UAAU,IAAI,MAAM,OAAO,0BAA0B;AAC7D,QAAM,UAAU,MAAM,OAAO;AAC/B,CAAC;AAEH,QACG,QAAQ,gBAAgB,EACxB,YAAY,iDAAiD,EAC7D,OAAO,eAAe,wEAAwE,EAC9F,OAAO,OAAO,YAAiC;AAC9C,QAAM,EAAE,qBAAqB,IAAI,MAAM,OAAO,8BAA8B;AAC5E,uBAAqB,EAAE,OAAO,QAAQ,MAAM,CAAC;AAC/C,CAAC;AAGH,IAAI,QAAQ,KAAK,WAAW,GAAG;AAC7B,UAAQ,KAAK;AACf;AAGA,QAAQ,MAAM;","names":[]}
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
buildArchitecture,
|
|
3
3
|
hasArchitectureDetected,
|
|
4
4
|
syncConfigCore
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-GR7LJEJ2.js";
|
|
6
6
|
import {
|
|
7
7
|
getEslintPeerMismatchWarning,
|
|
8
8
|
installPack,
|
|
@@ -211,6 +211,9 @@ function setupJavaScriptProject(cwd, ctx, packagesToInstall) {
|
|
|
211
211
|
}
|
|
212
212
|
if (syncResult.createdMainConfig) {
|
|
213
213
|
archFiles.push(".dependency-cruiser.cjs");
|
|
214
|
+
info(
|
|
215
|
+
" \u21B3 .dependency-cruiser.cjs extends rules from .safeword/depcruise-config.cjs \u2014 edit to add your own."
|
|
216
|
+
);
|
|
214
217
|
}
|
|
215
218
|
logArchitectureDetected(arch);
|
|
216
219
|
}
|
|
@@ -337,4 +340,4 @@ async function setup(options) {
|
|
|
337
340
|
export {
|
|
338
341
|
setup
|
|
339
342
|
};
|
|
340
|
-
//# sourceMappingURL=setup-
|
|
343
|
+
//# sourceMappingURL=setup-3VT6R5JB.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/setup.ts"],"sourcesContent":["/**\n * Setup command - Initialize safeword in a project\n *\n * Uses reconcile() with mode='install' to create all managed files.\n */\n\nimport { execSync } from 'node:child_process';\nimport { readdirSync } from 'node:fs';\nimport nodePath from 'node:path';\n\nimport { setupGoTooling } from '../packs/golang/setup.js';\nimport { installPack } from '../packs/install.js';\nimport {\n detectPythonLayers,\n detectPythonPackageManager,\n getPythonInstallCommand,\n hasRuffDependency,\n installPythonDependencies,\n} from '../packs/python/setup.js';\nimport { detectLanguages as detectLanguagePacks } from '../packs/registry.js';\nimport { reconcile, type ReconcileResult } from '../reconcile.js';\nimport { type ProjectContext, SAFEWORD_SCHEMA } from '../schema.js';\nimport { createProjectContext } from '../utils/context.js';\nimport { getEslintPeerMismatchWarning } from '../utils/eslint-peer-check.js';\nimport { exists, readJson, writeJson } from '../utils/fs.js';\nimport { installDependencies } from '../utils/install.js';\nimport { error, header, info, listItem, success, warn } from '../utils/output.js';\nimport { detectLanguages, type Languages } from '../utils/project-detector.js';\nimport { maybeAutoPatchOrNudge } from '../utils/vendored-ignores-nudge.js';\nimport { getWorkspacePatterns } from '../utils/workspaces.js';\nimport { VERSION } from '../version.js';\nimport { buildArchitecture, hasArchitectureDetected, syncConfigCore } from './sync-config.js';\n\ninterface PackageJson {\n name?: string;\n version?: string;\n scripts?: Record<string, string>;\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n 'lint-staged'?: Record<string, string[]>;\n workspaces?: string[] | { packages?: string[] };\n}\n\n/**\n * Process a glob workspace pattern (e.g., \"packages/*\").\n * Scans directory and adds format scripts to each package.\n */\nfunction processGlobWorkspacePattern(cwd: string, workspacePath: string): string[] {\n const updated: string[] = [];\n const fullPath = nodePath.join(cwd, workspacePath);\n\n if (!exists(fullPath)) return [];\n\n try {\n const entries = readdirSync(fullPath, { withFileTypes: true });\n for (const entry of entries) {\n if (!entry.isDirectory() || entry.name.startsWith('.')) continue;\n\n const packagePath = nodePath.join(fullPath, entry.name);\n if (addFormatScriptIfMissing(packagePath)) {\n updated.push(nodePath.join(workspacePath, entry.name, 'package.json'));\n }\n }\n } catch {\n // Directory not readable, skip\n }\n\n return updated;\n}\n\n/**\n * Process an explicit workspace path (e.g., \"tools/scripts\").\n */\nfunction processExplicitWorkspacePath(cwd: string, workspacePath: string): string[] {\n const fullPath = nodePath.join(cwd, workspacePath);\n if (addFormatScriptIfMissing(fullPath)) {\n return [nodePath.join(workspacePath, 'package.json')];\n }\n return [];\n}\n\n/**\n * Add format scripts to workspace packages that don't have them.\n * Only runs if root project uses Prettier (not an existing formatter like Biome).\n */\nfunction setupWorkspaceFormatScripts(cwd: string, ctx: ProjectContext): string[] {\n // Skip if root uses an existing formatter (Biome, dprint, etc.)\n if (ctx.projectType.existingFormatter) return [];\n\n const workspacePatterns = getWorkspacePatterns(cwd);\n if (workspacePatterns.length === 0) return [];\n\n const updated: string[] = [];\n\n for (const pattern of workspacePatterns) {\n const isGlobPattern = pattern.endsWith('/*');\n const workspacePath = isGlobPattern ? pattern.slice(0, -2) : pattern;\n\n const patternUpdates = isGlobPattern\n ? processGlobWorkspacePattern(cwd, workspacePath)\n : processExplicitWorkspacePath(cwd, workspacePath);\n\n updated.push(...patternUpdates);\n }\n\n return updated;\n}\n\n/**\n * Add format script to a package if it doesn't have one.\n * Returns true if the script was added.\n */\nfunction addFormatScriptIfMissing(packageDirectory: string): boolean {\n const packageJsonPath = nodePath.join(packageDirectory, 'package.json');\n if (!exists(packageJsonPath)) return false;\n\n const packageJson = readJson(packageJsonPath) as PackageJson | undefined;\n if (!packageJson) return false;\n\n // Skip if format script already exists\n if (packageJson.scripts?.format) return false;\n\n // Add format script\n const scripts = packageJson.scripts ?? {};\n scripts.format = 'prettier --write .';\n packageJson.scripts = scripts;\n writeJson(packageJsonPath, packageJson);\n\n return true;\n}\n\n/**\n * Create package.json if missing, unless non-JS-only project (Python, Go).\n * Returns true if created, false if already exists or skipped.\n */\nfunction ensurePackageJson(cwd: string): boolean {\n const packageJsonPath = nodePath.join(cwd, 'package.json');\n if (exists(packageJsonPath)) return false;\n\n // Skip for non-JS-only projects (no JS tooling needed)\n const languages = detectLanguages(cwd);\n const hasNonJs = languages.python || languages.golang || languages.rust;\n if (hasNonJs && !languages.javascript) return false;\n\n const dirName = nodePath.basename(cwd) || 'project';\n const defaultPackageJson: PackageJson = {\n name: dirName,\n version: '0.1.0',\n scripts: {},\n };\n writeJson(packageJsonPath, defaultPackageJson);\n return true;\n}\n\ninterface PythonSetupStatus {\n files: string[];\n installFailed: boolean;\n importLinter: boolean;\n}\n\n/** Base Python tools to install. Import-linter added when layers detected. */\nfunction getPythonTools(includeImportLinter: boolean): string[] {\n const tools = ['ruff', 'mypy', 'deadcode'];\n if (includeImportLinter) tools.push('import-linter');\n return tools;\n}\n\n/**\n * Configure Python tooling and install dependencies.\n * Config files (ruff.toml, mypy.ini, .importlinter) are created by reconciliation.\n * This function handles dependency installation.\n */\nfunction setupPython(cwd: string): PythonSetupStatus {\n let installFailed = false;\n\n // Detect layers for import-linter\n const layers = detectPythonLayers(cwd);\n const hasLayers = layers.length >= 2;\n\n // Install Python tools if not already in dependencies\n if (!hasRuffDependency(cwd)) {\n const tools = getPythonTools(hasLayers);\n const pm = detectPythonPackageManager(cwd);\n if (pm === 'pip') {\n installFailed = true;\n } else {\n info(`\\nInstalling Python tools (${tools.join(', ')})...`);\n const installed = installPythonDependencies(cwd, tools);\n if (installed) {\n success('Python tools installed');\n } else {\n installFailed = true;\n }\n }\n }\n\n // Note: files are now created by reconciliation, not returned here\n return { files: [], installFailed, importLinter: hasLayers };\n}\n\ninterface SetupSummaryOptions {\n cwd: string;\n result: ReconcileResult;\n packageJsonCreated: boolean;\n languages: Languages;\n archFiles?: string[];\n workspaceUpdates?: string[];\n pythonFiles?: string[];\n pythonInstallFailed?: boolean;\n pythonImportLinter?: boolean;\n}\n\n/**\n * Print list of created files.\n */\nfunction printCreatedFiles(createdFiles: string[], packageJsonCreated: boolean): void {\n if (createdFiles.length === 0 && !packageJsonCreated) return;\n\n info('\\nCreated:');\n if (packageJsonCreated) listItem('package.json');\n for (const file of createdFiles) listItem(file);\n}\n\n/**\n * Print list of modified files.\n */\nfunction printModifiedFiles(modifiedFiles: string[]): void {\n if (modifiedFiles.length === 0) return;\n\n info('\\nModified:');\n for (const file of modifiedFiles) listItem(file);\n}\n\n/**\n * Print language-specific next steps.\n */\nfunction printLanguageNextSteps(options: {\n cwd: string;\n languages: Languages;\n pythonInstallFailed: boolean;\n pythonImportLinter: boolean;\n golangciCreated: boolean;\n}): void {\n const { cwd, languages, pythonInstallFailed, pythonImportLinter, golangciCreated } = options;\n\n // Python: show install command only if auto-install failed\n if (languages.python && pythonInstallFailed) {\n listItem(\n `Install Python tools: ${getPythonInstallCommand(cwd, getPythonTools(pythonImportLinter))}`,\n );\n }\n\n // Go: show if .golangci.yml was created (Go tools are installed globally)\n if (languages.golang && golangciCreated) {\n listItem(\n 'Install Go tools: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest',\n );\n }\n}\n\nfunction printSetupSummary(options: SetupSummaryOptions): void {\n const {\n cwd,\n result,\n packageJsonCreated,\n languages,\n archFiles = [],\n workspaceUpdates = [],\n pythonFiles = [],\n pythonInstallFailed = false,\n pythonImportLinter = false,\n } = options;\n\n header('Setup Complete');\n\n // Collect created files (schema files + arch files + python config files)\n const createdFiles = [\n ...result.created,\n ...archFiles,\n ...pythonFiles.filter(f => f !== 'pyproject.toml'),\n ];\n printCreatedFiles(createdFiles, packageJsonCreated);\n\n // Collect modified files (schema updates + workspace updates + pyproject.toml)\n const modifiedFiles = [\n ...result.updated,\n ...workspaceUpdates,\n ...pythonFiles.filter(f => f === 'pyproject.toml'),\n ];\n printModifiedFiles(modifiedFiles);\n\n if (result.updated.includes('CLAUDE.md')) {\n info('\\nNote: CLAUDE.md — added one import line at the top. Your content is preserved.');\n }\n\n // Next steps\n info('\\nNext steps:');\n listItem('Run `safeword check` to verify setup');\n\n printLanguageNextSteps({\n cwd,\n languages,\n pythonInstallFailed,\n pythonImportLinter,\n golangciCreated: result.created.includes('.golangci.yml'),\n });\n\n listItem('Commit the new files to git');\n\n success(`\\nSafeword ${VERSION} installed successfully!`);\n}\n\n/**\n * Setup JavaScript project: architecture detection, depcruise config, workspace scripts\n */\nfunction setupJavaScriptProject(\n cwd: string,\n ctx: ProjectContext,\n packagesToInstall: string[],\n): { archFiles: string[]; workspaceUpdates: string[] } {\n const archFiles: string[] = [];\n const arch = buildArchitecture(cwd);\n\n if (hasArchitectureDetected(arch)) {\n const syncResult = syncConfigCore(cwd, arch);\n if (syncResult.generatedConfig) {\n archFiles.push('.safeword/depcruise-config.cjs');\n }\n if (syncResult.createdMainConfig) {\n archFiles.push('.dependency-cruiser.cjs');\n info(\n ' ↳ .dependency-cruiser.cjs extends rules from .safeword/depcruise-config.cjs — edit to add your own.',\n );\n }\n logArchitectureDetected(arch);\n }\n\n const workspaceUpdates = setupWorkspaceFormatScripts(cwd, ctx);\n if (workspaceUpdates.length > 0) {\n info(`\\nAdded format scripts to ${workspaceUpdates.length} workspace package(s)`);\n }\n\n logExistingFormatter(ctx);\n\n const eslintWarning = getEslintPeerMismatchWarning(cwd);\n if (eslintWarning) warn(`\\n${eslintWarning}`);\n\n installDependencies(cwd, packagesToInstall, 'linting devDependencies');\n info('These are dev-only tools — your application dependencies are unchanged.');\n\n return { archFiles, workspaceUpdates };\n}\n\n/**\n * Log detected architecture elements and workspaces\n */\nfunction logArchitectureDetected(arch: ReturnType<typeof buildArchitecture>): void {\n const detected: string[] = [];\n if (arch.elements.length > 0) {\n detected.push(arch.elements.map(element => element.location).join(', '));\n }\n if (arch.workspaces && arch.workspaces.length > 0) {\n detected.push(`workspaces: ${arch.workspaces.join(', ')}`);\n }\n info(`\\nArchitecture detected: ${detected.join('; ')}`);\n info('Generated dependency-cruiser config for /audit command');\n}\n\n/**\n * Log existing formatter detection and explain ESLint coexistence\n */\nfunction logExistingFormatter(ctx: ProjectContext): void {\n if (!ctx.projectType.existingFormatter) return;\n\n info('\\nDetected existing formatter (biome/dprint) — skipping Prettier.');\n info('ESLint is still installed for security scanning, complexity checks, and framework rules');\n info(\"that biome/dprint don't cover. Both tools coexist without conflict.\");\n}\n\n/**\n * Log detected language and skip message\n */\nfunction logDetectedLanguage(languages: Languages): void {\n if (languages.python && !languages.javascript) {\n info('Python project detected (skipping JS tooling)');\n }\n if (languages.golang && !languages.javascript) {\n info('Go project detected (skipping JS tooling)');\n }\n if (languages.rust && !languages.javascript) {\n info('Rust project detected (skipping JS tooling)');\n }\n}\n\n/**\n * Register and setup detected language packs\n */\nfunction registerLanguagePacks(cwd: string): void {\n const detectedPacks = detectLanguagePacks(cwd);\n for (const packId of detectedPacks) {\n installPack(packId, cwd);\n }\n}\n\n/**\n * Setup Python project (dependencies installation).\n * Config files are created by reconciliation.\n */\nfunction setupPythonProject(languages: Languages, cwd: string): PythonSetupStatus {\n if (!languages.python) {\n return { files: [], installFailed: false, importLinter: false };\n }\n return setupPython(cwd);\n}\n\n/**\n * Setup Go project tooling.\n * Config files (.golangci.yml) are created by reconciliation.\n */\nfunction setupGoProject(languages: Languages): void {\n if (languages.golang) {\n setupGoTooling();\n }\n}\n\n/** Warn if Bun is not available (hooks require it) */\nfunction warnIfBunMissing(): void {\n try {\n execSync('bun --version', { stdio: 'pipe' });\n } catch {\n warn('bun not found — quality hooks will not work without it.');\n info(' Install: curl -fsSL https://bun.sh/install | bash');\n info(' Hooks will hard-block at session start until bun is available.');\n }\n}\n\nexport interface SetupOptions {\n /** When true, skip auto-editing the project's eslint config; fall through to the print-only nudge. */\n noModify?: boolean;\n}\n\nexport async function setup(options: SetupOptions): Promise<void> {\n const cwd = process.cwd();\n const safewordDirectory = nodePath.join(cwd, '.safeword');\n\n if (exists(safewordDirectory)) {\n error('Already configured. Run `safeword upgrade` to update.');\n process.exit(1);\n }\n\n const packageJsonCreated = ensurePackageJson(cwd);\n\n header('Safeword Setup');\n info(`Version: ${VERSION}`);\n if (packageJsonCreated) info('Created package.json (none found)');\n warnIfBunMissing();\n\n try {\n info('\\nCreating safeword configuration...');\n const ctx = createProjectContext(cwd);\n const languages = ctx.languages ?? {\n javascript: false,\n python: false,\n golang: false,\n rust: false,\n sql: false,\n };\n const isNonJsOnly =\n (languages.python || languages.golang || languages.rust) && !languages.javascript;\n\n logDetectedLanguage(languages);\n\n const result = await reconcile(SAFEWORD_SCHEMA, 'install', ctx);\n success('Created .safeword directory and configuration');\n\n // Language-specific setup\n const { archFiles, workspaceUpdates } = isNonJsOnly\n ? { archFiles: [], workspaceUpdates: [] }\n : setupJavaScriptProject(cwd, ctx, result.packagesToInstall);\n const pythonStatus = setupPythonProject(languages, cwd);\n setupGoProject(languages);\n registerLanguagePacks(cwd);\n\n printSetupSummary({\n cwd,\n result,\n packageJsonCreated,\n languages,\n archFiles,\n workspaceUpdates,\n pythonFiles: pythonStatus.files,\n pythonInstallFailed: pythonStatus.installFailed,\n pythonImportLinter: pythonStatus.importLinter,\n });\n\n maybeAutoPatchOrNudge({\n cwd,\n existingEslintConfig: ctx.projectType.existingEslintConfig,\n hasJavaScript: languages.javascript,\n noModify: options.noModify,\n });\n } catch (error_) {\n error(`Setup failed: ${error_ instanceof Error ? error_.message : 'Unknown error'}`);\n process.exit(1);\n }\n}\n"],"mappings":"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","names":["detectLanguages"]}
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
hasArchitectureDetected,
|
|
4
4
|
syncConfig,
|
|
5
5
|
syncConfigCore
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-GR7LJEJ2.js";
|
|
7
7
|
import "./chunk-QARISSCT.js";
|
|
8
8
|
import "./chunk-VZ2E2QRM.js";
|
|
9
9
|
export {
|
|
@@ -12,4 +12,4 @@ export {
|
|
|
12
12
|
syncConfig,
|
|
13
13
|
syncConfigCore
|
|
14
14
|
};
|
|
15
|
-
//# sourceMappingURL=sync-config-
|
|
15
|
+
//# sourceMappingURL=sync-config-AFQRCLFM.js.map
|
package/package.json
CHANGED
|
@@ -23,11 +23,12 @@ This skill is required at the done-gate (ticket 147). The line below appends a s
|
|
|
23
23
|
cd "$CLAUDE_PROJECT_DIR" || exit 1
|
|
24
24
|
|
|
25
25
|
# =========================================================================
|
|
26
|
-
#
|
|
26
|
+
# DETECT CONFIG DRIFT (read-only — no writes)
|
|
27
27
|
# =========================================================================
|
|
28
28
|
|
|
29
|
-
# 0.
|
|
30
|
-
|
|
29
|
+
# 0. Compare generated vs on-disk depcruise config. Non-zero exit = drift.
|
|
30
|
+
# /audit must never mutate the working tree; surface stale config as W007.
|
|
31
|
+
bunx safeword@latest sync-config --check 2>&1 || echo "[W007] Stale .safeword/depcruise-config.cjs — run \`safeword sync-config\` to refresh and commit"
|
|
31
32
|
|
|
32
33
|
# =========================================================================
|
|
33
34
|
# ARCHITECTURE CHECKS (circular deps, layer violations)
|
|
@@ -240,6 +241,7 @@ Report findings by severity with codes:
|
|
|
240
241
|
- [W003] Staleness: `README.md` last modified 45 days ago (12 commits since)
|
|
241
242
|
- [W004] Gap: `@tanstack/query` not documented in ARCHITECTURE.md
|
|
242
243
|
- [W005] Stale config: `knip.json` — `lodash` can be removed from ignoreDependencies
|
|
244
|
+
- [W007] Stale .safeword/depcruise-config.cjs — run `safeword sync-config` to refresh and commit
|
|
243
245
|
|
|
244
246
|
### Code Quality
|
|
245
247
|
|
|
@@ -27,11 +27,12 @@ This skill is required at the done-gate (ticket 147). The line below appends a s
|
|
|
27
27
|
cd "${CLAUDE_PROJECT_DIR:-$(pwd)}" || exit 1
|
|
28
28
|
|
|
29
29
|
# =========================================================================
|
|
30
|
-
#
|
|
30
|
+
# DETECT CONFIG DRIFT (read-only — no writes)
|
|
31
31
|
# =========================================================================
|
|
32
32
|
|
|
33
|
-
# 0.
|
|
34
|
-
|
|
33
|
+
# 0. Compare generated vs on-disk depcruise config. Non-zero exit = drift.
|
|
34
|
+
# /audit must never mutate the working tree; surface stale config as W007.
|
|
35
|
+
bunx safeword@latest sync-config --check 2>&1 || echo "[W007] Stale .safeword/depcruise-config.cjs — run \`safeword sync-config\` to refresh and commit"
|
|
35
36
|
|
|
36
37
|
# =========================================================================
|
|
37
38
|
# ARCHITECTURE CHECKS (circular deps, layer violations)
|
|
@@ -271,6 +272,7 @@ Report findings by severity with codes:
|
|
|
271
272
|
- [W004] Gap: `@tanstack/query` not documented in ARCHITECTURE.md
|
|
272
273
|
- [W005] Stale config: `knip.json` — `lodash` can be removed from ignoreDependencies
|
|
273
274
|
- [W006] Learning file missing Covers: — `.safeword-project/learnings/foo.md` (absent from INDEX.md)
|
|
275
|
+
- [W007] Stale .safeword/depcruise-config.cjs — run `safeword sync-config` to refresh and commit
|
|
274
276
|
|
|
275
277
|
### Code Quality
|
|
276
278
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/commands/sync-config.ts","../src/utils/boundaries.ts","../src/utils/depcruise-config.ts"],"sourcesContent":["/**\n * Sync Config command - Regenerate depcruise config from current project structure\n *\n * Used by `/audit` slash command to refresh config before running checks.\n */\n\nimport { writeFileSync } from 'node:fs';\nimport nodePath from 'node:path';\n\nimport { detectArchitecture } from '../utils/boundaries.js';\nimport {\n type DepCruiseArchitecture,\n detectWorkspaces,\n generateDepCruiseConfigFile,\n generateDepCruiseMainConfig,\n} from '../utils/depcruise-config.js';\nimport { exists } from '../utils/fs.js';\nimport { error, info, success } from '../utils/output.js';\n\ninterface SyncConfigResult {\n generatedConfig: boolean;\n createdMainConfig: boolean;\n}\n\n/**\n * Core sync logic - writes depcruise configs to disk\n * Can be called from setup or as standalone command\n */\nexport function syncConfigCore(cwd: string, arch: DepCruiseArchitecture): SyncConfigResult {\n const safewordDirectory = nodePath.join(cwd, '.safeword');\n const result: SyncConfigResult = {\n generatedConfig: false,\n createdMainConfig: false,\n };\n\n // Generate and write .safeword/depcruise-config.cjs (CJS for compatibility)\n const generatedConfigPath = nodePath.join(safewordDirectory, 'depcruise-config.cjs');\n const generatedConfig = generateDepCruiseConfigFile(arch);\n writeFileSync(generatedConfigPath, generatedConfig);\n result.generatedConfig = true;\n\n // Create main config if not exists (self-healing)\n // Use .cjs extension to work in ESM projects (type: \"module\")\n const mainConfigPath = nodePath.join(cwd, '.dependency-cruiser.cjs');\n if (!exists(mainConfigPath)) {\n const mainConfig = generateDepCruiseMainConfig();\n writeFileSync(mainConfigPath, mainConfig);\n result.createdMainConfig = true;\n }\n\n return result;\n}\n\n/**\n * Build full architecture info by combining detected layers with workspaces\n */\nexport function buildArchitecture(cwd: string): DepCruiseArchitecture {\n const arch = detectArchitecture(cwd);\n const workspaces = detectWorkspaces(cwd);\n return { ...arch, workspaces };\n}\n\n/**\n * Check if architecture was detected (layers, monorepo structure, or workspaces)\n */\nexport function hasArchitectureDetected(arch: DepCruiseArchitecture): boolean {\n return arch.elements.length > 0 || arch.isMonorepo || (arch.workspaces?.length ?? 0) > 0;\n}\n\n/**\n * CLI command: Sync depcruise config with current project structure\n */\n\nexport async function syncConfig(): Promise<void> {\n // Public CLI command contract is Promise<void>; body is sync today but the\n // signature reserves room for async I/O. Token await keeps the contract honest.\n await Promise.resolve();\n const cwd = process.cwd();\n const safewordDirectory = nodePath.join(cwd, '.safeword');\n\n // Check if .safeword exists\n if (!exists(safewordDirectory)) {\n error('Not configured. Run `safeword setup` first.');\n process.exit(1);\n }\n\n // Detect current architecture and workspaces\n const arch = buildArchitecture(cwd);\n const result = syncConfigCore(cwd, arch);\n\n if (result.generatedConfig) {\n info('Generated .safeword/depcruise-config.cjs');\n }\n if (result.createdMainConfig) {\n info('Created .dependency-cruiser.cjs');\n }\n\n success('Config synced');\n}\n","/**\n * Architecture boundaries detection\n *\n * Auto-detects common architecture directories for use by\n * dependency-cruiser layer enforcement.\n *\n * Supports:\n * - Standard projects (src/utils, utils/)\n * - Monorepos (packages/*, apps/*)\n * - Various naming conventions (helpers, shared, core, etc.)\n */\n\nimport { readdirSync } from 'node:fs';\nimport nodePath from 'node:path';\n\nimport { exists } from './fs.js';\n\n/**\n * Architecture layer definitions with alternative names.\n * Each layer maps to equivalent directory names.\n * Order defines hierarchy: earlier = lower layer.\n */\nconst ARCHITECTURE_LAYERS = [\n // Layer 0: Pure types (no imports)\n { layer: 'types', dirs: ['types', 'interfaces', 'schemas'] },\n // Layer 1: Utilities (only types)\n { layer: 'utils', dirs: ['utils', 'helpers', 'shared', 'common', 'core'] },\n // Layer 2: Libraries (types, utils)\n { layer: 'lib', dirs: ['lib', 'libraries'] },\n // Layer 3: State & logic (types, utils, lib)\n { layer: 'hooks', dirs: ['hooks', 'composables'] },\n { layer: 'services', dirs: ['services', 'api', 'stores', 'state'] },\n // Layer 4: UI components (all above)\n { layer: 'components', dirs: ['components', 'ui'] },\n // Layer 5: Features (all above)\n { layer: 'features', dirs: ['features', 'modules', 'domains'] },\n // Layer 6: Entry points (can import everything)\n { layer: 'app', dirs: ['app', 'pages', 'views', 'routes', 'commands'] },\n] as const;\n\ntype Layer = (typeof ARCHITECTURE_LAYERS)[number]['layer'];\n\ninterface DetectedElement {\n layer: Layer;\n pattern: string; // glob pattern for boundaries config\n location: string; // human-readable location\n}\n\nexport interface DetectedArchitecture {\n elements: DetectedElement[];\n isMonorepo: boolean;\n}\n\n/**\n * Find monorepo package directories\n * @param projectDirectory\n */\nfunction findMonorepoPackages(projectDirectory: string): string[] {\n const packages: string[] = [];\n\n // Check common monorepo patterns\n const monorepoRoots = ['packages', 'apps', 'libs', 'modules'];\n\n for (const root of monorepoRoots) {\n const rootPath = nodePath.join(projectDirectory, root);\n if (!exists(rootPath)) continue;\n\n try {\n const entries = readdirSync(rootPath, { withFileTypes: true });\n for (const entry of entries) {\n if (entry.isDirectory() && !entry.name.startsWith('.')) {\n packages.push(nodePath.join(root, entry.name));\n }\n }\n } catch {\n // Directory not readable, skip\n }\n }\n\n return packages;\n}\n\n/**\n * Check if a layer already exists for this path prefix\n * @param elements\n * @param layer\n * @param pathPrefix\n */\nfunction hasLayerForPrefix(elements: DetectedElement[], layer: Layer, pathPrefix: string): boolean {\n return elements.some(\n element => element.layer === layer && element.pattern.startsWith(pathPrefix),\n );\n}\n\n/**\n * Scan a single search path for architecture layers\n * @param projectDirectory\n * @param searchPath\n * @param pathPrefix\n * @param elements\n */\nfunction scanSearchPath(\n projectDirectory: string,\n searchPath: string,\n pathPrefix: string,\n elements: DetectedElement[],\n): void {\n for (const layerDefinition of ARCHITECTURE_LAYERS) {\n for (const dirName of layerDefinition.dirs) {\n const fullPath = nodePath.join(projectDirectory, searchPath, dirName);\n if (exists(fullPath) && !hasLayerForPrefix(elements, layerDefinition.layer, pathPrefix)) {\n elements.push({\n layer: layerDefinition.layer,\n pattern: `${pathPrefix}${dirName}/**`,\n location: `${pathPrefix}${dirName}`,\n });\n }\n }\n }\n}\n\n/**\n * Scan a directory for architecture layers\n * @param projectDirectory\n * @param basePath\n */\nfunction scanForLayers(projectDirectory: string, basePath: string): DetectedElement[] {\n const elements: DetectedElement[] = [];\n const prefix = basePath ? `${basePath}/` : '';\n\n // Check src/ and root level\n scanSearchPath(projectDirectory, nodePath.join(basePath, 'src'), `${prefix}src/`, elements);\n scanSearchPath(projectDirectory, basePath, prefix, elements);\n\n return elements;\n}\n\n/**\n * Detects architecture directories in the project\n * Handles both standard projects and monorepos\n * @param projectDirectory\n */\nexport function detectArchitecture(projectDirectory: string): DetectedArchitecture {\n const elements: DetectedElement[] = [];\n\n // First, check for monorepo packages\n const packages = findMonorepoPackages(projectDirectory);\n const isMonorepo = packages.length > 0;\n\n if (isMonorepo) {\n // Scan each package\n for (const pkg of packages) {\n elements.push(...scanForLayers(projectDirectory, pkg));\n }\n }\n\n // Also scan root level (works for both monorepo root and standard projects)\n elements.push(...scanForLayers(projectDirectory, ''));\n\n // Deduplicate by pattern\n const seen = new Set<string>();\n const uniqueElements = elements.filter(element => {\n if (seen.has(element.pattern)) return false;\n seen.add(element.pattern);\n return true;\n });\n\n return { elements: uniqueElements, isMonorepo };\n}\n","/**\n * Dependency-cruiser config generator\n *\n * Generates dependency-cruiser configuration from detected architecture.\n * Used by `safeword sync-config` command and `/audit` slash command.\n */\n\nimport nodePath from 'node:path';\n\nimport type { DetectedArchitecture } from './boundaries.js';\nimport { readJson } from './fs.js';\n\nexport interface DepCruiseArchitecture extends DetectedArchitecture {\n workspaces?: string[];\n}\n\ninterface PackageJson {\n workspaces?: string[] | { packages?: string[] };\n}\n\n/**\n * Detect workspaces from package.json\n * Supports both array format and object format (yarn workspaces)\n */\nexport function detectWorkspaces(cwd: string): string[] | undefined {\n const packageJsonPath = nodePath.join(cwd, 'package.json');\n const packageJson = readJson(packageJsonPath) as PackageJson | undefined;\n\n if (!packageJson?.workspaces) return undefined;\n\n // Handle both formats: string[] or { packages: string[] }\n const workspaces = Array.isArray(packageJson.workspaces)\n ? packageJson.workspaces\n : packageJson.workspaces.packages;\n\n return workspaces && workspaces.length > 0 ? workspaces : undefined;\n}\n\n/**\n * Generate monorepo hierarchy rules based on workspace patterns\n */\nfunction generateMonorepoRules(workspaces: string[]): string {\n const rules: string[] = [];\n\n const hasLibs = workspaces.some(w => w.startsWith('libs'));\n const hasPackages = workspaces.some(w => w.startsWith('packages'));\n const hasApps = workspaces.some(w => w.startsWith('apps'));\n\n // libs cannot import packages or apps\n if (hasLibs && (hasPackages || hasApps)) {\n rules.push(` {\n name: 'libs-cannot-import-packages-or-apps',\n severity: 'error',\n from: { path: '^libs/' },\n to: { path: '^(packages|apps)/' },\n }`);\n }\n\n // packages cannot import apps\n if (hasPackages && hasApps) {\n rules.push(` {\n name: 'packages-cannot-import-apps',\n severity: 'error',\n from: { path: '^packages/' },\n to: { path: '^apps/' },\n }`);\n }\n\n return rules.join(',\\n');\n}\n\n/**\n * Generate .safeword/depcruise-config.cjs content (forbidden rules + options)\n */\nexport function generateDepCruiseConfigFile(arch: DepCruiseArchitecture): string {\n const monorepoRules = arch.workspaces ? generateMonorepoRules(arch.workspaces) : '';\n const hasMonorepoRules = monorepoRules.length > 0;\n\n return String.raw`module.exports = {\n forbidden: [\n // =========================================================================\n // ERROR RULES (block on violations)\n // =========================================================================\n {\n name: 'no-circular',\n // Runtime cycles cause initialization-order bugs and make code hard to reason about.\n // Type-only edges (import type) are erased at compile time and cannot cause runtime\n // cycles — TypeScript designed import type for exactly this case, and depcruise\n // documents viaOnly + dependencyTypesNot: ['type-only'] as the canonical opt-in.\n comment: 'Circular dependencies cause runtime issues and make code hard to reason about',\n severity: 'error',\n from: {},\n to: { circular: true, viaOnly: { dependencyTypesNot: ['type-only'] } },\n },\n {\n name: 'no-deprecated-deps',\n comment: 'Deprecated npm packages should be replaced - they may have security issues or be unmaintained',\n severity: 'error',\n from: {},\n to: { dependencyTypes: ['deprecated'] },\n },${hasMonorepoRules ? `\\n${monorepoRules},` : ''}\n\n // =========================================================================\n // WARNING RULES (flag issues but don't block)\n // =========================================================================\n {\n name: 'no-dev-deps-in-src',\n comment: 'Production code should not import devDependencies - may cause runtime failures',\n severity: 'warn',\n from: {\n path: ['^src', '^packages/[^/]+/src'],\n pathNot: '\\\\.test\\\\.[tj]sx?$',\n },\n to: { dependencyTypes: ['npm-dev'] },\n },\n {\n name: 'no-orphans',\n comment: 'Orphan modules are not imported anywhere - may be dead code',\n severity: 'warn',\n from: {\n orphan: true,\n pathNot: [\n // Entry points\n '(^|/)index\\\\.[tj]sx?$',\n '(^|/)main\\\\.[tj]sx?$',\n '(^|/)cli\\\\.[tj]s$',\n '\\\\.config\\\\.[tj]s$',\n '\\\\.config\\\\.mjs$',\n // Test files\n '\\\\.test\\\\.[tj]sx?$',\n '\\\\.spec\\\\.[tj]sx?$',\n '/tests/',\n '/__tests__/',\n // Astro/Next.js pages and content\n '/src/content/',\n '/src/pages/',\n '/app/',\n ],\n },\n to: {},\n },\n ],\n options: {\n doNotFollow: { path: ['node_modules', '.safeword'] },\n exclude: {\n path: ['node_modules', 'dist', 'build', 'coverage', '\\\\.d\\\\.ts$'],\n },\n tsPreCompilationDeps: true,\n tsConfig: { fileName: 'tsconfig.json' },\n enhancedResolveOptions: {\n extensions: ['.ts', '.tsx', '.js', '.jsx'],\n exportsFields: ['exports'],\n conditionNames: ['import', 'require', 'node', 'default'],\n },\n },\n};\n`;\n}\n\n/**\n * Generate .dependency-cruiser.js (main config that imports generated)\n */\nexport function generateDepCruiseMainConfig(): string {\n return `/**\n * Dependency Cruiser Configuration\n *\n * Imports auto-generated rules from .safeword/depcruise-config.cjs\n * ADD YOUR CUSTOM RULES BELOW the spread operator.\n */\n\nconst generated = require('./.safeword/depcruise-config.cjs');\n\nmodule.exports = {\n forbidden: [\n ...generated.forbidden,\n // ADD YOUR CUSTOM RULES BELOW:\n // { name: 'no-legacy', from: { path: 'legacy/' }, to: { path: 'new/' } },\n ],\n options: {\n ...generated.options,\n // Your overrides here\n },\n};\n`;\n}\n"],"mappings":"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","names":["nodePath","nodePath","nodePath","nodePath"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/commands/setup.ts"],"sourcesContent":["/**\n * Setup command - Initialize safeword in a project\n *\n * Uses reconcile() with mode='install' to create all managed files.\n */\n\nimport { execSync } from 'node:child_process';\nimport { readdirSync } from 'node:fs';\nimport nodePath from 'node:path';\n\nimport { setupGoTooling } from '../packs/golang/setup.js';\nimport { installPack } from '../packs/install.js';\nimport {\n detectPythonLayers,\n detectPythonPackageManager,\n getPythonInstallCommand,\n hasRuffDependency,\n installPythonDependencies,\n} from '../packs/python/setup.js';\nimport { detectLanguages as detectLanguagePacks } from '../packs/registry.js';\nimport { reconcile, type ReconcileResult } from '../reconcile.js';\nimport { type ProjectContext, SAFEWORD_SCHEMA } from '../schema.js';\nimport { createProjectContext } from '../utils/context.js';\nimport { getEslintPeerMismatchWarning } from '../utils/eslint-peer-check.js';\nimport { exists, readJson, writeJson } from '../utils/fs.js';\nimport { installDependencies } from '../utils/install.js';\nimport { error, header, info, listItem, success, warn } from '../utils/output.js';\nimport { detectLanguages, type Languages } from '../utils/project-detector.js';\nimport { maybeAutoPatchOrNudge } from '../utils/vendored-ignores-nudge.js';\nimport { getWorkspacePatterns } from '../utils/workspaces.js';\nimport { VERSION } from '../version.js';\nimport { buildArchitecture, hasArchitectureDetected, syncConfigCore } from './sync-config.js';\n\ninterface PackageJson {\n name?: string;\n version?: string;\n scripts?: Record<string, string>;\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n 'lint-staged'?: Record<string, string[]>;\n workspaces?: string[] | { packages?: string[] };\n}\n\n/**\n * Process a glob workspace pattern (e.g., \"packages/*\").\n * Scans directory and adds format scripts to each package.\n */\nfunction processGlobWorkspacePattern(cwd: string, workspacePath: string): string[] {\n const updated: string[] = [];\n const fullPath = nodePath.join(cwd, workspacePath);\n\n if (!exists(fullPath)) return [];\n\n try {\n const entries = readdirSync(fullPath, { withFileTypes: true });\n for (const entry of entries) {\n if (!entry.isDirectory() || entry.name.startsWith('.')) continue;\n\n const packagePath = nodePath.join(fullPath, entry.name);\n if (addFormatScriptIfMissing(packagePath)) {\n updated.push(nodePath.join(workspacePath, entry.name, 'package.json'));\n }\n }\n } catch {\n // Directory not readable, skip\n }\n\n return updated;\n}\n\n/**\n * Process an explicit workspace path (e.g., \"tools/scripts\").\n */\nfunction processExplicitWorkspacePath(cwd: string, workspacePath: string): string[] {\n const fullPath = nodePath.join(cwd, workspacePath);\n if (addFormatScriptIfMissing(fullPath)) {\n return [nodePath.join(workspacePath, 'package.json')];\n }\n return [];\n}\n\n/**\n * Add format scripts to workspace packages that don't have them.\n * Only runs if root project uses Prettier (not an existing formatter like Biome).\n */\nfunction setupWorkspaceFormatScripts(cwd: string, ctx: ProjectContext): string[] {\n // Skip if root uses an existing formatter (Biome, dprint, etc.)\n if (ctx.projectType.existingFormatter) return [];\n\n const workspacePatterns = getWorkspacePatterns(cwd);\n if (workspacePatterns.length === 0) return [];\n\n const updated: string[] = [];\n\n for (const pattern of workspacePatterns) {\n const isGlobPattern = pattern.endsWith('/*');\n const workspacePath = isGlobPattern ? pattern.slice(0, -2) : pattern;\n\n const patternUpdates = isGlobPattern\n ? processGlobWorkspacePattern(cwd, workspacePath)\n : processExplicitWorkspacePath(cwd, workspacePath);\n\n updated.push(...patternUpdates);\n }\n\n return updated;\n}\n\n/**\n * Add format script to a package if it doesn't have one.\n * Returns true if the script was added.\n */\nfunction addFormatScriptIfMissing(packageDirectory: string): boolean {\n const packageJsonPath = nodePath.join(packageDirectory, 'package.json');\n if (!exists(packageJsonPath)) return false;\n\n const packageJson = readJson(packageJsonPath) as PackageJson | undefined;\n if (!packageJson) return false;\n\n // Skip if format script already exists\n if (packageJson.scripts?.format) return false;\n\n // Add format script\n const scripts = packageJson.scripts ?? {};\n scripts.format = 'prettier --write .';\n packageJson.scripts = scripts;\n writeJson(packageJsonPath, packageJson);\n\n return true;\n}\n\n/**\n * Create package.json if missing, unless non-JS-only project (Python, Go).\n * Returns true if created, false if already exists or skipped.\n */\nfunction ensurePackageJson(cwd: string): boolean {\n const packageJsonPath = nodePath.join(cwd, 'package.json');\n if (exists(packageJsonPath)) return false;\n\n // Skip for non-JS-only projects (no JS tooling needed)\n const languages = detectLanguages(cwd);\n const hasNonJs = languages.python || languages.golang || languages.rust;\n if (hasNonJs && !languages.javascript) return false;\n\n const dirName = nodePath.basename(cwd) || 'project';\n const defaultPackageJson: PackageJson = {\n name: dirName,\n version: '0.1.0',\n scripts: {},\n };\n writeJson(packageJsonPath, defaultPackageJson);\n return true;\n}\n\ninterface PythonSetupStatus {\n files: string[];\n installFailed: boolean;\n importLinter: boolean;\n}\n\n/** Base Python tools to install. Import-linter added when layers detected. */\nfunction getPythonTools(includeImportLinter: boolean): string[] {\n const tools = ['ruff', 'mypy', 'deadcode'];\n if (includeImportLinter) tools.push('import-linter');\n return tools;\n}\n\n/**\n * Configure Python tooling and install dependencies.\n * Config files (ruff.toml, mypy.ini, .importlinter) are created by reconciliation.\n * This function handles dependency installation.\n */\nfunction setupPython(cwd: string): PythonSetupStatus {\n let installFailed = false;\n\n // Detect layers for import-linter\n const layers = detectPythonLayers(cwd);\n const hasLayers = layers.length >= 2;\n\n // Install Python tools if not already in dependencies\n if (!hasRuffDependency(cwd)) {\n const tools = getPythonTools(hasLayers);\n const pm = detectPythonPackageManager(cwd);\n if (pm === 'pip') {\n installFailed = true;\n } else {\n info(`\\nInstalling Python tools (${tools.join(', ')})...`);\n const installed = installPythonDependencies(cwd, tools);\n if (installed) {\n success('Python tools installed');\n } else {\n installFailed = true;\n }\n }\n }\n\n // Note: files are now created by reconciliation, not returned here\n return { files: [], installFailed, importLinter: hasLayers };\n}\n\ninterface SetupSummaryOptions {\n cwd: string;\n result: ReconcileResult;\n packageJsonCreated: boolean;\n languages: Languages;\n archFiles?: string[];\n workspaceUpdates?: string[];\n pythonFiles?: string[];\n pythonInstallFailed?: boolean;\n pythonImportLinter?: boolean;\n}\n\n/**\n * Print list of created files.\n */\nfunction printCreatedFiles(createdFiles: string[], packageJsonCreated: boolean): void {\n if (createdFiles.length === 0 && !packageJsonCreated) return;\n\n info('\\nCreated:');\n if (packageJsonCreated) listItem('package.json');\n for (const file of createdFiles) listItem(file);\n}\n\n/**\n * Print list of modified files.\n */\nfunction printModifiedFiles(modifiedFiles: string[]): void {\n if (modifiedFiles.length === 0) return;\n\n info('\\nModified:');\n for (const file of modifiedFiles) listItem(file);\n}\n\n/**\n * Print language-specific next steps.\n */\nfunction printLanguageNextSteps(options: {\n cwd: string;\n languages: Languages;\n pythonInstallFailed: boolean;\n pythonImportLinter: boolean;\n golangciCreated: boolean;\n}): void {\n const { cwd, languages, pythonInstallFailed, pythonImportLinter, golangciCreated } = options;\n\n // Python: show install command only if auto-install failed\n if (languages.python && pythonInstallFailed) {\n listItem(\n `Install Python tools: ${getPythonInstallCommand(cwd, getPythonTools(pythonImportLinter))}`,\n );\n }\n\n // Go: show if .golangci.yml was created (Go tools are installed globally)\n if (languages.golang && golangciCreated) {\n listItem(\n 'Install Go tools: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest',\n );\n }\n}\n\nfunction printSetupSummary(options: SetupSummaryOptions): void {\n const {\n cwd,\n result,\n packageJsonCreated,\n languages,\n archFiles = [],\n workspaceUpdates = [],\n pythonFiles = [],\n pythonInstallFailed = false,\n pythonImportLinter = false,\n } = options;\n\n header('Setup Complete');\n\n // Collect created files (schema files + arch files + python config files)\n const createdFiles = [\n ...result.created,\n ...archFiles,\n ...pythonFiles.filter(f => f !== 'pyproject.toml'),\n ];\n printCreatedFiles(createdFiles, packageJsonCreated);\n\n // Collect modified files (schema updates + workspace updates + pyproject.toml)\n const modifiedFiles = [\n ...result.updated,\n ...workspaceUpdates,\n ...pythonFiles.filter(f => f === 'pyproject.toml'),\n ];\n printModifiedFiles(modifiedFiles);\n\n if (result.updated.includes('CLAUDE.md')) {\n info('\\nNote: CLAUDE.md — added one import line at the top. Your content is preserved.');\n }\n\n // Next steps\n info('\\nNext steps:');\n listItem('Run `safeword check` to verify setup');\n\n printLanguageNextSteps({\n cwd,\n languages,\n pythonInstallFailed,\n pythonImportLinter,\n golangciCreated: result.created.includes('.golangci.yml'),\n });\n\n listItem('Commit the new files to git');\n\n success(`\\nSafeword ${VERSION} installed successfully!`);\n}\n\n/**\n * Setup JavaScript project: architecture detection, depcruise config, workspace scripts\n */\nfunction setupJavaScriptProject(\n cwd: string,\n ctx: ProjectContext,\n packagesToInstall: string[],\n): { archFiles: string[]; workspaceUpdates: string[] } {\n const archFiles: string[] = [];\n const arch = buildArchitecture(cwd);\n\n if (hasArchitectureDetected(arch)) {\n const syncResult = syncConfigCore(cwd, arch);\n if (syncResult.generatedConfig) {\n archFiles.push('.safeword/depcruise-config.cjs');\n }\n if (syncResult.createdMainConfig) {\n archFiles.push('.dependency-cruiser.cjs');\n }\n logArchitectureDetected(arch);\n }\n\n const workspaceUpdates = setupWorkspaceFormatScripts(cwd, ctx);\n if (workspaceUpdates.length > 0) {\n info(`\\nAdded format scripts to ${workspaceUpdates.length} workspace package(s)`);\n }\n\n logExistingFormatter(ctx);\n\n const eslintWarning = getEslintPeerMismatchWarning(cwd);\n if (eslintWarning) warn(`\\n${eslintWarning}`);\n\n installDependencies(cwd, packagesToInstall, 'linting devDependencies');\n info('These are dev-only tools — your application dependencies are unchanged.');\n\n return { archFiles, workspaceUpdates };\n}\n\n/**\n * Log detected architecture elements and workspaces\n */\nfunction logArchitectureDetected(arch: ReturnType<typeof buildArchitecture>): void {\n const detected: string[] = [];\n if (arch.elements.length > 0) {\n detected.push(arch.elements.map(element => element.location).join(', '));\n }\n if (arch.workspaces && arch.workspaces.length > 0) {\n detected.push(`workspaces: ${arch.workspaces.join(', ')}`);\n }\n info(`\\nArchitecture detected: ${detected.join('; ')}`);\n info('Generated dependency-cruiser config for /audit command');\n}\n\n/**\n * Log existing formatter detection and explain ESLint coexistence\n */\nfunction logExistingFormatter(ctx: ProjectContext): void {\n if (!ctx.projectType.existingFormatter) return;\n\n info('\\nDetected existing formatter (biome/dprint) — skipping Prettier.');\n info('ESLint is still installed for security scanning, complexity checks, and framework rules');\n info(\"that biome/dprint don't cover. Both tools coexist without conflict.\");\n}\n\n/**\n * Log detected language and skip message\n */\nfunction logDetectedLanguage(languages: Languages): void {\n if (languages.python && !languages.javascript) {\n info('Python project detected (skipping JS tooling)');\n }\n if (languages.golang && !languages.javascript) {\n info('Go project detected (skipping JS tooling)');\n }\n if (languages.rust && !languages.javascript) {\n info('Rust project detected (skipping JS tooling)');\n }\n}\n\n/**\n * Register and setup detected language packs\n */\nfunction registerLanguagePacks(cwd: string): void {\n const detectedPacks = detectLanguagePacks(cwd);\n for (const packId of detectedPacks) {\n installPack(packId, cwd);\n }\n}\n\n/**\n * Setup Python project (dependencies installation).\n * Config files are created by reconciliation.\n */\nfunction setupPythonProject(languages: Languages, cwd: string): PythonSetupStatus {\n if (!languages.python) {\n return { files: [], installFailed: false, importLinter: false };\n }\n return setupPython(cwd);\n}\n\n/**\n * Setup Go project tooling.\n * Config files (.golangci.yml) are created by reconciliation.\n */\nfunction setupGoProject(languages: Languages): void {\n if (languages.golang) {\n setupGoTooling();\n }\n}\n\n/** Warn if Bun is not available (hooks require it) */\nfunction warnIfBunMissing(): void {\n try {\n execSync('bun --version', { stdio: 'pipe' });\n } catch {\n warn('bun not found — quality hooks will not work without it.');\n info(' Install: curl -fsSL https://bun.sh/install | bash');\n info(' Hooks will hard-block at session start until bun is available.');\n }\n}\n\nexport interface SetupOptions {\n /** When true, skip auto-editing the project's eslint config; fall through to the print-only nudge. */\n noModify?: boolean;\n}\n\nexport async function setup(options: SetupOptions): Promise<void> {\n const cwd = process.cwd();\n const safewordDirectory = nodePath.join(cwd, '.safeword');\n\n if (exists(safewordDirectory)) {\n error('Already configured. Run `safeword upgrade` to update.');\n process.exit(1);\n }\n\n const packageJsonCreated = ensurePackageJson(cwd);\n\n header('Safeword Setup');\n info(`Version: ${VERSION}`);\n if (packageJsonCreated) info('Created package.json (none found)');\n warnIfBunMissing();\n\n try {\n info('\\nCreating safeword configuration...');\n const ctx = createProjectContext(cwd);\n const languages = ctx.languages ?? {\n javascript: false,\n python: false,\n golang: false,\n rust: false,\n sql: false,\n };\n const isNonJsOnly =\n (languages.python || languages.golang || languages.rust) && !languages.javascript;\n\n logDetectedLanguage(languages);\n\n const result = await reconcile(SAFEWORD_SCHEMA, 'install', ctx);\n success('Created .safeword directory and configuration');\n\n // Language-specific setup\n const { archFiles, workspaceUpdates } = isNonJsOnly\n ? { archFiles: [], workspaceUpdates: [] }\n : setupJavaScriptProject(cwd, ctx, result.packagesToInstall);\n const pythonStatus = setupPythonProject(languages, cwd);\n setupGoProject(languages);\n registerLanguagePacks(cwd);\n\n printSetupSummary({\n cwd,\n result,\n packageJsonCreated,\n languages,\n archFiles,\n workspaceUpdates,\n pythonFiles: pythonStatus.files,\n pythonInstallFailed: pythonStatus.installFailed,\n pythonImportLinter: pythonStatus.importLinter,\n });\n\n maybeAutoPatchOrNudge({\n cwd,\n existingEslintConfig: ctx.projectType.existingEslintConfig,\n hasJavaScript: languages.javascript,\n noModify: options.noModify,\n });\n } catch (error_) {\n error(`Setup failed: ${error_ instanceof Error ? error_.message : 'Unknown error'}`);\n process.exit(1);\n }\n}\n"],"mappings":"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","names":["detectLanguages"]}
|
|
File without changes
|