@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,335 @@
1
+ /**
2
+ * Multi-Repo バリデーション関数のテスト
3
+ * Task 1.2: バリデーション関数の実装
4
+ */
5
+
6
+ import { describe, it, expect } from 'vitest';
7
+ import {
8
+ validateProjectName,
9
+ validateJiraKey,
10
+ validateRepositoryUrl,
11
+ } from '../multi-repo-validator';
12
+
13
+ describe('validateProjectName', () => {
14
+ describe('正常ケース', () => {
15
+ it('有効なプロジェクト名を受け入れる', () => {
16
+ const validNames = [
17
+ 'my-project',
18
+ 'プロジェクト名',
19
+ 'project_v2',
20
+ 'My Project 123',
21
+ 'a',
22
+ 'a'.repeat(100),
23
+ ];
24
+
25
+ validNames.forEach((name) => {
26
+ const result = validateProjectName(name);
27
+ expect(result.isValid).toBe(true);
28
+ expect(result.errors).toHaveLength(0);
29
+ });
30
+ });
31
+ });
32
+
33
+ describe('パストラバーサル対策', () => {
34
+ it('スラッシュ(/)を含む場合はエラー', () => {
35
+ const result = validateProjectName('project/name');
36
+ expect(result.isValid).toBe(false);
37
+ expect(result.errors).toContain(
38
+ 'Project name must not contain path traversal characters (/, \\)',
39
+ );
40
+ });
41
+
42
+ it('バックスラッシュ(\\)を含む場合はエラー', () => {
43
+ const result = validateProjectName('project\\name');
44
+ expect(result.isValid).toBe(false);
45
+ expect(result.errors).toContain(
46
+ 'Project name must not contain path traversal characters (/, \\)',
47
+ );
48
+ });
49
+
50
+ it('../を含む場合はエラー', () => {
51
+ const result = validateProjectName('../etc/passwd');
52
+ expect(result.isValid).toBe(false);
53
+ expect(result.errors).toContain(
54
+ 'Project name must not contain path traversal characters (/, \\)',
55
+ );
56
+ });
57
+
58
+ it('/を含む場合はエラー', () => {
59
+ const result = validateProjectName('/etc/passwd');
60
+ expect(result.isValid).toBe(false);
61
+ expect(result.errors).toContain(
62
+ 'Project name must not contain path traversal characters (/, \\)',
63
+ );
64
+ });
65
+ });
66
+
67
+ describe('相対パス対策', () => {
68
+ it('ドット(.)単独の場合はエラー', () => {
69
+ const result = validateProjectName('.');
70
+ expect(result.isValid).toBe(false);
71
+ expect(result.errors).toContain(
72
+ 'Project name must not be relative path components (., ..)',
73
+ );
74
+ });
75
+
76
+ it('ダブルドット(..)単独の場合はエラー', () => {
77
+ const result = validateProjectName('..');
78
+ expect(result.isValid).toBe(false);
79
+ expect(result.errors).toContain(
80
+ 'Project name must not be relative path components (., ..)',
81
+ );
82
+ });
83
+ });
84
+
85
+ describe('制御文字対策', () => {
86
+ it('ヌル文字(\\x00)を含む場合はエラー', () => {
87
+ const result = validateProjectName('project\x00name');
88
+ expect(result.isValid).toBe(false);
89
+ expect(result.errors).toContain(
90
+ 'Project name must not contain control characters',
91
+ );
92
+ });
93
+
94
+ it('タブ文字(\\t)を含む場合はエラー', () => {
95
+ const result = validateProjectName('project\tname');
96
+ expect(result.isValid).toBe(false);
97
+ expect(result.errors).toContain(
98
+ 'Project name must not contain control characters',
99
+ );
100
+ });
101
+
102
+ it('改行文字(\\n)を含む場合はエラー', () => {
103
+ const result = validateProjectName('project\nname');
104
+ expect(result.isValid).toBe(false);
105
+ expect(result.errors).toContain(
106
+ 'Project name must not contain control characters',
107
+ );
108
+ });
109
+
110
+ it('改行文字(\\r)を含む場合はエラー', () => {
111
+ const result = validateProjectName('project\rname');
112
+ expect(result.isValid).toBe(false);
113
+ expect(result.errors).toContain(
114
+ 'Project name must not contain control characters',
115
+ );
116
+ });
117
+
118
+ it('エスケープ文字(\\x1B)を含む場合はエラー', () => {
119
+ const result = validateProjectName('project\x1Bname');
120
+ expect(result.isValid).toBe(false);
121
+ expect(result.errors).toContain(
122
+ 'Project name must not contain control characters',
123
+ );
124
+ });
125
+
126
+ it('削除文字(\\x7F)を含む場合はエラー', () => {
127
+ const result = validateProjectName('project\x7Fname');
128
+ expect(result.isValid).toBe(false);
129
+ expect(result.errors).toContain(
130
+ 'Project name must not contain control characters',
131
+ );
132
+ });
133
+ });
134
+
135
+ describe('長さチェック', () => {
136
+ it('101文字以上の場合はエラー', () => {
137
+ const longName = 'a'.repeat(101);
138
+ const result = validateProjectName(longName);
139
+ expect(result.isValid).toBe(false);
140
+ expect(result.errors).toContain(
141
+ 'Project name must be between 1 and 100 characters',
142
+ );
143
+ });
144
+
145
+ it('空文字列の場合はエラー', () => {
146
+ const result = validateProjectName('');
147
+ expect(result.isValid).toBe(false);
148
+ expect(result.errors).toContain(
149
+ 'Project name must be between 1 and 100 characters',
150
+ );
151
+ });
152
+
153
+ it('100文字の場合は正常', () => {
154
+ const name = 'a'.repeat(100);
155
+ const result = validateProjectName(name);
156
+ expect(result.isValid).toBe(true);
157
+ });
158
+
159
+ it('1文字の場合は正常', () => {
160
+ const result = validateProjectName('a');
161
+ expect(result.isValid).toBe(true);
162
+ });
163
+ });
164
+ });
165
+
166
+ describe('validateJiraKey', () => {
167
+ describe('正常ケース', () => {
168
+ it('2-10文字の大文字英字を受け入れる', () => {
169
+ const validKeys = ['AB', 'PROJ', 'TICKET', 'ABCDEFGHIJ'];
170
+
171
+ validKeys.forEach((key) => {
172
+ const result = validateJiraKey(key);
173
+ expect(result.isValid).toBe(true);
174
+ expect(result.errors).toHaveLength(0);
175
+ });
176
+ });
177
+ });
178
+
179
+ describe('異常ケース', () => {
180
+ it('小文字を含む場合はエラー', () => {
181
+ const result = validateJiraKey('abc');
182
+ expect(result.isValid).toBe(false);
183
+ expect(result.errors).toContain(
184
+ 'JIRA key must be 2-10 uppercase letters',
185
+ );
186
+ });
187
+
188
+ it('1文字の場合はエラー', () => {
189
+ const result = validateJiraKey('A');
190
+ expect(result.isValid).toBe(false);
191
+ expect(result.errors).toContain(
192
+ 'JIRA key must be 2-10 uppercase letters',
193
+ );
194
+ });
195
+
196
+ it('11文字以上の場合はエラー', () => {
197
+ const result = validateJiraKey('ABCDEFGHIJK');
198
+ expect(result.isValid).toBe(false);
199
+ expect(result.errors).toContain(
200
+ 'JIRA key must be 2-10 uppercase letters',
201
+ );
202
+ });
203
+
204
+ it('数字を含む場合はエラー', () => {
205
+ const result = validateJiraKey('ABC123');
206
+ expect(result.isValid).toBe(false);
207
+ expect(result.errors).toContain(
208
+ 'JIRA key must be 2-10 uppercase letters',
209
+ );
210
+ });
211
+
212
+ it('空文字列の場合はエラー', () => {
213
+ const result = validateJiraKey('');
214
+ expect(result.isValid).toBe(false);
215
+ expect(result.errors).toContain(
216
+ 'JIRA key must be 2-10 uppercase letters',
217
+ );
218
+ });
219
+
220
+ it('ハイフンを含む場合はエラー', () => {
221
+ const result = validateJiraKey('ABC-DEF');
222
+ expect(result.isValid).toBe(false);
223
+ expect(result.errors).toContain(
224
+ 'JIRA key must be 2-10 uppercase letters',
225
+ );
226
+ });
227
+
228
+ it('スペースを含む場合はエラー', () => {
229
+ const result = validateJiraKey('ABC DEF');
230
+ expect(result.isValid).toBe(false);
231
+ expect(result.errors).toContain(
232
+ 'JIRA key must be 2-10 uppercase letters',
233
+ );
234
+ });
235
+ });
236
+ });
237
+
238
+ describe('validateRepositoryUrl', () => {
239
+ describe('正常ケース', () => {
240
+ it('有効なGitHub HTTPS URLを受け入れる', () => {
241
+ const validUrls = [
242
+ 'https://github.com/owner/repo',
243
+ 'https://github.com/my-org/my-repo',
244
+ 'https://github.com/user123/project-name',
245
+ ];
246
+
247
+ validUrls.forEach((url) => {
248
+ const result = validateRepositoryUrl(url);
249
+ expect(result.isValid).toBe(true);
250
+ expect(result.errors).toHaveLength(0);
251
+ });
252
+ });
253
+ });
254
+
255
+ describe('異常ケース', () => {
256
+ it('GitLab URLの場合はエラー', () => {
257
+ const result = validateRepositoryUrl(
258
+ 'https://gitlab.com/owner/repo',
259
+ );
260
+ expect(result.isValid).toBe(false);
261
+ expect(result.errors).toContain(
262
+ 'Repository URL must be in GitHub format: https://github.com/{owner}/{repo}',
263
+ );
264
+ });
265
+
266
+ it('HTTP URLの場合はエラー', () => {
267
+ const result = validateRepositoryUrl('http://github.com/owner/repo');
268
+ expect(result.isValid).toBe(false);
269
+ expect(result.errors).toContain(
270
+ 'Repository URL must use HTTPS protocol',
271
+ );
272
+ });
273
+
274
+ it('SSH URLの場合はエラー', () => {
275
+ const result = validateRepositoryUrl('git@github.com:owner/repo.git');
276
+ expect(result.isValid).toBe(false);
277
+ expect(result.errors).toContain(
278
+ 'Repository URL must be in GitHub format: https://github.com/{owner}/{repo}',
279
+ );
280
+ });
281
+
282
+ it('不完全なURLの場合はエラー', () => {
283
+ const result = validateRepositoryUrl('https://github.com/owner');
284
+ expect(result.isValid).toBe(false);
285
+ expect(result.errors).toContain(
286
+ 'Repository URL must be in GitHub format: https://github.com/{owner}/{repo}',
287
+ );
288
+ });
289
+
290
+ it('空文字列の場合はエラー', () => {
291
+ const result = validateRepositoryUrl('');
292
+ expect(result.isValid).toBe(false);
293
+ expect(result.errors).toContain('Repository URL is empty');
294
+ });
295
+
296
+ it('無効なURL形式の場合はエラー', () => {
297
+ const result = validateRepositoryUrl('not-a-url');
298
+ expect(result.isValid).toBe(false);
299
+ expect(result.errors).toContain('Repository URL format is invalid');
300
+ });
301
+
302
+ it('.git拡張子を含む場合はエラー', () => {
303
+ const result = validateRepositoryUrl(
304
+ 'https://github.com/owner/repo.git',
305
+ );
306
+ expect(result.isValid).toBe(false);
307
+ expect(result.errors).toContain(
308
+ 'Repository URL must not include .git extension',
309
+ );
310
+ });
311
+ });
312
+
313
+ describe('プレースホルダー検出', () => {
314
+ it('プレースホルダーURLの場合はエラー', () => {
315
+ const placeholders = [
316
+ 'https://github.com/your-org/your-repo',
317
+ 'https://github.com/owner/repo-name',
318
+ ];
319
+
320
+ placeholders.forEach((url) => {
321
+ const result = validateRepositoryUrl(url);
322
+ if (
323
+ url.includes('your-org') ||
324
+ url.includes('your-repo') ||
325
+ url.includes('repo-name')
326
+ ) {
327
+ expect(result.isValid).toBe(false);
328
+ expect(result.errors).toContain(
329
+ 'Repository URL contains placeholder values',
330
+ );
331
+ }
332
+ });
333
+ });
334
+ });
335
+ });
@@ -3,12 +3,19 @@
3
3
  * デフォルト設定 + プロジェクト固有設定をマージ
