safeword 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{check-KTLIM6UR.js → check-INXMFCL5.js} +7 -5
- package/dist/{check-KTLIM6UR.js.map → check-INXMFCL5.js.map} +1 -1
- package/dist/chunk-6CVTH67L.js +43 -0
- package/dist/chunk-6CVTH67L.js.map +1 -0
- package/dist/{chunk-KEWQQP4C.js → chunk-75FKNZUM.js} +2 -2
- package/dist/{chunk-NI3Z5XHH.js → chunk-ARIAOK2F.js} +2 -34
- package/dist/chunk-ARIAOK2F.js.map +1 -0
- package/dist/chunk-FRPJITGG.js +35 -0
- package/dist/chunk-FRPJITGG.js.map +1 -0
- package/dist/chunk-IWWBZVHT.js +274 -0
- package/dist/chunk-IWWBZVHT.js.map +1 -0
- package/dist/cli.js +9 -5
- package/dist/cli.js.map +1 -1
- package/dist/{diff-TV24EW3X.js → diff-L7G22MG7.js} +7 -5
- package/dist/{diff-TV24EW3X.js.map → diff-L7G22MG7.js.map} +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/{reset-WJEQTYCX.js → reset-5SRM3P6J.js} +9 -7
- package/dist/reset-5SRM3P6J.js.map +1 -0
- package/dist/{setup-UYCYH5SE.js → setup-65EVU5OT.js} +179 -75
- package/dist/setup-65EVU5OT.js.map +1 -0
- package/dist/sync-4XBMKLXS.js +116 -0
- package/dist/sync-4XBMKLXS.js.map +1 -0
- package/dist/{upgrade-TJZYMNP2.js → upgrade-P3WX3ODU.js} +11 -9
- package/dist/{upgrade-TJZYMNP2.js.map → upgrade-P3WX3ODU.js.map} +1 -1
- package/package.json +2 -1
- package/templates/SAFEWORD.md +4 -4
- package/templates/commands/architecture.md +27 -0
- package/templates/commands/lint.md +15 -54
- package/templates/commands/quality-review.md +16 -13
- package/templates/hooks/post-tool-lint.sh +14 -53
- package/dist/chunk-74FG33MU.js +0 -211
- package/dist/chunk-74FG33MU.js.map +0 -1
- package/dist/chunk-NI3Z5XHH.js.map +0 -1
- package/dist/reset-WJEQTYCX.js.map +0 -1
- package/dist/setup-UYCYH5SE.js.map +0 -1
- package/templates/commands/arch-review.md +0 -28
- package/templates/hooks/git-pre-commit.sh +0 -18
- /package/dist/{chunk-KEWQQP4C.js.map → chunk-75FKNZUM.js.map} +0 -0
- /package/templates/prompts/{arch-review.md → architecture.md} +0 -0
- /package/templates/prompts/{quality-review.md → review.md} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/commands/diff.ts"],"sourcesContent":["/**\n * Diff command - Preview changes that would be made by upgrade\n */\n\nimport { join } from 'node:path';\nimport { VERSION } from '../version.js';\nimport { exists, readFileSafe, getTemplatesDir } from '../utils/fs.js';\nimport { info, success, error, header, listItem } from '../utils/output.js';\n\nexport interface DiffOptions {\n verbose?: boolean;\n}\n\ninterface FileDiff {\n path: string;\n status: 'added' | 'modified' | 'unchanged';\n currentContent?: string;\n newContent: string;\n}\n\n/**\n * Create a unified diff between two strings\n */\nfunction createUnifiedDiff(oldContent: string, newContent: string, filename: string): string {\n const oldLines = oldContent.split('\\n');\n const newLines = newContent.split('\\n');\n\n const lines: string[] = [];\n lines.push(`--- a/${filename}`);\n lines.push(`+++ b/${filename}`);\n\n // Simple diff - show all changes\n // A real implementation would use a proper diff algorithm\n let hasChanges = false;\n\n const maxLines = Math.max(oldLines.length, newLines.length);\n\n for (let i = 0; i < maxLines; i++) {\n const oldLine = oldLines[i];\n const newLine = newLines[i];\n\n if (oldLine === newLine) {\n lines.push(` ${oldLine ?? ''}`);\n } else {\n hasChanges = true;\n if (oldLine !== undefined) {\n lines.push(`-${oldLine}`);\n }\n if (newLine !== undefined) {\n lines.push(`+${newLine}`);\n }\n }\n }\n\n if (!hasChanges) {\n return '';\n }\n\n // Add context marker\n lines.splice(2, 0, `@@ -1,${oldLines.length} +1,${newLines.length} @@`);\n\n return lines.join('\\n');\n}\n\n/**\n * Get all files that would be changed by upgrade\n */\nfunction getFileDiffs(cwd: string): FileDiff[] {\n const templatesDir = getTemplatesDir();\n const diffs: FileDiff[] = [];\n\n // Define files to check (template source -> install destination)\n const files: Array<{ templatePath: string; installPath: string }> = [\n { templatePath: 'SAFEWORD.md', installPath: '.safeword/SAFEWORD.md' },\n { templatePath: 'hooks/agents-md-check.sh', installPath: '.safeword/hooks/agents-md-check.sh' },\n { templatePath: 'hooks/pre-commit.sh', installPath: '.safeword/hooks/pre-commit.sh' },\n { templatePath: 'hooks/post-tool.sh', installPath: '.safeword/hooks/post-tool.sh' },\n { templatePath: 'hooks/inject-timestamp.sh', installPath: '.safeword/hooks/inject-timestamp.sh' },\n { templatePath: 'skills/safeword-quality-reviewer/SKILL.md', installPath: '.claude/skills/safeword-quality-reviewer/SKILL.md' },\n ];\n\n // Add version file (not from templates)\n const versionPath = join(cwd, '.safeword/version');\n const currentVersion = readFileSafe(versionPath);\n if (currentVersion === null) {\n diffs.push({ path: '.safeword/version', status: 'added', newContent: VERSION });\n } else if (currentVersion.trim() !== VERSION) {\n diffs.push({ path: '.safeword/version', status: 'modified', currentContent: currentVersion, newContent: VERSION });\n } else {\n diffs.push({ path: '.safeword/version', status: 'unchanged', currentContent: currentVersion, newContent: VERSION });\n }\n\n for (const file of files) {\n const templateFullPath = join(templatesDir, file.templatePath);\n const installFullPath = join(cwd, file.installPath);\n\n const newContent = readFileSafe(templateFullPath);\n if (newContent === null) continue; // Skip if template doesn't exist\n\n const currentContent = readFileSafe(installFullPath);\n\n if (currentContent === null) {\n diffs.push({\n path: file.installPath,\n status: 'added',\n newContent,\n });\n } else if (currentContent.trim() !== newContent.trim()) {\n diffs.push({\n path: file.installPath,\n status: 'modified',\n currentContent,\n newContent,\n });\n } else {\n diffs.push({\n path: file.installPath,\n status: 'unchanged',\n currentContent,\n newContent,\n });\n }\n }\n\n return diffs;\n}\n\nexport async function diff(options: DiffOptions): Promise<void> {\n const cwd = process.cwd();\n const safewordDir = join(cwd, '.safeword');\n\n // Check if configured\n if (!exists(safewordDir)) {\n error('Not configured. Run `safeword setup` first.');\n process.exit(1);\n }\n\n // Read project version\n const versionPath = join(safewordDir, 'version');\n const projectVersion = readFileSafe(versionPath)?.trim() ?? 'unknown';\n\n header('Safeword Diff');\n info(`Changes from v${projectVersion} → v${VERSION}`);\n\n const diffs = getFileDiffs(cwd);\n\n const added = diffs.filter(d => d.status === 'added');\n const modified = diffs.filter(d => d.status === 'modified');\n const unchanged = diffs.filter(d => d.status === 'unchanged');\n\n // Summary\n info(\n `\\nSummary: ${added.length} added, ${modified.length} modified, ${unchanged.length} unchanged`,\n );\n\n // List by category\n if (added.length > 0) {\n info('\\nAdded:');\n for (const file of added) {\n listItem(file.path);\n }\n }\n\n if (modified.length > 0) {\n info('\\nModified:');\n for (const file of modified) {\n listItem(file.path);\n }\n }\n\n if (unchanged.length > 0) {\n info('\\nUnchanged:');\n for (const file of unchanged) {\n listItem(file.path);\n }\n }\n\n // Verbose output - show actual diffs\n if (options.verbose) {\n header('Detailed Changes');\n\n for (const file of modified) {\n if (file.currentContent) {\n info(`\\n${file.path}:`);\n const diffOutput = createUnifiedDiff(file.currentContent, file.newContent, file.path);\n if (diffOutput) {\n console.log(diffOutput);\n }\n }\n }\n\n for (const file of added) {\n info(`\\n${file.path}: (new file)`);\n const lines = file.newContent.split('\\n').slice(0, 10);\n for (const line of lines) {\n console.log(`+${line}`);\n }\n if (file.newContent.split('\\n').length > 10) {\n console.log('... (truncated)');\n }\n }\n }\n\n if (added.length === 0 && modified.length === 0) {\n success('\\nNo changes needed - configuration is up to date');\n } else {\n info('\\nRun `safeword upgrade` to apply these changes');\n }\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../src/commands/diff.ts"],"sourcesContent":["/**\n * Diff command - Preview changes that would be made by upgrade\n */\n\nimport { join } from 'node:path';\nimport { VERSION } from '../version.js';\nimport { exists, readFileSafe, getTemplatesDir } from '../utils/fs.js';\nimport { info, success, error, header, listItem } from '../utils/output.js';\n\nexport interface DiffOptions {\n verbose?: boolean;\n}\n\ninterface FileDiff {\n path: string;\n status: 'added' | 'modified' | 'unchanged';\n currentContent?: string;\n newContent: string;\n}\n\n/**\n * Create a unified diff between two strings\n */\nfunction createUnifiedDiff(oldContent: string, newContent: string, filename: string): string {\n const oldLines = oldContent.split('\\n');\n const newLines = newContent.split('\\n');\n\n const lines: string[] = [];\n lines.push(`--- a/${filename}`);\n lines.push(`+++ b/${filename}`);\n\n // Simple diff - show all changes\n // A real implementation would use a proper diff algorithm\n let hasChanges = false;\n\n const maxLines = Math.max(oldLines.length, newLines.length);\n\n for (let i = 0; i < maxLines; i++) {\n const oldLine = oldLines[i];\n const newLine = newLines[i];\n\n if (oldLine === newLine) {\n lines.push(` ${oldLine ?? ''}`);\n } else {\n hasChanges = true;\n if (oldLine !== undefined) {\n lines.push(`-${oldLine}`);\n }\n if (newLine !== undefined) {\n lines.push(`+${newLine}`);\n }\n }\n }\n\n if (!hasChanges) {\n return '';\n }\n\n // Add context marker\n lines.splice(2, 0, `@@ -1,${oldLines.length} +1,${newLines.length} @@`);\n\n return lines.join('\\n');\n}\n\n/**\n * Get all files that would be changed by upgrade\n */\nfunction getFileDiffs(cwd: string): FileDiff[] {\n const templatesDir = getTemplatesDir();\n const diffs: FileDiff[] = [];\n\n // Define files to check (template source -> install destination)\n const files: Array<{ templatePath: string; installPath: string }> = [\n { templatePath: 'SAFEWORD.md', installPath: '.safeword/SAFEWORD.md' },\n { templatePath: 'hooks/agents-md-check.sh', installPath: '.safeword/hooks/agents-md-check.sh' },\n { templatePath: 'hooks/pre-commit.sh', installPath: '.safeword/hooks/pre-commit.sh' },\n { templatePath: 'hooks/post-tool.sh', installPath: '.safeword/hooks/post-tool.sh' },\n { templatePath: 'hooks/inject-timestamp.sh', installPath: '.safeword/hooks/inject-timestamp.sh' },\n { templatePath: 'skills/safeword-quality-reviewer/SKILL.md', installPath: '.claude/skills/safeword-quality-reviewer/SKILL.md' },\n ];\n\n // Add version file (not from templates)\n const versionPath = join(cwd, '.safeword/version');\n const currentVersion = readFileSafe(versionPath);\n if (currentVersion === null) {\n diffs.push({ path: '.safeword/version', status: 'added', newContent: VERSION });\n } else if (currentVersion.trim() !== VERSION) {\n diffs.push({ path: '.safeword/version', status: 'modified', currentContent: currentVersion, newContent: VERSION });\n } else {\n diffs.push({ path: '.safeword/version', status: 'unchanged', currentContent: currentVersion, newContent: VERSION });\n }\n\n for (const file of files) {\n const templateFullPath = join(templatesDir, file.templatePath);\n const installFullPath = join(cwd, file.installPath);\n\n const newContent = readFileSafe(templateFullPath);\n if (newContent === null) continue; // Skip if template doesn't exist\n\n const currentContent = readFileSafe(installFullPath);\n\n if (currentContent === null) {\n diffs.push({\n path: file.installPath,\n status: 'added',\n newContent,\n });\n } else if (currentContent.trim() !== newContent.trim()) {\n diffs.push({\n path: file.installPath,\n status: 'modified',\n currentContent,\n newContent,\n });\n } else {\n diffs.push({\n path: file.installPath,\n status: 'unchanged',\n currentContent,\n newContent,\n });\n }\n }\n\n return diffs;\n}\n\nexport async function diff(options: DiffOptions): Promise<void> {\n const cwd = process.cwd();\n const safewordDir = join(cwd, '.safeword');\n\n // Check if configured\n if (!exists(safewordDir)) {\n error('Not configured. Run `safeword setup` first.');\n process.exit(1);\n }\n\n // Read project version\n const versionPath = join(safewordDir, 'version');\n const projectVersion = readFileSafe(versionPath)?.trim() ?? 'unknown';\n\n header('Safeword Diff');\n info(`Changes from v${projectVersion} → v${VERSION}`);\n\n const diffs = getFileDiffs(cwd);\n\n const added = diffs.filter(d => d.status === 'added');\n const modified = diffs.filter(d => d.status === 'modified');\n const unchanged = diffs.filter(d => d.status === 'unchanged');\n\n // Summary\n info(\n `\\nSummary: ${added.length} added, ${modified.length} modified, ${unchanged.length} unchanged`,\n );\n\n // List by category\n if (added.length > 0) {\n info('\\nAdded:');\n for (const file of added) {\n listItem(file.path);\n }\n }\n\n if (modified.length > 0) {\n info('\\nModified:');\n for (const file of modified) {\n listItem(file.path);\n }\n }\n\n if (unchanged.length > 0) {\n info('\\nUnchanged:');\n for (const file of unchanged) {\n listItem(file.path);\n }\n }\n\n // Verbose output - show actual diffs\n if (options.verbose) {\n header('Detailed Changes');\n\n for (const file of modified) {\n if (file.currentContent) {\n info(`\\n${file.path}:`);\n const diffOutput = createUnifiedDiff(file.currentContent, file.newContent, file.path);\n if (diffOutput) {\n console.log(diffOutput);\n }\n }\n }\n\n for (const file of added) {\n info(`\\n${file.path}: (new file)`);\n const lines = file.newContent.split('\\n').slice(0, 10);\n for (const line of lines) {\n console.log(`+${line}`);\n }\n if (file.newContent.split('\\n').length > 10) {\n console.log('... (truncated)');\n }\n }\n }\n\n if (added.length === 0 && modified.length === 0) {\n success('\\nNo changes needed - configuration is up to date');\n } else {\n info('\\nRun `safeword upgrade` to apply these changes');\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import {
|
|
2
2
|
filterOutSafewordHooks,
|
|
3
3
|
removeAgentsMdLink
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-IWWBZVHT.js";
|
|
5
5
|
import {
|
|
6
6
|
error,
|
|
7
|
-
exists,
|
|
8
7
|
header,
|
|
9
8
|
info,
|
|
10
|
-
listDir,
|
|
11
9
|
listItem,
|
|
10
|
+
success
|
|
11
|
+
} from "./chunk-FRPJITGG.js";
|
|
12
|
+
import {
|
|
13
|
+
exists,
|
|
14
|
+
listDir,
|
|
12
15
|
readJson,
|
|
13
16
|
remove,
|
|
14
|
-
success,
|
|
15
17
|
writeJson
|
|
16
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-ARIAOK2F.js";
|
|
17
19
|
|
|
18
20
|
// src/commands/reset.ts
|
|
19
21
|
import { join } from "path";
|
|
@@ -73,7 +75,7 @@ async function reset(options) {
|
|
|
73
75
|
}
|
|
74
76
|
}
|
|
75
77
|
const commandsDir = join(cwd, ".claude", "commands");
|
|
76
|
-
const safewordCommands = ["
|
|
78
|
+
const safewordCommands = ["review.md", "architecture.md", "lint.md"];
|
|
77
79
|
if (exists(commandsDir)) {
|
|
78
80
|
info("\nRemoving safeword commands...");
|
|
79
81
|
let commandsRemoved = false;
|
|
@@ -140,4 +142,4 @@ async function reset(options) {
|
|
|
140
142
|
export {
|
|
141
143
|
reset
|
|
142
144
|
};
|
|
143
|
-
//# sourceMappingURL=reset-
|
|
145
|
+
//# sourceMappingURL=reset-5SRM3P6J.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/reset.ts"],"sourcesContent":["/**\n * Reset command - Remove safeword configuration from project\n */\n\nimport { join } from 'node:path';\nimport { exists, remove, readJson, writeJson, listDir } from '../utils/fs.js';\nimport { info, success, error, header, listItem } from '../utils/output.js';\nimport { filterOutSafewordHooks } from '../utils/hooks.js';\nimport { removeAgentsMdLink } from '../utils/agents-md.js';\n\nexport interface ResetOptions {\n yes?: boolean;\n}\n\nexport async function reset(options: ResetOptions): Promise<void> {\n const cwd = process.cwd();\n const safewordDir = join(cwd, '.safeword');\n\n // Check if configured\n if (!exists(safewordDir)) {\n info('Nothing to remove. Project is not configured with safeword.');\n return;\n }\n\n const isNonInteractive = options.yes || !process.stdin.isTTY;\n\n // Confirmation (in interactive mode without --yes)\n if (!isNonInteractive) {\n // In a real implementation, we'd prompt here\n // For now, non-TTY mode auto-confirms\n }\n\n header('Safeword Reset');\n info('Removing safeword configuration...');\n\n const removed: string[] = [];\n\n try {\n // 1. Remove .safeword directory\n if (exists(safewordDir)) {\n remove(safewordDir);\n removed.push('.safeword/');\n success('Removed .safeword directory');\n }\n\n // 2. Remove safeword hooks from .claude/settings.json\n const settingsPath = join(cwd, '.claude', 'settings.json');\n\n if (exists(settingsPath)) {\n info('\\nRemoving hooks from .claude/settings.json...');\n\n interface SettingsJson {\n hooks?: Record<string, unknown[]>;\n [key: string]: unknown;\n }\n\n const settings = readJson<SettingsJson>(settingsPath);\n\n if (settings?.hooks) {\n let modified = false;\n\n for (const [event, hooks] of Object.entries(settings.hooks)) {\n if (Array.isArray(hooks)) {\n const filtered = filterOutSafewordHooks(hooks);\n if (filtered.length !== hooks.length) {\n settings.hooks[event] = filtered;\n modified = true;\n }\n }\n }\n\n if (modified) {\n writeJson(settingsPath, settings);\n removed.push('.claude/settings.json (hooks)');\n success('Removed safeword hooks');\n }\n }\n }\n\n // 3. Remove safeword skills\n const skillsDir = join(cwd, '.claude', 'skills');\n\n if (exists(skillsDir)) {\n info('\\nRemoving safeword skills...');\n\n const skills = listDir(skillsDir);\n for (const skill of skills) {\n if (skill.startsWith('safeword-')) {\n remove(join(skillsDir, skill));\n removed.push(`.claude/skills/${skill}/`);\n }\n }\n\n if (removed.some(r => r.includes('skills'))) {\n success('Removed safeword skills');\n }\n }\n\n // 3.5. Remove safeword slash commands\n const commandsDir = join(cwd, '.claude', 'commands');\n const safewordCommands = ['review.md', 'architecture.md', 'lint.md'];\n\n if (exists(commandsDir)) {\n info('\\nRemoving safeword commands...');\n\n let commandsRemoved = false;\n for (const cmd of safewordCommands) {\n const cmdPath = join(commandsDir, cmd);\n if (exists(cmdPath)) {\n remove(cmdPath);\n removed.push(`.claude/commands/${cmd}`);\n commandsRemoved = true;\n }\n }\n\n if (commandsRemoved) {\n success('Removed safeword commands');\n }\n }\n\n // 3.6. Remove MCP servers from .mcp.json\n const mcpConfigPath = join(cwd, '.mcp.json');\n\n if (exists(mcpConfigPath)) {\n info('\\nRemoving MCP servers...');\n\n interface McpConfig {\n mcpServers?: Record<string, unknown>;\n [key: string]: unknown;\n }\n\n const mcpConfig = readJson<McpConfig>(mcpConfigPath);\n\n if (mcpConfig?.mcpServers) {\n // Remove safeword MCP servers\n delete mcpConfig.mcpServers.context7;\n delete mcpConfig.mcpServers.playwright;\n\n // If no servers left, remove the file\n if (Object.keys(mcpConfig.mcpServers).length === 0) {\n remove(mcpConfigPath);\n removed.push('.mcp.json');\n } else {\n writeJson(mcpConfigPath, mcpConfig);\n removed.push('.mcp.json (context7, playwright)');\n }\n\n success('Removed MCP servers');\n }\n }\n\n // 4. Remove Husky directory\n const huskyDir = join(cwd, '.husky');\n if (exists(huskyDir)) {\n info('\\nRemoving Husky hooks...');\n remove(huskyDir);\n removed.push('.husky/');\n success('Removed Husky hooks');\n }\n\n // 5. Remove link from AGENTS.md\n info('\\nCleaning AGENTS.md...');\n if (removeAgentsMdLink(cwd)) {\n removed.push('AGENTS.md (link)');\n success('Removed safeword link from AGENTS.md');\n }\n\n // Print summary\n header('Reset Complete');\n\n if (removed.length > 0) {\n info('\\nRemoved:');\n for (const item of removed) {\n listItem(item);\n }\n }\n\n // Note about preserved linting\n info('\\nPreserved (remove manually if desired):');\n listItem('eslint.config.mjs');\n listItem('.prettierrc');\n listItem('.markdownlint-cli2.jsonc');\n listItem('package.json (scripts, lint-staged config)');\n listItem('devDependencies (eslint, prettier, husky, lint-staged, etc.)');\n\n success('\\nSafeword configuration removed');\n } catch (err) {\n error(`Reset failed: ${err instanceof Error ? err.message : 'Unknown error'}`);\n process.exit(1);\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
isGitRepo
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-75FKNZUM.js";
|
|
4
4
|
import {
|
|
5
5
|
VERSION
|
|
6
6
|
} from "./chunk-ORQHKDT2.js";
|
|
@@ -11,69 +11,137 @@ import {
|
|
|
11
11
|
ensureAgentsMdLink,
|
|
12
12
|
filterOutSafewordHooks,
|
|
13
13
|
getEslintConfig
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-IWWBZVHT.js";
|
|
15
|
+
import {
|
|
16
|
+
error,
|
|
17
|
+
header,
|
|
18
|
+
info,
|
|
19
|
+
listItem,
|
|
20
|
+
success,
|
|
21
|
+
warn
|
|
22
|
+
} from "./chunk-FRPJITGG.js";
|
|
23
|
+
import {
|
|
24
|
+
detectProjectType
|
|
25
|
+
} from "./chunk-6CVTH67L.js";
|
|
15
26
|
import {
|
|
16
27
|
copyDir,
|
|
17
28
|
copyFile,
|
|
18
29
|
ensureDir,
|
|
19
|
-
error,
|
|
20
30
|
exists,
|
|
21
31
|
getTemplatesDir,
|
|
22
|
-
header,
|
|
23
|
-
info,
|
|
24
|
-
listItem,
|
|
25
32
|
makeScriptsExecutable,
|
|
26
33
|
readJson,
|
|
27
|
-
success,
|
|
28
34
|
updateJson,
|
|
29
|
-
warn,
|
|
30
35
|
writeFile,
|
|
31
36
|
writeJson
|
|
32
|
-
} from "./chunk-
|
|
37
|
+
} from "./chunk-ARIAOK2F.js";
|
|
33
38
|
|
|
34
39
|
// src/commands/setup.ts
|
|
35
|
-
import { join, basename } from "path";
|
|
40
|
+
import { join as join2, basename } from "path";
|
|
36
41
|
|
|
37
|
-
// src/utils/
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
42
|
+
// src/utils/boundaries.ts
|
|
43
|
+
import { join } from "path";
|
|
44
|
+
var ARCHITECTURE_DIRS = [
|
|
45
|
+
"types",
|
|
46
|
+
// Bottom: can be imported by everything
|
|
47
|
+
"utils",
|
|
48
|
+
"lib",
|
|
49
|
+
"hooks",
|
|
50
|
+
"services",
|
|
51
|
+
"components",
|
|
52
|
+
"features",
|
|
53
|
+
"modules",
|
|
54
|
+
"app"
|
|
55
|
+
// Top: can import everything
|
|
56
|
+
];
|
|
57
|
+
var HIERARCHY = {
|
|
58
|
+
types: [],
|
|
59
|
+
// types can't import anything (pure type definitions)
|
|
60
|
+
utils: ["types"],
|
|
61
|
+
lib: ["utils", "types"],
|
|
62
|
+
hooks: ["lib", "utils", "types"],
|
|
63
|
+
services: ["lib", "utils", "types"],
|
|
64
|
+
components: ["hooks", "services", "lib", "utils", "types"],
|
|
65
|
+
features: ["components", "hooks", "services", "lib", "utils", "types"],
|
|
66
|
+
modules: ["components", "hooks", "services", "lib", "utils", "types"],
|
|
67
|
+
app: ["features", "modules", "components", "hooks", "services", "lib", "utils", "types"]
|
|
68
|
+
};
|
|
69
|
+
function detectArchitecture(projectDir) {
|
|
70
|
+
const foundInSrc = [];
|
|
71
|
+
const foundAtRoot = [];
|
|
72
|
+
for (const dir of ARCHITECTURE_DIRS) {
|
|
73
|
+
if (exists(join(projectDir, "src", dir))) {
|
|
74
|
+
foundInSrc.push(dir);
|
|
75
|
+
}
|
|
76
|
+
if (exists(join(projectDir, dir))) {
|
|
77
|
+
foundAtRoot.push(dir);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const inSrc = foundInSrc.length >= foundAtRoot.length;
|
|
81
|
+
const found = inSrc ? foundInSrc : foundAtRoot;
|
|
82
|
+
return { directories: found, inSrc };
|
|
83
|
+
}
|
|
84
|
+
function generateBoundariesConfig(arch) {
|
|
85
|
+
const prefix = arch.inSrc ? "src/" : "";
|
|
86
|
+
const hasDirectories = arch.directories.length > 0;
|
|
87
|
+
const elements = arch.directories.map((dir) => ` { type: '${dir}', pattern: '${prefix}${dir}/**', mode: 'full' }`).join(",\n");
|
|
88
|
+
const rules = arch.directories.filter((dir) => HIERARCHY[dir].length > 0).map((dir) => {
|
|
89
|
+
const allowed = HIERARCHY[dir].filter((dep) => arch.directories.includes(dep));
|
|
90
|
+
if (allowed.length === 0) return null;
|
|
91
|
+
return ` { from: ['${dir}'], allow: [${allowed.map((d) => `'${d}'`).join(", ")}] }`;
|
|
92
|
+
}).filter(Boolean).join(",\n");
|
|
93
|
+
const detectedInfo = hasDirectories ? `Detected directories: ${arch.directories.join(", ")} (${arch.inSrc ? "in src/" : "at root"})` : "No architecture directories detected yet - add types/, utils/, components/, etc.";
|
|
94
|
+
const elementsContent = elements || "";
|
|
95
|
+
const rulesContent = rules || "";
|
|
96
|
+
return `/**
|
|
97
|
+
* Architecture Boundaries Configuration (AUTO-GENERATED)
|
|
98
|
+
*
|
|
99
|
+
* ${detectedInfo}
|
|
100
|
+
*
|
|
101
|
+
* This enforces import boundaries between architectural layers:
|
|
102
|
+
* - Lower layers (types, utils) cannot import from higher layers (components, features)
|
|
103
|
+
* - Uses 'warn' severity - informative, not blocking
|
|
104
|
+
*
|
|
105
|
+
* Recognized directories (in hierarchy order):
|
|
106
|
+
* types \u2192 utils \u2192 lib \u2192 hooks/services \u2192 components \u2192 features/modules \u2192 app
|
|
107
|
+
*
|
|
108
|
+
* To customize, override in your eslint.config.mjs:
|
|
109
|
+
* rules: { 'boundaries/element-types': ['error', { ... }] }
|
|
110
|
+
*/
|
|
111
|
+
|
|
112
|
+
import boundaries from 'eslint-plugin-boundaries';
|
|
113
|
+
|
|
114
|
+
export default {
|
|
115
|
+
plugins: { boundaries },
|
|
116
|
+
settings: {
|
|
117
|
+
'boundaries/elements': [
|
|
118
|
+
${elementsContent}
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
rules: {
|
|
122
|
+
'boundaries/element-types': ['warn', {
|
|
123
|
+
default: 'disallow',
|
|
124
|
+
rules: [
|
|
125
|
+
${rulesContent}
|
|
126
|
+
],
|
|
127
|
+
}],
|
|
128
|
+
'boundaries/no-unknown': 'off', // Allow files outside defined elements
|
|
129
|
+
'boundaries/no-unknown-files': 'off', // Allow non-matching files
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
`;
|
|
65
133
|
}
|
|
66
134
|
|
|
67
135
|
// src/commands/setup.ts
|
|
68
136
|
import { execSync } from "child_process";
|
|
69
137
|
async function setup(options) {
|
|
70
138
|
const cwd = process.cwd();
|
|
71
|
-
const safewordDir =
|
|
139
|
+
const safewordDir = join2(cwd, ".safeword");
|
|
72
140
|
if (exists(safewordDir)) {
|
|
73
141
|
error("Already configured. Run `safeword upgrade` to update.");
|
|
74
142
|
process.exit(1);
|
|
75
143
|
}
|
|
76
|
-
const packageJsonPath =
|
|
144
|
+
const packageJsonPath = join2(cwd, "package.json");
|
|
77
145
|
let packageJsonCreated = false;
|
|
78
146
|
if (!exists(packageJsonPath)) {
|
|
79
147
|
const dirName = basename(cwd) || "project";
|
|
@@ -97,19 +165,19 @@ async function setup(options) {
|
|
|
97
165
|
const templatesDir = getTemplatesDir();
|
|
98
166
|
info("\nCreating .safeword directory...");
|
|
99
167
|
ensureDir(safewordDir);
|
|
100
|
-
ensureDir(
|
|
101
|
-
ensureDir(
|
|
102
|
-
ensureDir(
|
|
103
|
-
ensureDir(
|
|
104
|
-
copyFile(
|
|
105
|
-
writeFile(
|
|
106
|
-
copyDir(
|
|
107
|
-
copyDir(
|
|
108
|
-
copyDir(
|
|
109
|
-
copyDir(
|
|
110
|
-
makeScriptsExecutable(
|
|
111
|
-
copyDir(
|
|
112
|
-
makeScriptsExecutable(
|
|
168
|
+
ensureDir(join2(safewordDir, "learnings"));
|
|
169
|
+
ensureDir(join2(safewordDir, "planning", "user-stories"));
|
|
170
|
+
ensureDir(join2(safewordDir, "planning", "design"));
|
|
171
|
+
ensureDir(join2(safewordDir, "tickets", "completed"));
|
|
172
|
+
copyFile(join2(templatesDir, "SAFEWORD.md"), join2(safewordDir, "SAFEWORD.md"));
|
|
173
|
+
writeFile(join2(safewordDir, "version"), VERSION);
|
|
174
|
+
copyDir(join2(templatesDir, "guides"), join2(safewordDir, "guides"));
|
|
175
|
+
copyDir(join2(templatesDir, "doc-templates"), join2(safewordDir, "templates"));
|
|
176
|
+
copyDir(join2(templatesDir, "prompts"), join2(safewordDir, "prompts"));
|
|
177
|
+
copyDir(join2(templatesDir, "lib"), join2(safewordDir, "lib"));
|
|
178
|
+
makeScriptsExecutable(join2(safewordDir, "lib"));
|
|
179
|
+
copyDir(join2(templatesDir, "hooks"), join2(safewordDir, "hooks"));
|
|
180
|
+
makeScriptsExecutable(join2(safewordDir, "hooks"));
|
|
113
181
|
created.push(".safeword/");
|
|
114
182
|
success("Created .safeword directory");
|
|
115
183
|
info("\nConfiguring AGENTS.md...");
|
|
@@ -124,8 +192,8 @@ async function setup(options) {
|
|
|
124
192
|
info("AGENTS.md already has safeword link");
|
|
125
193
|
}
|
|
126
194
|
info("\nRegistering Claude Code hooks...");
|
|
127
|
-
const claudeDir =
|
|
128
|
-
const settingsPath =
|
|
195
|
+
const claudeDir = join2(cwd, ".claude");
|
|
196
|
+
const settingsPath = join2(claudeDir, "settings.json");
|
|
129
197
|
ensureDir(claudeDir);
|
|
130
198
|
try {
|
|
131
199
|
updateJson(settingsPath, (existing) => {
|
|
@@ -148,17 +216,17 @@ async function setup(options) {
|
|
|
148
216
|
process.exit(1);
|
|
149
217
|
}
|
|
150
218
|
info("\nInstalling skills...");
|
|
151
|
-
const skillsDir =
|
|
152
|
-
copyDir(
|
|
219
|
+
const skillsDir = join2(claudeDir, "skills");
|
|
220
|
+
copyDir(join2(templatesDir, "skills"), skillsDir);
|
|
153
221
|
created.push(".claude/skills/safeword-quality-reviewer/");
|
|
154
222
|
success("Installed skills");
|
|
155
223
|
info("\nInstalling slash commands...");
|
|
156
|
-
const commandsDir =
|
|
157
|
-
copyDir(
|
|
224
|
+
const commandsDir = join2(claudeDir, "commands");
|
|
225
|
+
copyDir(join2(templatesDir, "commands"), commandsDir);
|
|
158
226
|
created.push(".claude/commands/");
|
|
159
227
|
success("Installed slash commands");
|
|
160
228
|
info("\nConfiguring MCP servers...");
|
|
161
|
-
const mcpConfigPath =
|
|
229
|
+
const mcpConfigPath = join2(cwd, ".mcp.json");
|
|
162
230
|
updateJson(mcpConfigPath, (existing) => {
|
|
163
231
|
const mcpServers = existing?.mcpServers ?? {};
|
|
164
232
|
mcpServers.context7 = {
|
|
@@ -184,15 +252,24 @@ async function setup(options) {
|
|
|
184
252
|
process.exit(1);
|
|
185
253
|
}
|
|
186
254
|
const projectType = detectProjectType(packageJson);
|
|
187
|
-
const
|
|
255
|
+
const architecture = detectArchitecture(cwd);
|
|
256
|
+
const eslintConfigPath = join2(cwd, "eslint.config.mjs");
|
|
188
257
|
if (!exists(eslintConfigPath)) {
|
|
189
|
-
writeFile(eslintConfigPath, getEslintConfig(
|
|
258
|
+
writeFile(eslintConfigPath, getEslintConfig({ boundaries: true }));
|
|
190
259
|
created.push("eslint.config.mjs");
|
|
191
|
-
success("Created eslint.config.mjs");
|
|
260
|
+
success("Created eslint.config.mjs (dynamic - adapts to framework changes)");
|
|
192
261
|
} else {
|
|
193
262
|
info("eslint.config.mjs already exists");
|
|
194
263
|
}
|
|
195
|
-
const
|
|
264
|
+
const boundariesConfigPath = join2(safewordDir, "eslint-boundaries.config.mjs");
|
|
265
|
+
writeFile(boundariesConfigPath, generateBoundariesConfig(architecture));
|
|
266
|
+
if (architecture.directories.length > 0) {
|
|
267
|
+
info(`Detected architecture: ${architecture.directories.join(", ")} (${architecture.inSrc ? "in src/" : "at root"})`);
|
|
268
|
+
} else {
|
|
269
|
+
info("No architecture directories detected yet (boundaries ready when you add them)");
|
|
270
|
+
}
|
|
271
|
+
success("Created .safeword/eslint-boundaries.config.mjs");
|
|
272
|
+
const prettierrcPath = join2(cwd, ".prettierrc");
|
|
196
273
|
if (!exists(prettierrcPath)) {
|
|
197
274
|
writeFile(prettierrcPath, PRETTIERRC);
|
|
198
275
|
created.push(".prettierrc");
|
|
@@ -200,9 +277,9 @@ async function setup(options) {
|
|
|
200
277
|
} else {
|
|
201
278
|
info(".prettierrc already exists");
|
|
202
279
|
}
|
|
203
|
-
const markdownlintPath =
|
|
280
|
+
const markdownlintPath = join2(cwd, ".markdownlint-cli2.jsonc");
|
|
204
281
|
if (!exists(markdownlintPath)) {
|
|
205
|
-
copyFile(
|
|
282
|
+
copyFile(join2(templatesDir, "markdownlint-cli2.jsonc"), markdownlintPath);
|
|
206
283
|
created.push(".markdownlint-cli2.jsonc");
|
|
207
284
|
success("Created .markdownlint-cli2.jsonc");
|
|
208
285
|
} else {
|
|
@@ -227,8 +304,16 @@ async function setup(options) {
|
|
|
227
304
|
scripts["format:check"] = "prettier --check .";
|
|
228
305
|
packageJsonModified = true;
|
|
229
306
|
}
|
|
307
|
+
if (!scripts.knip) {
|
|
308
|
+
scripts.knip = "knip";
|
|
309
|
+
packageJsonModified = true;
|
|
310
|
+
}
|
|
311
|
+
if (projectType.publishableLibrary && !scripts.publint) {
|
|
312
|
+
scripts.publint = "publint";
|
|
313
|
+
packageJsonModified = true;
|
|
314
|
+
}
|
|
230
315
|
if (!scripts.prepare) {
|
|
231
|
-
scripts.prepare = "husky";
|
|
316
|
+
scripts.prepare = "husky || true";
|
|
232
317
|
packageJsonModified = true;
|
|
233
318
|
}
|
|
234
319
|
if (!packageJson["lint-staged"]) {
|
|
@@ -252,8 +337,12 @@ async function setup(options) {
|
|
|
252
337
|
"eslint",
|
|
253
338
|
"prettier",
|
|
254
339
|
"@eslint/js",
|
|
340
|
+
"eslint-plugin-import-x",
|
|
341
|
+
"eslint-plugin-sonarjs",
|
|
342
|
+
"@microsoft/eslint-plugin-sdl",
|
|
255
343
|
"eslint-config-prettier",
|
|
256
344
|
"markdownlint-cli2",
|
|
345
|
+
"knip",
|
|
257
346
|
"husky",
|
|
258
347
|
"lint-staged"
|
|
259
348
|
];
|
|
@@ -261,7 +350,7 @@ async function setup(options) {
|
|
|
261
350
|
devDeps.push("typescript-eslint");
|
|
262
351
|
}
|
|
263
352
|
if (projectType.react || projectType.nextjs) {
|
|
264
|
-
devDeps.push("eslint-plugin-react", "eslint-plugin-react-hooks");
|
|
353
|
+
devDeps.push("eslint-plugin-react", "eslint-plugin-react-hooks", "eslint-plugin-jsx-a11y");
|
|
265
354
|
}
|
|
266
355
|
if (projectType.nextjs) {
|
|
267
356
|
devDeps.push("@next/eslint-plugin-next");
|
|
@@ -275,25 +364,40 @@ async function setup(options) {
|
|
|
275
364
|
if (projectType.svelte) {
|
|
276
365
|
devDeps.push("eslint-plugin-svelte");
|
|
277
366
|
}
|
|
367
|
+
devDeps.push("eslint-plugin-boundaries");
|
|
368
|
+
if (projectType.electron) {
|
|
369
|
+
devDeps.push("@electron-toolkit/eslint-config");
|
|
370
|
+
}
|
|
371
|
+
if (projectType.vitest) {
|
|
372
|
+
devDeps.push("@vitest/eslint-plugin");
|
|
373
|
+
}
|
|
374
|
+
devDeps.push("eslint-plugin-playwright");
|
|
375
|
+
if (projectType.tailwind) {
|
|
376
|
+
devDeps.push("prettier-plugin-tailwindcss");
|
|
377
|
+
}
|
|
378
|
+
if (projectType.publishableLibrary) {
|
|
379
|
+
devDeps.push("publint");
|
|
380
|
+
}
|
|
278
381
|
try {
|
|
279
382
|
const installCmd = `npm install -D ${devDeps.join(" ")}`;
|
|
280
383
|
info(`Running: ${installCmd}`);
|
|
281
384
|
execSync(installCmd, { cwd, stdio: "inherit" });
|
|
282
385
|
success("Installed linting dependencies");
|
|
283
|
-
} catch
|
|
386
|
+
} catch {
|
|
284
387
|
warn("Failed to install dependencies. Run manually:");
|
|
285
388
|
listItem(`npm install -D ${devDeps.join(" ")}`);
|
|
286
389
|
}
|
|
287
390
|
info("\nConfiguring git hooks with Husky...");
|
|
288
391
|
if (isGitRepo(cwd)) {
|
|
289
392
|
try {
|
|
290
|
-
const huskyDir =
|
|
393
|
+
const huskyDir = join2(cwd, ".husky");
|
|
291
394
|
ensureDir(huskyDir);
|
|
292
|
-
const huskyPreCommit =
|
|
293
|
-
writeFile(huskyPreCommit, "npx lint-staged\n");
|
|
395
|
+
const huskyPreCommit = join2(huskyDir, "pre-commit");
|
|
396
|
+
writeFile(huskyPreCommit, "npx safeword sync --quiet --stage\nnpx lint-staged\n");
|
|
397
|
+
makeScriptsExecutable(huskyDir);
|
|
294
398
|
created.push(".husky/pre-commit");
|
|
295
399
|
success("Configured Husky with lint-staged pre-commit hook");
|
|
296
|
-
} catch
|
|
400
|
+
} catch {
|
|
297
401
|
warn("Failed to setup Husky. Run manually:");
|
|
298
402
|
listItem("mkdir -p .husky");
|
|
299
403
|
listItem('echo "npx lint-staged" > .husky/pre-commit');
|
|
@@ -302,7 +406,7 @@ async function setup(options) {
|
|
|
302
406
|
warn("Skipped Husky setup (no git repository)");
|
|
303
407
|
} else {
|
|
304
408
|
warn("Skipped Husky setup (no .git directory)");
|
|
305
|
-
info(
|
|
409
|
+
info("Initialize git and run safeword setup again to enable pre-commit hooks");
|
|
306
410
|
}
|
|
307
411
|
header("Setup Complete");
|
|
308
412
|
if (created.length > 0) {
|
|
@@ -330,4 +434,4 @@ Safeword ${VERSION} installed successfully!`);
|
|
|
330
434
|
export {
|
|
331
435
|
setup
|
|
332
436
|
};
|
|
333
|
-
//# sourceMappingURL=setup-
|
|
437
|
+
//# sourceMappingURL=setup-65EVU5OT.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/setup.ts","../src/utils/boundaries.ts"],"sourcesContent":["/**\n * Setup command - Initialize safeword in a project\n */\n\nimport { join, basename } from 'node:path';\nimport { VERSION } from '../version.js';\nimport {\n exists,\n ensureDir,\n writeFile,\n readJson,\n writeJson,\n updateJson,\n copyDir,\n copyFile,\n getTemplatesDir,\n makeScriptsExecutable,\n} from '../utils/fs.js';\nimport { info, success, warn, error, header, listItem } from '../utils/output.js';\nimport { isGitRepo } from '../utils/git.js';\nimport { detectProjectType } from '../utils/project-detector.js';\nimport { filterOutSafewordHooks } from '../utils/hooks.js';\nimport { ensureAgentsMdLink } from '../utils/agents-md.js';\nimport { PRETTIERRC, LINT_STAGED_CONFIG, getEslintConfig, SETTINGS_HOOKS } from '../templates/index.js';\nimport { detectArchitecture, generateBoundariesConfig } from '../utils/boundaries.js';\nimport { execSync } from 'node:child_process';\n\nexport interface SetupOptions {\n yes?: boolean;\n}\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}\n\nexport async function setup(options: SetupOptions): Promise<void> {\n const cwd = process.cwd();\n const safewordDir = join(cwd, '.safeword');\n\n // Check if already configured\n if (exists(safewordDir)) {\n error('Already configured. Run `safeword upgrade` to update.');\n process.exit(1);\n }\n\n // Check for package.json, create if missing\n const packageJsonPath = join(cwd, 'package.json');\n let packageJsonCreated = false;\n if (!exists(packageJsonPath)) {\n const dirName = basename(cwd) || 'project';\n const defaultPackageJson: PackageJson = {\n name: dirName,\n version: '0.1.0',\n scripts: {},\n };\n writeJson(packageJsonPath, defaultPackageJson);\n packageJsonCreated = true;\n }\n\n const isNonInteractive = options.yes || !process.stdin.isTTY;\n\n header('Safeword Setup');\n info(`Version: ${VERSION}`);\n\n if (packageJsonCreated) {\n info('Created package.json (none found)');\n }\n\n // Track created files for summary\n const created: string[] = packageJsonCreated ? ['package.json'] : [];\n const modified: string[] = [];\n\n try {\n const templatesDir = getTemplatesDir();\n\n // 1. Create .safeword directory structure and copy templates\n info('\\nCreating .safeword directory...');\n\n ensureDir(safewordDir);\n ensureDir(join(safewordDir, 'learnings'));\n ensureDir(join(safewordDir, 'planning', 'user-stories'));\n ensureDir(join(safewordDir, 'planning', 'design'));\n ensureDir(join(safewordDir, 'tickets', 'completed'));\n\n // Copy full SAFEWORD.md from templates\n copyFile(join(templatesDir, 'SAFEWORD.md'), join(safewordDir, 'SAFEWORD.md'));\n writeFile(join(safewordDir, 'version'), VERSION);\n\n // Copy methodology guides\n copyDir(join(templatesDir, 'guides'), join(safewordDir, 'guides'));\n\n // Copy document templates (to 'templates' to match links in SAFEWORD.md)\n copyDir(join(templatesDir, 'doc-templates'), join(safewordDir, 'templates'));\n\n // Copy review prompts\n copyDir(join(templatesDir, 'prompts'), join(safewordDir, 'prompts'));\n\n // Copy lib scripts and make executable\n copyDir(join(templatesDir, 'lib'), join(safewordDir, 'lib'));\n makeScriptsExecutable(join(safewordDir, 'lib'));\n\n // Copy hook scripts and make executable\n copyDir(join(templatesDir, 'hooks'), join(safewordDir, 'hooks'));\n makeScriptsExecutable(join(safewordDir, 'hooks'));\n\n created.push('.safeword/');\n success('Created .safeword directory');\n\n // 2. Handle AGENTS.md\n info('\\nConfiguring AGENTS.md...');\n const agentsMdResult = ensureAgentsMdLink(cwd);\n if (agentsMdResult === 'created') {\n created.push('AGENTS.md');\n success('Created AGENTS.md');\n } else if (agentsMdResult === 'modified') {\n modified.push('AGENTS.md');\n success('Prepended link to AGENTS.md');\n } else {\n info('AGENTS.md already has safeword link');\n }\n\n // 3. Register Claude Code hooks\n info('\\nRegistering Claude Code hooks...');\n\n const claudeDir = join(cwd, '.claude');\n const settingsPath = join(claudeDir, 'settings.json');\n\n ensureDir(claudeDir);\n\n try {\n updateJson<{ hooks?: Record<string, unknown[]> }>(settingsPath, existing => {\n const hooks = existing?.hooks ?? {};\n\n // Merge hooks, preserving existing non-safeword hooks\n for (const [event, newHooks] of Object.entries(SETTINGS_HOOKS)) {\n const existingHooks = (hooks[event] as unknown[]) ?? [];\n const nonSafewordHooks = filterOutSafewordHooks(existingHooks);\n hooks[event] = [...nonSafewordHooks, ...newHooks];\n }\n\n return { ...existing, hooks };\n });\n\n if (exists(settingsPath)) {\n modified.push('.claude/settings.json');\n } else {\n created.push('.claude/settings.json');\n }\n success('Registered hooks in .claude/settings.json');\n } catch (err) {\n error(`Failed to register hooks: ${err instanceof Error ? err.message : 'Unknown error'}`);\n process.exit(1);\n }\n\n // 4. Copy skills\n info('\\nInstalling skills...');\n\n const skillsDir = join(claudeDir, 'skills');\n copyDir(join(templatesDir, 'skills'), skillsDir);\n\n created.push('.claude/skills/safeword-quality-reviewer/');\n success('Installed skills');\n\n // 5. Copy slash commands\n info('\\nInstalling slash commands...');\n\n const commandsDir = join(claudeDir, 'commands');\n copyDir(join(templatesDir, 'commands'), commandsDir);\n\n created.push('.claude/commands/');\n success('Installed slash commands');\n\n // 6. Setup MCP servers\n info('\\nConfiguring MCP servers...');\n\n const mcpConfigPath = join(cwd, '.mcp.json');\n\n updateJson<{ mcpServers?: Record<string, unknown> }>(mcpConfigPath, existing => {\n const mcpServers = existing?.mcpServers ?? {};\n\n // Add safeword MCP servers (context7 and playwright)\n mcpServers.context7 = {\n command: 'npx',\n args: ['-y', '@upstash/context7-mcp@latest'],\n };\n mcpServers.playwright = {\n command: 'npx',\n args: ['@playwright/mcp@latest'],\n };\n\n return { ...existing, mcpServers };\n });\n\n if (exists(mcpConfigPath)) {\n modified.push('.mcp.json');\n } else {\n created.push('.mcp.json');\n }\n success('Configured MCP servers');\n\n // 7. Setup linting\n info('\\nConfiguring linting...');\n\n const packageJson = readJson<PackageJson>(packageJsonPath);\n if (!packageJson) {\n error('Failed to read package.json');\n process.exit(1);\n }\n\n const projectType = detectProjectType(packageJson);\n\n // Detect architecture boundaries (always configured, rules depend on detected dirs)\n const architecture = detectArchitecture(cwd);\n\n // Create dynamic ESLint config (detects frameworks from package.json at runtime)\n const eslintConfigPath = join(cwd, 'eslint.config.mjs');\n if (!exists(eslintConfigPath)) {\n writeFile(eslintConfigPath, getEslintConfig({ boundaries: true }));\n created.push('eslint.config.mjs');\n success('Created eslint.config.mjs (dynamic - adapts to framework changes)');\n } else {\n info('eslint.config.mjs already exists');\n }\n\n // Always create boundaries config (rules depend on detected architecture dirs)\n const boundariesConfigPath = join(safewordDir, 'eslint-boundaries.config.mjs');\n writeFile(boundariesConfigPath, generateBoundariesConfig(architecture));\n if (architecture.directories.length > 0) {\n info(`Detected architecture: ${architecture.directories.join(', ')} (${architecture.inSrc ? 'in src/' : 'at root'})`);\n } else {\n info('No architecture directories detected yet (boundaries ready when you add them)');\n }\n success('Created .safeword/eslint-boundaries.config.mjs');\n\n // Create Prettier config\n const prettierrcPath = join(cwd, '.prettierrc');\n if (!exists(prettierrcPath)) {\n writeFile(prettierrcPath, PRETTIERRC);\n created.push('.prettierrc');\n success('Created .prettierrc');\n } else {\n info('.prettierrc already exists');\n }\n\n // Create markdownlint config (using cli2 preferred filename)\n const markdownlintPath = join(cwd, '.markdownlint-cli2.jsonc');\n if (!exists(markdownlintPath)) {\n copyFile(join(templatesDir, 'markdownlint-cli2.jsonc'), markdownlintPath);\n created.push('.markdownlint-cli2.jsonc');\n success('Created .markdownlint-cli2.jsonc');\n } else {\n info('.markdownlint-cli2.jsonc already exists');\n }\n\n // Add scripts and lint-staged config to package.json\n try {\n const scripts = packageJson.scripts ?? {};\n let packageJsonModified = false;\n\n if (!scripts.lint) {\n scripts.lint = 'eslint .';\n packageJsonModified = true;\n }\n\n if (!scripts['lint:md']) {\n scripts['lint:md'] = 'markdownlint-cli2 \"**/*.md\" \"#node_modules\"';\n packageJsonModified = true;\n }\n\n if (!scripts.format) {\n scripts.format = 'prettier --write .';\n packageJsonModified = true;\n }\n\n if (!scripts['format:check']) {\n scripts['format:check'] = 'prettier --check .';\n packageJsonModified = true;\n }\n\n if (!scripts.knip) {\n scripts.knip = 'knip';\n packageJsonModified = true;\n }\n\n // Add publint script for publishable libraries\n if (projectType.publishableLibrary && !scripts.publint) {\n scripts.publint = 'publint';\n packageJsonModified = true;\n }\n\n // Add prepare script for Husky (runs on npm install)\n // The || true fallback prevents npm install --production from failing\n // when husky (a devDependency) isn't installed\n if (!scripts.prepare) {\n scripts.prepare = 'husky || true';\n packageJsonModified = true;\n }\n\n // Add lint-staged config\n if (!packageJson['lint-staged']) {\n packageJson['lint-staged'] = LINT_STAGED_CONFIG;\n packageJsonModified = true;\n }\n\n if (packageJsonModified) {\n packageJson.scripts = scripts;\n writeJson(packageJsonPath, packageJson);\n modified.push('package.json');\n success('Added lint scripts and lint-staged config');\n }\n } catch (err) {\n error(\n `Failed to update package.json: ${err instanceof Error ? err.message : 'Unknown error'}`,\n );\n process.exit(1);\n }\n\n // 8. Install dependencies\n info('\\nInstalling linting dependencies...');\n\n // Build the list of packages to install\n const devDeps: string[] = [\n 'eslint',\n 'prettier',\n '@eslint/js',\n 'eslint-plugin-import-x',\n 'eslint-plugin-sonarjs',\n '@microsoft/eslint-plugin-sdl',\n 'eslint-config-prettier',\n 'markdownlint-cli2',\n 'knip',\n 'husky',\n 'lint-staged',\n ];\n\n if (projectType.typescript) {\n devDeps.push('typescript-eslint');\n }\n if (projectType.react || projectType.nextjs) {\n devDeps.push('eslint-plugin-react', 'eslint-plugin-react-hooks', 'eslint-plugin-jsx-a11y');\n }\n if (projectType.nextjs) {\n devDeps.push('@next/eslint-plugin-next');\n }\n if (projectType.astro) {\n devDeps.push('eslint-plugin-astro');\n }\n if (projectType.vue) {\n devDeps.push('eslint-plugin-vue');\n }\n if (projectType.svelte) {\n devDeps.push('eslint-plugin-svelte');\n }\n // Always install boundaries - configured only when 3+ architecture directories exist\n devDeps.push('eslint-plugin-boundaries');\n if (projectType.electron) {\n devDeps.push('@electron-toolkit/eslint-config');\n }\n if (projectType.vitest) {\n devDeps.push('@vitest/eslint-plugin');\n }\n // Always include Playwright - safeword sets up e2e testing with Playwright\n devDeps.push('eslint-plugin-playwright');\n\n // Tailwind: use official Prettier plugin for class sorting\n if (projectType.tailwind) {\n devDeps.push('prettier-plugin-tailwindcss');\n }\n\n // Publishable libraries: validate package.json for npm publishing\n if (projectType.publishableLibrary) {\n devDeps.push('publint');\n }\n\n try {\n const installCmd = `npm install -D ${devDeps.join(' ')}`;\n info(`Running: ${installCmd}`);\n execSync(installCmd, { cwd, stdio: 'inherit' });\n success('Installed linting dependencies');\n } catch {\n warn('Failed to install dependencies. Run manually:');\n listItem(`npm install -D ${devDeps.join(' ')}`);\n }\n\n // 9. Setup Husky for git hooks (manually, not using husky init which overwrites prepare script)\n info('\\nConfiguring git hooks with Husky...');\n\n if (isGitRepo(cwd)) {\n try {\n // Create .husky directory and pre-commit hook manually\n // (husky init unconditionally sets prepare script, which we don't want)\n const huskyDir = join(cwd, '.husky');\n ensureDir(huskyDir);\n\n // Create pre-commit hook that syncs linting plugins and runs lint-staged\n const huskyPreCommit = join(huskyDir, 'pre-commit');\n writeFile(huskyPreCommit, 'npx safeword sync --quiet --stage\\nnpx lint-staged\\n');\n\n // Make hook executable (required for git hooks on Unix)\n makeScriptsExecutable(huskyDir);\n\n created.push('.husky/pre-commit');\n success('Configured Husky with lint-staged pre-commit hook');\n } catch {\n warn('Failed to setup Husky. Run manually:');\n listItem('mkdir -p .husky');\n listItem('echo \"npx lint-staged\" > .husky/pre-commit');\n }\n } else if (isNonInteractive) {\n warn('Skipped Husky setup (no git repository)');\n } else {\n warn('Skipped Husky setup (no .git directory)');\n info('Initialize git and run safeword setup again to enable pre-commit hooks');\n }\n\n // Print summary\n header('Setup Complete');\n\n if (created.length > 0) {\n info('\\nCreated:');\n for (const file of created) {\n listItem(file);\n }\n }\n\n if (modified.length > 0) {\n info('\\nModified:');\n for (const file of modified) {\n listItem(file);\n }\n }\n\n info('\\nNext steps:');\n listItem('Run `safeword check` to verify setup');\n listItem('Commit the new files to git');\n\n success(`\\nSafeword ${VERSION} installed successfully!`);\n } catch (err) {\n error(`Setup failed: ${err instanceof Error ? err.message : 'Unknown error'}`);\n process.exit(1);\n }\n}\n","/**\n * Architecture boundaries detection and config generation\n *\n * Auto-detects common architecture directories and generates\n * eslint-plugin-boundaries config with sensible hierarchy rules.\n */\n\nimport { join } from 'node:path';\nimport { exists } from './fs.js';\n\n/**\n * Architecture directories to detect, ordered from bottom to top of hierarchy.\n * Lower items can be imported by higher items, not vice versa.\n */\nconst ARCHITECTURE_DIRS = [\n 'types', // Bottom: can be imported by everything\n 'utils',\n 'lib',\n 'hooks',\n 'services',\n 'components',\n 'features',\n 'modules',\n 'app', // Top: can import everything\n] as const;\n\ntype ArchDir = (typeof ARCHITECTURE_DIRS)[number];\n\n/**\n * Hierarchy rules: what each layer can import\n * Lower layers have fewer import permissions\n */\nconst HIERARCHY: Record<ArchDir, ArchDir[]> = {\n types: [], // types can't import anything (pure type definitions)\n utils: ['types'],\n lib: ['utils', 'types'],\n hooks: ['lib', 'utils', 'types'],\n services: ['lib', 'utils', 'types'],\n components: ['hooks', 'services', 'lib', 'utils', 'types'],\n features: ['components', 'hooks', 'services', 'lib', 'utils', 'types'],\n modules: ['components', 'hooks', 'services', 'lib', 'utils', 'types'],\n app: ['features', 'modules', 'components', 'hooks', 'services', 'lib', 'utils', 'types'],\n};\n\nexport interface DetectedArchitecture {\n directories: ArchDir[];\n inSrc: boolean; // true if dirs are in src/, false if at root\n}\n\n/**\n * Detects architecture directories in the project\n * Always returns a result (even with 0 directories) - boundaries is always configured\n */\nexport function detectArchitecture(projectDir: string): DetectedArchitecture {\n const foundInSrc: ArchDir[] = [];\n const foundAtRoot: ArchDir[] = [];\n\n for (const dir of ARCHITECTURE_DIRS) {\n if (exists(join(projectDir, 'src', dir))) {\n foundInSrc.push(dir);\n }\n if (exists(join(projectDir, dir))) {\n foundAtRoot.push(dir);\n }\n }\n\n // Prefer src/ location if more dirs found there\n const inSrc = foundInSrc.length >= foundAtRoot.length;\n const found = inSrc ? foundInSrc : foundAtRoot;\n\n return { directories: found, inSrc };\n}\n\n/**\n * Generates the boundaries config file content\n */\nexport function generateBoundariesConfig(arch: DetectedArchitecture): string {\n const prefix = arch.inSrc ? 'src/' : '';\n const hasDirectories = arch.directories.length > 0;\n\n // Generate element definitions with mode: 'full' to match from project root only\n const elements = arch.directories\n .map(dir => ` { type: '${dir}', pattern: '${prefix}${dir}/**', mode: 'full' }`)\n .join(',\\n');\n\n // Generate rules (what each layer can import)\n const rules = arch.directories\n .filter(dir => HIERARCHY[dir].length > 0)\n .map(dir => {\n const allowed = HIERARCHY[dir].filter(dep => arch.directories.includes(dep));\n if (allowed.length === 0) return null;\n return ` { from: ['${dir}'], allow: [${allowed.map(d => `'${d}'`).join(', ')}] }`;\n })\n .filter(Boolean)\n .join(',\\n');\n\n const detectedInfo = hasDirectories\n ? `Detected directories: ${arch.directories.join(', ')} (${arch.inSrc ? 'in src/' : 'at root'})`\n : 'No architecture directories detected yet - add types/, utils/, components/, etc.';\n\n // Build elements array content (empty array if no directories)\n const elementsContent = elements || '';\n const rulesContent = rules || '';\n\n return `/**\n * Architecture Boundaries Configuration (AUTO-GENERATED)\n *\n * ${detectedInfo}\n *\n * This enforces import boundaries between architectural layers:\n * - Lower layers (types, utils) cannot import from higher layers (components, features)\n * - Uses 'warn' severity - informative, not blocking\n *\n * Recognized directories (in hierarchy order):\n * types → utils → lib → hooks/services → components → features/modules → app\n *\n * To customize, override in your eslint.config.mjs:\n * rules: { 'boundaries/element-types': ['error', { ... }] }\n */\n\nimport boundaries from 'eslint-plugin-boundaries';\n\nexport default {\n plugins: { boundaries },\n settings: {\n 'boundaries/elements': [\n${elementsContent}\n ],\n },\n rules: {\n 'boundaries/element-types': ['warn', {\n default: 'disallow',\n rules: [\n${rulesContent}\n ],\n }],\n 'boundaries/no-unknown': 'off', // Allow files outside defined elements\n 'boundaries/no-unknown-files': 'off', // Allow non-matching files\n },\n};\n`;\n}\n"],"mappings":"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","names":["join","join"]}
|