@sk8metal/michi-cli 0.5.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (284) hide show
  1. package/CHANGELOG.md +136 -1
  2. package/README.md +2 -1
  3. package/dist/scripts/config/config-schema.d.ts +35 -0
  4. package/dist/scripts/config/config-schema.d.ts.map +1 -1
  5. package/dist/scripts/config/config-schema.js +56 -0
  6. package/dist/scripts/config/config-schema.js.map +1 -1
  7. package/dist/scripts/confluence-sync.d.ts.map +1 -1
  8. package/dist/scripts/confluence-sync.js +15 -2
  9. package/dist/scripts/confluence-sync.js.map +1 -1
  10. package/dist/scripts/github-actions-client.d.ts +79 -0
  11. package/dist/scripts/github-actions-client.d.ts.map +1 -0
  12. package/dist/scripts/github-actions-client.js +182 -0
  13. package/dist/scripts/github-actions-client.js.map +1 -0
  14. package/dist/scripts/health-check-service.d.ts +46 -0
  15. package/dist/scripts/health-check-service.d.ts.map +1 -0
  16. package/dist/scripts/health-check-service.js +114 -0
  17. package/dist/scripts/health-check-service.js.map +1 -0
  18. package/dist/scripts/markdown-to-confluence.d.ts.map +1 -1
  19. package/dist/scripts/markdown-to-confluence.js +25 -3
  20. package/dist/scripts/markdown-to-confluence.js.map +1 -1
  21. package/dist/scripts/mermaid-converter.d.ts +24 -0
  22. package/dist/scripts/mermaid-converter.d.ts.map +1 -0
  23. package/dist/scripts/mermaid-converter.js +49 -0
  24. package/dist/scripts/mermaid-converter.js.map +1 -0
  25. package/dist/scripts/template/multi-repo-renderer.d.ts +67 -0
  26. package/dist/scripts/template/multi-repo-renderer.d.ts.map +1 -0
  27. package/dist/scripts/template/multi-repo-renderer.js +123 -0
  28. package/dist/scripts/template/multi-repo-renderer.js.map +1 -0
  29. package/dist/scripts/template/renderer.d.ts +4 -0
  30. package/dist/scripts/template/renderer.d.ts.map +1 -1
  31. package/dist/scripts/template/renderer.js.map +1 -1
  32. package/dist/scripts/test-execution-generator.d.ts.map +1 -1
  33. package/dist/scripts/test-execution-generator.js +94 -11
  34. package/dist/scripts/test-execution-generator.js.map +1 -1
  35. package/dist/scripts/test-script-runner.d.ts +33 -0
  36. package/dist/scripts/test-script-runner.d.ts.map +1 -0
  37. package/dist/scripts/test-script-runner.js +77 -0
  38. package/dist/scripts/test-script-runner.js.map +1 -0
  39. package/dist/scripts/utils/config-loader.d.ts +21 -1
  40. package/dist/scripts/utils/config-loader.d.ts.map +1 -1
  41. package/dist/scripts/utils/config-loader.js +149 -3
  42. package/dist/scripts/utils/config-loader.js.map +1 -1
  43. package/dist/scripts/utils/multi-repo-validator.d.ts +30 -0
  44. package/dist/scripts/utils/multi-repo-validator.d.ts.map +1 -0
  45. package/dist/scripts/utils/multi-repo-validator.js +105 -0
  46. package/dist/scripts/utils/multi-repo-validator.js.map +1 -0
  47. package/dist/scripts/utils/spec-archiver.d.ts +38 -0
  48. package/dist/scripts/utils/spec-archiver.d.ts.map +1 -0
  49. package/dist/scripts/utils/spec-archiver.js +210 -0
  50. package/dist/scripts/utils/spec-archiver.js.map +1 -0
  51. package/dist/scripts/utils/spec-updater.d.ts +4 -0
  52. package/dist/scripts/utils/spec-updater.d.ts.map +1 -1
  53. package/dist/scripts/utils/spec-updater.js.map +1 -1
  54. package/dist/src/cli.d.ts.map +1 -1
  55. package/dist/src/cli.js +262 -14
  56. package/dist/src/cli.js.map +1 -1
  57. package/dist/src/commands/multi-repo-add-repo.d.ts +26 -0
  58. package/dist/src/commands/multi-repo-add-repo.d.ts.map +1 -0
  59. package/dist/src/commands/multi-repo-add-repo.js +56 -0
  60. package/dist/src/commands/multi-repo-add-repo.js.map +1 -0
  61. package/dist/src/commands/multi-repo-ci-status.d.ts +46 -0
  62. package/dist/src/commands/multi-repo-ci-status.d.ts.map +1 -0
  63. package/dist/src/commands/multi-repo-ci-status.js +285 -0
  64. package/dist/src/commands/multi-repo-ci-status.js.map +1 -0
  65. package/dist/src/commands/multi-repo-confluence-sync.d.ts +45 -0
  66. package/dist/src/commands/multi-repo-confluence-sync.d.ts.map +1 -0
  67. package/dist/src/commands/multi-repo-confluence-sync.js +135 -0
  68. package/dist/src/commands/multi-repo-confluence-sync.js.map +1 -0
  69. package/dist/src/commands/multi-repo-init.d.ts +26 -0
  70. package/dist/src/commands/multi-repo-init.d.ts.map +1 -0
  71. package/dist/src/commands/multi-repo-init.js +101 -0
  72. package/dist/src/commands/multi-repo-init.js.map +1 -0
  73. package/dist/src/commands/multi-repo-list.d.ts +28 -0
  74. package/dist/src/commands/multi-repo-list.d.ts.map +1 -0
  75. package/dist/src/commands/multi-repo-list.js +38 -0
  76. package/dist/src/commands/multi-repo-list.js.map +1 -0
  77. package/dist/src/commands/multi-repo-test.d.ts +56 -0
  78. package/dist/src/commands/multi-repo-test.d.ts.map +1 -0
  79. package/dist/src/commands/multi-repo-test.js +70 -0
  80. package/dist/src/commands/multi-repo-test.js.map +1 -0
  81. package/dist/src/commands/setup-existing.d.ts.map +1 -1
  82. package/dist/src/commands/setup-existing.js +30 -8
  83. package/dist/src/commands/setup-existing.js.map +1 -1
  84. package/dist/src/commands/spec-archive.d.ts +17 -0
  85. package/dist/src/commands/spec-archive.d.ts.map +1 -0
  86. package/dist/src/commands/spec-archive.js +40 -0
  87. package/dist/src/commands/spec-archive.js.map +1 -0
  88. package/dist/src/commands/spec-list.d.ts +15 -0
  89. package/dist/src/commands/spec-list.d.ts.map +1 -0
  90. package/dist/src/commands/spec-list.js +55 -0
  91. package/dist/src/commands/spec-list.js.map +1 -0
  92. package/docs/user-guide/guides/multi-repo-guide.md +591 -0
  93. package/docs/user-guide/guides/multi-repo-migration-guide.md +516 -0
  94. package/docs/user-guide/reference/multi-repo-api.md +771 -0
  95. package/docs/user-guide/reference/quick-reference.md +22 -37
  96. package/package.json +1 -4
  97. package/scripts/__tests__/config-loader-multi-repo.test.ts +342 -0
  98. package/scripts/__tests__/github-actions-client.test.ts +543 -0
  99. package/scripts/__tests__/health-check-service.test.ts +142 -0
  100. package/scripts/__tests__/markdown-to-confluence.test.ts +262 -0
  101. package/scripts/__tests__/mermaid-converter.test.ts +236 -0
  102. package/scripts/__tests__/multi-repo-config-schema.test.ts +335 -0
  103. package/scripts/__tests__/multi-repo-validator.test.ts +524 -0
  104. package/scripts/__tests__/spec-archiver.test.ts +512 -0
  105. package/scripts/__tests__/test-script-runner.test.ts +217 -0
  106. package/scripts/config/config-schema.ts +64 -0
  107. package/scripts/confluence-sync.ts +16 -2
  108. package/scripts/github-actions-client.ts +258 -0
  109. package/scripts/health-check-service.ts +171 -0
  110. package/scripts/markdown-to-confluence.ts +37 -6
  111. package/scripts/mermaid-converter.ts +56 -0
  112. package/scripts/template/__tests__/multi-repo-renderer.test.ts +261 -0
  113. package/scripts/template/multi-repo-renderer.ts +172 -0
  114. package/scripts/template/renderer.ts +5 -0
  115. package/scripts/test-execution-generator.ts +104 -11
  116. package/scripts/test-script-runner.ts +130 -0
  117. package/scripts/utils/__tests__/config-validator.test.ts +104 -6
  118. package/scripts/utils/__tests__/multi-repo-validator.test.ts +335 -0
  119. package/scripts/utils/config-loader.ts +197 -3
  120. package/scripts/utils/multi-repo-validator.ts +141 -0
  121. package/scripts/utils/spec-archiver.ts +260 -0
  122. package/scripts/utils/spec-updater.ts +4 -0
  123. package/templates/claude/agents/pr-size-monitor/AGENT.md +330 -0
  124. package/templates/claude/commands/michi/spec-impl.md +208 -35
  125. package/templates/claude-agent/agents/doc-reviewer.md +176 -0
  126. package/templates/claude-agent/rules/doc-review-rules.md +98 -0
  127. package/templates/claude-agent/rules/doc-review.md +91 -0
  128. package/templates/multi-repo/docs/ci-status.md +51 -0
  129. package/templates/multi-repo/docs/release-notes.md +99 -0
  130. package/templates/multi-repo/overview/architecture.md +102 -0
  131. package/templates/multi-repo/overview/requirements.md +68 -0
  132. package/templates/multi-repo/overview/sequence.md +79 -0
  133. package/templates/multi-repo/steering/multi-repo.md +74 -0
  134. package/templates/multi-repo/tests/strategy.md +89 -0
  135. package/dist/scripts/__tests__/create-project.test.d.ts +0 -2
  136. package/dist/scripts/__tests__/create-project.test.d.ts.map +0 -1
  137. package/dist/scripts/__tests__/create-project.test.js +0 -243
  138. package/dist/scripts/__tests__/create-project.test.js.map +0 -1
  139. package/dist/scripts/__tests__/jira-transitions.test.d.ts +0 -5
  140. package/dist/scripts/__tests__/jira-transitions.test.d.ts.map +0 -1
  141. package/dist/scripts/__tests__/jira-transitions.test.js +0 -172
  142. package/dist/scripts/__tests__/jira-transitions.test.js.map +0 -1
  143. package/dist/scripts/__tests__/multi-project-estimate.test.d.ts +0 -2
  144. package/dist/scripts/__tests__/multi-project-estimate.test.d.ts.map +0 -1
  145. package/dist/scripts/__tests__/multi-project-estimate.test.js +0 -118
  146. package/dist/scripts/__tests__/multi-project-estimate.test.js.map +0 -1
  147. package/dist/scripts/__tests__/setup-existing-project.test.d.ts +0 -2
  148. package/dist/scripts/__tests__/setup-existing-project.test.d.ts.map +0 -1
  149. package/dist/scripts/__tests__/setup-existing-project.test.js +0 -208
  150. package/dist/scripts/__tests__/setup-existing-project.test.js.map +0 -1
  151. package/dist/scripts/__tests__/setup-interactive.test.d.ts +0 -2
  152. package/dist/scripts/__tests__/setup-interactive.test.d.ts.map +0 -1
  153. package/dist/scripts/__tests__/setup-interactive.test.js +0 -166
  154. package/dist/scripts/__tests__/setup-interactive.test.js.map +0 -1
  155. package/dist/scripts/__tests__/spec-impl-workflow.test.d.ts +0 -5
  156. package/dist/scripts/__tests__/spec-impl-workflow.test.d.ts.map +0 -1
  157. package/dist/scripts/__tests__/spec-impl-workflow.test.js +0 -323
  158. package/dist/scripts/__tests__/spec-impl-workflow.test.js.map +0 -1
  159. package/dist/scripts/__tests__/spec-loader.test.d.ts +0 -5
  160. package/dist/scripts/__tests__/spec-loader.test.d.ts.map +0 -1
  161. package/dist/scripts/__tests__/spec-loader.test.js +0 -153
  162. package/dist/scripts/__tests__/spec-loader.test.js.map +0 -1
  163. package/dist/scripts/__tests__/validate-phase.test.d.ts +0 -5
  164. package/dist/scripts/__tests__/validate-phase.test.d.ts.map +0 -1
  165. package/dist/scripts/__tests__/validate-phase.test.js +0 -249
  166. package/dist/scripts/__tests__/validate-phase.test.js.map +0 -1
  167. package/dist/scripts/constants/__tests__/environments.test.d.ts +0 -2
  168. package/dist/scripts/constants/__tests__/environments.test.d.ts.map +0 -1
  169. package/dist/scripts/constants/__tests__/environments.test.js +0 -125
  170. package/dist/scripts/constants/__tests__/environments.test.js.map +0 -1
  171. package/dist/scripts/constants/__tests__/languages.test.d.ts +0 -2
  172. package/dist/scripts/constants/__tests__/languages.test.d.ts.map +0 -1
  173. package/dist/scripts/constants/__tests__/languages.test.js +0 -82
  174. package/dist/scripts/constants/__tests__/languages.test.js.map +0 -1
  175. package/dist/scripts/create-project.d.ts +0 -16
  176. package/dist/scripts/create-project.d.ts.map +0 -1
  177. package/dist/scripts/create-project.js +0 -334
  178. package/dist/scripts/create-project.js.map +0 -1
  179. package/dist/scripts/list-projects.d.ts +0 -7
  180. package/dist/scripts/list-projects.d.ts.map +0 -1
  181. package/dist/scripts/list-projects.js +0 -88
  182. package/dist/scripts/list-projects.js.map +0 -1
  183. package/dist/scripts/template/__tests__/renderer.test.d.ts +0 -2
  184. package/dist/scripts/template/__tests__/renderer.test.d.ts.map +0 -1
  185. package/dist/scripts/template/__tests__/renderer.test.js +0 -165
  186. package/dist/scripts/template/__tests__/renderer.test.js.map +0 -1
  187. package/dist/scripts/utils/__tests__/aidlc-parser.test.d.ts +0 -5
  188. package/dist/scripts/utils/__tests__/aidlc-parser.test.d.ts.map +0 -1
  189. package/dist/scripts/utils/__tests__/aidlc-parser.test.js +0 -315
  190. package/dist/scripts/utils/__tests__/aidlc-parser.test.js.map +0 -1
  191. package/dist/scripts/utils/__tests__/business-days.test.d.ts +0 -5
  192. package/dist/scripts/utils/__tests__/business-days.test.d.ts.map +0 -1
  193. package/dist/scripts/utils/__tests__/business-days.test.js +0 -171
  194. package/dist/scripts/utils/__tests__/business-days.test.js.map +0 -1
  195. package/dist/scripts/utils/__tests__/config-loader.test.d.ts +0 -5
  196. package/dist/scripts/utils/__tests__/config-loader.test.d.ts.map +0 -1
  197. package/dist/scripts/utils/__tests__/config-loader.test.js +0 -314
  198. package/dist/scripts/utils/__tests__/config-loader.test.js.map +0 -1
  199. package/dist/scripts/utils/__tests__/config-validator.test.d.ts +0 -5
  200. package/dist/scripts/utils/__tests__/config-validator.test.d.ts.map +0 -1
  201. package/dist/scripts/utils/__tests__/config-validator.test.js +0 -396
  202. package/dist/scripts/utils/__tests__/config-validator.test.js.map +0 -1
  203. package/dist/scripts/utils/__tests__/env-config.test.d.ts +0 -5
  204. package/dist/scripts/utils/__tests__/env-config.test.d.ts.map +0 -1
  205. package/dist/scripts/utils/__tests__/env-config.test.js +0 -216
  206. package/dist/scripts/utils/__tests__/env-config.test.js.map +0 -1
  207. package/dist/scripts/utils/__tests__/feature-name-validator.test.d.ts +0 -5
  208. package/dist/scripts/utils/__tests__/feature-name-validator.test.d.ts.map +0 -1
  209. package/dist/scripts/utils/__tests__/feature-name-validator.test.js +0 -106
  210. package/dist/scripts/utils/__tests__/feature-name-validator.test.js.map +0 -1
  211. package/dist/scripts/utils/__tests__/jira-issue-type-fetcher.test.d.ts +0 -5
  212. package/dist/scripts/utils/__tests__/jira-issue-type-fetcher.test.d.ts.map +0 -1
  213. package/dist/scripts/utils/__tests__/jira-issue-type-fetcher.test.js +0 -202
  214. package/dist/scripts/utils/__tests__/jira-issue-type-fetcher.test.js.map +0 -1
  215. package/dist/scripts/utils/__tests__/project-meta.test.d.ts +0 -6
  216. package/dist/scripts/utils/__tests__/project-meta.test.d.ts.map +0 -1
  217. package/dist/scripts/utils/__tests__/project-meta.test.js +0 -154
  218. package/dist/scripts/utils/__tests__/project-meta.test.js.map +0 -1
  219. package/dist/scripts/utils/__tests__/security-validator.test.d.ts +0 -6
  220. package/dist/scripts/utils/__tests__/security-validator.test.d.ts.map +0 -1
  221. package/dist/scripts/utils/__tests__/security-validator.test.js +0 -219
  222. package/dist/scripts/utils/__tests__/security-validator.test.js.map +0 -1
  223. package/dist/scripts/utils/__tests__/spec-updater.test.d.ts +0 -5
  224. package/dist/scripts/utils/__tests__/spec-updater.test.d.ts.map +0 -1
  225. package/dist/scripts/utils/__tests__/spec-updater.test.js +0 -158
  226. package/dist/scripts/utils/__tests__/spec-updater.test.js.map +0 -1
  227. package/dist/scripts/utils/__tests__/tasks-converter.test.d.ts +0 -5
  228. package/dist/scripts/utils/__tests__/tasks-converter.test.d.ts.map +0 -1
  229. package/dist/scripts/utils/__tests__/tasks-converter.test.js +0 -500
  230. package/dist/scripts/utils/__tests__/tasks-converter.test.js.map +0 -1
  231. package/dist/scripts/utils/__tests__/tasks-format-validator.test.d.ts +0 -5
  232. package/dist/scripts/utils/__tests__/tasks-format-validator.test.d.ts.map +0 -1
  233. package/dist/scripts/utils/__tests__/tasks-format-validator.test.js +0 -314
  234. package/dist/scripts/utils/__tests__/tasks-format-validator.test.js.map +0 -1
  235. package/dist/scripts/utils/__tests__/test-runner.test.d.ts +0 -5
  236. package/dist/scripts/utils/__tests__/test-runner.test.d.ts.map +0 -1
  237. package/dist/scripts/utils/__tests__/test-runner.test.js +0 -64
  238. package/dist/scripts/utils/__tests__/test-runner.test.js.map +0 -1
  239. package/dist/src/__tests__/cli.test.d.ts +0 -5
  240. package/dist/src/__tests__/cli.test.d.ts.map +0 -1
  241. package/dist/src/__tests__/cli.test.js +0 -58
  242. package/dist/src/__tests__/cli.test.js.map +0 -1
  243. package/dist/src/__tests__/integration/internationalization.test.d.ts +0 -8
  244. package/dist/src/__tests__/integration/internationalization.test.d.ts.map +0 -1
  245. package/dist/src/__tests__/integration/internationalization.test.js +0 -333
  246. package/dist/src/__tests__/integration/internationalization.test.js.map +0 -1
  247. package/dist/src/__tests__/integration/setup/claude-agent.test.d.ts +0 -5
  248. package/dist/src/__tests__/integration/setup/claude-agent.test.d.ts.map +0 -1
  249. package/dist/src/__tests__/integration/setup/claude-agent.test.js +0 -122
  250. package/dist/src/__tests__/integration/setup/claude-agent.test.js.map +0 -1
  251. package/dist/src/__tests__/integration/setup/claude.test.d.ts +0 -5
  252. package/dist/src/__tests__/integration/setup/claude.test.d.ts.map +0 -1
  253. package/dist/src/__tests__/integration/setup/claude.test.js +0 -193
  254. package/dist/src/__tests__/integration/setup/claude.test.js.map +0 -1
  255. package/dist/src/__tests__/integration/setup/cursor.test.d.ts +0 -5
  256. package/dist/src/__tests__/integration/setup/cursor.test.d.ts.map +0 -1
  257. package/dist/src/__tests__/integration/setup/cursor.test.js +0 -166
  258. package/dist/src/__tests__/integration/setup/cursor.test.js.map +0 -1
  259. package/dist/src/__tests__/integration/setup/helpers/fs-assertions.d.ts +0 -32
  260. package/dist/src/__tests__/integration/setup/helpers/fs-assertions.d.ts.map +0 -1
  261. package/dist/src/__tests__/integration/setup/helpers/fs-assertions.js +0 -72
  262. package/dist/src/__tests__/integration/setup/helpers/fs-assertions.js.map +0 -1
  263. package/dist/src/__tests__/integration/setup/helpers/test-project.d.ts +0 -38
  264. package/dist/src/__tests__/integration/setup/helpers/test-project.d.ts.map +0 -1
  265. package/dist/src/__tests__/integration/setup/helpers/test-project.js +0 -83
  266. package/dist/src/__tests__/integration/setup/helpers/test-project.js.map +0 -1
  267. package/dist/src/__tests__/integration/setup/init.test.d.ts +0 -5
  268. package/dist/src/__tests__/integration/setup/init.test.d.ts.map +0 -1
  269. package/dist/src/__tests__/integration/setup/init.test.js +0 -352
  270. package/dist/src/__tests__/integration/setup/init.test.js.map +0 -1
  271. package/dist/src/__tests__/integration/setup/validation.test.d.ts +0 -5
  272. package/dist/src/__tests__/integration/setup/validation.test.d.ts.map +0 -1
  273. package/dist/src/__tests__/integration/setup/validation.test.js +0 -301
  274. package/dist/src/__tests__/integration/setup/validation.test.js.map +0 -1
  275. package/dist/src/commands/__tests__/init.test.d.ts +0 -5
  276. package/dist/src/commands/__tests__/init.test.d.ts.map +0 -1
  277. package/dist/src/commands/__tests__/init.test.js +0 -255
  278. package/dist/src/commands/__tests__/init.test.js.map +0 -1
  279. package/dist/src/commands/__tests__/migrate.test.d.ts +0 -5
  280. package/dist/src/commands/__tests__/migrate.test.d.ts.map +0 -1
  281. package/dist/src/commands/__tests__/migrate.test.js +0 -216
  282. package/dist/src/commands/__tests__/migrate.test.js.map +0 -1
  283. package/scripts/create-project.ts +0 -386
  284. package/scripts/list-projects.ts +0 -112
