@sk8metal/michi-cli 0.0.7 → 0.0.9

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 (175) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +205 -11
  3. package/dist/scripts/__tests__/create-project.test.d.ts +2 -0
  4. package/dist/scripts/__tests__/create-project.test.d.ts.map +1 -0
  5. package/dist/scripts/__tests__/create-project.test.js +247 -0
  6. package/dist/scripts/__tests__/create-project.test.js.map +1 -0
  7. package/dist/scripts/__tests__/multi-project-estimate.test.d.ts +2 -0
  8. package/dist/scripts/__tests__/multi-project-estimate.test.d.ts.map +1 -0
  9. package/dist/scripts/__tests__/multi-project-estimate.test.js +119 -0
  10. package/dist/scripts/__tests__/multi-project-estimate.test.js.map +1 -0
  11. package/dist/scripts/__tests__/setup-existing-project.test.d.ts +2 -0
  12. package/dist/scripts/__tests__/setup-existing-project.test.d.ts.map +1 -0
  13. package/dist/scripts/__tests__/setup-existing-project.test.js +129 -0
  14. package/dist/scripts/__tests__/setup-existing-project.test.js.map +1 -0
  15. package/dist/scripts/__tests__/setup-interactive.test.d.ts +2 -0
  16. package/dist/scripts/__tests__/setup-interactive.test.d.ts.map +1 -0
  17. package/dist/scripts/__tests__/setup-interactive.test.js +160 -0
  18. package/dist/scripts/__tests__/setup-interactive.test.js.map +1 -0
  19. package/dist/scripts/config/default-config.json +57 -0
  20. package/dist/scripts/confluence-sync.d.ts +4 -0
  21. package/dist/scripts/confluence-sync.d.ts.map +1 -1
  22. package/dist/scripts/confluence-sync.js +12 -23
  23. package/dist/scripts/confluence-sync.js.map +1 -1
  24. package/dist/scripts/constants/__tests__/environments.test.d.ts +2 -0
  25. package/dist/scripts/constants/__tests__/environments.test.d.ts.map +1 -0
  26. package/dist/scripts/constants/__tests__/environments.test.js +91 -0
  27. package/dist/scripts/constants/__tests__/environments.test.js.map +1 -0
  28. package/dist/scripts/constants/__tests__/languages.test.d.ts +2 -0
  29. package/dist/scripts/constants/__tests__/languages.test.d.ts.map +1 -0
  30. package/dist/scripts/constants/__tests__/languages.test.js +82 -0
  31. package/dist/scripts/constants/__tests__/languages.test.js.map +1 -0
  32. package/dist/scripts/constants/environments.d.ts +33 -0
  33. package/dist/scripts/constants/environments.d.ts.map +1 -0
  34. package/dist/scripts/constants/environments.js +49 -0
  35. package/dist/scripts/constants/environments.js.map +1 -0
  36. package/dist/scripts/constants/languages.d.ts +23 -0
  37. package/dist/scripts/constants/languages.d.ts.map +1 -0
  38. package/dist/scripts/constants/languages.js +53 -0
  39. package/dist/scripts/constants/languages.js.map +1 -0
  40. package/dist/scripts/create-project.d.ts +4 -0
  41. package/dist/scripts/create-project.d.ts.map +1 -1
  42. package/dist/scripts/create-project.js +227 -137
  43. package/dist/scripts/create-project.js.map +1 -1
  44. package/dist/scripts/jira-sync.d.ts.map +1 -1
  45. package/dist/scripts/jira-sync.js +15 -0
  46. package/dist/scripts/jira-sync.js.map +1 -1
  47. package/dist/scripts/list-projects.d.ts.map +1 -1
  48. package/dist/scripts/list-projects.js +42 -15
  49. package/dist/scripts/list-projects.js.map +1 -1
  50. package/dist/scripts/multi-project-estimate.d.ts.map +1 -1
  51. package/dist/scripts/multi-project-estimate.js +56 -21
  52. package/dist/scripts/multi-project-estimate.js.map +1 -1
  53. package/dist/scripts/resource-dashboard.d.ts.map +1 -1
  54. package/dist/scripts/resource-dashboard.js +74 -17
  55. package/dist/scripts/resource-dashboard.js.map +1 -1
  56. package/dist/scripts/setup-existing-project.d.ts +3 -1
  57. package/dist/scripts/setup-existing-project.d.ts.map +1 -1
  58. package/dist/scripts/setup-existing-project.js +306 -217
  59. package/dist/scripts/setup-existing-project.js.map +1 -1
  60. package/dist/scripts/setup-interactive.d.ts +10 -0
  61. package/dist/scripts/setup-interactive.d.ts.map +1 -0
  62. package/dist/scripts/setup-interactive.js +413 -0
  63. package/dist/scripts/setup-interactive.js.map +1 -0
  64. package/dist/scripts/template/__tests__/renderer.test.d.ts +2 -0
  65. package/dist/scripts/template/__tests__/renderer.test.d.ts.map +1 -0
  66. package/dist/scripts/template/__tests__/renderer.test.js +165 -0
  67. package/dist/scripts/template/__tests__/renderer.test.js.map +1 -0
  68. package/dist/scripts/template/renderer.d.ts +70 -0
  69. package/dist/scripts/template/renderer.d.ts.map +1 -0
  70. package/dist/scripts/template/renderer.js +99 -0
  71. package/dist/scripts/template/renderer.js.map +1 -0
  72. package/dist/scripts/utils/__tests__/config-validator.test.js +5 -0
  73. package/dist/scripts/utils/__tests__/config-validator.test.js.map +1 -1
  74. package/dist/scripts/utils/__tests__/spec-updater.test.d.ts +5 -0
  75. package/dist/scripts/utils/__tests__/spec-updater.test.d.ts.map +1 -0
  76. package/dist/scripts/utils/__tests__/spec-updater.test.js +158 -0
  77. package/dist/scripts/utils/__tests__/spec-updater.test.js.map +1 -0
  78. package/dist/scripts/utils/confluence-hierarchy.d.ts +2 -1
  79. package/dist/scripts/utils/confluence-hierarchy.d.ts.map +1 -1
  80. package/dist/scripts/utils/confluence-hierarchy.js +5 -0
  81. package/dist/scripts/utils/confluence-hierarchy.js.map +1 -1
  82. package/dist/scripts/utils/project-finder.d.ts +30 -0
  83. package/dist/scripts/utils/project-finder.d.ts.map +1 -0
  84. package/dist/scripts/utils/project-finder.js +147 -0
  85. package/dist/scripts/utils/project-finder.js.map +1 -0
  86. package/dist/scripts/utils/spec-updater.d.ts +72 -0
  87. package/dist/scripts/utils/spec-updater.d.ts.map +1 -0
  88. package/dist/scripts/utils/spec-updater.js +141 -0
  89. package/dist/scripts/utils/spec-updater.js.map +1 -0
  90. package/dist/scripts/utils/template-finder.d.ts +37 -0
  91. package/dist/scripts/utils/template-finder.d.ts.map +1 -0
  92. package/dist/scripts/utils/template-finder.js +63 -0
  93. package/dist/scripts/utils/template-finder.js.map +1 -0
  94. package/dist/src/__tests__/integration/setup/claude-agent.test.d.ts +5 -0
  95. package/dist/src/__tests__/integration/setup/claude-agent.test.d.ts.map +1 -0
  96. package/dist/src/__tests__/integration/setup/claude-agent.test.js +125 -0
  97. package/dist/src/__tests__/integration/setup/claude-agent.test.js.map +1 -0
  98. package/dist/src/__tests__/integration/setup/claude.test.d.ts +5 -0
  99. package/dist/src/__tests__/integration/setup/claude.test.d.ts.map +1 -0
  100. package/dist/src/__tests__/integration/setup/claude.test.js +111 -0
  101. package/dist/src/__tests__/integration/setup/claude.test.js.map +1 -0
  102. package/dist/src/__tests__/integration/setup/cursor.test.d.ts +5 -0
  103. package/dist/src/__tests__/integration/setup/cursor.test.d.ts.map +1 -0
  104. package/dist/src/__tests__/integration/setup/cursor.test.js +162 -0
  105. package/dist/src/__tests__/integration/setup/cursor.test.js.map +1 -0
  106. package/dist/src/__tests__/integration/setup/helpers/fs-assertions.d.ts +32 -0
  107. package/dist/src/__tests__/integration/setup/helpers/fs-assertions.d.ts.map +1 -0
  108. package/dist/src/__tests__/integration/setup/helpers/fs-assertions.js +72 -0
  109. package/dist/src/__tests__/integration/setup/helpers/fs-assertions.js.map +1 -0
  110. package/dist/src/__tests__/integration/setup/helpers/test-project.d.ts +38 -0
  111. package/dist/src/__tests__/integration/setup/helpers/test-project.d.ts.map +1 -0
  112. package/dist/src/__tests__/integration/setup/helpers/test-project.js +83 -0
  113. package/dist/src/__tests__/integration/setup/helpers/test-project.js.map +1 -0
  114. package/dist/src/__tests__/integration/setup/validation.test.d.ts +5 -0
  115. package/dist/src/__tests__/integration/setup/validation.test.d.ts.map +1 -0
  116. package/dist/src/__tests__/integration/setup/validation.test.js +318 -0
  117. package/dist/src/__tests__/integration/setup/validation.test.js.map +1 -0
  118. package/dist/src/cli.d.ts.map +1 -1
  119. package/dist/src/cli.js +20 -0
  120. package/dist/src/cli.js.map +1 -1
  121. package/dist/src/commands/setup-existing.d.ts +22 -0
  122. package/dist/src/commands/setup-existing.d.ts.map +1 -0
  123. package/dist/src/commands/setup-existing.js +408 -0
  124. package/dist/src/commands/setup-existing.js.map +1 -0
  125. package/dist/vitest.config.d.ts.map +1 -1
  126. package/dist/vitest.config.js +9 -6
  127. package/dist/vitest.config.js.map +1 -1
  128. package/docs/README.md +2 -2
  129. package/docs/contributing/development.md +37 -0
  130. package/docs/getting-started/{new-project-setup.md → new-repository-setup.md} +165 -38
  131. package/docs/getting-started/quick-start.md +57 -6
  132. package/docs/getting-started/setup.md +551 -180
  133. package/docs/guides/customization.md +4 -4
  134. package/docs/guides/multi-project.md +12 -9
  135. package/docs/reference/quick-reference.md +27 -18
  136. package/docs/testing/integration-tests.md +297 -0
  137. package/docs/testing-strategy.md +87 -0
  138. package/package.json +23 -6
  139. package/scripts/__tests__/create-project.test.ts +292 -0
  140. package/scripts/__tests__/multi-project-estimate.test.ts +145 -0
  141. package/scripts/__tests__/setup-existing-project.test.ts +147 -0
  142. package/scripts/__tests__/setup-interactive.test.ts +199 -0
  143. package/scripts/confluence-sync.ts +17 -29
  144. package/scripts/constants/__tests__/environments.test.ts +110 -0
  145. package/scripts/constants/__tests__/languages.test.ts +100 -0
  146. package/scripts/constants/environments.ts +61 -0
  147. package/scripts/constants/languages.ts +70 -0
  148. package/scripts/copy-static-assets.js +50 -0
  149. package/scripts/create-project.ts +251 -158
  150. package/scripts/jira-sync.ts +16 -1
  151. package/scripts/list-projects.ts +51 -24
  152. package/scripts/multi-project-estimate.ts +58 -22
  153. package/scripts/resource-dashboard.ts +91 -26
  154. package/scripts/setup-existing-project.ts +350 -230
  155. package/scripts/setup-existing.sh +159 -25
  156. package/scripts/setup-interactive.ts +565 -0
  157. package/scripts/template/__tests__/renderer.test.ts +207 -0
  158. package/scripts/template/renderer.ts +133 -0
  159. package/scripts/utils/__tests__/config-validator.test.ts +6 -0
  160. package/scripts/utils/__tests__/spec-updater.test.ts +220 -0
  161. package/scripts/utils/confluence-hierarchy.ts +7 -1
  162. package/scripts/utils/project-finder.ts +184 -0
  163. package/scripts/utils/spec-updater.ts +212 -0
  164. package/scripts/utils/template-finder.ts +75 -0
  165. package/templates/claude/commands/michi/confluence-sync.md +38 -0
  166. package/templates/claude/commands/michi/project-switch.md +36 -0
  167. package/templates/claude/rules/atlassian-integration.md +35 -0
  168. package/templates/claude/rules/michi-core.md +54 -0
  169. package/templates/claude-agent/README.md +25 -0
  170. package/templates/cursor/commands/michi/confluence-sync.md +76 -0
  171. package/templates/cursor/commands/michi/project-switch.md +69 -0
  172. package/templates/cursor/rules/atlassian-mcp.mdc +188 -0
  173. package/templates/cursor/rules/github-ssot.mdc +151 -0
  174. package/templates/cursor/rules/multi-project.mdc +81 -0
  175. package/scripts/setup-env.sh +0 -52
