safeword 0.2.3 → 0.2.4

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.
Files changed (235) hide show
  1. package/.claude/commands/arch-review.md +32 -0
  2. package/.claude/commands/lint.md +6 -0
  3. package/.claude/commands/quality-review.md +13 -0
  4. package/.claude/commands/setup-linting.md +6 -0
  5. package/.claude/hooks/auto-lint.sh +6 -0
  6. package/.claude/hooks/auto-quality-review.sh +170 -0
  7. package/.claude/hooks/check-linting-sync.sh +17 -0
  8. package/.claude/hooks/inject-timestamp.sh +6 -0
  9. package/.claude/hooks/question-protocol.sh +12 -0
  10. package/.claude/hooks/run-linters.sh +8 -0
  11. package/.claude/hooks/run-quality-review.sh +76 -0
  12. package/.claude/hooks/version-check.sh +10 -0
  13. package/.claude/mcp/README.md +96 -0
  14. package/.claude/mcp/arcade.sample.json +9 -0
  15. package/.claude/mcp/context7.sample.json +7 -0
  16. package/.claude/mcp/playwright.sample.json +7 -0
  17. package/.claude/settings.json +62 -0
  18. package/.claude/skills/quality-reviewer/SKILL.md +190 -0
  19. package/.claude/skills/safeword-quality-reviewer/SKILL.md +13 -0
  20. package/.env.arcade.example +4 -0
  21. package/.env.example +11 -0
  22. package/.gitmodules +4 -0
  23. package/.safeword/SAFEWORD.md +33 -0
  24. package/.safeword/eslint/eslint-base.mjs +101 -0
  25. package/.safeword/guides/architecture-guide.md +404 -0
  26. package/.safeword/guides/code-philosophy.md +174 -0
  27. package/.safeword/guides/context-files-guide.md +405 -0
  28. package/.safeword/guides/data-architecture-guide.md +183 -0
  29. package/.safeword/guides/design-doc-guide.md +165 -0
  30. package/.safeword/guides/learning-extraction.md +515 -0
  31. package/.safeword/guides/llm-instruction-design.md +239 -0
  32. package/.safeword/guides/llm-prompting.md +95 -0
  33. package/.safeword/guides/tdd-best-practices.md +570 -0
  34. package/.safeword/guides/test-definitions-guide.md +243 -0
  35. package/.safeword/guides/testing-methodology.md +573 -0
  36. package/.safeword/guides/user-story-guide.md +237 -0
  37. package/.safeword/guides/zombie-process-cleanup.md +214 -0
  38. package/{templates → .safeword}/hooks/agents-md-check.sh +0 -0
  39. package/{templates → .safeword}/hooks/post-tool.sh +0 -0
  40. package/{templates → .safeword}/hooks/pre-commit.sh +0 -0
  41. package/.safeword/planning/002-user-story-quality-evaluation.md +1840 -0
  42. package/.safeword/planning/003-langsmith-eval-setup-prompt.md +363 -0
  43. package/.safeword/planning/004-llm-eval-test-cases.md +3226 -0
  44. package/.safeword/planning/005-architecture-enforcement-system.md +169 -0
  45. package/.safeword/planning/006-reactive-fix-prevention-research.md +135 -0
  46. package/.safeword/planning/011-cli-ux-vision.md +330 -0
  47. package/.safeword/planning/012-project-structure-cleanup.md +154 -0
  48. package/.safeword/planning/README.md +39 -0
  49. package/.safeword/planning/automation-plan-v2.md +1225 -0
  50. package/.safeword/planning/automation-plan-v3.md +1291 -0
  51. package/.safeword/planning/automation-plan.md +3058 -0
  52. package/.safeword/planning/design/005-cli-implementation.md +343 -0
  53. package/.safeword/planning/design/013-cli-self-contained-templates.md +596 -0
  54. package/.safeword/planning/design/013a-eslint-plugin-suite.md +256 -0
  55. package/.safeword/planning/design/013b-implementation-snippets.md +385 -0
  56. package/.safeword/planning/design/013c-config-isolation-strategy.md +242 -0
  57. package/.safeword/planning/design/code-philosophy-improvements.md +60 -0
  58. package/.safeword/planning/mcp-analysis.md +545 -0
  59. package/.safeword/planning/phase2-subagents-vs-skills-analysis.md +451 -0
  60. package/.safeword/planning/settings-improvements.md +970 -0
  61. package/.safeword/planning/test-definitions/005-cli-implementation.md +1301 -0
  62. package/.safeword/planning/test-definitions/cli-self-contained-templates.md +205 -0
  63. package/.safeword/planning/user-stories/001-guides-review-user-stories.md +1381 -0
  64. package/.safeword/planning/user-stories/003-reactive-fix-prevention.md +132 -0
  65. package/.safeword/planning/user-stories/004-technical-constraints.md +86 -0
  66. package/.safeword/planning/user-stories/005-cli-implementation.md +311 -0
  67. package/.safeword/planning/user-stories/cli-self-contained-templates.md +172 -0
  68. package/.safeword/planning/versioned-distribution.md +740 -0
  69. package/.safeword/prompts/arch-review.md +43 -0
  70. package/.safeword/prompts/quality-review.md +11 -0
  71. package/.safeword/scripts/arch-review.sh +235 -0
  72. package/.safeword/scripts/check-linting-sync.sh +58 -0
  73. package/.safeword/scripts/setup-linting.sh +559 -0
  74. package/.safeword/templates/architecture-template.md +136 -0
  75. package/.safeword/templates/ci/architecture-check.yml +79 -0
  76. package/.safeword/templates/design-doc-template.md +127 -0
  77. package/.safeword/templates/test-definitions-feature.md +100 -0
  78. package/.safeword/templates/ticket-template.md +74 -0
  79. package/.safeword/templates/user-stories-template.md +82 -0
  80. package/.safeword/tickets/001-guides-review-user-stories.md +83 -0
  81. package/.safeword/tickets/002-architecture-enforcement.md +211 -0
  82. package/.safeword/tickets/003-reactive-fix-prevention.md +57 -0
  83. package/.safeword/tickets/004-technical-constraints-in-user-stories.md +39 -0
  84. package/.safeword/tickets/005-cli-implementation.md +248 -0
  85. package/.safeword/tickets/006-flesh-out-skills.md +43 -0
  86. package/.safeword/tickets/007-flesh-out-questioning.md +44 -0
  87. package/.safeword/tickets/008-upgrade-questioning.md +58 -0
  88. package/.safeword/tickets/009-naming-conventions.md +41 -0
  89. package/.safeword/tickets/010-safeword-md-cleanup.md +34 -0
  90. package/.safeword/tickets/011-cursor-setup.md +86 -0
  91. package/.safeword/tickets/README.md +73 -0
  92. package/.safeword/version +1 -0
  93. package/AGENTS.md +59 -0
  94. package/CLAUDE.md +12 -0
  95. package/README.md +347 -0
  96. package/docs/001-cli-implementation-plan.md +856 -0
  97. package/docs/elite-dx-implementation-plan.md +1034 -0
  98. package/framework/README.md +131 -0
  99. package/framework/mcp/README.md +96 -0
  100. package/framework/mcp/arcade.sample.json +8 -0
  101. package/framework/mcp/context7.sample.json +6 -0
  102. package/framework/mcp/playwright.sample.json +6 -0
  103. package/framework/scripts/arch-review.sh +235 -0
  104. package/framework/scripts/check-linting-sync.sh +58 -0
  105. package/framework/scripts/load-env.sh +49 -0
  106. package/framework/scripts/setup-claude.sh +223 -0
  107. package/framework/scripts/setup-linting.sh +559 -0
  108. package/framework/scripts/setup-quality.sh +477 -0
  109. package/framework/scripts/setup-safeword.sh +550 -0
  110. package/framework/templates/ci/architecture-check.yml +78 -0
  111. package/learnings/ai-sdk-v5-breaking-changes.md +178 -0
  112. package/learnings/e2e-test-zombie-processes.md +231 -0
  113. package/learnings/milkdown-crepe-editor-property.md +96 -0
  114. package/learnings/prosemirror-fragment-traversal.md +119 -0
  115. package/package.json +19 -43
  116. package/packages/cli/AGENTS.md +1 -0
  117. package/packages/cli/ARCHITECTURE.md +279 -0
  118. package/packages/cli/package.json +51 -0
  119. package/packages/cli/src/cli.ts +63 -0
  120. package/packages/cli/src/commands/check.ts +166 -0
  121. package/packages/cli/src/commands/diff.ts +209 -0
  122. package/packages/cli/src/commands/reset.ts +190 -0
  123. package/packages/cli/src/commands/setup.ts +325 -0
  124. package/packages/cli/src/commands/upgrade.ts +163 -0
  125. package/packages/cli/src/index.ts +3 -0
  126. package/packages/cli/src/templates/config.ts +58 -0
  127. package/packages/cli/src/templates/content.ts +18 -0
  128. package/packages/cli/src/templates/index.ts +12 -0
  129. package/packages/cli/src/utils/agents-md.ts +66 -0
  130. package/packages/cli/src/utils/fs.ts +179 -0
  131. package/packages/cli/src/utils/git.ts +124 -0
  132. package/packages/cli/src/utils/hooks.ts +29 -0
  133. package/packages/cli/src/utils/output.ts +60 -0
  134. package/packages/cli/src/utils/project-detector.test.ts +185 -0
  135. package/packages/cli/src/utils/project-detector.ts +44 -0
  136. package/packages/cli/src/utils/version.ts +28 -0
  137. package/packages/cli/src/version.ts +6 -0
  138. package/packages/cli/templates/SAFEWORD.md +776 -0
  139. package/packages/cli/templates/doc-templates/architecture-template.md +136 -0
  140. package/packages/cli/templates/doc-templates/design-doc-template.md +134 -0
  141. package/packages/cli/templates/doc-templates/test-definitions-feature.md +131 -0
  142. package/packages/cli/templates/doc-templates/ticket-template.md +82 -0
  143. package/packages/cli/templates/doc-templates/user-stories-template.md +92 -0
  144. package/packages/cli/templates/guides/architecture-guide.md +423 -0
  145. package/packages/cli/templates/guides/code-philosophy.md +195 -0
  146. package/packages/cli/templates/guides/context-files-guide.md +457 -0
  147. package/packages/cli/templates/guides/data-architecture-guide.md +200 -0
  148. package/packages/cli/templates/guides/design-doc-guide.md +171 -0
  149. package/packages/cli/templates/guides/learning-extraction.md +552 -0
  150. package/packages/cli/templates/guides/llm-instruction-design.md +248 -0
  151. package/packages/cli/templates/guides/llm-prompting.md +102 -0
  152. package/packages/cli/templates/guides/tdd-best-practices.md +615 -0
  153. package/packages/cli/templates/guides/test-definitions-guide.md +334 -0
  154. package/packages/cli/templates/guides/testing-methodology.md +618 -0
  155. package/packages/cli/templates/guides/user-story-guide.md +256 -0
  156. package/packages/cli/templates/guides/zombie-process-cleanup.md +219 -0
  157. package/packages/cli/templates/hooks/agents-md-check.sh +27 -0
  158. package/packages/cli/templates/hooks/post-tool.sh +4 -0
  159. package/packages/cli/templates/hooks/pre-commit.sh +10 -0
  160. package/packages/cli/templates/prompts/arch-review.md +43 -0
  161. package/packages/cli/templates/prompts/quality-review.md +10 -0
  162. package/packages/cli/templates/skills/safeword-quality-reviewer/SKILL.md +207 -0
  163. package/packages/cli/tests/commands/check.test.ts +129 -0
  164. package/packages/cli/tests/commands/cli.test.ts +89 -0
  165. package/packages/cli/tests/commands/diff.test.ts +115 -0
  166. package/packages/cli/tests/commands/reset.test.ts +310 -0
  167. package/packages/cli/tests/commands/self-healing.test.ts +170 -0
  168. package/packages/cli/tests/commands/setup-blocking.test.ts +71 -0
  169. package/packages/cli/tests/commands/setup-core.test.ts +135 -0
  170. package/packages/cli/tests/commands/setup-git.test.ts +139 -0
  171. package/packages/cli/tests/commands/setup-hooks.test.ts +334 -0
  172. package/packages/cli/tests/commands/setup-linting.test.ts +189 -0
  173. package/packages/cli/tests/commands/setup-noninteractive.test.ts +80 -0
  174. package/packages/cli/tests/commands/setup-templates.test.ts +181 -0
  175. package/packages/cli/tests/commands/upgrade.test.ts +215 -0
  176. package/packages/cli/tests/helpers.ts +243 -0
  177. package/packages/cli/tests/npm-package.test.ts +83 -0
  178. package/packages/cli/tests/technical-constraints.test.ts +96 -0
  179. package/packages/cli/tsconfig.json +25 -0
  180. package/packages/cli/tsup.config.ts +11 -0
  181. package/packages/cli/vitest.config.ts +23 -0
  182. package/promptfoo.yaml +3270 -0
  183. package/dist/check-3NGQ4NR5.js +0 -129
  184. package/dist/check-3NGQ4NR5.js.map +0 -1
  185. package/dist/chunk-2XWIUEQK.js +0 -190
  186. package/dist/chunk-2XWIUEQK.js.map +0 -1
  187. package/dist/chunk-GZRQL3SX.js +0 -146
  188. package/dist/chunk-GZRQL3SX.js.map +0 -1
  189. package/dist/chunk-ORQHKDT2.js +0 -10
  190. package/dist/chunk-ORQHKDT2.js.map +0 -1
  191. package/dist/chunk-W66Z3C5H.js +0 -21
  192. package/dist/chunk-W66Z3C5H.js.map +0 -1
  193. package/dist/cli.d.ts +0 -1
  194. package/dist/cli.js +0 -34
  195. package/dist/cli.js.map +0 -1
  196. package/dist/diff-Y6QTAW4O.js +0 -166
  197. package/dist/diff-Y6QTAW4O.js.map +0 -1
  198. package/dist/index.d.ts +0 -11
  199. package/dist/index.js +0 -7
  200. package/dist/index.js.map +0 -1
  201. package/dist/reset-3ACTIYYE.js +0 -143
  202. package/dist/reset-3ACTIYYE.js.map +0 -1
  203. package/dist/setup-RR4M334C.js +0 -266
  204. package/dist/setup-RR4M334C.js.map +0 -1
  205. package/dist/upgrade-6AR3DHUV.js +0 -134
  206. package/dist/upgrade-6AR3DHUV.js.map +0 -1
  207. /package/{templates → framework}/SAFEWORD.md +0 -0
  208. /package/{templates → framework}/guides/architecture-guide.md +0 -0
  209. /package/{templates → framework}/guides/code-philosophy.md +0 -0
  210. /package/{templates → framework}/guides/context-files-guide.md +0 -0
  211. /package/{templates → framework}/guides/data-architecture-guide.md +0 -0
  212. /package/{templates → framework}/guides/design-doc-guide.md +0 -0
  213. /package/{templates → framework}/guides/learning-extraction.md +0 -0
  214. /package/{templates → framework}/guides/llm-instruction-design.md +0 -0
  215. /package/{templates → framework}/guides/llm-prompting.md +0 -0
  216. /package/{templates → framework}/guides/tdd-best-practices.md +0 -0
  217. /package/{templates → framework}/guides/test-definitions-guide.md +0 -0
  218. /package/{templates → framework}/guides/testing-methodology.md +0 -0
  219. /package/{templates → framework}/guides/user-story-guide.md +0 -0
  220. /package/{templates → framework}/guides/zombie-process-cleanup.md +0 -0
  221. /package/{templates → framework}/prompts/arch-review.md +0 -0
  222. /package/{templates → framework}/prompts/quality-review.md +0 -0
  223. /package/{templates/skills/safeword-quality-reviewer → framework/skills/quality-reviewer}/SKILL.md +0 -0
  224. /package/{templates/doc-templates → framework/templates}/architecture-template.md +0 -0
  225. /package/{templates/doc-templates → framework/templates}/design-doc-template.md +0 -0
  226. /package/{templates/doc-templates → framework/templates}/test-definitions-feature.md +0 -0
  227. /package/{templates/doc-templates → framework/templates}/ticket-template.md +0 -0
  228. /package/{templates/doc-templates → framework/templates}/user-stories-template.md +0 -0
  229. /package/{templates → packages/cli/templates}/commands/arch-review.md +0 -0
  230. /package/{templates → packages/cli/templates}/commands/lint.md +0 -0
  231. /package/{templates → packages/cli/templates}/commands/quality-review.md +0 -0
  232. /package/{templates → packages/cli/templates}/hooks/inject-timestamp.sh +0 -0
  233. /package/{templates → packages/cli/templates}/lib/common.sh +0 -0
  234. /package/{templates → packages/cli/templates}/lib/jq-fallback.sh +0 -0
  235. /package/{templates → packages/cli/templates}/markdownlint.jsonc +0 -0
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Test Suite 7: Git Repository Handling
3
+ *
4
+ * Tests for git detection and hook installation.
5
+ */
6
+
7
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
8
+ import {
9
+ createTempDir,
10
+ removeTempDir,
11
+ createTypeScriptPackageJson,
12
+ runCli,
13
+ readTestFile,
14
+ writeTestFile,
15
+ fileExists,
16
+ initGitRepo,
17
+ } from '../helpers';
18
+
19
+ describe('Test Suite 7: Git Repository Handling', () => {
20
+ let tempDir: string;
21
+
22
+ beforeEach(() => {
23
+ tempDir = createTempDir();
24
+ });
25
+
26
+ afterEach(() => {
27
+ removeTempDir(tempDir);
28
+ });
29
+
30
+ describe('Test 7.1: Prompts for git init when no .git', () => {
31
+ it('should mention git initialization in output', async () => {
32
+ createTypeScriptPackageJson(tempDir);
33
+ // No git init
34
+
35
+ // In non-interactive mode with --yes, we expect it to skip
36
+ // The prompt behavior is tested via TTY simulation
37
+ const result = await runCli(['setup', '--yes'], { cwd: tempDir });
38
+
39
+ // Output should mention git
40
+ const output = result.stdout + result.stderr;
41
+ expect(output.toLowerCase()).toMatch(/git/i);
42
+ });
43
+ });
44
+
45
+ describe('Test 7.2: Runs git init when user confirms', () => {
46
+ // This test would require TTY simulation for interactive input
47
+ // For now, we test that git hooks work when .git exists
48
+ it('should work with existing git repository', async () => {
49
+ createTypeScriptPackageJson(tempDir);
50
+ initGitRepo(tempDir);
51
+
52
+ const result = await runCli(['setup', '--yes'], { cwd: tempDir });
53
+
54
+ expect(result.exitCode).toBe(0);
55
+ expect(fileExists(tempDir, '.git')).toBe(true);
56
+ });
57
+ });
58
+
59
+ describe('Test 7.3: Skips git init when user declines', () => {
60
+ // In --yes mode, git init is skipped by default
61
+ it('should skip git init in non-interactive mode', async () => {
62
+ createTypeScriptPackageJson(tempDir);
63
+ // No git init
64
+
65
+ const result = await runCli(['setup', '--yes'], { cwd: tempDir });
66
+
67
+ expect(result.exitCode).toBe(0);
68
+ // Git should NOT be initialized
69
+ expect(fileExists(tempDir, '.git')).toBe(false);
70
+
71
+ // Should warn about skipped git hooks
72
+ const output = result.stdout + result.stderr;
73
+ expect(output.toLowerCase()).toMatch(/skip|warning|git/i);
74
+ });
75
+ });
76
+
77
+ describe('Test 7.4: Installs git hooks when .git present', () => {
78
+ it('should create pre-commit hook with safeword markers', async () => {
79
+ createTypeScriptPackageJson(tempDir);
80
+ initGitRepo(tempDir);
81
+
82
+ await runCli(['setup', '--yes'], { cwd: tempDir });
83
+
84
+ expect(fileExists(tempDir, '.git/hooks/pre-commit')).toBe(true);
85
+
86
+ const content = readTestFile(tempDir, '.git/hooks/pre-commit');
87
+ expect(content).toContain('SAFEWORD_ARCH_CHECK_START');
88
+ expect(content).toContain('SAFEWORD_ARCH_CHECK_END');
89
+ });
90
+ });
91
+
92
+ describe('Test 7.5: Preserves existing pre-commit hooks', () => {
93
+ it('should preserve custom hook content', async () => {
94
+ createTypeScriptPackageJson(tempDir);
95
+ initGitRepo(tempDir);
96
+
97
+ // Create existing pre-commit hook
98
+ const customHook = `#!/bin/bash
99
+ # Custom hook for running tests
100
+ echo "Running custom tests..."
101
+ npm test
102
+ `;
103
+ writeTestFile(tempDir, '.git/hooks/pre-commit', customHook);
104
+
105
+ await runCli(['setup', '--yes'], { cwd: tempDir });
106
+
107
+ const content = readTestFile(tempDir, '.git/hooks/pre-commit');
108
+
109
+ // Original content preserved
110
+ expect(content).toContain('Running custom tests');
111
+ expect(content).toContain('npm test');
112
+
113
+ // Safeword markers added
114
+ expect(content).toContain('SAFEWORD_ARCH_CHECK_START');
115
+ expect(content).toContain('SAFEWORD_ARCH_CHECK_END');
116
+ });
117
+
118
+ it('should not duplicate markers on repeated setup', async () => {
119
+ createTypeScriptPackageJson(tempDir);
120
+ initGitRepo(tempDir);
121
+
122
+ // Run setup twice
123
+ await runCli(['setup', '--yes'], { cwd: tempDir });
124
+
125
+ // For second run, we need to remove .safeword to allow setup
126
+ // Or use upgrade. Let's test via upgrade path
127
+ await runCli(['upgrade'], { cwd: tempDir });
128
+
129
+ const content = readTestFile(tempDir, '.git/hooks/pre-commit');
130
+
131
+ // Markers should appear exactly once
132
+ const startCount = (content.match(/SAFEWORD_ARCH_CHECK_START/g) || []).length;
133
+ const endCount = (content.match(/SAFEWORD_ARCH_CHECK_END/g) || []).length;
134
+
135
+ expect(startCount).toBe(1);
136
+ expect(endCount).toBe(1);
137
+ });
138
+ });
139
+ });
@@ -0,0 +1,334 @@
1
+ /**
2
+ * Test Suite 3: Setup - Hooks and Skills
3
+ *
4
+ * Tests for Claude Code hook registration and skill copying.
5
+ */
6
+
7
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
8
+ import { chmodSync, readdirSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+ import {
11
+ createTempDir,
12
+ removeTempDir,
13
+ createTypeScriptPackageJson,
14
+ runCli,
15
+ readTestFile,
16
+ writeTestFile,
17
+ fileExists,
18
+ initGitRepo,
19
+ } from '../helpers';
20
+
21
+ describe('Test Suite 3: Setup - Hooks and Skills', () => {
22
+ let tempDir: string;
23
+
24
+ beforeEach(() => {
25
+ tempDir = createTempDir();
26
+ });
27
+
28
+ afterEach(() => {
29
+ removeTempDir(tempDir);
30
+ });
31
+
32
+ describe('Test 3.1: Registers hooks in settings.json', () => {
33
+ it('should create .claude/settings.json with hooks', async () => {
34
+ createTypeScriptPackageJson(tempDir);
35
+ initGitRepo(tempDir);
36
+
37
+ await runCli(['setup', '--yes'], { cwd: tempDir });
38
+
39
+ expect(fileExists(tempDir, '.claude/settings.json')).toBe(true);
40
+
41
+ const settings = JSON.parse(readTestFile(tempDir, '.claude/settings.json'));
42
+
43
+ expect(settings.hooks).toBeDefined();
44
+ expect(settings.hooks.SessionStart).toBeDefined();
45
+ expect(Array.isArray(settings.hooks.SessionStart)).toBe(true);
46
+ expect(settings.hooks.UserPromptSubmit).toBeDefined();
47
+ expect(Array.isArray(settings.hooks.UserPromptSubmit)).toBe(true);
48
+ expect(settings.hooks.PostToolUse).toBeDefined();
49
+ expect(Array.isArray(settings.hooks.PostToolUse)).toBe(true);
50
+ });
51
+
52
+ it('should reference .safeword/hooks paths', async () => {
53
+ createTypeScriptPackageJson(tempDir);
54
+ initGitRepo(tempDir);
55
+
56
+ await runCli(['setup', '--yes'], { cwd: tempDir });
57
+
58
+ const settings = JSON.parse(readTestFile(tempDir, '.claude/settings.json'));
59
+ const settingsStr = JSON.stringify(settings);
60
+
61
+ // At least one hook should reference .safeword/hooks
62
+ expect(settingsStr).toContain('.safeword/hooks');
63
+ });
64
+ });
65
+
66
+ describe('Test 3.2: Copies skills to .claude/skills', () => {
67
+ it('should create skills directory with safeword skills', async () => {
68
+ createTypeScriptPackageJson(tempDir);
69
+ initGitRepo(tempDir);
70
+
71
+ await runCli(['setup', '--yes'], { cwd: tempDir });
72
+
73
+ expect(fileExists(tempDir, '.claude/skills')).toBe(true);
74
+
75
+ // Check for safeword skill directory
76
+ // Should match pattern safeword-* (e.g., safeword-quality-reviewer)
77
+ expect(fileExists(tempDir, '.claude/skills/safeword-quality-reviewer')).toBe(true);
78
+
79
+ // The skill should contain a SKILL.md file
80
+ expect(fileExists(tempDir, '.claude/skills/safeword-quality-reviewer/SKILL.md')).toBe(true);
81
+
82
+ const skillContent = readTestFile(
83
+ tempDir,
84
+ '.claude/skills/safeword-quality-reviewer/SKILL.md',
85
+ );
86
+ expect(skillContent).toContain('Quality Reviewer');
87
+ });
88
+ });
89
+
90
+ describe('Test 3.3: Preserves existing hooks', () => {
91
+ it('should append to existing hooks without replacing', async () => {
92
+ createTypeScriptPackageJson(tempDir);
93
+ initGitRepo(tempDir);
94
+
95
+ // Create existing settings with custom hook
96
+ const existingSettings = {
97
+ hooks: {
98
+ SessionStart: [
99
+ {
100
+ command: 'echo "My custom hook"',
101
+ description: 'Custom SessionStart hook',
102
+ },
103
+ ],
104
+ },
105
+ };
106
+ writeTestFile(tempDir, '.claude/settings.json', JSON.stringify(existingSettings, null, 2));
107
+
108
+ await runCli(['setup', '--yes'], { cwd: tempDir });
109
+
110
+ const settings = JSON.parse(readTestFile(tempDir, '.claude/settings.json'));
111
+
112
+ // Custom hook should still be present
113
+ const hasCustomHook = settings.hooks.SessionStart.some(
114
+ (hook: { command?: string }) => hook.command === 'echo "My custom hook"',
115
+ );
116
+ expect(hasCustomHook).toBe(true);
117
+
118
+ // Safeword hooks should be added
119
+ expect(settings.hooks.SessionStart.length).toBeGreaterThan(1);
120
+ });
121
+ });
122
+
123
+ describe('Test 3.4: Includes SessionStart hook for AGENTS.md', () => {
124
+ it('should include AGENTS.md verification hook', async () => {
125
+ createTypeScriptPackageJson(tempDir);
126
+ initGitRepo(tempDir);
127
+
128
+ await runCli(['setup', '--yes'], { cwd: tempDir });
129
+
130
+ const settings = JSON.parse(readTestFile(tempDir, '.claude/settings.json'));
131
+
132
+ // Find hook that references AGENTS.md check
133
+ const agentsHook = settings.hooks.SessionStart.find(
134
+ (hook: { command?: string }) =>
135
+ hook.command && (hook.command.includes('AGENTS') || hook.command.includes('agents')),
136
+ );
137
+
138
+ expect(agentsHook).toBeDefined();
139
+
140
+ // Hook script should exist
141
+ if (agentsHook?.command) {
142
+ // Extract script path from command if it's a bash script
143
+ const scriptMatch = agentsHook.command.match(/bash\s+([^\s]+)/);
144
+ if (scriptMatch) {
145
+ expect(fileExists(tempDir, scriptMatch[1])).toBe(true);
146
+ }
147
+ }
148
+ });
149
+ });
150
+
151
+ describe('Test 3.6: Includes UserPromptSubmit hook for timestamp injection', () => {
152
+ it('should include timestamp injection hook', async () => {
153
+ createTypeScriptPackageJson(tempDir);
154
+ initGitRepo(tempDir);
155
+
156
+ await runCli(['setup', '--yes'], { cwd: tempDir });
157
+
158
+ const settings = JSON.parse(readTestFile(tempDir, '.claude/settings.json'));
159
+
160
+ // Find hook that references timestamp injection
161
+ const timestampHook = settings.hooks.UserPromptSubmit.find(
162
+ (hook: { command?: string }) =>
163
+ hook.command && hook.command.includes('inject-timestamp'),
164
+ );
165
+
166
+ expect(timestampHook).toBeDefined();
167
+
168
+ // Hook script should exist
169
+ expect(fileExists(tempDir, '.safeword/hooks/inject-timestamp.sh')).toBe(true);
170
+
171
+ // Script should contain timestamp output
172
+ const scriptContent = readTestFile(tempDir, '.safeword/hooks/inject-timestamp.sh');
173
+ expect(scriptContent).toContain('date');
174
+ });
175
+ });
176
+
177
+ describe('Test 3.5: Exit 1 if hook registration fails', () => {
178
+ it('should fail with exit 1 when settings.json is not writable', async () => {
179
+ createTypeScriptPackageJson(tempDir);
180
+ initGitRepo(tempDir);
181
+
182
+ // Create .claude directory with read-only settings.json
183
+ writeTestFile(tempDir, '.claude/settings.json', '{}');
184
+ chmodSync(join(tempDir, '.claude/settings.json'), 0o444);
185
+
186
+ const result = await runCli(['setup', '--yes'], { cwd: tempDir });
187
+
188
+ // Restore permissions for cleanup
189
+ chmodSync(join(tempDir, '.claude/settings.json'), 0o644);
190
+
191
+ expect(result.exitCode).toBe(1);
192
+ expect(result.stderr.toLowerCase()).toMatch(/hook|permission|write|failed/i);
193
+ });
194
+ });
195
+
196
+ describe('Test 3.7: Installs lib scripts', () => {
197
+ it('should install shared library scripts to .safeword/lib/', async () => {
198
+ createTypeScriptPackageJson(tempDir);
199
+ initGitRepo(tempDir);
200
+
201
+ await runCli(['setup', '--yes'], { cwd: tempDir });
202
+
203
+ // Lib directory should exist with shell scripts
204
+ expect(fileExists(tempDir, '.safeword/lib')).toBe(true);
205
+
206
+ const libDir = join(tempDir, '.safeword/lib');
207
+ const shFiles = readdirSync(libDir).filter((f) => f.endsWith('.sh'));
208
+ expect(shFiles.length).toBeGreaterThan(0);
209
+ });
210
+ });
211
+
212
+ describe('Test 3.8: Sets up MCP servers', () => {
213
+ it('should create .mcp.json with context7 and playwright', async () => {
214
+ createTypeScriptPackageJson(tempDir);
215
+ initGitRepo(tempDir);
216
+
217
+ await runCli(['setup', '--yes'], { cwd: tempDir });
218
+
219
+ expect(fileExists(tempDir, '.mcp.json')).toBe(true);
220
+
221
+ const mcpConfig = JSON.parse(readTestFile(tempDir, '.mcp.json'));
222
+ expect(mcpConfig.mcpServers).toBeDefined();
223
+ expect(mcpConfig.mcpServers.context7).toBeDefined();
224
+ expect(mcpConfig.mcpServers.playwright).toBeDefined();
225
+ });
226
+ });
227
+
228
+ describe('Test 3.9: Preserves existing MCP servers', () => {
229
+ it('should preserve user MCP servers when adding safeword servers', async () => {
230
+ createTypeScriptPackageJson(tempDir);
231
+ initGitRepo(tempDir);
232
+
233
+ // Create existing .mcp.json with custom server
234
+ const existingMcp = {
235
+ mcpServers: {
236
+ 'my-custom-server': {
237
+ command: 'my-server',
238
+ args: ['--port', '3000'],
239
+ },
240
+ },
241
+ };
242
+ writeTestFile(tempDir, '.mcp.json', JSON.stringify(existingMcp, null, 2));
243
+
244
+ await runCli(['setup', '--yes'], { cwd: tempDir });
245
+
246
+ const mcpConfig = JSON.parse(readTestFile(tempDir, '.mcp.json'));
247
+
248
+ // Custom server should be preserved
249
+ expect(mcpConfig.mcpServers['my-custom-server']).toBeDefined();
250
+ expect(mcpConfig.mcpServers['my-custom-server'].command).toBe('my-server');
251
+
252
+ // Safeword servers should be added
253
+ expect(mcpConfig.mcpServers.context7).toBeDefined();
254
+ expect(mcpConfig.mcpServers.playwright).toBeDefined();
255
+ });
256
+ });
257
+
258
+ describe('Test 3.10: Installs slash commands', () => {
259
+ it('should install slash commands to .claude/commands/', async () => {
260
+ createTypeScriptPackageJson(tempDir);
261
+ initGitRepo(tempDir);
262
+
263
+ await runCli(['setup', '--yes'], { cwd: tempDir });
264
+
265
+ // Commands directory should exist
266
+ expect(fileExists(tempDir, '.claude/commands')).toBe(true);
267
+
268
+ // Should contain markdown command files
269
+ const commandsDir = join(tempDir, '.claude/commands');
270
+ const mdFiles = readdirSync(commandsDir).filter((f) => f.endsWith('.md'));
271
+ expect(mdFiles.length).toBeGreaterThan(0);
272
+ });
273
+ });
274
+
275
+ describe('Test 3.11: MCP servers work out of the box', () => {
276
+ it('should configure context7 with correct npx command', async () => {
277
+ createTypeScriptPackageJson(tempDir);
278
+ initGitRepo(tempDir);
279
+
280
+ await runCli(['setup', '--yes'], { cwd: tempDir });
281
+
282
+ expect(fileExists(tempDir, '.mcp.json')).toBe(true);
283
+
284
+ const mcpConfig = JSON.parse(readTestFile(tempDir, '.mcp.json'));
285
+
286
+ // Context7 should use npx with the correct package
287
+ expect(mcpConfig.mcpServers.context7).toBeDefined();
288
+ expect(mcpConfig.mcpServers.context7.command).toBe('npx');
289
+ expect(mcpConfig.mcpServers.context7.args).toContain('@upstash/context7-mcp@latest');
290
+ });
291
+
292
+ it('should configure playwright with correct npx command', async () => {
293
+ createTypeScriptPackageJson(tempDir);
294
+ initGitRepo(tempDir);
295
+
296
+ await runCli(['setup', '--yes'], { cwd: tempDir });
297
+
298
+ const mcpConfig = JSON.parse(readTestFile(tempDir, '.mcp.json'));
299
+
300
+ // Playwright should use npx with the correct package
301
+ expect(mcpConfig.mcpServers.playwright).toBeDefined();
302
+ expect(mcpConfig.mcpServers.playwright.command).toBe('npx');
303
+ expect(mcpConfig.mcpServers.playwright.args).toEqual(
304
+ expect.arrayContaining([expect.stringContaining('@playwright/mcp')]),
305
+ );
306
+ });
307
+
308
+ it('should have MCP packages that can be resolved by npx', async () => {
309
+ // Verify the packages exist on npm (can be resolved)
310
+ // This is a lightweight check - just verify npm can find the packages
311
+ const { execSync } = await import('node:child_process');
312
+
313
+ // Check context7 package exists
314
+ try {
315
+ execSync('npm view @upstash/context7-mcp version', {
316
+ encoding: 'utf-8',
317
+ timeout: 10000,
318
+ });
319
+ } catch {
320
+ expect.fail('@upstash/context7-mcp package not found on npm');
321
+ }
322
+
323
+ // Check playwright package exists
324
+ try {
325
+ execSync('npm view @playwright/mcp version', {
326
+ encoding: 'utf-8',
327
+ timeout: 10000,
328
+ });
329
+ } catch {
330
+ expect.fail('@playwright/mcp package not found on npm');
331
+ }
332
+ });
333
+ });
334
+ });
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Test Suite 4: Setup - Linting (Integration Tests)
3
+ *
4
+ * Tests for ESLint + Prettier configuration.
5
+ *
6
+ * Note: Unit tests for project type detection (Tests 4.1-4.3) are in
7
+ * src/utils/project-detector.test.ts. This file contains only the
8
+ * integration tests (Tests 4.4-4.8).
9
+ */
10
+
11
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
12
+ import { chmodSync } from 'node:fs';
13
+ import { join } from 'node:path';
14
+ import {
15
+ createTempDir,
16
+ removeTempDir,
17
+ createTypeScriptPackageJson,
18
+ runCli,
19
+ readTestFile,
20
+ fileExists,
21
+ initGitRepo,
22
+ } from '../helpers';
23
+
24
+ describe('Test Suite 4: Setup - Linting (Integration)', () => {
25
+ let tempDir: string;
26
+
27
+ beforeEach(() => {
28
+ tempDir = createTempDir();
29
+ });
30
+
31
+ afterEach(() => {
32
+ removeTempDir(tempDir);
33
+ });
34
+
35
+ describe('Test 4.4: Creates eslint.config.mjs', () => {
36
+ it('should create ESLint flat config', async () => {
37
+ createTypeScriptPackageJson(tempDir);
38
+ initGitRepo(tempDir);
39
+
40
+ await runCli(['setup', '--yes'], { cwd: tempDir });
41
+
42
+ expect(fileExists(tempDir, 'eslint.config.mjs')).toBe(true);
43
+
44
+ const content = readTestFile(tempDir, 'eslint.config.mjs');
45
+ // Should be a valid ESLint flat config
46
+ expect(content).toContain('export');
47
+ });
48
+
49
+ it('should include TypeScript config when detected', async () => {
50
+ createTypeScriptPackageJson(tempDir);
51
+ initGitRepo(tempDir);
52
+
53
+ await runCli(['setup', '--yes'], { cwd: tempDir });
54
+
55
+ const content = readTestFile(tempDir, 'eslint.config.mjs');
56
+ expect(content).toMatch(/typescript|@typescript-eslint/i);
57
+ });
58
+ });
59
+
60
+ describe('Test 4.5: Creates .prettierrc', () => {
61
+ it('should create Prettier config', async () => {
62
+ createTypeScriptPackageJson(tempDir);
63
+ initGitRepo(tempDir);
64
+
65
+ await runCli(['setup', '--yes'], { cwd: tempDir });
66
+
67
+ expect(fileExists(tempDir, '.prettierrc')).toBe(true);
68
+
69
+ const content = readTestFile(tempDir, '.prettierrc');
70
+ // Should be valid JSON
71
+ expect(() => JSON.parse(content)).not.toThrow();
72
+ });
73
+ });
74
+
75
+ describe('Test 4.6: Adds lint script to package.json', () => {
76
+ it('should add lint script', async () => {
77
+ createTypeScriptPackageJson(tempDir);
78
+ initGitRepo(tempDir);
79
+
80
+ await runCli(['setup', '--yes'], { cwd: tempDir });
81
+
82
+ const packageJson = JSON.parse(readTestFile(tempDir, 'package.json'));
83
+ expect(packageJson.scripts?.lint).toBe('eslint .');
84
+ });
85
+
86
+ it('should not overwrite existing lint script', async () => {
87
+ createTypeScriptPackageJson(tempDir, {
88
+ scripts: {
89
+ lint: 'eslint src/',
90
+ },
91
+ });
92
+ initGitRepo(tempDir);
93
+
94
+ await runCli(['setup', '--yes'], { cwd: tempDir });
95
+
96
+ const packageJson = JSON.parse(readTestFile(tempDir, 'package.json'));
97
+ // Original script should be preserved
98
+ expect(packageJson.scripts?.lint).toBe('eslint src/');
99
+ });
100
+ });
101
+
102
+ describe('Test 4.7: Adds format script to package.json', () => {
103
+ it('should add format script', async () => {
104
+ createTypeScriptPackageJson(tempDir);
105
+ initGitRepo(tempDir);
106
+
107
+ await runCli(['setup', '--yes'], { cwd: tempDir });
108
+
109
+ const packageJson = JSON.parse(readTestFile(tempDir, 'package.json'));
110
+ expect(packageJson.scripts?.format).toBe('prettier --write .');
111
+ });
112
+
113
+ it('should not overwrite existing format script', async () => {
114
+ createTypeScriptPackageJson(tempDir, {
115
+ scripts: {
116
+ format: 'prettier --write src/',
117
+ },
118
+ });
119
+ initGitRepo(tempDir);
120
+
121
+ await runCli(['setup', '--yes'], { cwd: tempDir });
122
+
123
+ const packageJson = JSON.parse(readTestFile(tempDir, 'package.json'));
124
+ // Original script should be preserved
125
+ expect(packageJson.scripts?.format).toBe('prettier --write src/');
126
+ });
127
+ });
128
+
129
+ describe('Test 4.8: Exit 1 if linting setup fails', () => {
130
+ it('should fail with exit 1 when package.json is not writable', async () => {
131
+ createTypeScriptPackageJson(tempDir);
132
+ initGitRepo(tempDir);
133
+
134
+ // Make package.json read-only
135
+ chmodSync(join(tempDir, 'package.json'), 0o444);
136
+
137
+ const result = await runCli(['setup', '--yes'], { cwd: tempDir });
138
+
139
+ // Restore permissions for cleanup
140
+ chmodSync(join(tempDir, 'package.json'), 0o644);
141
+
142
+ expect(result.exitCode).toBe(1);
143
+ expect(result.stderr.toLowerCase()).toMatch(/lint|permission|write|failed|package/i);
144
+ });
145
+ });
146
+
147
+ describe('Test 4.9: Creates markdownlint config', () => {
148
+ it('should create .markdownlint.jsonc', async () => {
149
+ createTypeScriptPackageJson(tempDir);
150
+ initGitRepo(tempDir);
151
+
152
+ await runCli(['setup', '--yes'], { cwd: tempDir });
153
+
154
+ // Implementation may place in .safeword/ or root
155
+ const hasMarkdownlintConfig =
156
+ fileExists(tempDir, '.markdownlint.jsonc') ||
157
+ fileExists(tempDir, '.safeword/.markdownlint.jsonc');
158
+
159
+ expect(hasMarkdownlintConfig).toBe(true);
160
+ });
161
+ });
162
+
163
+ describe('Test 4.10: Adds lint:md script', () => {
164
+ it('should add lint:md script to package.json', async () => {
165
+ createTypeScriptPackageJson(tempDir);
166
+ initGitRepo(tempDir);
167
+
168
+ await runCli(['setup', '--yes'], { cwd: tempDir });
169
+
170
+ const packageJson = JSON.parse(readTestFile(tempDir, 'package.json'));
171
+ expect(packageJson.scripts?.['lint:md']).toBeDefined();
172
+ expect(packageJson.scripts?.['lint:md']).toContain('markdownlint');
173
+ });
174
+ });
175
+
176
+ describe('Test 4.11: Adds format:check script', () => {
177
+ it('should add format:check script to package.json', async () => {
178
+ createTypeScriptPackageJson(tempDir);
179
+ initGitRepo(tempDir);
180
+
181
+ await runCli(['setup', '--yes'], { cwd: tempDir });
182
+
183
+ const packageJson = JSON.parse(readTestFile(tempDir, 'package.json'));
184
+ expect(packageJson.scripts?.['format:check']).toBeDefined();
185
+ expect(packageJson.scripts?.['format:check']).toContain('prettier');
186
+ expect(packageJson.scripts?.['format:check']).toContain('--check');
187
+ });
188
+ });
189
+ });