safeword 0.2.2 → 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-M73LGONJ.js +0 -129
  184. package/dist/check-M73LGONJ.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-V5G6BGOK.js +0 -26
  190. package/dist/chunk-V5G6BGOK.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-FSFDCBL5.js +0 -166
  197. package/dist/diff-FSFDCBL5.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-MKVVQTVA.js +0 -266
  204. package/dist/setup-MKVVQTVA.js.map +0 -1
  205. package/dist/upgrade-FQOL6AF5.js +0 -134
  206. package/dist/upgrade-FQOL6AF5.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,80 @@
1
+ /**
2
+ * Test Suite 6: Non-Interactive Setup
3
+ *
4
+ * Tests for CI/headless operation.
5
+ */
6
+
7
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
8
+ import {
9
+ createTempDir,
10
+ removeTempDir,
11
+ createTypeScriptPackageJson,
12
+ runCli,
13
+ fileExists,
14
+ } from '../helpers';
15
+
16
+ describe('Test Suite 6: Non-Interactive Setup', () => {
17
+ let tempDir: string;
18
+
19
+ beforeEach(() => {
20
+ tempDir = createTempDir();
21
+ });
22
+
23
+ afterEach(() => {
24
+ removeTempDir(tempDir);
25
+ });
26
+
27
+ describe('Test 6.1: --yes flag skips all prompts', () => {
28
+ it('should complete without hanging', async () => {
29
+ createTypeScriptPackageJson(tempDir);
30
+ // No git init - should skip git prompt with --yes
31
+
32
+ const result = await runCli(['setup', '--yes'], {
33
+ cwd: tempDir,
34
+ timeout: 30000,
35
+ });
36
+
37
+ expect(result.exitCode).toBe(0);
38
+ expect(fileExists(tempDir, '.safeword')).toBe(true);
39
+
40
+ // Git should be skipped (no .git created)
41
+ expect(fileExists(tempDir, '.git')).toBe(false);
42
+ });
43
+ });
44
+
45
+ describe('Test 6.2: No TTY uses defaults', () => {
46
+ it('should complete without stdin in non-TTY mode', async () => {
47
+ createTypeScriptPackageJson(tempDir);
48
+ // No git init
49
+
50
+ // Force non-TTY by setting environment
51
+ const result = await runCli(['setup'], {
52
+ cwd: tempDir,
53
+ timeout: 30000,
54
+ env: {
55
+ CI: 'true', // Many tools detect CI and use non-interactive mode
56
+ TERM: 'dumb',
57
+ },
58
+ });
59
+
60
+ // Should complete (either with defaults or --yes required message)
61
+ // The exact behavior depends on implementation
62
+ expect(result.exitCode).toBeDefined();
63
+ });
64
+ });
65
+
66
+ describe('Test 6.3: Warning shown when git skipped', () => {
67
+ it('should show warning about skipped git initialization', async () => {
68
+ createTypeScriptPackageJson(tempDir);
69
+ // No git init
70
+
71
+ const result = await runCli(['setup', '--yes'], { cwd: tempDir });
72
+
73
+ expect(result.exitCode).toBe(0);
74
+
75
+ // Should mention skipped git
76
+ const output = result.stdout + result.stderr;
77
+ expect(output.toLowerCase()).toMatch(/skipped|git|warning/i);
78
+ });
79
+ });
80
+ });
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Test Suite: Setup - Template Bundling (Story 1)
3
+ *
4
+ * Tests for the "self-contained templates" feature.
5
+ * The CLI should bundle full methodology files (not stubs) so
6
+ * `npx safeword setup` works without external dependencies.
7
+ *
8
+ * First 3 tests FAIL until Story 1 is implemented.
9
+ */
10
+
11
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
12
+ import { readdirSync, readFileSync } from 'node:fs';
13
+ import { join } from 'node:path';
14
+ import {
15
+ createTempDir,
16
+ removeTempDir,
17
+ createTypeScriptPackageJson,
18
+ runCli,
19
+ readTestFile,
20
+ writeTestFile,
21
+ fileExists,
22
+ initGitRepo,
23
+ } from '../helpers';
24
+
25
+ describe('Setup - Template Bundling (Story 1)', () => {
26
+ let tempDir: string;
27
+
28
+ beforeEach(() => {
29
+ tempDir = createTempDir();
30
+ });
31
+
32
+ afterEach(() => {
33
+ removeTempDir(tempDir);
34
+ });
35
+
36
+ it('should install full SAFEWORD.md (not a stub)', async () => {
37
+ createTypeScriptPackageJson(tempDir);
38
+ initGitRepo(tempDir);
39
+
40
+ await runCli(['setup', '--yes'], { cwd: tempDir });
41
+
42
+ expect(fileExists(tempDir, '.safeword/SAFEWORD.md')).toBe(true);
43
+
44
+ const content = readTestFile(tempDir, '.safeword/SAFEWORD.md');
45
+ // Full file is ~31KB, stub is <1KB
46
+ expect(content.length).toBeGreaterThan(1000);
47
+ // Verify it's the full methodology file, not a stub
48
+ expect(content).toContain('# Global Instructions for AI Coding Agents');
49
+ });
50
+
51
+ it('should install methodology guides to .safeword/guides/', async () => {
52
+ createTypeScriptPackageJson(tempDir);
53
+ initGitRepo(tempDir);
54
+
55
+ await runCli(['setup', '--yes'], { cwd: tempDir });
56
+
57
+ expect(fileExists(tempDir, '.safeword/guides')).toBe(true);
58
+
59
+ const guidesDir = join(tempDir, '.safeword/guides');
60
+ const mdFiles = readdirSync(guidesDir).filter((f) => f.endsWith('.md'));
61
+
62
+ expect(mdFiles.length).toBeGreaterThan(0);
63
+ });
64
+
65
+ it('should install document templates', async () => {
66
+ createTypeScriptPackageJson(tempDir);
67
+ initGitRepo(tempDir);
68
+
69
+ await runCli(['setup', '--yes'], { cwd: tempDir });
70
+
71
+ expect(fileExists(tempDir, '.safeword/templates')).toBe(true);
72
+
73
+ const templatesDir = join(tempDir, '.safeword/templates');
74
+ const mdFiles = readdirSync(templatesDir).filter((f) => f.endsWith('.md'));
75
+
76
+ expect(mdFiles.length).toBeGreaterThan(0);
77
+ });
78
+
79
+ it('should install review prompts to .safeword/prompts/', async () => {
80
+ createTypeScriptPackageJson(tempDir);
81
+ initGitRepo(tempDir);
82
+
83
+ await runCli(['setup', '--yes'], { cwd: tempDir });
84
+
85
+ expect(fileExists(tempDir, '.safeword/prompts')).toBe(true);
86
+
87
+ const promptsDir = join(tempDir, '.safeword/prompts');
88
+ const mdFiles = readdirSync(promptsDir).filter((f) => f.endsWith('.md'));
89
+
90
+ // Should have 2 review prompts
91
+ expect(mdFiles.length).toBeGreaterThan(0);
92
+ });
93
+
94
+ it('should block re-run and preserve user content', async () => {
95
+ createTypeScriptPackageJson(tempDir);
96
+ initGitRepo(tempDir);
97
+
98
+ await runCli(['setup', '--yes'], { cwd: tempDir });
99
+
100
+ // Create user content
101
+ writeTestFile(
102
+ tempDir,
103
+ '.safeword/learnings/my-learning.md',
104
+ '# My Learning\n\nImportant info.',
105
+ );
106
+
107
+ // Re-run setup should exit with error (already configured)
108
+ const result = await runCli(['setup', '--yes'], { cwd: tempDir });
109
+ expect(result.exitCode).toBe(1);
110
+ expect(result.stderr).toContain('Already configured');
111
+
112
+ // User content should be untouched
113
+ expect(fileExists(tempDir, '.safeword/learnings/my-learning.md')).toBe(true);
114
+ const content = readTestFile(tempDir, '.safeword/learnings/my-learning.md');
115
+ expect(content).toContain('My Learning');
116
+ });
117
+
118
+ it('should have no broken internal links in installed templates', async () => {
119
+ createTypeScriptPackageJson(tempDir);
120
+ initGitRepo(tempDir);
121
+
122
+ await runCli(['setup', '--yes'], { cwd: tempDir });
123
+
124
+ // Collect all markdown files in .safeword/
125
+ const safewordDir = join(tempDir, '.safeword');
126
+ const allMdFiles: string[] = [];
127
+
128
+ function collectMdFiles(dir: string) {
129
+ if (!fileExists(tempDir, dir.replace(tempDir + '/', ''))) return;
130
+ const entries = readdirSync(dir, { withFileTypes: true });
131
+ for (const entry of entries) {
132
+ const fullPath = join(dir, entry.name);
133
+ if (entry.isDirectory()) {
134
+ collectMdFiles(fullPath);
135
+ } else if (entry.name.endsWith('.md')) {
136
+ allMdFiles.push(fullPath);
137
+ }
138
+ }
139
+ }
140
+
141
+ collectMdFiles(safewordDir);
142
+
143
+ // Must have at least SAFEWORD.md with links to verify
144
+ expect(allMdFiles.length).toBeGreaterThan(0);
145
+
146
+ // Extract all @./.safeword/ links and verify targets exist
147
+ // Pattern: @./.safeword/path.md - stop at whitespace, backticks, quotes, parens, or markdown formatting
148
+ const linkPattern = /@\.\/\.safeword\/[a-zA-Z0-9_\-/]+\.md/g;
149
+ const brokenLinks: { file: string; link: string }[] = [];
150
+ let totalLinks = 0;
151
+
152
+ for (const mdFile of allMdFiles) {
153
+ const content = readFileSync(mdFile, 'utf-8');
154
+ const links = content.match(linkPattern) || [];
155
+ totalLinks += links.length;
156
+
157
+ for (const link of links) {
158
+ // Convert @./.safeword/path to relative path
159
+ const relativePath = link.replace('@./', '');
160
+
161
+ if (!fileExists(tempDir, relativePath)) {
162
+ brokenLinks.push({
163
+ file: mdFile.replace(tempDir + '/', ''),
164
+ link,
165
+ });
166
+ }
167
+ }
168
+ }
169
+
170
+ // Report all broken links
171
+ if (brokenLinks.length > 0) {
172
+ const report = brokenLinks
173
+ .map((b) => ` ${b.file}: ${b.link}`)
174
+ .join('\n');
175
+ expect.fail(`Found ${brokenLinks.length} broken links:\n${report}`);
176
+ }
177
+
178
+ // Should have found at least some links to validate
179
+ expect(totalLinks).toBeGreaterThan(0);
180
+ });
181
+ });
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Test Suite 9: Upgrade
3
+ *
4
+ * Tests for `safeword upgrade` command.
5
+ */
6
+
7
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
8
+ import {
9
+ createTempDir,
10
+ removeTempDir,
11
+ createTypeScriptPackageJson,
12
+ createConfiguredProject,
13
+ runCli,
14
+ readTestFile,
15
+ writeTestFile,
16
+ fileExists,
17
+ } from '../helpers';
18
+
19
+ describe('Test Suite 9: Upgrade', () => {
20
+ let tempDir: string;
21
+
22
+ beforeEach(() => {
23
+ tempDir = createTempDir();
24
+ });
25
+
26
+ afterEach(() => {
27
+ removeTempDir(tempDir);
28
+ });
29
+
30
+ describe('Test 9.1: Overwrites .safeword files', () => {
31
+ it('should restore modified files to CLI version', async () => {
32
+ await createConfiguredProject(tempDir);
33
+
34
+ // Modify a safeword file
35
+ const originalContent = readTestFile(tempDir, '.safeword/SAFEWORD.md');
36
+ writeTestFile(tempDir, '.safeword/SAFEWORD.md', '# Modified content\n');
37
+
38
+ await runCli(['upgrade'], { cwd: tempDir });
39
+
40
+ const restoredContent = readTestFile(tempDir, '.safeword/SAFEWORD.md');
41
+ // Should be restored (not the modified content)
42
+ expect(restoredContent).not.toBe('# Modified content\n');
43
+ });
44
+
45
+ it('should update .safeword/version', async () => {
46
+ await createConfiguredProject(tempDir);
47
+
48
+ // Set an older version
49
+ writeTestFile(tempDir, '.safeword/version', '0.0.1');
50
+
51
+ await runCli(['upgrade'], { cwd: tempDir });
52
+
53
+ const version = readTestFile(tempDir, '.safeword/version').trim();
54
+ expect(version).not.toBe('0.0.1');
55
+ expect(version).toMatch(/^\d+\.\d+\.\d+/);
56
+ });
57
+ });
58
+
59
+ describe('Test 9.2: Updates skills', () => {
60
+ it('should restore modified skill files', async () => {
61
+ await createConfiguredProject(tempDir);
62
+
63
+ // Find and modify a skill file if it exists
64
+ if (fileExists(tempDir, '.claude/skills')) {
65
+ // The actual skill path depends on implementation
66
+ // This test structure is correct for when skills are implemented
67
+ }
68
+
69
+ const result = await runCli(['upgrade'], { cwd: tempDir });
70
+ expect(result.exitCode).toBe(0);
71
+ });
72
+ });
73
+
74
+ describe('Test 9.3: Preserves non-safeword hooks', () => {
75
+ it('should preserve custom hooks', async () => {
76
+ await createConfiguredProject(tempDir);
77
+
78
+ // Add a custom hook
79
+ const settings = JSON.parse(readTestFile(tempDir, '.claude/settings.json'));
80
+ settings.hooks = settings.hooks || {};
81
+ settings.hooks.SessionStart = settings.hooks.SessionStart || [];
82
+ settings.hooks.SessionStart.push({
83
+ command: 'echo "My custom hook"',
84
+ description: 'Custom hook',
85
+ });
86
+ writeTestFile(tempDir, '.claude/settings.json', JSON.stringify(settings, null, 2));
87
+
88
+ await runCli(['upgrade'], { cwd: tempDir });
89
+
90
+ const updatedSettings = JSON.parse(readTestFile(tempDir, '.claude/settings.json'));
91
+ const hasCustomHook = updatedSettings.hooks.SessionStart.some(
92
+ (hook: { command?: string }) => hook.command === 'echo "My custom hook"',
93
+ );
94
+
95
+ expect(hasCustomHook).toBe(true);
96
+ });
97
+ });
98
+
99
+ describe('Test 9.4: Same-version reinstalls', () => {
100
+ it('should restore files even at same version', async () => {
101
+ await createConfiguredProject(tempDir);
102
+
103
+ // Get current version
104
+ const version = readTestFile(tempDir, '.safeword/version').trim();
105
+
106
+ // Modify a file
107
+ writeTestFile(tempDir, '.safeword/SAFEWORD.md', '# Corrupted\n');
108
+
109
+ await runCli(['upgrade'], { cwd: tempDir });
110
+
111
+ // File should be restored
112
+ const content = readTestFile(tempDir, '.safeword/SAFEWORD.md');
113
+ expect(content).not.toBe('# Corrupted\n');
114
+
115
+ // Version should remain same
116
+ const newVersion = readTestFile(tempDir, '.safeword/version').trim();
117
+ expect(newVersion).toBe(version);
118
+ });
119
+ });
120
+
121
+ describe('Test 9.5: Refuses to downgrade', () => {
122
+ it('should error when project is newer than CLI', async () => {
123
+ await createConfiguredProject(tempDir);
124
+
125
+ // Set a future version
126
+ writeTestFile(tempDir, '.safeword/version', '99.99.99');
127
+
128
+ const result = await runCli(['upgrade'], { cwd: tempDir });
129
+
130
+ expect(result.exitCode).toBe(1);
131
+ expect(result.stderr.toLowerCase()).toMatch(/older|downgrade|cli|update/i);
132
+ });
133
+ });
134
+
135
+ describe('Test 9.6: Unconfigured project error', () => {
136
+ it('should error on unconfigured project', async () => {
137
+ createTypeScriptPackageJson(tempDir);
138
+ // No setup
139
+
140
+ const result = await runCli(['upgrade'], { cwd: tempDir });
141
+
142
+ expect(result.exitCode).toBe(1);
143
+ expect(result.stderr.toLowerCase()).toContain('not configured');
144
+ expect(result.stderr.toLowerCase()).toContain('setup');
145
+ });
146
+ });
147
+
148
+ describe('Test 9.7: Prints summary of changes', () => {
149
+ it('should show what changed', async () => {
150
+ await createConfiguredProject(tempDir);
151
+
152
+ // Modify to create changes
153
+ writeTestFile(tempDir, '.safeword/version', '0.0.1');
154
+
155
+ const result = await runCli(['upgrade'], { cwd: tempDir });
156
+
157
+ expect(result.exitCode).toBe(0);
158
+ // Should show some summary of changes
159
+ expect(result.stdout.toLowerCase()).toMatch(/upgrad|update|version|file/i);
160
+ });
161
+ });
162
+
163
+ describe('Test 9.8: Preserves learnings directory', () => {
164
+ it('should preserve user learnings on upgrade', async () => {
165
+ await createConfiguredProject(tempDir);
166
+
167
+ // Create user learning file
168
+ writeTestFile(
169
+ tempDir,
170
+ '.safeword/learnings/my-custom-learning.md',
171
+ '# My Learning\n\nImportant discovery about the codebase.',
172
+ );
173
+
174
+ // Modify version to trigger upgrade
175
+ writeTestFile(tempDir, '.safeword/version', '0.0.1');
176
+
177
+ await runCli(['upgrade'], { cwd: tempDir });
178
+
179
+ // User learning should be preserved
180
+ expect(fileExists(tempDir, '.safeword/learnings/my-custom-learning.md')).toBe(true);
181
+
182
+ const content = readTestFile(tempDir, '.safeword/learnings/my-custom-learning.md');
183
+ expect(content).toContain('My Learning');
184
+ expect(content).toContain('Important discovery');
185
+ });
186
+ });
187
+
188
+ describe('Test 9.9: Creates backup before upgrade', () => {
189
+ it('should create .safeword.backup directory', async () => {
190
+ await createConfiguredProject(tempDir);
191
+
192
+ // Modify version to trigger upgrade
193
+ writeTestFile(tempDir, '.safeword/version', '0.0.1');
194
+
195
+ // Check for backup during upgrade (it may be deleted after success)
196
+ // We verify by checking that upgrade succeeds without data loss
197
+ const originalSafeword = readTestFile(tempDir, '.safeword/SAFEWORD.md');
198
+
199
+ await runCli(['upgrade'], { cwd: tempDir });
200
+
201
+ // After successful upgrade, backup should be deleted
202
+ expect(fileExists(tempDir, '.safeword.backup')).toBe(false);
203
+
204
+ // Files should still exist
205
+ expect(fileExists(tempDir, '.safeword/SAFEWORD.md')).toBe(true);
206
+ });
207
+ });
208
+
209
+ describe('Test 9.10: Restores backup on failure', () => {
210
+ it.skip('should restore from backup if upgrade fails mid-way', async () => {
211
+ // This test would require a way to force upgrade failure
212
+ // Skipped as it requires mocking internal failures
213
+ });
214
+ });
215
+ });