4
4
  */
5
5
 
6
- import { readFileSync, existsSync, statSync } from 'fs';
7
- import { resolve, relative, isAbsolute } from 'path';
6
+ import { readFileSync, writeFileSync, existsSync, statSync, renameSync, unlinkSync, mkdirSync } from 'fs';
7
+ import { resolve, relative, isAbsolute, dirname } from 'path';
8
8
  import { fileURLToPath } from 'url';
9
9
  import { homedir } from 'os';
10
10
  import { config, parse as dotenvParse } from 'dotenv';
11
- import { AppConfigSchema, type AppConfig } from '../config/config-schema.js';
11
+ import {
12
+ AppConfigSchema,
13
+ MultiRepoProjectSchema,
14
+ RepositorySchema,
15
+ type AppConfig,
16
+ type MultiRepoProject,
17
+ type Repository,
18
+ } from '../config/config-schema.js';
12
19
 
13
20
  // 環境変数読み込み
14
21
  config();
@@ -612,3 +619,190 @@ export function getConfigPath(projectRoot: string = process.cwd()): string {
612
619
  return resolveConfigPath(projectRoot);
613
620
  }
614
621
 
622
+ /**
623
+ * Multi-Repo管理関数
624
+ */
625
+
626
+ /**
627
+ * 設定ファイルをアトミックに保存
628
+ * 一時ファイルに書き込んでからrenameすることでアトミック性を保証
629
+ */
630
+ function saveConfig(
631
+ config: Partial<AppConfig>,
632
+ projectRoot: string = process.cwd(),
633
+ ): void {
634
+ const configPath = resolveConfigPath(projectRoot);
635
+ const tempPath = `${configPath}.tmp`;
636
+
637
+ try {
638
+ // .michiディレクトリが存在しない場合は作成
639
+ const configDir = dirname(configPath);
640
+ if (!existsSync(configDir)) {
641
+ mkdirSync(configDir, { recursive: true });
642
+ }
643
+
644
+ // 一時ファイルに書き込み
645
+ writeFileSync(tempPath, JSON.stringify(config, null, 2), 'utf-8');
646
+
647
+ // アトミックにリネーム
648
+ renameSync(tempPath, configPath);
649
+
650
+ // キャッシュをクリア
651
+ clearConfigCache();
652
+ } catch (error) {
653
+ // エラー時は一時ファイルを削除
654
+ if (existsSync(tempPath)) {
655
+ try {
656
+ unlinkSync(tempPath);
657
+ } catch {
658
+ // 削除失敗は無視
659
+ }
660
+ }
661
+ throw error;
662
+ }
663
+ }
664
+
665
+ /**
666
+ * プロジェクト名でMulti-Repoプロジェクトを検索
667
+ */
668
+ export function findProject(
669
+ projectName: string,
670
+ projectRoot: string = process.cwd(),
671
+ ): MultiRepoProject | null {
672
+ const configPath = resolveConfigPath(projectRoot);
673
+
674
+ if (!existsSync(configPath)) {
675
+ return null;
676
+ }
677
+
678
+ try {
679
+ const config = loadProjectConfig(projectRoot);
680
+ if (!config?.multiRepoProjects) {
681
+ return null;
682
+ }
683
+
684
+ const project = config.multiRepoProjects.find(
685
+ (p) => p.name === projectName,
686
+ );
687
+ return project || null;
688
+ } catch {
689
+ return null;
690
+ }
691
+ }
692
+
693
+ /**
694
+ * Multi-Repoプロジェクトを追加
695
+ */
696
+ export function addMultiRepoProject(
697
+ project: MultiRepoProject,
698
+ projectRoot: string = process.cwd(),
699
+ ): { success: boolean; project?: MultiRepoProject; error?: string } {
700
+ try {
701
+ // Zodスキーマでバリデーション
702
+ const validatedProject = MultiRepoProjectSchema.parse(project);
703
+
704
+ // 既存の設定を読み込み
705
+ const existingConfig = loadProjectConfig(projectRoot) || {};
706
+ const multiRepoProjects = existingConfig.multiRepoProjects || [];
707
+
708
+ // 重複チェック
709
+ const existingProject = multiRepoProjects.find(
710
+ (p) => p.name === validatedProject.name,
711
+ );
712
+ if (existingProject) {
713
+ return {
714
+ success: false,
715
+ error: `Project "${validatedProject.name}" already exists`,
716
+ };
717
+ }
718
+
719
+ // プロジェクトを追加
720
+ const updatedConfig = {
721
+ ...existingConfig,
722
+ multiRepoProjects: [...multiRepoProjects, validatedProject],
723
+ };
724
+
725
+ // 設定を保存
726
+ saveConfig(updatedConfig, projectRoot);
727
+
728
+ return {
729
+ success: true,
730
+ project: validatedProject,
731
+ };
732
+ } catch (error) {
733
+ return {
734
+ success: false,
735
+ error: error instanceof Error ? error.message : 'Unknown error',
736
+ };
737
+ }
738
+ }
739
+
740
+ /**
741
+ * Multi-Repoプロジェクトにリポジトリを追加
742
+ */
743
+ export function addRepositoryToProject(
744
+ projectName: string,
745
+ repository: Repository,
746
+ projectRoot: string = process.cwd(),
747
+ ): { success: boolean; repository?: Repository; error?: string } {
748
+ try {
749
+ // Zodスキーマでバリデーション
750
+ const validatedRepo = RepositorySchema.parse(repository);
751
+
752
+ // 既存の設定を読み込み
753
+ const existingConfig = loadProjectConfig(projectRoot) || {};
754
+ const multiRepoProjects = existingConfig.multiRepoProjects || [];
755
+
756
+ // プロジェクトを検索
757
+ const projectIndex = multiRepoProjects.findIndex(
758
+ (p) => p.name === projectName,
759
+ );
760
+ if (projectIndex === -1) {
761
+ return {
762
+ success: false,
763
+ error: `Project "${projectName}" not found`,
764
+ };
765
+ }
766
+
767
+ const project = multiRepoProjects[projectIndex];
768
+
769
+ // 重複チェック
770
+ const existingRepo = project.repositories.find(
771
+ (r) => r.name === validatedRepo.name,
772
+ );
773
+ if (existingRepo) {
774
+ return {
775
+ success: false,
776
+ error: `Repository "${validatedRepo.name}" already exists in project "${projectName}"`,
777
+ };
778
+ }
779
+
780
+ // リポジトリを追加
781
+ const updatedProject = {
782
+ ...project,
783
+ repositories: [...project.repositories, validatedRepo],
784
+ };
785
+
786
+ // プロジェクト配列を更新
787
+ const updatedProjects = [...multiRepoProjects];
788
+ updatedProjects[projectIndex] = updatedProject;
789
+
790
+ // 設定を保存
791
+ const updatedConfig = {
792
+ ...existingConfig,
793
+ multiRepoProjects: updatedProjects,
794
+ };
795
+ saveConfig(updatedConfig, projectRoot);
796
+
797
+ return {
798
+ success: true,
799
+ repository: validatedRepo,
800
+ };
801
+ } catch (error) {
802
+ return {
803
+ success: false,
804
+ error: error instanceof Error ? error.message : 'Unknown error',
805
+ };
806
+ }
807
+ }
808
+
@@ -0,0 +1,141 @@
1
+ /**
2
+ * multi-repo-validator.ts
3
+ * Multi-Repo機能のバリデーションユーティリティ
4
+ *
5
+ * プロジェクト名、JIRAキー、リポジトリURLのバリデーションとセキュリティチェックを行います。
6
+ */
7
+
8
+ /**
9
+ * バリデーション結果
10
+ */
11
+ export interface ValidationResult {
12
+ isValid: boolean;
13
+ errors: string[];
14
+ warnings: string[];
15
+ }
16
+
17
+ /**
18
+ * プロジェクト名のバリデーション
19
+ * セキュリティ対策: パストラバーサル、相対パス、制御文字をチェック
20
+ */
21
+ export function validateProjectName(name: string): ValidationResult {
22
+ const errors: string[] = [];
23
+ const warnings: string[] = [];
24
+
25
+ // 長さチェック: 1-100文字
26
+ if (name.length < 1 || name.length > 100) {
27
+ errors.push('Project name must be between 1 and 100 characters');
28
+ }
29
+
30
+ // パストラバーサル対策: '/', '\' 禁止
31
+ if (name.includes('/') || name.includes('\\')) {
32
+ errors.push(
33
+ 'Project name must not contain path traversal characters (/, \\)',
34
+ );
35
+ }
36
+
37
+ // 相対パス対策: '.', '..' 禁止
38
+ if (name === '.' || name === '..') {
39
+ errors.push(
40
+ 'Project name must not be relative path components (., ..)',
41
+ );
42
+ }
43
+
44
+ // 制御文字対策: \x00-\x1F, \x7F 禁止
45
+ // eslint-disable-next-line no-control-regex
46
+ const controlCharRegex = /[\x00-\x1F\x7F]/;
47
+ if (controlCharRegex.test(name)) {
48
+ errors.push('Project name must not contain control characters');
49
+ }
50
+
51
+ return {
52
+ isValid: errors.length === 0,
53
+ errors,
54
+ warnings,
55
+ };
56
+ }
57
+
58
+ /**
59
+ * JIRAキーのバリデーション
60
+ * 2-10文字の大文字英字のみ許可
61
+ */
62
+ export function validateJiraKey(key: string): ValidationResult {
63
+ const errors: string[] = [];
64
+ const warnings: string[] = [];
65
+
66
+ // 正規表現: 2-10文字の大文字英字
67
+ const jiraKeyRegex = /^[A-Z]{2,10}$/;
68
+
69
+ if (!jiraKeyRegex.test(key)) {
70
+ errors.push('JIRA key must be 2-10 uppercase letters');
71
+ }
72
+
73
+ return {
74
+ isValid: errors.length === 0,
75
+ errors,
76
+ warnings,
77
+ };
78
+ }
79
+
80
+ /**
81
+ * リポジトリURLのバリデーション
82
+ * GitHub HTTPS URL形式のみ許可
83
+ */
84
+ export function validateRepositoryUrl(url: string): ValidationResult {
85
+ const errors: string[] = [];
86
+ const warnings: string[] = [];
87
+
88
+ // 空文字列チェック
89
+ if (!url || url.trim() === '') {
90
+ errors.push('Repository URL is empty');
91
+ return { isValid: false, errors, warnings };
92
+ }
93
+
94
+ // SSH URL検出(git@github.com:形式)
95
+ if (url.startsWith('git@')) {
96
+ errors.push(
97
+ 'Repository URL must be in GitHub format: https://github.com/{owner}/{repo}',
98
+ );
99
+ return { isValid: false, errors, warnings };
100
+ }
101
+
102
+ // URL形式チェック
103
+ try {
104
+ const parsedUrl = new URL(url);
105
+
106
+ // HTTPSプロトコルチェック
107
+ if (parsedUrl.protocol !== 'https:') {
108
+ errors.push('Repository URL must use HTTPS protocol');
109
+ }
110
+
111
+ // GitHub URL形式チェック: https://github.com/{owner}/{repo}
112
+ const githubPattern = /^https:\/\/github\.com\/[^/]+\/[^/]+$/;
113
+ if (!githubPattern.test(url)) {
114
+ errors.push(
115
+ 'Repository URL must be in GitHub format: https://github.com/{owner}/{repo}',
116
+ );
117
+ }
118
+
119
+ // .git拡張子チェック
120
+ if (url.endsWith('.git')) {
121
+ errors.push('Repository URL must not include .git extension');
122
+ }
123
+
124
+ // プレースホルダー検出
125
+ if (
126
+ url.includes('your-org') ||
127
+ url.includes('your-repo') ||
128
+ url.includes('repo-name')
129
+ ) {
130
+ errors.push('Repository URL contains placeholder values');
131
+ }
132
+ } catch (_error) {
133
+ errors.push('Repository URL format is invalid');
134
+ }
135
+
136
+ return {
137
+ isValid: errors.length === 0,
138
+ errors,
139
+ warnings,
140
+ };
141
+ }