@sk8metal/michi-cli 0.5.0 → 0.7.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 (278) hide show
  1. package/CHANGELOG.md +122 -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/spec-archive.d.ts +17 -0
  82. package/dist/src/commands/spec-archive.d.ts.map +1 -0
  83. package/dist/src/commands/spec-archive.js +40 -0
  84. package/dist/src/commands/spec-archive.js.map +1 -0
  85. package/dist/src/commands/spec-list.d.ts +15 -0
  86. package/dist/src/commands/spec-list.d.ts.map +1 -0
  87. package/dist/src/commands/spec-list.js +55 -0
  88. package/dist/src/commands/spec-list.js.map +1 -0
  89. package/docs/user-guide/guides/multi-repo-guide.md +591 -0
  90. package/docs/user-guide/guides/multi-repo-migration-guide.md +516 -0
  91. package/docs/user-guide/reference/multi-repo-api.md +771 -0
  92. package/docs/user-guide/reference/quick-reference.md +22 -37
  93. package/package.json +1 -4
  94. package/scripts/__tests__/config-loader-multi-repo.test.ts +342 -0
  95. package/scripts/__tests__/github-actions-client.test.ts +543 -0
  96. package/scripts/__tests__/health-check-service.test.ts +142 -0
  97. package/scripts/__tests__/markdown-to-confluence.test.ts +262 -0
  98. package/scripts/__tests__/mermaid-converter.test.ts +236 -0
  99. package/scripts/__tests__/multi-repo-config-schema.test.ts +335 -0
  100. package/scripts/__tests__/multi-repo-validator.test.ts +524 -0
  101. package/scripts/__tests__/spec-archiver.test.ts +512 -0
  102. package/scripts/__tests__/test-script-runner.test.ts +217 -0
  103. package/scripts/config/config-schema.ts +64 -0
  104. package/scripts/confluence-sync.ts +16 -2
  105. package/scripts/github-actions-client.ts +258 -0
  106. package/scripts/health-check-service.ts +171 -0
  107. package/scripts/markdown-to-confluence.ts +37 -6
  108. package/scripts/mermaid-converter.ts +56 -0
  109. package/scripts/template/__tests__/multi-repo-renderer.test.ts +261 -0
  110. package/scripts/template/multi-repo-renderer.ts +172 -0
  111. package/scripts/template/renderer.ts +5 -0
  112. package/scripts/test-execution-generator.ts +104 -11
  113. package/scripts/test-script-runner.ts +130 -0
  114. package/scripts/utils/__tests__/config-validator.test.ts +104 -6
  115. package/scripts/utils/__tests__/multi-repo-validator.test.ts +335 -0
  116. package/scripts/utils/config-loader.ts +197 -3
  117. package/scripts/utils/multi-repo-validator.ts +141 -0
  118. package/scripts/utils/spec-archiver.ts +260 -0
  119. package/scripts/utils/spec-updater.ts +4 -0
  120. package/templates/claude/agents/pr-size-monitor/AGENT.md +330 -0
  121. package/templates/claude/commands/michi/spec-impl.md +208 -35
  122. package/templates/multi-repo/docs/ci-status.md +51 -0
  123. package/templates/multi-repo/docs/release-notes.md +99 -0
  124. package/templates/multi-repo/overview/architecture.md +102 -0
  125. package/templates/multi-repo/overview/requirements.md +68 -0
  126. package/templates/multi-repo/overview/sequence.md +79 -0
  127. package/templates/multi-repo/steering/multi-repo.md +74 -0
  128. package/templates/multi-repo/tests/strategy.md +89 -0
  129. package/dist/scripts/__tests__/create-project.test.d.ts +0 -2
  130. package/dist/scripts/__tests__/create-project.test.d.ts.map +0 -1
  131. package/dist/scripts/__tests__/create-project.test.js +0 -243
  132. package/dist/scripts/__tests__/create-project.test.js.map +0 -1
  133. package/dist/scripts/__tests__/jira-transitions.test.d.ts +0 -5
  134. package/dist/scripts/__tests__/jira-transitions.test.d.ts.map +0 -1
  135. package/dist/scripts/__tests__/jira-transitions.test.js +0 -172
  136. package/dist/scripts/__tests__/jira-transitions.test.js.map +0 -1
  137. package/dist/scripts/__tests__/multi-project-estimate.test.d.ts +0 -2
  138. package/dist/scripts/__tests__/multi-project-estimate.test.d.ts.map +0 -1
  139. package/dist/scripts/__tests__/multi-project-estimate.test.js +0 -118
  140. package/dist/scripts/__tests__/multi-project-estimate.test.js.map +0 -1
  141. package/dist/scripts/__tests__/setup-existing-project.test.d.ts +0 -2
  142. package/dist/scripts/__tests__/setup-existing-project.test.d.ts.map +0 -1
  143. package/dist/scripts/__tests__/setup-existing-project.test.js +0 -208
  144. package/dist/scripts/__tests__/setup-existing-project.test.js.map +0 -1
  145. package/dist/scripts/__tests__/setup-interactive.test.d.ts +0 -2
  146. package/dist/scripts/__tests__/setup-interactive.test.d.ts.map +0 -1
  147. package/dist/scripts/__tests__/setup-interactive.test.js +0 -166
  148. package/dist/scripts/__tests__/setup-interactive.test.js.map +0 -1
  149. package/dist/scripts/__tests__/spec-impl-workflow.test.d.ts +0 -5
  150. package/dist/scripts/__tests__/spec-impl-workflow.test.d.ts.map +0 -1
  151. package/dist/scripts/__tests__/spec-impl-workflow.test.js +0 -323
  152. package/dist/scripts/__tests__/spec-impl-workflow.test.js.map +0 -1
  153. package/dist/scripts/__tests__/spec-loader.test.d.ts +0 -5
  154. package/dist/scripts/__tests__/spec-loader.test.d.ts.map +0 -1
  155. package/dist/scripts/__tests__/spec-loader.test.js +0 -153
  156. package/dist/scripts/__tests__/spec-loader.test.js.map +0 -1
  157. package/dist/scripts/__tests__/validate-phase.test.d.ts +0 -5
  158. package/dist/scripts/__tests__/validate-phase.test.d.ts.map +0 -1
  159. package/dist/scripts/__tests__/validate-phase.test.js +0 -249
  160. package/dist/scripts/__tests__/validate-phase.test.js.map +0 -1
  161. package/dist/scripts/constants/__tests__/environments.test.d.ts +0 -2
  162. package/dist/scripts/constants/__tests__/environments.test.d.ts.map +0 -1
  163. package/dist/scripts/constants/__tests__/environments.test.js +0 -125
  164. package/dist/scripts/constants/__tests__/environments.test.js.map +0 -1
  165. package/dist/scripts/constants/__tests__/languages.test.d.ts +0 -2
  166. package/dist/scripts/constants/__tests__/languages.test.d.ts.map +0 -1
  167. package/dist/scripts/constants/__tests__/languages.test.js +0 -82
  168. package/dist/scripts/constants/__tests__/languages.test.js.map +0 -1
  169. package/dist/scripts/create-project.d.ts +0 -16
  170. package/dist/scripts/create-project.d.ts.map +0 -1
  171. package/dist/scripts/create-project.js +0 -334
  172. package/dist/scripts/create-project.js.map +0 -1
  173. package/dist/scripts/list-projects.d.ts +0 -7
  174. package/dist/scripts/list-projects.d.ts.map +0 -1
  175. package/dist/scripts/list-projects.js +0 -88
  176. package/dist/scripts/list-projects.js.map +0 -1
  177. package/dist/scripts/template/__tests__/renderer.test.d.ts +0 -2
  178. package/dist/scripts/template/__tests__/renderer.test.d.ts.map +0 -1
  179. package/dist/scripts/template/__tests__/renderer.test.js +0 -165
  180. package/dist/scripts/template/__tests__/renderer.test.js.map +0 -1
  181. package/dist/scripts/utils/__tests__/aidlc-parser.test.d.ts +0 -5
  182. package/dist/scripts/utils/__tests__/aidlc-parser.test.d.ts.map +0 -1
  183. package/dist/scripts/utils/__tests__/aidlc-parser.test.js +0 -315
  184. package/dist/scripts/utils/__tests__/aidlc-parser.test.js.map +0 -1
  185. package/dist/scripts/utils/__tests__/business-days.test.d.ts +0 -5
  186. package/dist/scripts/utils/__tests__/business-days.test.d.ts.map +0 -1
  187. package/dist/scripts/utils/__tests__/business-days.test.js +0 -171
  188. package/dist/scripts/utils/__tests__/business-days.test.js.map +0 -1
  189. package/dist/scripts/utils/__tests__/config-loader.test.d.ts +0 -5
  190. package/dist/scripts/utils/__tests__/config-loader.test.d.ts.map +0 -1
  191. package/dist/scripts/utils/__tests__/config-loader.test.js +0 -314
  192. package/dist/scripts/utils/__tests__/config-loader.test.js.map +0 -1
  193. package/dist/scripts/utils/__tests__/config-validator.test.d.ts +0 -5
  194. package/dist/scripts/utils/__tests__/config-validator.test.d.ts.map +0 -1
  195. package/dist/scripts/utils/__tests__/config-validator.test.js +0 -396
  196. package/dist/scripts/utils/__tests__/config-validator.test.js.map +0 -1
  197. package/dist/scripts/utils/__tests__/env-config.test.d.ts +0 -5
  198. package/dist/scripts/utils/__tests__/env-config.test.d.ts.map +0 -1
  199. package/dist/scripts/utils/__tests__/env-config.test.js +0 -216
  200. package/dist/scripts/utils/__tests__/env-config.test.js.map +0 -1
  201. package/dist/scripts/utils/__tests__/feature-name-validator.test.d.ts +0 -5
  202. package/dist/scripts/utils/__tests__/feature-name-validator.test.d.ts.map +0 -1
  203. package/dist/scripts/utils/__tests__/feature-name-validator.test.js +0 -106
  204. package/dist/scripts/utils/__tests__/feature-name-validator.test.js.map +0 -1
  205. package/dist/scripts/utils/__tests__/jira-issue-type-fetcher.test.d.ts +0 -5
  206. package/dist/scripts/utils/__tests__/jira-issue-type-fetcher.test.d.ts.map +0 -1
  207. package/dist/scripts/utils/__tests__/jira-issue-type-fetcher.test.js +0 -202
  208. package/dist/scripts/utils/__tests__/jira-issue-type-fetcher.test.js.map +0 -1
  209. package/dist/scripts/utils/__tests__/project-meta.test.d.ts +0 -6
  210. package/dist/scripts/utils/__tests__/project-meta.test.d.ts.map +0 -1
  211. package/dist/scripts/utils/__tests__/project-meta.test.js +0 -154
  212. package/dist/scripts/utils/__tests__/project-meta.test.js.map +0 -1
  213. package/dist/scripts/utils/__tests__/security-validator.test.d.ts +0 -6
  214. package/dist/scripts/utils/__tests__/security-validator.test.d.ts.map +0 -1
  215. package/dist/scripts/utils/__tests__/security-validator.test.js +0 -219
  216. package/dist/scripts/utils/__tests__/security-validator.test.js.map +0 -1
  217. package/dist/scripts/utils/__tests__/spec-updater.test.d.ts +0 -5
  218. package/dist/scripts/utils/__tests__/spec-updater.test.d.ts.map +0 -1
  219. package/dist/scripts/utils/__tests__/spec-updater.test.js +0 -158
  220. package/dist/scripts/utils/__tests__/spec-updater.test.js.map +0 -1
  221. package/dist/scripts/utils/__tests__/tasks-converter.test.d.ts +0 -5
  222. package/dist/scripts/utils/__tests__/tasks-converter.test.d.ts.map +0 -1
  223. package/dist/scripts/utils/__tests__/tasks-converter.test.js +0 -500
  224. package/dist/scripts/utils/__tests__/tasks-converter.test.js.map +0 -1
  225. package/dist/scripts/utils/__tests__/tasks-format-validator.test.d.ts +0 -5
  226. package/dist/scripts/utils/__tests__/tasks-format-validator.test.d.ts.map +0 -1
  227. package/dist/scripts/utils/__tests__/tasks-format-validator.test.js +0 -314
  228. package/dist/scripts/utils/__tests__/tasks-format-validator.test.js.map +0 -1
  229. package/dist/scripts/utils/__tests__/test-runner.test.d.ts +0 -5
  230. package/dist/scripts/utils/__tests__/test-runner.test.d.ts.map +0 -1
  231. package/dist/scripts/utils/__tests__/test-runner.test.js +0 -64
  232. package/dist/scripts/utils/__tests__/test-runner.test.js.map +0 -1
  233. package/dist/src/__tests__/cli.test.d.ts +0 -5
  234. package/dist/src/__tests__/cli.test.d.ts.map +0 -1
  235. package/dist/src/__tests__/cli.test.js +0 -58
  236. package/dist/src/__tests__/cli.test.js.map +0 -1
  237. package/dist/src/__tests__/integration/internationalization.test.d.ts +0 -8
  238. package/dist/src/__tests__/integration/internationalization.test.d.ts.map +0 -1
  239. package/dist/src/__tests__/integration/internationalization.test.js +0 -333
  240. package/dist/src/__tests__/integration/internationalization.test.js.map +0 -1
  241. package/dist/src/__tests__/integration/setup/claude-agent.test.d.ts +0 -5
  242. package/dist/src/__tests__/integration/setup/claude-agent.test.d.ts.map +0 -1
  243. package/dist/src/__tests__/integration/setup/claude-agent.test.js +0 -122
  244. package/dist/src/__tests__/integration/setup/claude-agent.test.js.map +0 -1
  245. package/dist/src/__tests__/integration/setup/claude.test.d.ts +0 -5
  246. package/dist/src/__tests__/integration/setup/claude.test.d.ts.map +0 -1
  247. package/dist/src/__tests__/integration/setup/claude.test.js +0 -193
  248. package/dist/src/__tests__/integration/setup/claude.test.js.map +0 -1
  249. package/dist/src/__tests__/integration/setup/cursor.test.d.ts +0 -5
  250. package/dist/src/__tests__/integration/setup/cursor.test.d.ts.map +0 -1
  251. package/dist/src/__tests__/integration/setup/cursor.test.js +0 -166
  252. package/dist/src/__tests__/integration/setup/cursor.test.js.map +0 -1
  253. package/dist/src/__tests__/integration/setup/helpers/fs-assertions.d.ts +0 -32
  254. package/dist/src/__tests__/integration/setup/helpers/fs-assertions.d.ts.map +0 -1
  255. package/dist/src/__tests__/integration/setup/helpers/fs-assertions.js +0 -72
  256. package/dist/src/__tests__/integration/setup/helpers/fs-assertions.js.map +0 -1
  257. package/dist/src/__tests__/integration/setup/helpers/test-project.d.ts +0 -38
  258. package/dist/src/__tests__/integration/setup/helpers/test-project.d.ts.map +0 -1
  259. package/dist/src/__tests__/integration/setup/helpers/test-project.js +0 -83
  260. package/dist/src/__tests__/integration/setup/helpers/test-project.js.map +0 -1
  261. package/dist/src/__tests__/integration/setup/init.test.d.ts +0 -5
  262. package/dist/src/__tests__/integration/setup/init.test.d.ts.map +0 -1
  263. package/dist/src/__tests__/integration/setup/init.test.js +0 -352
  264. package/dist/src/__tests__/integration/setup/init.test.js.map +0 -1
  265. package/dist/src/__tests__/integration/setup/validation.test.d.ts +0 -5
  266. package/dist/src/__tests__/integration/setup/validation.test.d.ts.map +0 -1
  267. package/dist/src/__tests__/integration/setup/validation.test.js +0 -301
  268. package/dist/src/__tests__/integration/setup/validation.test.js.map +0 -1
  269. package/dist/src/commands/__tests__/init.test.d.ts +0 -5
  270. package/dist/src/commands/__tests__/init.test.d.ts.map +0 -1
  271. package/dist/src/commands/__tests__/init.test.js +0 -255
  272. package/dist/src/commands/__tests__/init.test.js.map +0 -1
  273. package/dist/src/commands/__tests__/migrate.test.d.ts +0 -5
  274. package/dist/src/commands/__tests__/migrate.test.d.ts.map +0 -1
  275. package/dist/src/commands/__tests__/migrate.test.js +0 -216
  276. package/dist/src/commands/__tests__/migrate.test.js.map +0 -1
  277. package/scripts/create-project.ts +0 -386
  278. 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
+ }