@@ -0,0 +1,512 @@
1
+ /**
2
+ * spec-archiver.ts のテスト
3
+ */
4
+
5
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
6
+ import { existsSync, renameSync, mkdirSync, readdirSync, readFileSync, writeFileSync, type Dirent } from 'fs';
7
+
8
+ // fs モジュールをモック
9
+ vi.mock('fs', () => ({
10
+ existsSync: vi.fn(),
11
+ renameSync: vi.fn(),
12
+ mkdirSync: vi.fn(),
13
+ readdirSync: vi.fn(),
14
+ readFileSync: vi.fn(),
15
+ writeFileSync: vi.fn(),
16
+ }));
17
+
18
+ import {
19
+ canArchiveSpec,
20
+ archiveSpec,
21
+ listSpecs,
22
+ type SpecInfo,
23
+ } from '../utils/spec-archiver.js';
24
+
25
+ const mockExistsSync = vi.mocked(existsSync);
26
+ const mockRenameSync = vi.mocked(renameSync);
27
+ const mockMkdirSync = vi.mocked(mkdirSync);
28
+ const mockReaddirSync = vi.mocked(readdirSync);
29
+ const mockReadFileSync = vi.mocked(readFileSync);
30
+ const mockWriteFileSync = vi.mocked(writeFileSync);
31
+
32
+ describe('spec-archiver', () => {
33
+ beforeEach(() => {
34
+ vi.clearAllMocks();
35
+ });
36
+
37
+ afterEach(() => {
38
+ vi.resetAllMocks();
39
+ });
40
+
41
+ describe('canArchiveSpec - セキュリティ検証', () => {
42
+ it('パストラバーサル攻撃を防ぐ: ".." を含む場合', () => {
43
+ const result = canArchiveSpec('../../../etc/passwd');
44
+
45
+ expect(result.canArchive).toBe(false);
46
+ expect(result.reason).toContain('path traversal detected');
47
+ });
48
+
49
+ it('パストラバーサル攻撃を防ぐ: "/" を含む場合', () => {
50
+ const result = canArchiveSpec('foo/bar');
51
+
52
+ expect(result.canArchive).toBe(false);
53
+ expect(result.reason).toContain('path traversal detected');
54
+ });
55
+
56
+ it('パストラバーサル攻撃を防ぐ: "\\" を含む場合', () => {
57
+ const result = canArchiveSpec('foo\\bar');
58
+
59
+ expect(result.canArchive).toBe(false);
60
+ expect(result.reason).toContain('path traversal detected');
61
+ });
62
+
63
+ it('パストラバーサル攻撃を防ぐ: "." で始まる場合', () => {
64
+ const result = canArchiveSpec('.hidden');
65
+
66
+ expect(result.canArchive).toBe(false);
67
+ expect(result.reason).toContain('path traversal detected');
68
+ });
69
+
70
+ it('不正な文字を含む場合', () => {
71
+ const result = canArchiveSpec('feature@123');
72
+
73
+ expect(result.canArchive).toBe(false);
74
+ expect(result.reason).toContain('feature名が無効な形式です');
75
+ });
76
+
77
+ it('空文字の場合', () => {
78
+ const result = canArchiveSpec('');
79
+
80
+ expect(result.canArchive).toBe(false);
81
+ expect(result.reason).toContain('cannot be empty');
82
+ });
83
+
84
+ it('スペースのみの場合', () => {
85
+ const result = canArchiveSpec(' ');
86
+
87
+ expect(result.canArchive).toBe(false);
88
+ expect(result.reason).toContain('cannot be empty');
89
+ });
90
+
91
+ it('正常な feature name: 英数字とハイフン', () => {
92
+ mockExistsSync.mockReturnValue(true);
93
+ mockReadFileSync.mockReturnValue(
94
+ JSON.stringify({
95
+ phase: 'implementation-complete',
96
+ }),
97
+ );
98
+ mockReaddirSync.mockReturnValue(['release-notes-v1.md'] as unknown as Dirent[]);
99
+
100
+ const result = canArchiveSpec('user-auth-123');
101
+
102
+ expect(result.canArchive).toBe(true);
103
+ expect(mockExistsSync).toHaveBeenCalled();
104
+ });
105
+
106
+ it('kebab-caseのみ許可: アンダースコアは不可', () => {
107
+ const result = canArchiveSpec('user_auth');
108
+
109
+ expect(result.canArchive).toBe(false);
110
+ expect(result.reason).toContain('アンダースコア(_)が含まれています');
111
+ });
112
+ });
113
+
114
+ describe('canArchiveSpec - 通常機能', () => {
115
+ it('spec が存在しない場合', () => {
116
+ mockExistsSync.mockReturnValue(false);
117
+
118
+ const result = canArchiveSpec('non-existent');
119
+
120
+ expect(result.canArchive).toBe(false);
121
+ expect(result.reason).toBe('Spec not found');
122
+ });
123
+
124
+ it('spec.json が存在しない場合', () => {
125
+ mockExistsSync
126
+ .mockReturnValueOnce(true) // specDir exists
127
+ .mockReturnValueOnce(false); // specJsonPath does not exist
128
+
129
+ const result = canArchiveSpec('feature-a');
130
+
131
+ expect(result.canArchive).toBe(false);
132
+ expect(result.reason).toBe('spec.json not found');
133
+ });
134
+
135
+ it('既にアーカイブ済みの場合', () => {
136
+ mockExistsSync.mockReturnValue(true);
137
+ mockReadFileSync.mockReturnValue(
138
+ JSON.stringify({
139
+ phase: 'implementation-complete',
140
+ archived: { at: '2024-01-01T00:00:00Z' },
141
+ }),
142
+ );
143
+
144
+ const result = canArchiveSpec('feature-b');
145
+
146
+ expect(result.canArchive).toBe(false);
147
+ expect(result.reason).toBe('Already archived');
148
+ });
149
+
150
+ it('phase が implementation-complete でない場合', () => {
151
+ mockExistsSync.mockReturnValue(true);
152
+ mockReadFileSync.mockReturnValue(
153
+ JSON.stringify({
154
+ phase: 'requirements',
155
+ }),
156
+ );
157
+
158
+ const result = canArchiveSpec('feature-c');
159
+
160
+ expect(result.canArchive).toBe(false);
161
+ expect(result.reason).toContain('Phase is requirements');
162
+ });
163
+
164
+ it('release-notes がない場合', () => {
165
+ mockExistsSync.mockReturnValue(true);
166
+ mockReadFileSync.mockReturnValue(
167
+ JSON.stringify({
168
+ phase: 'implementation-complete',
169
+ }),
170
+ );
171
+ mockReaddirSync.mockReturnValue(['design.md', 'tasks.md'] as unknown as Dirent[]);
172
+
173
+ const result = canArchiveSpec('feature-d');
174
+
175
+ expect(result.canArchive).toBe(false);
176
+ expect(result.reason).toBe('No release notes found');
177
+ });
178
+
179
+ it('アーカイブ可能な場合', () => {
180
+ mockExistsSync.mockReturnValue(true);
181
+ mockReadFileSync.mockReturnValue(
182
+ JSON.stringify({
183
+ phase: 'implementation-complete',
184
+ }),
185
+ );
186
+ mockReaddirSync.mockReturnValue([
187
+ 'design.md',
188
+ 'release-notes-v1.0.0.md',
189
+ ] as unknown as Dirent[]);
190
+
191
+ const result = canArchiveSpec('feature-e');
192
+
193
+ expect(result.canArchive).toBe(true);
194
+ expect(result.reason).toBeUndefined();
195
+ });
196
+
197
+ it('spec.json の読み取りエラー', () => {
198
+ mockExistsSync.mockReturnValue(true);
199
+ mockReadFileSync.mockImplementation(() => {
200
+ throw new Error('Permission denied');
201
+ });
202
+
203
+ const result = canArchiveSpec('feature-f');
204
+
205
+ expect(result.canArchive).toBe(false);
206
+ expect(result.reason).toContain('Error reading spec.json');
207
+ });
208
+ });
209
+
210
+ describe('archiveSpec', () => {
211
+ it('不正な feature name の場合はエラー', () => {
212
+ const result = archiveSpec('../../../etc/passwd');
213
+
214
+ expect(result.success).toBe(false);
215
+ expect(result.feature).toBe('../../../etc/passwd');
216
+ expect(result.error).toContain('path traversal detected');
217
+ expect(mockRenameSync).not.toHaveBeenCalled();
218
+ });
219
+
220
+ it('アーカイブ不可能な場合', () => {
221
+ mockExistsSync.mockReturnValue(false);
222
+
223
+ const result = archiveSpec('non-existent');
224
+
225
+ expect(result.success).toBe(false);
226
+ expect(result.feature).toBe('non-existent');
227
+ expect(result.error).toBe('Spec not found');
228
+ });
229
+
230
+ it('正常にアーカイブできる場合', () => {
231
+ mockExistsSync
232
+ .mockReturnValueOnce(true) // specDir exists
233
+ .mockReturnValueOnce(true) // specJsonPath exists
234
+ .mockReturnValueOnce(false); // archiveDir does not exist
235
+ mockReadFileSync
236
+ .mockReturnValueOnce(
237
+ JSON.stringify({
238
+ phase: 'implementation-complete',
239
+ }),
240
+ )
241
+ .mockReturnValueOnce(
242
+ JSON.stringify({
243
+ phase: 'implementation-complete',
244
+ }),
245
+ );
246
+ mockReaddirSync.mockReturnValue(['release-notes-v1.md'] as unknown as Dirent[]);
247
+
248
+ const result = archiveSpec('feature-g');
249
+
250
+ expect(result.success).toBe(true);
251
+ expect(result.feature).toBe('feature-g');
252
+ expect(result.archivePath).toBeDefined();
253
+ expect(mockMkdirSync).toHaveBeenCalled();
254
+ expect(mockRenameSync).toHaveBeenCalled();
255
+ expect(mockWriteFileSync).toHaveBeenCalled();
256
+ });
257
+
258
+ it('アーカイブ理由を指定できる', () => {
259
+ mockExistsSync
260
+ .mockReturnValueOnce(true)
261
+ .mockReturnValueOnce(true)
262
+ .mockReturnValueOnce(true);
263
+ mockReadFileSync
264
+ .mockReturnValueOnce(
265
+ JSON.stringify({
266
+ phase: 'implementation-complete',
267
+ }),
268
+ )
269
+ .mockReturnValueOnce(
270
+ JSON.stringify({
271
+ phase: 'implementation-complete',
272
+ }),
273
+ );
274
+ mockReaddirSync.mockReturnValue(['release-notes-v1.md'] as unknown as Dirent[]);
275
+
276
+ const result = archiveSpec('feature-h', { reason: 'Released to production' });
277
+
278
+ expect(result.success).toBe(true);
279
+ // writeFileSyncの呼び出しを検証
280
+ const writeCall = mockWriteFileSync.mock.calls[0];
281
+ const writtenContent = JSON.parse(writeCall[1] as string);
282
+ expect(writtenContent.archived.reason).toBe('Released to production');
283
+ });
284
+
285
+ it('ファイル移動エラーの場合', () => {
286
+ mockExistsSync
287
+ .mockReturnValueOnce(true)
288
+ .mockReturnValueOnce(true)
289
+ .mockReturnValueOnce(true);
290
+ mockReadFileSync.mockReturnValueOnce(
291
+ JSON.stringify({
292
+ phase: 'implementation-complete',
293
+ }),
294
+ );
295
+ mockReaddirSync.mockReturnValue(['release-notes-v1.md'] as unknown as Dirent[]);
296
+ mockRenameSync.mockImplementation(() => {
297
+ throw new Error('Permission denied');
298
+ });
299
+
300
+ const result = archiveSpec('feature-i');
301
+
302
+ expect(result.success).toBe(false);
303
+ expect(result.error).toContain('Failed to archive');
304
+ });
305
+
306
+ it('カスタム projectRoot を使用できる', () => {
307
+ mockExistsSync
308
+ .mockReturnValueOnce(true)
309
+ .mockReturnValueOnce(true)
310
+ .mockReturnValueOnce(true);
311
+ mockReadFileSync
312
+ .mockReturnValueOnce(
313
+ JSON.stringify({
314
+ phase: 'implementation-complete',
315
+ }),
316
+ )
317
+ .mockReturnValueOnce(
318
+ JSON.stringify({
319
+ phase: 'implementation-complete',
320
+ }),
321
+ );
322
+ mockReaddirSync.mockReturnValue(['release-notes-v1.md'] as unknown as Dirent[]);
323
+
324
+ archiveSpec('feature-j', undefined, '/custom/path');
325
+
326
+ expect(mockExistsSync).toHaveBeenCalled();
327
+ const firstCall = mockExistsSync.mock.calls[0][0] as string;
328
+ expect(firstCall).toContain('/custom/path');
329
+ });
330
+ });
331
+
332
+ describe('listSpecs', () => {
333
+ it('specs ディレクトリが存在しない場合は空配列', () => {
334
+ mockExistsSync.mockReturnValue(false);
335
+
336
+ const result = listSpecs();
337
+
338
+ expect(result).toEqual([]);
339
+ });
340
+
341
+ it('非アーカイブの仕様書を一覧取得', () => {
342
+ mockExistsSync.mockReturnValue(true);
343
+ mockReaddirSync
344
+ .mockReturnValueOnce([
345
+ { name: 'feature-a', isDirectory: () => true },
346
+ { name: 'feature-b', isDirectory: () => true },
347
+ { name: 'some-file.txt', isDirectory: () => false },
348
+ ] as unknown as Dirent[])
349
+ .mockReturnValueOnce(['design.md'] as unknown as Dirent[])
350
+ .mockReturnValueOnce(['release-notes-v1.md'] as unknown as Dirent[]);
351
+ mockReadFileSync
352
+ .mockReturnValueOnce(
353
+ JSON.stringify({
354
+ phase: 'requirements',
355
+ }),
356
+ )
357
+ .mockReturnValueOnce(
358
+ JSON.stringify({
359
+ phase: 'implementation-complete',
360
+ }),
361
+ );
362
+
363
+ const result = listSpecs();
364
+
365
+ expect(result).toHaveLength(2);
366
+ expect(result[0]).toEqual<SpecInfo>({
367
+ feature: 'feature-a',
368
+ phase: 'requirements',
369
+ archived: false,
370
+ hasReleaseNotes: false,
371
+ });
372
+ expect(result[1]).toEqual<SpecInfo>({
373
+ feature: 'feature-b',
374
+ phase: 'implementation-complete',
375
+ archived: false,
376
+ hasReleaseNotes: true,
377
+ });
378
+ });
379
+
380
+ it('不正なディレクトリ名をスキップ', () => {
381
+ mockExistsSync.mockReturnValue(true);
382
+ mockReaddirSync
383
+ .mockReturnValueOnce([
384
+ { name: 'valid-feature', isDirectory: () => true },
385
+ { name: '../../../etc', isDirectory: () => true },
386
+ { name: 'foo/bar', isDirectory: () => true },
387
+ ] as unknown as Dirent[])
388
+ .mockReturnValueOnce(['design.md'] as unknown as Dirent[]);
389
+ mockReadFileSync.mockReturnValueOnce(
390
+ JSON.stringify({
391
+ phase: 'requirements',
392
+ }),
393
+ );
394
+
395
+ const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
396
+
397
+ const result = listSpecs();
398
+
399
+ expect(result).toHaveLength(1);
400
+ expect(result[0].feature).toBe('valid-feature');
401
+ expect(consoleWarnSpy).toHaveBeenCalledTimes(2);
402
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
403
+ expect.stringContaining('Skipping invalid spec directory name'),
404
+ );
405
+
406
+ consoleWarnSpy.mockRestore();
407
+ });
408
+
409
+ it('アーカイブ済み仕様書も含めて取得', () => {
410
+ mockExistsSync.mockReturnValue(true);
411
+ mockReaddirSync
412
+ .mockReturnValueOnce([
413
+ { name: 'feature-a', isDirectory: () => true },
414
+ { name: 'archive', isDirectory: () => true },
415
+ ] as unknown as Dirent[])
416
+ .mockReturnValueOnce(['design.md'] as unknown as Dirent[])
417
+ .mockReturnValueOnce([
418
+ { name: 'archived-feature', isDirectory: () => true },
419
+ ] as unknown as Dirent[])
420
+ .mockReturnValueOnce(['release-notes-v1.md'] as unknown as Dirent[]);
421
+
422
+ mockReadFileSync
423
+ .mockReturnValueOnce(
424
+ JSON.stringify({
425
+ phase: 'requirements',
426
+ }),
427
+ )
428
+ .mockReturnValueOnce(
429
+ JSON.stringify({
430
+ phase: 'implementation-complete',
431
+ archived: { at: '2024-01-01T00:00:00Z' },
432
+ }),
433
+ );
434
+
435
+ const result = listSpecs({ includeArchived: true });
436
+
437
+ expect(result).toHaveLength(2);
438
+ expect(result[0].archived).toBe(false);
439
+ expect(result[1]).toMatchObject({
440
+ feature: 'archived-feature',
441
+ phase: 'implementation-complete',
442
+ archived: true,
443
+ archivedAt: '2024-01-01T00:00:00Z',
444
+ hasReleaseNotes: true,
445
+ });
446
+ });
447
+
448
+ it('アーカイブディレクトリ内の不正な名前をスキップ', () => {
449
+ mockExistsSync.mockReturnValue(true);
450
+ mockReaddirSync
451
+ .mockReturnValueOnce([
452
+ { name: 'archive', isDirectory: () => true },
453
+ ] as unknown as Dirent[])
454
+ .mockReturnValueOnce([
455
+ { name: 'valid-archived', isDirectory: () => true },
456
+ { name: '../../../etc', isDirectory: () => true },
457
+ ] as unknown as Dirent[]);
458
+
459
+ mockReadFileSync.mockReturnValueOnce(
460
+ JSON.stringify({
461
+ phase: 'implementation-complete',
462
+ archived: { at: '2024-01-01T00:00:00Z' },
463
+ }),
464
+ );
465
+ mockReaddirSync.mockReturnValueOnce(['release-notes-v1.md'] as unknown as Dirent[]);
466
+
467
+ const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
468
+
469
+ const result = listSpecs({ includeArchived: true });
470
+
471
+ expect(result).toHaveLength(1);
472
+ expect(result[0].feature).toBe('valid-archived');
473
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
474
+ expect.stringContaining('Skipping invalid archived directory name'),
475
+ );
476
+
477
+ consoleWarnSpy.mockRestore();
478
+ });
479
+
480
+ it('spec.json 読み取りエラーを無視', () => {
481
+ mockExistsSync.mockReturnValue(true);
482
+ mockReaddirSync.mockReturnValue([
483
+ { name: 'feature-error', isDirectory: () => true },
484
+ ] as unknown as Dirent[]);
485
+ mockReadFileSync.mockImplementation(() => {
486
+ throw new Error('Parse error');
487
+ });
488
+
489
+ const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
490
+
491
+ const result = listSpecs();
492
+
493
+ expect(result).toEqual([]);
494
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
495
+ expect.stringContaining('Failed to read spec'),
496
+ );
497
+
498
+ consoleWarnSpy.mockRestore();
499
+ });
500
+
501
+ it('カスタム projectRoot を使用', () => {
502
+ mockExistsSync.mockReturnValue(true);
503
+ mockReaddirSync.mockReturnValue([] as unknown as Dirent[]);
504
+
505
+ listSpecs(undefined, '/custom/path');
506
+
507
+ expect(mockExistsSync).toHaveBeenCalled();
508
+ const firstCall = mockExistsSync.mock.calls[0][0] as string;
509
+ expect(firstCall).toContain('/custom/path');
510
+ });
511
+ });
512
+ });