@@ -0,0 +1,207 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ createTemplateContext,
4
+ renderTemplate,
5
+ renderJsonTemplate,
6
+ renderTemplates,
7
+ type TemplateContext
8
+ } from '../renderer.js';
9
+
10
+ describe('renderer', () => {
11
+ describe('createTemplateContext', () => {
12
+ it('should create context with all required fields', () => {
13
+ const context = createTemplateContext('ja', '.kiro', '.cursor');
14
+
15
+ expect(context).toHaveProperty('LANG_CODE');
16
+ expect(context).toHaveProperty('DEV_GUIDELINES');
17
+ expect(context).toHaveProperty('KIRO_DIR');
18
+ expect(context).toHaveProperty('AGENT_DIR');
19
+ expect(context.LANG_CODE).toBe('ja');
20
+ expect(context.KIRO_DIR).toBe('.kiro');
21
+ expect(context.AGENT_DIR).toBe('.cursor');
22
+ });
23
+
24
+ it('should include language-specific guidelines', () => {
25
+ const contextJa = createTemplateContext('ja', '.kiro', '.cursor');
26
+ expect(contextJa.DEV_GUIDELINES).toContain('日本語');
27
+
28
+ const contextEn = createTemplateContext('en', '.kiro', '.cursor');
29
+ expect(contextEn.DEV_GUIDELINES).toContain('English');
30
+ });
31
+ });
32
+
33
+ describe('renderTemplate', () => {
34
+ it('should replace single placeholder', () => {
35
+ const template = 'Language: {{LANG_CODE}}';
36
+ const context = createTemplateContext('ja', '.kiro', '.cursor');
37
+ const result = renderTemplate(template, context);
38
+
39
+ expect(result).toBe('Language: ja');
40
+ });
41
+
42
+ it('should replace multiple placeholders', () => {
43
+ const template = 'Lang: {{LANG_CODE}}, Kiro: {{KIRO_DIR}}, Agent: {{AGENT_DIR}}';
44
+ const context = createTemplateContext('en', '.kiro', '.claude');
45
+ const result = renderTemplate(template, context);
46
+
47
+ expect(result).toBe('Lang: en, Kiro: .kiro, Agent: .claude');
48
+ });
49
+
50
+ it('should replace same placeholder multiple times', () => {
51
+ const template = '{{LANG_CODE}} is {{LANG_CODE}}';
52
+ const context = createTemplateContext('ja', '.kiro', '.cursor');
53
+ const result = renderTemplate(template, context);
54
+
55
+ expect(result).toBe('ja is ja');
56
+ });
57
+
58
+ it('should leave unknown placeholders unchanged', () => {
59
+ const template = 'Known: {{LANG_CODE}}, Unknown: {{UNKNOWN}}';
60
+ const context = createTemplateContext('ja', '.kiro', '.cursor');
61
+ const result = renderTemplate(template, context);
62
+
63
+ expect(result).toBe('Known: ja, Unknown: {{UNKNOWN}}');
64
+ });
65
+
66
+ it('should handle DEV_GUIDELINES placeholder', () => {
67
+ const template = '{{DEV_GUIDELINES}}';
68
+ const context = createTemplateContext('ja', '.kiro', '.cursor');
69
+ const result = renderTemplate(template, context);
70
+
71
+ expect(result).toContain('Think in English');
72
+ expect(result).toContain('日本語');
73
+ });
74
+
75
+ it('should handle empty template', () => {
76
+ const template = '';
77
+ const context = createTemplateContext('ja', '.kiro', '.cursor');
78
+ const result = renderTemplate(template, context);
79
+
80
+ expect(result).toBe('');
81
+ });
82
+
83
+ it('should handle template with no placeholders', () => {
84
+ const template = 'No placeholders here';
85
+ const context = createTemplateContext('ja', '.kiro', '.cursor');
86
+ const result = renderTemplate(template, context);
87
+
88
+ expect(result).toBe('No placeholders here');
89
+ });
90
+
91
+ it('should handle multiline template', () => {
92
+ const template = `Line 1: {{LANG_CODE}}
93
+ Line 2: {{KIRO_DIR}}
94
+ Line 3: {{AGENT_DIR}}`;
95
+ const context = createTemplateContext('en', '.kiro', '.cursor');
96
+ const result = renderTemplate(template, context);
97
+
98
+ expect(result).toBe(`Line 1: en
99
+ Line 2: .kiro
100
+ Line 3: .cursor`);
101
+ });
102
+ });
103
+
104
+ describe('renderJsonTemplate', () => {
105
+ it('should render and parse JSON template', () => {
106
+ const template = '{"lang": "{{LANG_CODE}}", "dir": "{{KIRO_DIR}}"}';
107
+ const context = createTemplateContext('ja', '.kiro', '.cursor');
108
+ const result = renderJsonTemplate(template, context);
109
+
110
+ expect(result).toEqual({ lang: 'ja', dir: '.kiro' });
111
+ });
112
+
113
+ it('should handle nested JSON', () => {
114
+ const template = '{"config": {"lang": "{{LANG_CODE}}", "paths": {"kiro": "{{KIRO_DIR}}"}}}';
115
+ const context = createTemplateContext('en', '.kiro', '.claude');
116
+ const result = renderJsonTemplate(template, context);
117
+
118
+ expect(result).toEqual({
119
+ config: {
120
+ lang: 'en',
121
+ paths: {
122
+ kiro: '.kiro'
123
+ }
124
+ }
125
+ });
126
+ });
127
+
128
+ it('should handle JSON arrays', () => {
129
+ const template = '["{{LANG_CODE}}", "{{KIRO_DIR}}", "{{AGENT_DIR}}"]';
130
+ const context = createTemplateContext('ja', '.kiro', '.cursor');
131
+ const result = renderJsonTemplate(template, context);
132
+
133
+ expect(result).toEqual(['ja', '.kiro', '.cursor']);
134
+ });
135
+
136
+ it('should throw on invalid JSON', () => {
137
+ const template = 'invalid json {{LANG_CODE}}';
138
+ const context = createTemplateContext('ja', '.kiro', '.cursor');
139
+
140
+ expect(() => renderJsonTemplate(template, context)).toThrow();
141
+ });
142
+
143
+ it('should throw with descriptive error for invalid JSON', () => {
144
+ const template = '{"incomplete": {{LANG_CODE}}';
145
+ const context = createTemplateContext('ja', '.kiro', '.cursor');
146
+
147
+ try {
148
+ renderJsonTemplate(template, context);
149
+ expect.fail('Should have thrown an error');
150
+ } catch (error) {
151
+ expect(error).toBeInstanceOf(Error);
152
+ const errorMessage = (error as Error).message;
153
+
154
+ // Should contain descriptive information
155
+ expect(errorMessage).toContain('Failed to parse rendered JSON template');
156
+ expect(errorMessage).toContain('Original error:');
157
+ expect(errorMessage).toContain('Rendered output');
158
+ expect(errorMessage).toContain('Template context');
159
+ expect(errorMessage).toContain('LANG_CODE=ja');
160
+ expect(errorMessage).toContain('KIRO_DIR=.kiro');
161
+ }
162
+ });
163
+
164
+ it('should preserve original error stack', () => {
165
+ const template = '{invalid json}';
166
+ const context = createTemplateContext('en', '.kiro', '.cursor');
167
+
168
+ try {
169
+ renderJsonTemplate(template, context);
170
+ expect.fail('Should have thrown an error');
171
+ } catch (error) {
172
+ expect(error).toBeInstanceOf(Error);
173
+ const errorStack = (error as Error).stack;
174
+
175
+ // Should contain original error stack
176
+ expect(errorStack).toContain('Original error stack:');
177
+ }
178
+ });
179
+ });
180
+
181
+ describe('renderTemplates', () => {
182
+ it('should render multiple templates', () => {
183
+ const templates = {
184
+ template1: 'Lang: {{LANG_CODE}}',
185
+ template2: 'Dir: {{KIRO_DIR}}',
186
+ template3: 'Agent: {{AGENT_DIR}}'
187
+ };
188
+ const context = createTemplateContext('ja', '.kiro', '.cursor');
189
+ const results = renderTemplates(templates, context);
190
+
191
+ expect(results).toEqual({
192
+ template1: 'Lang: ja',
193
+ template2: 'Dir: .kiro',
194
+ template3: 'Agent: .cursor'
195
+ });
196
+ });
197
+
198
+ it('should handle empty templates object', () => {
199
+ const templates = {};
200
+ const context = createTemplateContext('ja', '.kiro', '.cursor');
201
+ const results = renderTemplates(templates, context);
202
+
203
+ expect(results).toEqual({});
204
+ });
205
+ });
206
+ });
207
+
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Template renderer with placeholder replacement
3
+ *
4
+ * Issue #37: 環境別コピー実装
5
+ */
6
+
7
+ import { SupportedLanguage, getDevGuidelines } from '../constants/languages.js';
8
+
9
+ export interface TemplateContext {
10
+ LANG_CODE: SupportedLanguage;
11
+ DEV_GUIDELINES: string;
12
+ KIRO_DIR: string;
13
+ AGENT_DIR: string;
14
+ PROJECT_ID?: string;
15
+ FEATURE_NAME?: string;
16
+ TIMESTAMP?: string;
17
+ }
18
+
19
+ /**
20
+ * Create template context for rendering
21
+ *
22
+ * @param lang - Language code
23
+ * @param kiroDir - .kiro directory name
24
+ * @param agentDir - Agent directory name (e.g., .cursor, .claude)
25
+ * @returns Template context object
26
+ */
27
+ export const createTemplateContext = (
28
+ lang: SupportedLanguage,
29
+ kiroDir: string,
30
+ agentDir: string
31
+ ): TemplateContext => ({
32
+ LANG_CODE: lang,
33
+ DEV_GUIDELINES: getDevGuidelines(lang),
34
+ KIRO_DIR: kiroDir,
35
+ AGENT_DIR: agentDir,
36
+ });
37
+
38
+ /**
39
+ * Render template with placeholder replacement
40
+ *
41
+ * Replaces {{KEY}} patterns with values from context
42
+ *
43
+ * @param template - Template string with {{PLACEHOLDER}} patterns
44
+ * @param context - Template context with replacement values
45
+ * @returns Rendered template string
46
+ *
47
+ * @example
48
+ * ```typescript
49
+ * const template = "Hello {{NAME}}, welcome to {{PLACE}}!";
50
+ * const context = { NAME: "Alice", PLACE: "Wonderland" };
51
+ * const result = renderTemplate(template, context);
52
+ * // Result: "Hello Alice, welcome to Wonderland!"
53
+ * ```
54
+ */
55
+ export const renderTemplate = (
56
+ template: string,
57
+ context: TemplateContext
58
+ ): string => {
59
+ return template.replace(/\{\{([A-Z_]+)\}\}/g, (match, key) => {
60
+ const value = context[key as keyof TemplateContext];
61
+ return value !== undefined ? String(value) : match;
62
+ });
63
+ };
64
+
65
+ /**
66
+ * Render JSON template with placeholder replacement
67
+ *
68
+ * Parses the template as JSON after placeholder replacement
69
+ *
70
+ * @param template - JSON template string with {{PLACEHOLDER}} patterns
71
+ * @param context - Template context with replacement values
72
+ * @returns Parsed JSON object
73
+ * @throws {Error} If the rendered template is not valid JSON
74
+ *
75
+ * @example
76
+ * ```typescript
77
+ * const template = '{"lang": "{{LANG_CODE}}", "dir": "{{KIRO_DIR}}"}';
78
+ * const context = { LANG_CODE: "ja", KIRO_DIR: ".kiro" };
79
+ * const result = renderJsonTemplate(template, context);
80
+ * // Result: { lang: "ja", dir: ".kiro" }
81
+ * ```
82
+ */
83
+ export const renderJsonTemplate = <T = any>(
84
+ template: string,
85
+ context: TemplateContext
86
+ ): T => {
87
+ const rendered = renderTemplate(template, context);
88
+
89
+ try {
90
+ return JSON.parse(rendered);
91
+ } catch (error) {
92
+ const errorMessage = error instanceof Error ? error.message : String(error);
93
+ const errorStack = error instanceof Error ? error.stack : undefined;
94
+
95
+ // Create descriptive error with context for debugging
96
+ const debugInfo = [
97
+ 'Failed to parse rendered JSON template',
98
+ `Original error: ${errorMessage}`,
99
+ `Rendered output (first 500 chars): ${rendered.substring(0, 500)}${rendered.length > 500 ? '...' : ''}`,
100
+ `Template context: LANG_CODE=${context.LANG_CODE}, KIRO_DIR=${context.KIRO_DIR}, AGENT_DIR=${context.AGENT_DIR}`
101
+ ].join('\n');
102
+
103
+ const detailedError = new Error(debugInfo);
104
+
105
+ // Preserve original error stack for diagnostics
106
+ if (errorStack) {
107
+ detailedError.stack = `${detailedError.stack}\n\nOriginal error stack:\n${errorStack}`;
108
+ }
109
+
110
+ throw detailedError;
111
+ }
112
+ };
113
+
114
+ /**
115
+ * Batch render multiple templates
116
+ *
117
+ * @param templates - Map of template names to template strings
118
+ * @param context - Template context with replacement values
119
+ * @returns Map of template names to rendered strings
120
+ */
121
+ export const renderTemplates = (
122
+ templates: Record<string, string>,
123
+ context: TemplateContext
124
+ ): Record<string, string> => {
125
+ const rendered: Record<string, string> = {};
126
+
127
+ for (const [name, template] of Object.entries(templates)) {
128
+ rendered[name] = renderTemplate(template, context);
129
+ }
130
+
131
+ return rendered;
132
+ };
133
+
@@ -231,6 +231,12 @@ describe('config-validator', () => {
231
231
  });
