safeword 0.36.0 → 0.38.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 +5 -5
- 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/dist/{ticket-new-FMXU2P4A.js → ticket-new-X7N6UD45.js} +23 -5
- package/dist/ticket-new-X7N6UD45.js.map +1 -0
- package/package.json +1 -1
- package/templates/commands/audit.md +5 -3
- package/templates/hooks/lib/active-ticket.ts +6 -4
- 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/ticket-new-FMXU2P4A.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":"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","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,13 +34,13 @@ 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) => {
|
|
43
|
-
const { ticketNew } = await import("./ticket-new-
|
|
43
|
+
const { ticketNew } = await import("./ticket-new-X7N6UD45.js");
|
|
44
44
|
await ticketNew(slug, options);
|
|
45
45
|
});
|
|
46
46
|
program.command("sync-learnings").description("Regenerate .safeword-project/learnings/INDEX.md").option("-q, --quiet", "Suppress success output (still prints skipped-file warnings to stderr)").action(async (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
|
|
@@ -50,10 +50,11 @@ function stripDashEdges(value) {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
// src/utils/ticket-writer.ts
|
|
53
|
-
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
|
53
|
+
import { existsSync, mkdirSync, readdirSync, writeFileSync } from "fs";
|
|
54
54
|
import nodePath from "path";
|
|
55
55
|
var TICKETS_SUBPATH = [".safeword-project", "tickets"];
|
|
56
56
|
var RETRY_BUDGET = 5;
|
|
57
|
+
var NON_TICKET_ENTRIES = /* @__PURE__ */ new Set(["completed", "tmp"]);
|
|
57
58
|
var TicketIdCollisionError = class extends Error {
|
|
58
59
|
constructor(attemptedIds, retryBudget) {
|
|
59
60
|
super(
|
|
@@ -71,16 +72,21 @@ function createTicket(cwd, minter, options) {
|
|
|
71
72
|
if (!existsSync(ticketsDirectory)) {
|
|
72
73
|
mkdirSync(ticketsDirectory, { recursive: true });
|
|
73
74
|
}
|
|
74
|
-
const { id, folderPath } = mintAndClaim(ticketsDirectory, minter);
|
|
75
|
+
const { id, folderPath } = mintAndClaim(ticketsDirectory, minter, options.slug);
|
|
75
76
|
const ticketPath = nodePath.join(folderPath, "ticket.md");
|
|
76
77
|
writeFileSync(ticketPath, renderTicketMarkdown(id, options));
|
|
77
78
|
return { id, folderPath, ticketPath };
|
|
78
79
|
}
|
|
79
|
-
function mintAndClaim(ticketsDirectory, minter) {
|
|
80
|
+
function mintAndClaim(ticketsDirectory, minter, slug) {
|
|
81
|
+
const takenIds = idsAlreadyTaken(ticketsDirectory);
|
|
80
82
|
const attempted = [];
|
|
81
83
|
for (let attempt = 0; attempt < RETRY_BUDGET; attempt++) {
|
|
82
84
|
const id = minter.mint();
|
|
83
|
-
|
|
85
|
+
if (takenIds.has(id)) {
|
|
86
|
+
attempted.push(id);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const folderPath = nodePath.join(ticketsDirectory, `${id}-${slug}`);
|
|
84
90
|
try {
|
|
85
91
|
mkdirSync(folderPath);
|
|
86
92
|
return { id, folderPath };
|
|
@@ -92,6 +98,18 @@ function mintAndClaim(ticketsDirectory, minter) {
|
|
|
92
98
|
}
|
|
93
99
|
throw new TicketIdCollisionError(attempted, RETRY_BUDGET);
|
|
94
100
|
}
|
|
101
|
+
function idsAlreadyTaken(ticketsDirectory) {
|
|
102
|
+
const ids = /* @__PURE__ */ new Set();
|
|
103
|
+
try {
|
|
104
|
+
for (const entry of readdirSync(ticketsDirectory)) {
|
|
105
|
+
if (NON_TICKET_ENTRIES.has(entry)) continue;
|
|
106
|
+
const dashIndex = entry.indexOf("-");
|
|
107
|
+
ids.add(dashIndex === -1 ? entry : entry.slice(0, dashIndex));
|
|
108
|
+
}
|
|
109
|
+
} catch {
|
|
110
|
+
}
|
|
111
|
+
return ids;
|
|
112
|
+
}
|
|
95
113
|
function renderTicketMarkdown(id, options) {
|
|
96
114
|
const type = options.type ?? "task";
|
|
97
115
|
const now = (options.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
|
|
@@ -177,4 +195,4 @@ function resolveMinter() {
|
|
|
177
195
|
export {
|
|
178
196
|
ticketNew
|
|
179
197
|
};
|
|
180
|
-
//# sourceMappingURL=ticket-new-
|
|
198
|
+
//# sourceMappingURL=ticket-new-X7N6UD45.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/ticket-new.ts","../src/utils/id-minter.ts","../src/utils/slug.ts","../src/utils/ticket-writer.ts"],"sourcesContent":["/**\n * `safeword ticket new <slug>` — mint a Crockford Base32 ticket ID and create\n * the ticket folder at `.safeword-project/tickets/{ID}/ticket.md` (ticket 158).\n *\n * Replaces the prompt-driven \"find highest folder + 1\" instruction in the\n * ticket-system skill, which was a race condition across parallel sessions\n * and silently colliding across git branches.\n */\n\nimport process from 'node:process';\n\nimport { cryptoIdMinter, type IdMinter } from '../utils/id-minter.js';\nimport { header, info, success } from '../utils/output.js';\nimport { normalizeSlug, SlugError } from '../utils/slug.js';\nimport { createTicket, TicketIdCollisionError, type TicketType } from '../utils/ticket-writer.js';\n\nconst VALID_TYPES: ReadonlySet<TicketType> = new Set(['patch', 'task', 'feature']);\n\nexport interface TicketNewOptions {\n type?: string;\n title?: string;\n}\n\nexport function ticketNew(slug: string, options: TicketNewOptions): Promise<void> {\n ticketNewSync(slug, options);\n return Promise.resolve();\n}\n\nfunction ticketNewSync(slug: string, options: TicketNewOptions): void {\n const type = resolveType(options.type);\n if (type === 'invalid') {\n process.stderr.write(\n `Invalid --type=${String(options.type)}. Must be one of: patch, task, feature.\\n`,\n );\n process.exit(1);\n }\n\n let normalizedSlug: string;\n try {\n normalizedSlug = normalizeSlug(slug);\n } catch (error: unknown) {\n if (error instanceof SlugError) {\n process.stderr.write(`${error.message}\\n`);\n process.exit(1);\n }\n throw error;\n }\n\n header('Create ticket');\n\n try {\n const result = createTicket(process.cwd(), resolveMinter(), {\n slug: normalizedSlug,\n type,\n title: options.title,\n });\n success(`Created ticket ${result.id}`);\n info(`Folder: ${result.folderPath}`);\n info(`File: ${result.ticketPath}`);\n } catch (error: unknown) {\n if (error instanceof TicketIdCollisionError) {\n process.stderr.write(`${error.message}\\n`);\n process.exit(1);\n }\n throw error;\n }\n}\n\nfunction resolveType(value: string | undefined): TicketType | undefined | 'invalid' {\n if (value === undefined) return undefined;\n return VALID_TYPES.has(value as TicketType) ? (value as TicketType) : 'invalid';\n}\n\n// Test-only injection point: SAFEWORD_TICKET_ID_OVERRIDE forces a specific\n// minted ID so cross-branch collision scenarios can be exercised deterministically.\n// The override is never set in production — the env var is intentionally\n// undocumented to discourage real-world use.\nfunction resolveMinter(): IdMinter {\n const override = process.env.SAFEWORD_TICKET_ID_OVERRIDE;\n if (override !== undefined && override !== '') {\n return { mint: () => override };\n }\n return cryptoIdMinter();\n}\n","/**\n * Crockford Base32 ticket ID minter (ticket 158).\n *\n * Mints uppercase 6-char IDs from the Crockford alphabet\n * `0123456789ABCDEFGHJKMNPQRSTVWXYZ` (no I/L/O/U). 32^6 ≈ 10⁹ ID space.\n *\n * Two implementations:\n * - cryptoIdMinter() — production default, uses crypto.randomInt\n * - seededIdMinter(seed) — deterministic, for tests that need reproducibility\n */\n\nimport { randomInt } from 'node:crypto';\n\nexport const CROCKFORD_ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';\nconst ID_LENGTH = 6;\n\nexport interface IdMinter {\n mint(): string;\n}\n\n/**\n * Build an ID from a constrained-range RNG that returns indices in\n * `[0, CROCKFORD_ALPHABET.length)`. The two exported minters differ only in\n * which RNG they pass in.\n */\nfunction buildId(nextIndex: () => number): string {\n const chars: string[] = [];\n for (let index = 0; index < ID_LENGTH; index++) {\n chars.push(CROCKFORD_ALPHABET.charAt(nextIndex()));\n }\n return chars.join('');\n}\n\nexport function cryptoIdMinter(): IdMinter {\n return { mint: () => buildId(() => randomInt(CROCKFORD_ALPHABET.length)) };\n}\n\n/**\n * Seeded PRNG (mulberry32) for deterministic test sequences.\n * Not cryptographically secure — production must use cryptoIdMinter.\n */\nexport function seededIdMinter(seed: number): IdMinter {\n let state = seed >>> 0;\n function nextUint32(): number {\n state = (state + 0x6d_2b_79_f5) >>> 0;\n let t = state;\n t = Math.imul(t ^ (t >>> 15), t | 1);\n t ^= t + Math.imul(t ^ (t >>> 7), t | 61);\n return (t ^ (t >>> 14)) >>> 0;\n }\n return { mint: () => buildId(() => nextUint32() % CROCKFORD_ALPHABET.length) };\n}\n","/**\n * Slug normalization for `safeword ticket new <slug>` (ticket 158, slice 3).\n *\n * Slugs are stored in frontmatter and feed work-log filenames. Normalize at the\n * CLI boundary so the canonical form is always lowercase kebab-case:\n * - NFKD-fold (decomposes accents)\n * - drop combining marks (Unicode block U+0300–U+036F)\n * - lowercase\n * - replace non-alphanumeric runs with a single `-`\n * - strip leading/trailing `-`\n * Empty result throws SlugError so the CLI can exit with a clear message.\n */\n\nexport class SlugError extends Error {\n constructor(public readonly input: string) {\n super(\n input === ''\n ? 'Slug cannot be empty.'\n : `Slug \"${input}\" normalizes to empty (no alphanumeric content).`,\n );\n this.name = 'SlugError';\n }\n}\n\n// `\\p{Mn}` matches Nonspacing-Mark characters — the combining marks NFKD\n// decomposition exposes when it pulls accents off their base letter.\nconst COMBINING_MARKS = /\\p{Mn}/gu;\nconst NON_ALNUM = /[^a-z\\d]+/g;\n\nexport function normalizeSlug(input: string): string {\n const folded = input.normalize('NFKD').replaceAll(COMBINING_MARKS, '');\n const collapsed = stripDashEdges(folded.toLowerCase().replaceAll(NON_ALNUM, '-'));\n if (collapsed === '') throw new SlugError(input);\n return collapsed;\n}\n\nfunction stripDashEdges(value: string): string {\n let start = 0;\n let end = value.length;\n while (start < end && value.charAt(start) === '-') start++;\n while (end > start && value.charAt(end - 1) === '-') end--;\n return value.slice(start, end);\n}\n","/**\n * Creates a new ticket folder + ticket.md (ticket 158).\n *\n * Folder layout: `.safeword-project/tickets/{ID}-{slug}/ticket.md`. The ID\n * stays the unique key (stored in frontmatter `id:` and used by the duplicate\n * detector); the slug suffix is for human/agent legibility when scanning\n * `ls` output. Mint-time collision check rejects any minted ID already in\n * use by an existing folder, regardless of that folder's slug suffix.\n *\n * Safety layers against duplicate IDs (PR #160 trade-off):\n * 1. Mint-time: idsAlreadyTaken() — within one working copy, blocks re-mint.\n * 2. Post-merge: check-ticket-ids.ts (pre-commit + CI) — across branches,\n * duplicate `id:` in frontmatter is the loud failure. The previous\n * layout (`{ID}/` alone) used identical filesystem paths as an extra\n * merge-time conflict layer; the slug suffix breaks that, so detection\n * shifts entirely to the post-merge detector.\n *\n * Mint-collision retry + fresh-install (no tickets dir yet) handled here.\n */\n\nimport { existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs';\nimport nodePath from 'node:path';\n\nimport type { IdMinter } from './id-minter.js';\n\nconst TICKETS_SUBPATH = ['.safeword-project', 'tickets'];\nconst RETRY_BUDGET = 5;\nconst NON_TICKET_ENTRIES = new Set(['completed', 'tmp']);\n\nexport type TicketType = 'patch' | 'task' | 'feature';\n\nexport interface NewTicketOptions {\n slug: string;\n type?: TicketType;\n title?: string;\n /** Override `new Date()` for tests. */\n now?: () => Date;\n}\n\nexport interface NewTicketResult {\n id: string;\n folderPath: string;\n ticketPath: string;\n}\n\nexport class TicketIdCollisionError extends Error {\n constructor(\n public readonly attemptedIds: string[],\n public readonly retryBudget: number,\n ) {\n super(\n `Failed to mint a unique ticket ID after ${retryBudget} attempts. Tried: ${attemptedIds.join(', ')}.`,\n );\n this.name = 'TicketIdCollisionError';\n }\n}\n\nexport function createTicket(\n cwd: string,\n minter: IdMinter,\n options: NewTicketOptions,\n): NewTicketResult {\n const ticketsDirectory = nodePath.join(cwd, ...TICKETS_SUBPATH);\n if (!existsSync(ticketsDirectory)) {\n mkdirSync(ticketsDirectory, { recursive: true });\n }\n\n const { id, folderPath } = mintAndClaim(ticketsDirectory, minter, options.slug);\n const ticketPath = nodePath.join(folderPath, 'ticket.md');\n writeFileSync(ticketPath, renderTicketMarkdown(id, options));\n\n return { id, folderPath, ticketPath };\n}\n\nfunction mintAndClaim(\n ticketsDirectory: string,\n minter: IdMinter,\n slug: string,\n): { id: string; folderPath: string } {\n const takenIds = idsAlreadyTaken(ticketsDirectory);\n const attempted: string[] = [];\n for (let attempt = 0; attempt < RETRY_BUDGET; attempt++) {\n const id = minter.mint();\n if (takenIds.has(id)) {\n attempted.push(id);\n continue;\n }\n const folderPath = nodePath.join(ticketsDirectory, `${id}-${slug}`);\n try {\n mkdirSync(folderPath);\n return { id, folderPath };\n } catch (error: unknown) {\n const code = (error as NodeJS.ErrnoException).code;\n if (code !== 'EEXIST') throw error;\n attempted.push(id);\n }\n }\n throw new TicketIdCollisionError(attempted, RETRY_BUDGET);\n}\n\n// Extract the ID portion of every existing ticket folder. Folders use either\n// `{id}` (legacy opaque) or `{id}-{slug}` — split on the first `-`. This is the\n// loud-failure mechanism that keeps mint-time ID collisions from coexisting on\n// disk regardless of slug suffix.\nfunction idsAlreadyTaken(ticketsDirectory: string): Set<string> {\n const ids = new Set<string>();\n try {\n for (const entry of readdirSync(ticketsDirectory)) {\n if (NON_TICKET_ENTRIES.has(entry)) continue;\n const dashIndex = entry.indexOf('-');\n ids.add(dashIndex === -1 ? entry : entry.slice(0, dashIndex));\n }\n } catch {\n // tickets dir may not exist yet on fresh installs — caller creates it.\n }\n return ids;\n}\n\nfunction renderTicketMarkdown(id: string, options: NewTicketOptions): string {\n const type = options.type ?? 'task';\n const now = (options.now ?? (() => new Date()))().toISOString();\n const title = options.title ?? options.slug;\n\n return `---\nid: ${id}\nslug: ${options.slug}\ntype: ${type}\nphase: intake\nstatus: in_progress\ncreated: ${now}\nlast_modified: ${now}\n---\n\n# ${title}\n\n**Goal:** {One sentence: what are we trying to achieve?}\n\n**Why:** {One sentence: why does this matter?}\n\n## Work Log\n\n- ${now} Started: Created ticket ${id}\n`;\n}\n"],"mappings":"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","names":[]}
|
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
|
|
|
@@ -35,10 +35,12 @@ const EMPTY_DETAILS: TicketDetails = {
|
|
|
35
35
|
* Look up a specific ticket's phase and status by ID.
|
|
36
36
|
*
|
|
37
37
|
* Resolves two folder layouts:
|
|
38
|
-
* -
|
|
39
|
-
* `
|
|
40
|
-
*
|
|
41
|
-
*
|
|
38
|
+
* - `{id}-{slug}/` — folder name starts with `${id}-` (e.g. `080-foo`,
|
|
39
|
+
* `G2E72G-yolo-mode`). Canonical going forward; case-sensitive on the prefix
|
|
40
|
+
* since Base32 IDs are minted uppercase and legacy numeric IDs have no case.
|
|
41
|
+
* - `{id}/` — folder name equals the ID exactly. Historical shape used by
|
|
42
|
+
* opaque Base32 tickets minted before the slug suffix was added; lookup is
|
|
43
|
+
* case-insensitive on input.
|
|
42
44
|
*
|
|
43
45
|
* If two folders both resolve to the input ID (manual mistake or copy-paste),
|
|
44
46
|
* lookup writes a warning to stderr and returns empty details rather than
|
|
@@ -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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAMA,SAAS,gBAAgB;AACzB,SAAS,mBAAmB;AAC5B,OAAO,cAAc;AAuCrB,SAAS,4BAA4B,KAAa,eAAiC;AACjF,QAAM,UAAoB,CAAC;AAC3B,QAAM,WAAW,SAAS,KAAK,KAAK,aAAa;AAEjD,MAAI,CAAC,OAAO,QAAQ,EAAG,QAAO,CAAC;AAE/B,MAAI;AACF,UAAM,UAAU,YAAY,UAAU,EAAE,eAAe,KAAK,CAAC;AAC7D,eAAW,SAAS,SAAS;AAC3B,UAAI,CAAC,MAAM,YAAY,KAAK,MAAM,KAAK,WAAW,GAAG,EAAG;AAExD,YAAM,cAAc,SAAS,KAAK,UAAU,MAAM,IAAI;AACtD,UAAI,yBAAyB,WAAW,GAAG;AACzC,gBAAQ,KAAK,SAAS,KAAK,eAAe,MAAM,MAAM,cAAc,CAAC;AAAA,MACvE;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAER;AAEA,SAAO;AACT;AAKA,SAAS,6BAA6B,KAAa,eAAiC;AAClF,QAAM,WAAW,SAAS,KAAK,KAAK,aAAa;AACjD,MAAI,yBAAyB,QAAQ,GAAG;AACtC,WAAO,CAAC,SAAS,KAAK,eAAe,cAAc,CAAC;AAAA,EACtD;AACA,SAAO,CAAC;AACV;AAMA,SAAS,4BAA4B,KAAa,KAA+B;AAE/E,MAAI,IAAI,YAAY,kBAAmB,QAAO,CAAC;AAE/C,QAAM,oBAAoB,qBAAqB,GAAG;AAClD,MAAI,kBAAkB,WAAW,EAAG,QAAO,CAAC;AAE5C,QAAM,UAAoB,CAAC;AAE3B,aAAW,WAAW,mBAAmB;AACvC,UAAM,gBAAgB,QAAQ,SAAS,IAAI;AAC3C,UAAM,gBAAgB,gBAAgB,QAAQ,MAAM,GAAG,EAAE,IAAI;AAE7D,UAAM,iBAAiB,gBACnB,4BAA4B,KAAK,aAAa,IAC9C,6BAA6B,KAAK,aAAa;AAEnD,YAAQ,KAAK,GAAG,cAAc;AAAA,EAChC;AAEA,SAAO;AACT;AAMA,SAAS,yBAAyB,kBAAmC;AACnE,QAAM,kBAAkB,SAAS,KAAK,kBAAkB,cAAc;AACtE,MAAI,CAAC,OAAO,eAAe,EAAG,QAAO;AAErC,QAAM,cAAc,SAAS,eAAe;AAC5C,MAAI,CAAC,YAAa,QAAO;AAGzB,MAAI,YAAY,SAAS,OAAQ,QAAO;AAGxC,QAAM,UAAU,YAAY,WAAW,CAAC;AACxC,UAAQ,SAAS;AACjB,cAAY,UAAU;AACtB,YAAU,iBAAiB,WAAW;AAEtC,SAAO;AACT;AAMA,SAAS,kBAAkB,KAAsB;AAC/C,QAAM,kBAAkB,SAAS,KAAK,KAAK,cAAc;AACzD,MAAI,OAAO,eAAe,EAAG,QAAO;AAGpC,QAAM,YAAYA,iBAAgB,GAAG;AACrC,QAAM,WAAW,UAAU,UAAU,UAAU,UAAU,UAAU;AACnE,MAAI,YAAY,CAAC,UAAU,WAAY,QAAO;AAE9C,QAAM,UAAU,SAAS,SAAS,GAAG,KAAK;AAC1C,QAAM,qBAAkC;AAAA,IACtC,MAAM;AAAA,IACN,SAAS;AAAA,IACT,SAAS,CAAC;AAAA,EACZ;AACA,YAAU,iBAAiB,kBAAkB;AAC7C,SAAO;AACT;AASA,SAAS,eAAe,qBAAwC;AAC9D,QAAM,QAAQ,CAAC,QAAQ,QAAQ,UAAU;AACzC,MAAI,oBAAqB,OAAM,KAAK,eAAe;AACnD,SAAO;AACT;AAOA,SAAS,YAAY,KAAgC;AACnD,MAAI,gBAAgB;AAGpB,QAAM,SAAS,mBAAmB,GAAG;AACrC,QAAM,YAAY,OAAO,UAAU;AAGnC,MAAI,CAAC,kBAAkB,GAAG,GAAG;AAC3B,UAAM,QAAQ,eAAe,SAAS;AACtC,UAAM,KAAK,2BAA2B,GAAG;AACzC,QAAI,OAAO,OAAO;AAChB,sBAAgB;AAAA,IAClB,OAAO;AACL,WAAK;AAAA,2BAA8B,MAAM,KAAK,IAAI,CAAC,MAAM;AACzD,YAAM,YAAY,0BAA0B,KAAK,KAAK;AACtD,UAAI,WAAW;AACb,gBAAQ,wBAAwB;AAAA,MAClC,OAAO;AACL,wBAAgB;AAAA,MAClB;AAAA,IACF;AAAA,EACF;AAGA,SAAO,EAAE,OAAO,CAAC,GAAG,eAAe,cAAc,UAAU;AAC7D;AAiBA,SAAS,kBAAkB,cAAwB,oBAAmC;AACpF,MAAI,aAAa,WAAW,KAAK,CAAC,mBAAoB;AAEtD,OAAK,YAAY;AACjB,MAAI,mBAAoB,UAAS,cAAc;AAC/C,aAAW,QAAQ,aAAc,UAAS,IAAI;AAChD;AAKA,SAAS,mBAAmB,eAA+B;AACzD,MAAI,cAAc,WAAW,EAAG;AAEhC,OAAK,aAAa;AAClB,aAAW,QAAQ,cAAe,UAAS,IAAI;AACjD;AAKA,SAAS,uBAAuB,SAMvB;AACP,QAAM,EAAE,KAAK,WAAW,qBAAqB,oBAAoB,gBAAgB,IAAI;AAGrF,MAAI,UAAU,UAAU,qBAAqB;AAC3C;AAAA,MACE,yBAAyB,wBAAwB,KAAK,eAAe,kBAAkB,CAAC,CAAC;AAAA,IAC3F;AAAA,EACF;AAGA,MAAI,UAAU,UAAU,iBAAiB;AACvC;AAAA,MACE;AAAA,IACF;AAAA,EACF;AACF;AAEA,SAAS,kBAAkB,SAAoC;AAC7D,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,YAAY,CAAC;AAAA,IACb,mBAAmB,CAAC;AAAA,IACpB,cAAc,CAAC;AAAA,IACf,sBAAsB;AAAA,IACtB,qBAAqB;AAAA,EACvB,IAAI;AAEJ,SAAO,gBAAgB;AAGvB,QAAM,eAAe;AAAA,IACnB,GAAG,OAAO;AAAA,IACV,GAAG;AAAA,IACH,GAAG,YAAY,OAAO,OAAK,MAAM,gBAAgB;AAAA,EACnD;AACA,oBAAkB,cAAc,kBAAkB;AAGlD,QAAM,gBAAgB;AAAA,IACpB,GAAG,OAAO;AAAA,IACV,GAAG;AAAA,IACH,GAAG,YAAY,OAAO,OAAK,MAAM,gBAAgB;AAAA,EACnD;AACA,qBAAmB,aAAa;AAEhC,MAAI,OAAO,QAAQ,SAAS,WAAW,GAAG;AACxC,SAAK,uFAAkF;AAAA,EACzF;AAGA,OAAK,eAAe;AACpB,WAAS,sCAAsC;AAE/C,yBAAuB;AAAA,IACrB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,iBAAiB,OAAO,QAAQ,SAAS,eAAe;AAAA,EAC1D,CAAC;AAED,WAAS,6BAA6B;AAEtC,UAAQ;AAAA,WAAc,OAAO,0BAA0B;AACzD;AAKA,SAAS,uBACP,KACA,KACA,mBACqD;AACrD,QAAM,YAAsB,CAAC;AAC7B,QAAM,OAAO,kBAAkB,GAAG;AAElC,MAAI,wBAAwB,IAAI,GAAG;AACjC,UAAM,aAAa,eAAe,KAAK,IAAI;AAC3C,QAAI,WAAW,iBAAiB;AAC9B,gBAAU,KAAK,gCAAgC;AAAA,IACjD;AACA,QAAI,WAAW,mBAAmB;AAChC,gBAAU,KAAK,yBAAyB;AAAA,IAC1C;AACA,4BAAwB,IAAI;AAAA,EAC9B;AAEA,QAAM,mBAAmB,4BAA4B,KAAK,GAAG;AAC7D,MAAI,iBAAiB,SAAS,GAAG;AAC/B,SAAK;AAAA,0BAA6B,iBAAiB,MAAM,uBAAuB;AAAA,EAClF;AAEA,uBAAqB,GAAG;AAExB,QAAM,gBAAgB,6BAA6B,GAAG;AACtD,MAAI,cAAe,MAAK;AAAA,EAAK,aAAa,EAAE;AAE5C,sBAAoB,KAAK,mBAAmB,yBAAyB;AACrE,OAAK,8EAAyE;AAE9E,SAAO,EAAE,WAAW,iBAAiB;AACvC;AAKA,SAAS,wBAAwB,MAAkD;AACjF,QAAM,WAAqB,CAAC;AAC5B,MAAI,KAAK,SAAS,SAAS,GAAG;AAC5B,aAAS,KAAK,KAAK,SAAS,IAAI,aAAW,QAAQ,QAAQ,EAAE,KAAK,IAAI,CAAC;AAAA,EACzE;AACA,MAAI,KAAK,cAAc,KAAK,WAAW,SAAS,GAAG;AACjD,aAAS,KAAK,eAAe,KAAK,WAAW,KAAK,IAAI,CAAC,EAAE;AAAA,EAC3D;AACA,OAAK;AAAA,yBAA4B,SAAS,KAAK,IAAI,CAAC,EAAE;AACtD,OAAK,wDAAwD;AAC/D;AAKA,SAAS,qBAAqB,KAA2B;AACvD,MAAI,CAAC,IAAI,YAAY,kBAAmB;AAExC,OAAK,wEAAmE;AACxE,OAAK,yFAAyF;AAC9F,OAAK,qEAAqE;AAC5E;AAKA,SAAS,oBAAoB,WAA4B;AACvD,MAAI,UAAU,UAAU,CAAC,UAAU,YAAY;AAC7C,SAAK,+CAA+C;AAAA,EACtD;AACA,MAAI,UAAU,UAAU,CAAC,UAAU,YAAY;AAC7C,SAAK,2CAA2C;AAAA,EAClD;AACA,MAAI,UAAU,QAAQ,CAAC,UAAU,YAAY;AAC3C,SAAK,6CAA6C;AAAA,EACpD;AACF;AAKA,SAAS,sBAAsB,KAAmB;AAChD,QAAM,gBAAgB,gBAAoB,GAAG;AAC7C,aAAW,UAAU,eAAe;AAClC,gBAAY,QAAQ,GAAG;AAAA,EACzB;AACF;AAMA,SAAS,mBAAmB,WAAsB,KAAgC;AAChF,MAAI,CAAC,UAAU,QAAQ;AACrB,WAAO,EAAE,OAAO,CAAC,GAAG,eAAe,OAAO,cAAc,MAAM;AAAA,EAChE;AACA,SAAO,YAAY,GAAG;AACxB;AAMA,SAAS,eAAe,WAA4B;AAClD,MAAI,UAAU,QAAQ;AACpB,mBAAe;AAAA,EACjB;AACF;AAGA,SAAS,mBAAyB;AAChC,MAAI;AACF,aAAS,iBAAiB,EAAE,OAAO,OAAO,CAAC;AAAA,EAC7C,QAAQ;AACN,SAAK,8DAAyD;AAC9D,SAAK,qDAAqD;AAC1D,SAAK,kEAAkE;AAAA,EACzE;AACF;AAOA,eAAsB,MAAM,SAAsC;AAChE,QAAM,MAAM,QAAQ,IAAI;AACxB,QAAM,oBAAoB,SAAS,KAAK,KAAK,WAAW;AAExD,MAAI,OAAO,iBAAiB,GAAG;AAC7B,UAAM,uDAAuD;AAC7D,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,QAAM,qBAAqB,kBAAkB,GAAG;AAEhD,SAAO,gBAAgB;AACvB,OAAK,YAAY,OAAO,EAAE;AAC1B,MAAI,mBAAoB,MAAK,mCAAmC;AAChE,mBAAiB;AAEjB,MAAI;AACF,SAAK,sCAAsC;AAC3C,UAAM,MAAM,qBAAqB,GAAG;AACpC,UAAM,YAAY,IAAI,aAAa;AAAA,MACjC,YAAY;AAAA,MACZ,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,KAAK;AAAA,IACP;AACA,UAAM,eACH,UAAU,UAAU,UAAU,UAAU,UAAU,SAAS,CAAC,UAAU;AAEzE,wBAAoB,SAAS;AAE7B,UAAM,SAAS,MAAM,UAAU,iBAAiB,WAAW,GAAG;AAC9D,YAAQ,+CAA+C;AAGvD,UAAM,EAAE,WAAW,iBAAiB,IAAI,cACpC,EAAE,WAAW,CAAC,GAAG,kBAAkB,CAAC,EAAE,IACtC,uBAAuB,KAAK,KAAK,OAAO,iBAAiB;AAC7D,UAAM,eAAe,mBAAmB,WAAW,GAAG;AACtD,mBAAe,SAAS;AACxB,0BAAsB,GAAG;AAEzB,sBAAkB;AAAA,MAChB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,aAAa,aAAa;AAAA,MAC1B,qBAAqB,aAAa;AAAA,MAClC,oBAAoB,aAAa;AAAA,IACnC,CAAC;AAED,0BAAsB;AAAA,MACpB;AAAA,MACA,sBAAsB,IAAI,YAAY;AAAA,MACtC,eAAe,UAAU;AAAA,MACzB,UAAU,QAAQ;AAAA,IACpB,CAAC;AAAA,EACH,SAAS,QAAQ;AACf,UAAM,iBAAiB,kBAAkB,QAAQ,OAAO,UAAU,eAAe,EAAE;AACnF,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF;","names":["detectLanguages"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/commands/ticket-new.ts","../src/utils/id-minter.ts","../src/utils/slug.ts","../src/utils/ticket-writer.ts"],"sourcesContent":["/**\n * `safeword ticket new <slug>` — mint a Crockford Base32 ticket ID and create\n * the ticket folder at `.safeword-project/tickets/{ID}/ticket.md` (ticket 158).\n *\n * Replaces the prompt-driven \"find highest folder + 1\" instruction in the\n * ticket-system skill, which was a race condition across parallel sessions\n * and silently colliding across git branches.\n */\n\nimport process from 'node:process';\n\nimport { cryptoIdMinter, type IdMinter } from '../utils/id-minter.js';\nimport { header, info, success } from '../utils/output.js';\nimport { normalizeSlug, SlugError } from '../utils/slug.js';\nimport { createTicket, TicketIdCollisionError, type TicketType } from '../utils/ticket-writer.js';\n\nconst VALID_TYPES: ReadonlySet<TicketType> = new Set(['patch', 'task', 'feature']);\n\nexport interface TicketNewOptions {\n type?: string;\n title?: string;\n}\n\nexport function ticketNew(slug: string, options: TicketNewOptions): Promise<void> {\n ticketNewSync(slug, options);\n return Promise.resolve();\n}\n\nfunction ticketNewSync(slug: string, options: TicketNewOptions): void {\n const type = resolveType(options.type);\n if (type === 'invalid') {\n process.stderr.write(\n `Invalid --type=${String(options.type)}. Must be one of: patch, task, feature.\\n`,\n );\n process.exit(1);\n }\n\n let normalizedSlug: string;\n try {\n normalizedSlug = normalizeSlug(slug);\n } catch (error: unknown) {\n if (error instanceof SlugError) {\n process.stderr.write(`${error.message}\\n`);\n process.exit(1);\n }\n throw error;\n }\n\n header('Create ticket');\n\n try {\n const result = createTicket(process.cwd(), resolveMinter(), {\n slug: normalizedSlug,\n type,\n title: options.title,\n });\n success(`Created ticket ${result.id}`);\n info(`Folder: ${result.folderPath}`);\n info(`File: ${result.ticketPath}`);\n } catch (error: unknown) {\n if (error instanceof TicketIdCollisionError) {\n process.stderr.write(`${error.message}\\n`);\n process.exit(1);\n }\n throw error;\n }\n}\n\nfunction resolveType(value: string | undefined): TicketType | undefined | 'invalid' {\n if (value === undefined) return undefined;\n return VALID_TYPES.has(value as TicketType) ? (value as TicketType) : 'invalid';\n}\n\n// Test-only injection point: SAFEWORD_TICKET_ID_OVERRIDE forces a specific\n// minted ID so cross-branch collision scenarios can be exercised deterministically.\n// The override is never set in production — the env var is intentionally\n// undocumented to discourage real-world use.\nfunction resolveMinter(): IdMinter {\n const override = process.env.SAFEWORD_TICKET_ID_OVERRIDE;\n if (override !== undefined && override !== '') {\n return { mint: () => override };\n }\n return cryptoIdMinter();\n}\n","/**\n * Crockford Base32 ticket ID minter (ticket 158).\n *\n * Mints uppercase 6-char IDs from the Crockford alphabet\n * `0123456789ABCDEFGHJKMNPQRSTVWXYZ` (no I/L/O/U). 32^6 ≈ 10⁹ ID space.\n *\n * Two implementations:\n * - cryptoIdMinter() — production default, uses crypto.randomInt\n * - seededIdMinter(seed) — deterministic, for tests that need reproducibility\n */\n\nimport { randomInt } from 'node:crypto';\n\nexport const CROCKFORD_ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';\nconst ID_LENGTH = 6;\n\nexport interface IdMinter {\n mint(): string;\n}\n\n/**\n * Build an ID from a constrained-range RNG that returns indices in\n * `[0, CROCKFORD_ALPHABET.length)`. The two exported minters differ only in\n * which RNG they pass in.\n */\nfunction buildId(nextIndex: () => number): string {\n const chars: string[] = [];\n for (let index = 0; index < ID_LENGTH; index++) {\n chars.push(CROCKFORD_ALPHABET.charAt(nextIndex()));\n }\n return chars.join('');\n}\n\nexport function cryptoIdMinter(): IdMinter {\n return { mint: () => buildId(() => randomInt(CROCKFORD_ALPHABET.length)) };\n}\n\n/**\n * Seeded PRNG (mulberry32) for deterministic test sequences.\n * Not cryptographically secure — production must use cryptoIdMinter.\n */\nexport function seededIdMinter(seed: number): IdMinter {\n let state = seed >>> 0;\n function nextUint32(): number {\n state = (state + 0x6d_2b_79_f5) >>> 0;\n let t = state;\n t = Math.imul(t ^ (t >>> 15), t | 1);\n t ^= t + Math.imul(t ^ (t >>> 7), t | 61);\n return (t ^ (t >>> 14)) >>> 0;\n }\n return { mint: () => buildId(() => nextUint32() % CROCKFORD_ALPHABET.length) };\n}\n","/**\n * Slug normalization for `safeword ticket new <slug>` (ticket 158, slice 3).\n *\n * Slugs are stored in frontmatter and feed work-log filenames. Normalize at the\n * CLI boundary so the canonical form is always lowercase kebab-case:\n * - NFKD-fold (decomposes accents)\n * - drop combining marks (Unicode block U+0300–U+036F)\n * - lowercase\n * - replace non-alphanumeric runs with a single `-`\n * - strip leading/trailing `-`\n * Empty result throws SlugError so the CLI can exit with a clear message.\n */\n\nexport class SlugError extends Error {\n constructor(public readonly input: string) {\n super(\n input === ''\n ? 'Slug cannot be empty.'\n : `Slug \"${input}\" normalizes to empty (no alphanumeric content).`,\n );\n this.name = 'SlugError';\n }\n}\n\n// `\\p{Mn}` matches Nonspacing-Mark characters — the combining marks NFKD\n// decomposition exposes when it pulls accents off their base letter.\nconst COMBINING_MARKS = /\\p{Mn}/gu;\nconst NON_ALNUM = /[^a-z\\d]+/g;\n\nexport function normalizeSlug(input: string): string {\n const folded = input.normalize('NFKD').replaceAll(COMBINING_MARKS, '');\n const collapsed = stripDashEdges(folded.toLowerCase().replaceAll(NON_ALNUM, '-'));\n if (collapsed === '') throw new SlugError(input);\n return collapsed;\n}\n\nfunction stripDashEdges(value: string): string {\n let start = 0;\n let end = value.length;\n while (start < end && value.charAt(start) === '-') start++;\n while (end > start && value.charAt(end - 1) === '-') end--;\n return value.slice(start, end);\n}\n","/**\n * Creates a new ticket folder + ticket.md (ticket 158).\n *\n * Folder layout: `.safeword-project/tickets/{ID}/ticket.md`. Folder name is the\n * Crockford ID alone — slug lives in frontmatter. Any duplicate ID becomes a\n * real git merge conflict instead of two silently-coexisting folders.\n *\n * EEXIST retry + fresh-install (no tickets dir yet) handled here. Slice 1 sets\n * up the structure; slice 2 wires the deterministic retry tests.\n */\n\nimport { existsSync, mkdirSync, writeFileSync } from 'node:fs';\nimport nodePath from 'node:path';\n\nimport type { IdMinter } from './id-minter.js';\n\nconst TICKETS_SUBPATH = ['.safeword-project', 'tickets'];\nconst RETRY_BUDGET = 5;\n\nexport type TicketType = 'patch' | 'task' | 'feature';\n\nexport interface NewTicketOptions {\n slug: string;\n type?: TicketType;\n title?: string;\n /** Override `new Date()` for tests. */\n now?: () => Date;\n}\n\nexport interface NewTicketResult {\n id: string;\n folderPath: string;\n ticketPath: string;\n}\n\nexport class TicketIdCollisionError extends Error {\n constructor(\n public readonly attemptedIds: string[],\n public readonly retryBudget: number,\n ) {\n super(\n `Failed to mint a unique ticket ID after ${retryBudget} attempts. Tried: ${attemptedIds.join(', ')}.`,\n );\n this.name = 'TicketIdCollisionError';\n }\n}\n\nexport function createTicket(\n cwd: string,\n minter: IdMinter,\n options: NewTicketOptions,\n): NewTicketResult {\n const ticketsDirectory = nodePath.join(cwd, ...TICKETS_SUBPATH);\n if (!existsSync(ticketsDirectory)) {\n mkdirSync(ticketsDirectory, { recursive: true });\n }\n\n const { id, folderPath } = mintAndClaim(ticketsDirectory, minter);\n const ticketPath = nodePath.join(folderPath, 'ticket.md');\n writeFileSync(ticketPath, renderTicketMarkdown(id, options));\n\n return { id, folderPath, ticketPath };\n}\n\nfunction mintAndClaim(\n ticketsDirectory: string,\n minter: IdMinter,\n): { id: string; folderPath: string } {\n const attempted: string[] = [];\n for (let attempt = 0; attempt < RETRY_BUDGET; attempt++) {\n const id = minter.mint();\n const folderPath = nodePath.join(ticketsDirectory, id);\n try {\n mkdirSync(folderPath);\n return { id, folderPath };\n } catch (error: unknown) {\n const code = (error as NodeJS.ErrnoException).code;\n if (code !== 'EEXIST') throw error;\n attempted.push(id);\n }\n }\n throw new TicketIdCollisionError(attempted, RETRY_BUDGET);\n}\n\nfunction renderTicketMarkdown(id: string, options: NewTicketOptions): string {\n const type = options.type ?? 'task';\n const now = (options.now ?? (() => new Date()))().toISOString();\n const title = options.title ?? options.slug;\n\n return `---\nid: ${id}\nslug: ${options.slug}\ntype: ${type}\nphase: intake\nstatus: in_progress\ncreated: ${now}\nlast_modified: ${now}\n---\n\n# ${title}\n\n**Goal:** {One sentence: what are we trying to achieve?}\n\n**Why:** {One sentence: why does this matter?}\n\n## Work Log\n\n- ${now} Started: Created ticket ${id}\n`;\n}\n"],"mappings":"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","names":[]}
|
|
File without changes
|