@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.
- package/CHANGELOG.md +55 -0
- package/README.md +205 -11
- package/dist/scripts/__tests__/create-project.test.d.ts +2 -0
- package/dist/scripts/__tests__/create-project.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/create-project.test.js +247 -0
- package/dist/scripts/__tests__/create-project.test.js.map +1 -0
- package/dist/scripts/__tests__/multi-project-estimate.test.d.ts +2 -0
- package/dist/scripts/__tests__/multi-project-estimate.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/multi-project-estimate.test.js +119 -0
- package/dist/scripts/__tests__/multi-project-estimate.test.js.map +1 -0
- package/dist/scripts/__tests__/setup-existing-project.test.d.ts +2 -0
- package/dist/scripts/__tests__/setup-existing-project.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/setup-existing-project.test.js +129 -0
- package/dist/scripts/__tests__/setup-existing-project.test.js.map +1 -0
- package/dist/scripts/__tests__/setup-interactive.test.d.ts +2 -0
- package/dist/scripts/__tests__/setup-interactive.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/setup-interactive.test.js +160 -0
- package/dist/scripts/__tests__/setup-interactive.test.js.map +1 -0
- package/dist/scripts/config/default-config.json +57 -0
- package/dist/scripts/confluence-sync.d.ts +4 -0
- package/dist/scripts/confluence-sync.d.ts.map +1 -1
- package/dist/scripts/confluence-sync.js +12 -23
- package/dist/scripts/confluence-sync.js.map +1 -1
- package/dist/scripts/constants/__tests__/environments.test.d.ts +2 -0
- package/dist/scripts/constants/__tests__/environments.test.d.ts.map +1 -0
- package/dist/scripts/constants/__tests__/environments.test.js +91 -0
- package/dist/scripts/constants/__tests__/environments.test.js.map +1 -0
- package/dist/scripts/constants/__tests__/languages.test.d.ts +2 -0
- package/dist/scripts/constants/__tests__/languages.test.d.ts.map +1 -0
- package/dist/scripts/constants/__tests__/languages.test.js +82 -0
- package/dist/scripts/constants/__tests__/languages.test.js.map +1 -0
- package/dist/scripts/constants/environments.d.ts +33 -0
- package/dist/scripts/constants/environments.d.ts.map +1 -0
- package/dist/scripts/constants/environments.js +49 -0
- package/dist/scripts/constants/environments.js.map +1 -0
- package/dist/scripts/constants/languages.d.ts +23 -0
- package/dist/scripts/constants/languages.d.ts.map +1 -0
- package/dist/scripts/constants/languages.js +53 -0
- package/dist/scripts/constants/languages.js.map +1 -0
- package/dist/scripts/create-project.d.ts +4 -0
- package/dist/scripts/create-project.d.ts.map +1 -1
- package/dist/scripts/create-project.js +227 -137
- package/dist/scripts/create-project.js.map +1 -1
- package/dist/scripts/jira-sync.d.ts.map +1 -1
- package/dist/scripts/jira-sync.js +15 -0
- package/dist/scripts/jira-sync.js.map +1 -1
- package/dist/scripts/list-projects.d.ts.map +1 -1
- package/dist/scripts/list-projects.js +42 -15
- package/dist/scripts/list-projects.js.map +1 -1
- package/dist/scripts/multi-project-estimate.d.ts.map +1 -1
- package/dist/scripts/multi-project-estimate.js +56 -21
- package/dist/scripts/multi-project-estimate.js.map +1 -1
- package/dist/scripts/resource-dashboard.d.ts.map +1 -1
- package/dist/scripts/resource-dashboard.js +74 -17
- package/dist/scripts/resource-dashboard.js.map +1 -1
- package/dist/scripts/setup-existing-project.d.ts +3 -1
- package/dist/scripts/setup-existing-project.d.ts.map +1 -1
- package/dist/scripts/setup-existing-project.js +306 -217
- package/dist/scripts/setup-existing-project.js.map +1 -1
- package/dist/scripts/setup-interactive.d.ts +10 -0
- package/dist/scripts/setup-interactive.d.ts.map +1 -0
- package/dist/scripts/setup-interactive.js +413 -0
- package/dist/scripts/setup-interactive.js.map +1 -0
- package/dist/scripts/template/__tests__/renderer.test.d.ts +2 -0
- package/dist/scripts/template/__tests__/renderer.test.d.ts.map +1 -0
- package/dist/scripts/template/__tests__/renderer.test.js +165 -0
- package/dist/scripts/template/__tests__/renderer.test.js.map +1 -0
- package/dist/scripts/template/renderer.d.ts +70 -0
- package/dist/scripts/template/renderer.d.ts.map +1 -0
- package/dist/scripts/template/renderer.js +99 -0
- package/dist/scripts/template/renderer.js.map +1 -0
- package/dist/scripts/utils/__tests__/config-validator.test.js +5 -0
- package/dist/scripts/utils/__tests__/config-validator.test.js.map +1 -1
- package/dist/scripts/utils/__tests__/spec-updater.test.d.ts +5 -0
- package/dist/scripts/utils/__tests__/spec-updater.test.d.ts.map +1 -0
- package/dist/scripts/utils/__tests__/spec-updater.test.js +158 -0
- package/dist/scripts/utils/__tests__/spec-updater.test.js.map +1 -0
- package/dist/scripts/utils/confluence-hierarchy.d.ts +2 -1
- package/dist/scripts/utils/confluence-hierarchy.d.ts.map +1 -1
- package/dist/scripts/utils/confluence-hierarchy.js +5 -0
- package/dist/scripts/utils/confluence-hierarchy.js.map +1 -1
- package/dist/scripts/utils/project-finder.d.ts +30 -0
- package/dist/scripts/utils/project-finder.d.ts.map +1 -0
- package/dist/scripts/utils/project-finder.js +147 -0
- package/dist/scripts/utils/project-finder.js.map +1 -0
- package/dist/scripts/utils/spec-updater.d.ts +72 -0
- package/dist/scripts/utils/spec-updater.d.ts.map +1 -0
- package/dist/scripts/utils/spec-updater.js +141 -0
- package/dist/scripts/utils/spec-updater.js.map +1 -0
- package/dist/scripts/utils/template-finder.d.ts +37 -0
- package/dist/scripts/utils/template-finder.d.ts.map +1 -0
- package/dist/scripts/utils/template-finder.js +63 -0
- package/dist/scripts/utils/template-finder.js.map +1 -0
- package/dist/src/__tests__/integration/setup/claude-agent.test.d.ts +5 -0
- package/dist/src/__tests__/integration/setup/claude-agent.test.d.ts.map +1 -0
- package/dist/src/__tests__/integration/setup/claude-agent.test.js +125 -0
- package/dist/src/__tests__/integration/setup/claude-agent.test.js.map +1 -0
- package/dist/src/__tests__/integration/setup/claude.test.d.ts +5 -0
- package/dist/src/__tests__/integration/setup/claude.test.d.ts.map +1 -0
- package/dist/src/__tests__/integration/setup/claude.test.js +111 -0
- package/dist/src/__tests__/integration/setup/claude.test.js.map +1 -0
- package/dist/src/__tests__/integration/setup/cursor.test.d.ts +5 -0
- package/dist/src/__tests__/integration/setup/cursor.test.d.ts.map +1 -0
- package/dist/src/__tests__/integration/setup/cursor.test.js +162 -0
- package/dist/src/__tests__/integration/setup/cursor.test.js.map +1 -0
- package/dist/src/__tests__/integration/setup/helpers/fs-assertions.d.ts +32 -0
- package/dist/src/__tests__/integration/setup/helpers/fs-assertions.d.ts.map +1 -0
- package/dist/src/__tests__/integration/setup/helpers/fs-assertions.js +72 -0
- package/dist/src/__tests__/integration/setup/helpers/fs-assertions.js.map +1 -0
- package/dist/src/__tests__/integration/setup/helpers/test-project.d.ts +38 -0
- package/dist/src/__tests__/integration/setup/helpers/test-project.d.ts.map +1 -0
- package/dist/src/__tests__/integration/setup/helpers/test-project.js +83 -0
- package/dist/src/__tests__/integration/setup/helpers/test-project.js.map +1 -0
- package/dist/src/__tests__/integration/setup/validation.test.d.ts +5 -0
- package/dist/src/__tests__/integration/setup/validation.test.d.ts.map +1 -0
- package/dist/src/__tests__/integration/setup/validation.test.js +318 -0
- package/dist/src/__tests__/integration/setup/validation.test.js.map +1 -0
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +20 -0
- package/dist/src/cli.js.map +1 -1
- package/dist/src/commands/setup-existing.d.ts +22 -0
- package/dist/src/commands/setup-existing.d.ts.map +1 -0
- package/dist/src/commands/setup-existing.js +408 -0
- package/dist/src/commands/setup-existing.js.map +1 -0
- package/dist/vitest.config.d.ts.map +1 -1
- package/dist/vitest.config.js +9 -6
- package/dist/vitest.config.js.map +1 -1
- package/docs/README.md +2 -2
- package/docs/contributing/development.md +37 -0
- package/docs/getting-started/{new-project-setup.md → new-repository-setup.md} +165 -38
- package/docs/getting-started/quick-start.md +57 -6
- package/docs/getting-started/setup.md +551 -180
- package/docs/guides/customization.md +4 -4
- package/docs/guides/multi-project.md +12 -9
- package/docs/reference/quick-reference.md +27 -18
- package/docs/testing/integration-tests.md +297 -0
- package/docs/testing-strategy.md +87 -0
- package/package.json +23 -6
- package/scripts/__tests__/create-project.test.ts +292 -0
- package/scripts/__tests__/multi-project-estimate.test.ts +145 -0
- package/scripts/__tests__/setup-existing-project.test.ts +147 -0
- package/scripts/__tests__/setup-interactive.test.ts +199 -0
- package/scripts/confluence-sync.ts +17 -29
- package/scripts/constants/__tests__/environments.test.ts +110 -0
- package/scripts/constants/__tests__/languages.test.ts +100 -0
- package/scripts/constants/environments.ts +61 -0
- package/scripts/constants/languages.ts +70 -0
- package/scripts/copy-static-assets.js +50 -0
- package/scripts/create-project.ts +251 -158
- package/scripts/jira-sync.ts +16 -1
- package/scripts/list-projects.ts +51 -24
- package/scripts/multi-project-estimate.ts +58 -22
- package/scripts/resource-dashboard.ts +91 -26
- package/scripts/setup-existing-project.ts +350 -230
- package/scripts/setup-existing.sh +159 -25
- package/scripts/setup-interactive.ts +565 -0
- package/scripts/template/__tests__/renderer.test.ts +207 -0
- package/scripts/template/renderer.ts +133 -0
- package/scripts/utils/__tests__/config-validator.test.ts +6 -0
- package/scripts/utils/__tests__/spec-updater.test.ts +220 -0
- package/scripts/utils/confluence-hierarchy.ts +7 -1
- package/scripts/utils/project-finder.ts +184 -0
- package/scripts/utils/spec-updater.ts +212 -0
- package/scripts/utils/template-finder.ts +75 -0
- package/templates/claude/commands/michi/confluence-sync.md +38 -0
- package/templates/claude/commands/michi/project-switch.md +36 -0
- package/templates/claude/rules/atlassian-integration.md +35 -0
- package/templates/claude/rules/michi-core.md +54 -0
- package/templates/claude-agent/README.md +25 -0
- package/templates/cursor/commands/michi/confluence-sync.md +76 -0
- package/templates/cursor/commands/michi/project-switch.md +69 -0
- package/templates/cursor/rules/atlassian-mcp.mdc +188 -0
- package/templates/cursor/rules/github-ssot.mdc +151 -0
- package/templates/cursor/rules/multi-project.mdc +81 -0
- 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
|