232
232
 
233
233
  describe('validateForJiraSync', () => {
234
+ beforeEach(() => {
235
+ // 環境変数をクリア
236
+ delete process.env.JIRA_ISSUE_TYPE_STORY;
237
+ delete process.env.JIRA_ISSUE_TYPE_SUBTASK;
238
+ });
239
+
234
240
  it('issueTypes.story設定がない場合はエラー', () => {
235
241
  const configPath = join(testProjectRoot, '.michi/config.json');
236
242
  writeFileSync(configPath, JSON.stringify({
@@ -0,0 +1,220 @@
1
+ /**
2
+ * spec-updater のテスト
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
6
+ import { mkdirSync, rmSync, existsSync, readFileSync, writeFileSync } from 'fs';
7
+ import { resolve } from 'path';
8
+ import {
9
+ loadSpecJson,
10
+ saveSpecJson,
11
+ updateSpecJsonAfterConfluenceSync,
12
+ updateSpecJsonAfterJiraSync,
13
+ type SpecJson
14
+ } from '../spec-updater.js';
15
+
16
+ const testDir = resolve(__dirname, '../../../.test-tmp');
17
+ const testFeatureName = 'test-feature';
18
+
19
+ describe('spec-updater', () => {
20
+ beforeEach(() => {
21
+ // テスト用ディレクトリを作成
22
+ if (existsSync(testDir)) {
23
+ rmSync(testDir, { recursive: true });
24
+ }
25
+ mkdirSync(resolve(testDir, '.kiro/specs', testFeatureName), { recursive: true });
26
+ });
27
+
28
+ afterEach(() => {
29
+ // テスト用ディレクトリを削除
30
+ if (existsSync(testDir)) {
31
+ rmSync(testDir, { recursive: true });
32
+ }
33
+ });
34
+
35
+ describe('loadSpecJson', () => {
36
+ it('spec.jsonが存在しない場合、最小限の構造を返す', () => {
37
+ const spec = loadSpecJson(testFeatureName, testDir);
38
+
39
+ expect(spec).toEqual({
40
+ featureName: testFeatureName,
41
+ confluence: {},
42
+ jira: {},
43
+ milestones: {}
44
+ });
45
+ });
46
+
47
+ it('spec.jsonが存在する場合、その内容を返す', () => {
48
+ const specPath = resolve(testDir, '.kiro/specs', testFeatureName, 'spec.json');
49
+ const testSpec: SpecJson = {
50
+ featureName: testFeatureName,
51
+ projectName: 'Test Project',
52
+ confluence: {
53
+ spaceKey: 'TEST'
54
+ },
55
+ jira: {},
56
+ milestones: {}
57
+ };
58
+
59
+ writeFileSync(specPath, JSON.stringify(testSpec, null, 2));
60
+
61
+ const spec = loadSpecJson(testFeatureName, testDir);
62
+ expect(spec).toEqual(testSpec);
63
+ });
64
+ });
65
+
66
+ describe('saveSpecJson', () => {
67
+ it('spec.jsonを保存し、lastUpdatedを追加する', () => {
68
+ const spec: SpecJson = {
69
+ featureName: testFeatureName,
70
+ confluence: {},
71
+ jira: {},
72
+ milestones: {}
73
+ };
74
+
75
+ saveSpecJson(testFeatureName, spec, testDir);
76
+
77
+ const specPath = resolve(testDir, '.kiro/specs', testFeatureName, 'spec.json');
78
+ expect(existsSync(specPath)).toBe(true);
79
+
80
+ const saved = JSON.parse(readFileSync(specPath, 'utf-8'));
81
+ expect(saved.featureName).toBe(testFeatureName);
82
+ expect(saved.lastUpdated).toBeDefined();
83
+ });
84
+ });
85
+
86
+ describe('updateSpecJsonAfterConfluenceSync', () => {
87
+ it('Confluence同期後にspec.jsonを正しく更新する(requirements)', () => {
88
+ updateSpecJsonAfterConfluenceSync(
89
+ testFeatureName,
90
+ 'requirements',
91
+ {
92
+ pageId: 'page123',
93
+ url: 'https://example.atlassian.net/wiki/spaces/TEST/pages/page123',
94
+ title: 'Test Requirements',
95
+ spaceKey: 'TEST'
96
+ },
97
+ testDir
98
+ );
99
+
100
+ const spec = loadSpecJson(testFeatureName, testDir);
101
+
102
+ expect(spec.confluence?.spaceKey).toBe('TEST');
103
+ expect(spec.confluence?.requirements).toEqual({
104
+ pageId: 'page123',
105
+ url: 'https://example.atlassian.net/wiki/spaces/TEST/pages/page123',
106
+ title: 'Test Requirements'
107
+ });
108
+ expect(spec.milestones?.requirementsCompleted).toBe(true);
109
+ });
110
+
111
+ it('Confluence同期後にspec.jsonを正しく更新する(design)', () => {
112
+ updateSpecJsonAfterConfluenceSync(
113
+ testFeatureName,
114
+ 'design',
115
+ {
116
+ pageId: 'page456',
117
+ url: 'https://example.atlassian.net/wiki/spaces/TEST/pages/page456',
118
+ title: 'Test Design',
119
+ spaceKey: 'TEST'
120
+ },
121
+ testDir
122
+ );
123
+
124
+ const spec = loadSpecJson(testFeatureName, testDir);
125
+
126
+ expect(spec.confluence?.design).toEqual({
127
+ pageId: 'page456',
128
+ url: 'https://example.atlassian.net/wiki/spaces/TEST/pages/page456',
129
+ title: 'Test Design'
130
+ });
131
+ expect(spec.milestones?.designCompleted).toBe(true);
132
+ });
133
+
134
+ it('Confluence同期後にspec.jsonを正しく更新する(tasks)', () => {
135
+ updateSpecJsonAfterConfluenceSync(
136
+ testFeatureName,
137
+ 'tasks',
138
+ {
139
+ pageId: 'page789',
140
+ url: 'https://example.atlassian.net/wiki/spaces/TEST/pages/page789',
141
+ title: 'Test Tasks',
142
+ spaceKey: 'TEST'
143
+ },
144
+ testDir
145
+ );
146
+
147
+ const spec = loadSpecJson(testFeatureName, testDir);
148
+
149
+ expect(spec.confluence?.tasks).toEqual({
150
+ pageId: 'page789',
151
+ url: 'https://example.atlassian.net/wiki/spaces/TEST/pages/page789',
152
+ title: 'Test Tasks'
153
+ });
154
+ expect(spec.milestones?.tasksCompleted).toBe(true);
155
+ });
156
+ });
157
+
158
+ describe('updateSpecJsonAfterJiraSync', () => {
159
+ it('JIRA同期後にspec.jsonを正しく更新する', () => {
160
+ updateSpecJsonAfterJiraSync(
161
+ testFeatureName,
162
+ {
163
+ projectKey: 'TEST',
164
+ epicKey: 'TEST-123',
165
+ epicUrl: 'https://example.atlassian.net/browse/TEST-123',
166
+ storyKeys: ['TEST-124', 'TEST-125', 'TEST-126']
167
+ },
168
+ testDir
169
+ );
170
+
171
+ const spec = loadSpecJson(testFeatureName, testDir);
172
+
173
+ expect(spec.jira).toEqual({
174
+ projectKey: 'TEST',
175
+ epicKey: 'TEST-123',
176
+ epicUrl: 'https://example.atlassian.net/browse/TEST-123',
177
+ storyKeys: ['TEST-124', 'TEST-125', 'TEST-126']
178
+ });
179
+ expect(spec.milestones?.jiraSyncCompleted).toBe(true);
180
+ });
181
+ });
182
+
183
+ describe('統合シナリオ', () => {
184
+ it('Confluence → JIRA の順に同期した場合、両方の情報が保持される', () => {
185
+ // 1. Confluence 同期(requirements)
186
+ updateSpecJsonAfterConfluenceSync(
187
+ testFeatureName,
188
+ 'requirements',
189
+ {
190
+ pageId: 'page123',
191
+ url: 'https://example.atlassian.net/wiki/spaces/TEST/pages/page123',
192
+ title: 'Test Requirements',
193
+ spaceKey: 'TEST'
194
+ },
195
+ testDir
196
+ );
197
+
198
+ // 2. JIRA 同期
199
+ updateSpecJsonAfterJiraSync(
200
+ testFeatureName,
201
+ {
202
+ projectKey: 'TEST',
203
+ epicKey: 'TEST-123',
204
+ epicUrl: 'https://example.atlassian.net/browse/TEST-123',
205
+ storyKeys: ['TEST-124']
206
+ },
207
+ testDir
208
+ );
209
+
210
+ // 両方の情報が保持されているか確認
211
+ const spec = loadSpecJson(testFeatureName, testDir);
212
+
213
+ expect(spec.confluence?.spaceKey).toBe('TEST');
214
+ expect(spec.confluence?.requirements).toBeDefined();
215
+ expect(spec.jira?.epicKey).toBe('TEST-123');
216
+ expect(spec.milestones?.requirementsCompleted).toBe(true);
217
+ expect(spec.milestones?.jiraSyncCompleted).toBe(true);
218
+ });
219
+ });
220
+ });
@@ -14,8 +14,9 @@ import type { ConfluenceConfig, ConfluencePageCreationGranularity } from '../con
14
14
  * ページ作成結果
15
15
  */
16
16
  export interface PageCreationResult {
17
+ id: string; // ページID(idとpageIdの両方をサポート)
18
+ pageId: string; // ページID(後方互換性のため)
17
19
  url: string;
18
- pageId: string;
19
20
  title: string;
20
21
  }
21
22
 
@@ -281,6 +282,7 @@ export async function createSinglePage(
281
282
  const baseUrl = process.env.ATLASSIAN_URL || '';
282
283
  return {
283
284
  pages: [{
285
+ id: page.id,
284
286
  url: `${baseUrl}/wiki${page._links.webui}`,
285
287
  pageId: page.id,
286
288
  title: pageTitle
@@ -351,6 +353,7 @@ export async function createBySectionPages(
351
353
 
352
354
  const baseUrl = process.env.ATLASSIAN_URL || '';
353
355
  pages.push({
356
+ id: page.id,
354
357
  url: `${baseUrl}/wiki${page._links.webui}`,
355
358
  pageId: page.id,
356
359
  title: pageTitle
@@ -474,6 +477,7 @@ export async function createByHierarchySimplePages(
474
477
  const baseUrl = process.env.ATLASSIAN_URL || '';
475
478
  return {
476
479
  pages: [{
480
+ id: childPage.id,
477
481
  url: `${baseUrl}/wiki${childPage._links.webui}`,
478
482
  pageId: childPage.id,
479
483
  title: childPageTitle
@@ -609,6 +613,7 @@ export async function createByHierarchyNestedPages(
609
613
 
610
614
  const baseUrl = process.env.ATLASSIAN_URL || '';
611
615
  pages.push({
616
+ id: sectionPage.id,
612
617
  url: `${baseUrl}/wiki${sectionPage._links.webui}`,
613
618
  pageId: sectionPage.id,
614
619
  title: sectionPageTitle
@@ -756,6 +761,7 @@ export async function createManualPages(
756
761
 
757
762
  const baseUrl = process.env.ATLASSIAN_URL || '';
758
763
  pages.push({
764
+ id: page.id,
759
765
  url: `${baseUrl}/wiki${page._links.webui}`,
760
766
  pageId: page.id,
761
767
  title: pageTitle