specweave 0.17.15 → 0.17.17
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/CLAUDE.md +405 -2495
- package/README.md +92 -2
- package/dist/locales/de/.gitkeep +0 -0
- package/dist/locales/de/cli.json +108 -0
- package/dist/locales/en/cli.json +287 -0
- package/dist/locales/en/errors.json +7 -0
- package/dist/locales/en/templates.json +6 -0
- package/dist/locales/es/.gitkeep +0 -0
- package/dist/locales/es/cli.json +41 -0
- package/dist/locales/fr/.gitkeep +0 -0
- package/dist/locales/fr/cli.json +108 -0
- package/dist/locales/ja/.gitkeep +0 -0
- package/dist/locales/ja/cli.json +108 -0
- package/dist/locales/ko/.gitkeep +0 -0
- package/dist/locales/ko/cli.json +108 -0
- package/dist/locales/pt/.gitkeep +0 -0
- package/dist/locales/pt/cli.json +108 -0
- package/dist/locales/ru/.gitkeep +0 -0
- package/dist/locales/ru/cli.json +269 -0
- package/dist/locales/zh/.gitkeep +0 -0
- package/dist/locales/zh/cli.json +108 -0
- package/dist/plugins/specweave/lib/hooks/sync-living-docs.d.ts.map +1 -1
- package/dist/plugins/specweave/lib/hooks/sync-living-docs.js +188 -36
- package/dist/plugins/specweave/lib/hooks/sync-living-docs.js.map +1 -1
- package/dist/plugins/specweave-ado/lib/ado-status-sync.d.ts +54 -0
- package/dist/plugins/specweave-ado/lib/ado-status-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-ado/lib/ado-status-sync.js +86 -0
- package/dist/plugins/specweave-ado/lib/ado-status-sync.js.map +1 -0
- package/dist/plugins/specweave-ado/lib/enhanced-ado-sync.d.ts +25 -0
- package/dist/plugins/specweave-ado/lib/enhanced-ado-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-ado/lib/enhanced-ado-sync.js +191 -0
- package/dist/plugins/specweave-ado/lib/enhanced-ado-sync.js.map +1 -0
- package/dist/plugins/specweave-github/lib/duplicate-detector.d.ts +139 -0
- package/dist/plugins/specweave-github/lib/duplicate-detector.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/duplicate-detector.js +389 -0
- package/dist/plugins/specweave-github/lib/duplicate-detector.js.map +1 -0
- package/dist/plugins/specweave-github/lib/enhanced-github-sync.d.ts +26 -0
- package/dist/plugins/specweave-github/lib/enhanced-github-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/enhanced-github-sync.js +249 -0
- package/dist/plugins/specweave-github/lib/enhanced-github-sync.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-client.d.ts +1 -1
- package/dist/plugins/specweave-github/lib/github-client.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-client.js +25 -13
- package/dist/plugins/specweave-github/lib/github-client.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-epic-sync.d.ts +83 -0
- package/dist/plugins/specweave-github/lib/github-epic-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-epic-sync.js +451 -0
- package/dist/plugins/specweave-github/lib/github-epic-sync.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-status-sync.d.ts +43 -0
- package/dist/plugins/specweave-github/lib/github-status-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-status-sync.js +82 -0
- package/dist/plugins/specweave-github/lib/github-status-sync.js.map +1 -0
- package/dist/plugins/specweave-github/lib/task-sync.d.ts +5 -0
- package/dist/plugins/specweave-github/lib/task-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/task-sync.js +38 -2
- package/dist/plugins/specweave-github/lib/task-sync.js.map +1 -1
- package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.d.ts +26 -0
- package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.js +195 -0
- package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.js.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-epic-sync.d.ts +66 -0
- package/dist/plugins/specweave-jira/lib/jira-epic-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-epic-sync.js +274 -0
- package/dist/plugins/specweave-jira/lib/jira-epic-sync.js.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-status-sync.d.ts +56 -0
- package/dist/plugins/specweave-jira/lib/jira-status-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-status-sync.js +93 -0
- package/dist/plugins/specweave-jira/lib/jira-status-sync.js.map +1 -0
- package/dist/spec-parser.js +629 -0
- package/dist/src/cli/helpers/issue-tracker/index.d.ts.map +1 -1
- package/dist/src/cli/helpers/issue-tracker/index.js +48 -3
- package/dist/src/cli/helpers/issue-tracker/index.js.map +1 -1
- package/dist/src/core/living-docs/hierarchy-mapper.d.ts +142 -0
- package/dist/src/core/living-docs/hierarchy-mapper.d.ts.map +1 -0
- package/dist/src/core/living-docs/hierarchy-mapper.js +453 -0
- package/dist/src/core/living-docs/hierarchy-mapper.js.map +1 -0
- package/dist/src/core/living-docs/index.d.ts +10 -84
- package/dist/src/core/living-docs/index.d.ts.map +1 -1
- package/dist/src/core/living-docs/index.js +10 -164
- package/dist/src/core/living-docs/index.js.map +1 -1
- package/dist/src/core/living-docs/spec-distributor.d.ts +106 -0
- package/dist/src/core/living-docs/spec-distributor.d.ts.map +1 -0
- package/dist/src/core/living-docs/spec-distributor.js +823 -0
- package/dist/src/core/living-docs/spec-distributor.js.map +1 -0
- package/dist/src/core/living-docs/types.d.ts +201 -0
- package/dist/src/core/living-docs/types.d.ts.map +1 -0
- package/dist/src/core/living-docs/types.js +15 -0
- package/dist/src/core/living-docs/types.js.map +1 -0
- package/dist/src/core/logging/prompt-logger.d.ts +70 -0
- package/dist/src/core/logging/prompt-logger.d.ts.map +1 -0
- package/dist/src/core/logging/prompt-logger.js +247 -0
- package/dist/src/core/logging/prompt-logger.js.map +1 -0
- package/dist/src/core/status-line/status-line-manager.d.ts +15 -24
- package/dist/src/core/status-line/status-line-manager.d.ts.map +1 -1
- package/dist/src/core/status-line/status-line-manager.js +33 -70
- package/dist/src/core/status-line/status-line-manager.js.map +1 -1
- package/dist/src/core/status-line/types.d.ts +19 -31
- package/dist/src/core/status-line/types.d.ts.map +1 -1
- package/dist/src/core/status-line/types.js +5 -9
- package/dist/src/core/status-line/types.js.map +1 -1
- package/dist/src/core/sync/conflict-resolver.d.ts +66 -0
- package/dist/src/core/sync/conflict-resolver.d.ts.map +1 -0
- package/dist/src/core/sync/conflict-resolver.js +108 -0
- package/dist/src/core/sync/conflict-resolver.js.map +1 -0
- package/dist/src/core/sync/enhanced-content-builder.d.ts +77 -0
- package/dist/src/core/sync/enhanced-content-builder.d.ts.map +1 -0
- package/dist/src/core/sync/enhanced-content-builder.js +199 -0
- package/dist/src/core/sync/enhanced-content-builder.js.map +1 -0
- package/dist/src/core/sync/label-detector.d.ts +66 -0
- package/dist/src/core/sync/label-detector.d.ts.map +1 -0
- package/dist/src/core/sync/label-detector.js +211 -0
- package/dist/src/core/sync/label-detector.js.map +1 -0
- package/dist/src/core/sync/retry-logic.d.ts +64 -0
- package/dist/src/core/sync/retry-logic.d.ts.map +1 -0
- package/dist/src/core/sync/retry-logic.js +165 -0
- package/dist/src/core/sync/retry-logic.js.map +1 -0
- package/dist/src/core/sync/spec-content-sync.d.ts +88 -0
- package/dist/src/core/sync/spec-content-sync.d.ts.map +1 -0
- package/dist/src/core/sync/spec-content-sync.js +5 -0
- package/dist/src/core/sync/spec-content-sync.js.map +1 -0
- package/dist/src/core/sync/spec-increment-mapper.d.ts +100 -0
- package/dist/src/core/sync/spec-increment-mapper.d.ts.map +1 -0
- package/dist/src/core/sync/spec-increment-mapper.js +424 -0
- package/dist/src/core/sync/spec-increment-mapper.js.map +1 -0
- package/dist/src/core/sync/status-cache.d.ts +91 -0
- package/dist/src/core/sync/status-cache.d.ts.map +1 -0
- package/dist/src/core/sync/status-cache.js +140 -0
- package/dist/src/core/sync/status-cache.js.map +1 -0
- package/dist/src/core/sync/status-mapper.d.ts +69 -0
- package/dist/src/core/sync/status-mapper.d.ts.map +1 -0
- package/dist/src/core/sync/status-mapper.js +90 -0
- package/dist/src/core/sync/status-mapper.js.map +1 -0
- package/dist/src/core/sync/status-sync-engine.d.ts +162 -0
- package/dist/src/core/sync/status-sync-engine.d.ts.map +1 -0
- package/dist/src/core/sync/status-sync-engine.js +347 -0
- package/dist/src/core/sync/status-sync-engine.js.map +1 -0
- package/dist/src/core/sync/sync-event-logger.d.ts +99 -0
- package/dist/src/core/sync/sync-event-logger.d.ts.map +1 -0
- package/dist/src/core/sync/sync-event-logger.js +103 -0
- package/dist/src/core/sync/sync-event-logger.js.map +1 -0
- package/dist/src/core/sync/workflow-detector.d.ts +95 -0
- package/dist/src/core/sync/workflow-detector.d.ts.map +1 -0
- package/dist/src/core/sync/workflow-detector.js +175 -0
- package/dist/src/core/sync/workflow-detector.js.map +1 -0
- package/dist/src/core/types/config.d.ts.map +1 -1
- package/dist/src/core/types/config.js +31 -0
- package/dist/src/core/types/config.js.map +1 -1
- package/dist/src/utils/github-url.d.ts +53 -0
- package/dist/src/utils/github-url.d.ts.map +1 -0
- package/dist/src/utils/github-url.js +90 -0
- package/dist/src/utils/github-url.js.map +1 -0
- package/dist/src/utils/plugin-validator.d.ts +9 -0
- package/dist/src/utils/plugin-validator.d.ts.map +1 -1
- package/dist/src/utils/plugin-validator.js +86 -19
- package/dist/src/utils/plugin-validator.js.map +1 -1
- package/dist/src/utils/spec-parser.d.ts +145 -0
- package/dist/src/utils/spec-parser.d.ts.map +1 -0
- package/dist/src/utils/spec-parser.js +640 -0
- package/dist/src/utils/spec-parser.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +1 -1
- package/plugins/specweave/agents/pm/AGENT.md +1 -1
- package/plugins/specweave/agents/pm/templates/increment-spec.md +158 -0
- package/plugins/specweave/agents/pm/templates/living-docs-spec.md +113 -0
- package/plugins/specweave/commands/specweave-done.md +163 -0
- package/plugins/specweave/hooks/lib/update-status-line.sh +79 -111
- package/plugins/specweave/hooks/post-increment-planning.sh +107 -35
- package/plugins/specweave/lib/hooks/sync-living-docs.js +139 -34
- package/plugins/specweave/lib/hooks/sync-living-docs.ts +234 -38
- package/plugins/specweave/skills/SKILLS-INDEX.md +4 -24
- package/plugins/specweave/skills/increment-planner/SKILL.md +94 -0
- package/plugins/specweave/skills/increment-work-router/SKILL.md +466 -0
- package/plugins/specweave/skills/plugin-validator/SKILL.md +16 -13
- package/plugins/specweave-ado/lib/ado-status-sync.js +80 -0
- package/plugins/specweave-ado/lib/ado-status-sync.ts +121 -0
- package/plugins/specweave-ado/lib/enhanced-ado-sync.js +170 -0
- package/plugins/specweave-github/commands/specweave-github-cleanup-duplicates.md +205 -0
- package/plugins/specweave-github/commands/specweave-github-sync-epic.md +248 -0
- package/plugins/specweave-github/lib/duplicate-detector.js +370 -0
- package/plugins/specweave-github/lib/duplicate-detector.ts +525 -0
- package/plugins/specweave-github/lib/enhanced-github-sync.js +220 -0
- package/plugins/specweave-github/lib/enhanced-github-sync.ts +322 -0
- package/plugins/specweave-github/lib/github-client.js +21 -10
- package/plugins/specweave-github/lib/github-client.ts +27 -16
- package/plugins/specweave-github/lib/github-epic-sync.js +489 -0
- package/plugins/specweave-github/lib/github-epic-sync.ts +690 -0
- package/plugins/specweave-github/lib/github-status-sync.js +71 -0
- package/plugins/specweave-github/lib/github-status-sync.ts +107 -0
- package/plugins/specweave-github/lib/task-sync.js +33 -2
- package/plugins/specweave-github/lib/task-sync.ts +44 -2
- package/plugins/specweave-jira/commands/specweave-jira-sync-epic.md +267 -0
- package/plugins/specweave-jira/lib/enhanced-jira-sync.ts.disabled +222 -0
- package/plugins/specweave-jira/lib/jira-epic-sync.js +304 -0
- package/plugins/specweave-jira/lib/jira-epic-sync.ts +459 -0
- package/plugins/specweave-jira/lib/jira-status-sync.js +79 -0
- package/plugins/specweave-jira/lib/jira-status-sync.ts +139 -0
- package/src/templates/AGENTS.md.template +88 -1
- package/src/templates/CLAUDE.md.template +49 -0
- package/plugins/specweave/skills/increment-quality-judge/SKILL.md +0 -524
- package/plugins/specweave/skills/plugin-installer/SKILL.md +0 -353
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced JIRA Spec Content Sync
|
|
3
|
+
*
|
|
4
|
+
* Uses EnhancedContentBuilder and SpecIncrementMapper for rich epic descriptions.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { JiraClientV2 } from './jira-client-v2.js';
|
|
8
|
+
import { EnhancedContentBuilder, EnhancedSpecContent } from '../../../src/core/sync/enhanced-content-builder.js';
|
|
9
|
+
import { SpecIncrementMapper, TaskInfo } from '../../../src/core/sync/spec-increment-mapper.js';
|
|
10
|
+
import { parseSpecContent } from '../../../src/core/spec-content-sync.js';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import fs from 'fs/promises';
|
|
13
|
+
|
|
14
|
+
export interface EnhancedJiraSyncOptions {
|
|
15
|
+
specPath: string;
|
|
16
|
+
domain?: string;
|
|
17
|
+
project?: string;
|
|
18
|
+
dryRun?: boolean;
|
|
19
|
+
verbose?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface EnhancedJiraSyncResult {
|
|
23
|
+
success: boolean;
|
|
24
|
+
action: 'created' | 'updated' | 'no-change' | 'error';
|
|
25
|
+
epicKey?: string;
|
|
26
|
+
epicUrl?: string;
|
|
27
|
+
error?: string;
|
|
28
|
+
tasksLinked?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Enhanced sync with rich content including task mappings
|
|
33
|
+
*/
|
|
34
|
+
export async function syncSpecToJiraWithEnhancedContent(
|
|
35
|
+
options: EnhancedJiraSyncOptions
|
|
36
|
+
): Promise<EnhancedJiraSyncResult> {
|
|
37
|
+
const { specPath, domain, project, dryRun = false, verbose = false } = options;
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
// 1. Parse spec content
|
|
41
|
+
const baseSpec = await parseSpecContent(specPath);
|
|
42
|
+
if (!baseSpec) {
|
|
43
|
+
return {
|
|
44
|
+
success: false,
|
|
45
|
+
action: 'error',
|
|
46
|
+
error: 'Failed to parse spec content',
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (verbose) {
|
|
51
|
+
console.log(`📄 Parsed spec: ${baseSpec.identifier.compact}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 2. Build enhanced spec with task mappings
|
|
55
|
+
const specId = baseSpec.identifier.full || baseSpec.identifier.compact;
|
|
56
|
+
const rootDir = await findSpecWeaveRoot(specPath);
|
|
57
|
+
const mapper = new SpecIncrementMapper(rootDir);
|
|
58
|
+
const mapping = await mapper.mapSpecToIncrements(specId);
|
|
59
|
+
|
|
60
|
+
if (verbose) {
|
|
61
|
+
console.log(`🔗 Found ${mapping.increments.length} related increments`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 3. Build enhanced spec content
|
|
65
|
+
const taskMapping = buildTaskMapping(mapping.increments);
|
|
66
|
+
const architectureDocs = await findArchitectureDocs(rootDir, specId);
|
|
67
|
+
|
|
68
|
+
const enhancedSpec: EnhancedSpecContent = {
|
|
69
|
+
...baseSpec,
|
|
70
|
+
summary: baseSpec.description,
|
|
71
|
+
taskMapping,
|
|
72
|
+
architectureDocs
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// 4. Build external description
|
|
76
|
+
const builder = new EnhancedContentBuilder();
|
|
77
|
+
const description = builder.buildExternalDescription(enhancedSpec);
|
|
78
|
+
|
|
79
|
+
if (verbose) {
|
|
80
|
+
console.log(`📝 Generated description: ${description.length} characters`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (dryRun) {
|
|
84
|
+
console.log('🔍 DRY RUN - Would create/update epic with:');
|
|
85
|
+
console.log(` Summary: ${baseSpec.title}`);
|
|
86
|
+
console.log(` Description length: ${description.length}`);
|
|
87
|
+
return {
|
|
88
|
+
success: true,
|
|
89
|
+
action: 'no-change',
|
|
90
|
+
tasksLinked: taskMapping?.tasks.length || 0
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 5. Create or update JIRA epic
|
|
95
|
+
if (!domain || !project) {
|
|
96
|
+
return {
|
|
97
|
+
success: false,
|
|
98
|
+
action: 'error',
|
|
99
|
+
error: 'JIRA domain/project not specified',
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const client = new JiraClientV2({ domain, project });
|
|
104
|
+
|
|
105
|
+
// Check if epic already exists
|
|
106
|
+
const existingEpic = await findExistingEpic(client, baseSpec.identifier.compact);
|
|
107
|
+
|
|
108
|
+
let result: EnhancedJiraSyncResult;
|
|
109
|
+
|
|
110
|
+
if (existingEpic) {
|
|
111
|
+
// Update existing epic
|
|
112
|
+
await client.updateEpic(existingEpic.key, {
|
|
113
|
+
summary: `[${baseSpec.identifier.compact}] ${baseSpec.title}`,
|
|
114
|
+
description
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
result = {
|
|
118
|
+
success: true,
|
|
119
|
+
action: 'updated',
|
|
120
|
+
epicKey: existingEpic.key,
|
|
121
|
+
epicUrl: `https://${domain}/browse/${existingEpic.key}`,
|
|
122
|
+
tasksLinked: taskMapping?.tasks.length || 0
|
|
123
|
+
};
|
|
124
|
+
} else {
|
|
125
|
+
// Create new epic
|
|
126
|
+
const epic = await client.createEpic({
|
|
127
|
+
summary: `[${baseSpec.identifier.compact}] ${baseSpec.title}`,
|
|
128
|
+
description,
|
|
129
|
+
labels: ['spec', 'external-tool-sync']
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
result = {
|
|
133
|
+
success: true,
|
|
134
|
+
action: 'created',
|
|
135
|
+
epicKey: epic.key,
|
|
136
|
+
epicUrl: `https://${domain}/browse/${epic.key}`,
|
|
137
|
+
tasksLinked: taskMapping?.tasks.length || 0
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (verbose) {
|
|
142
|
+
console.log(`✅ ${result.action === 'created' ? 'Created' : 'Updated'} epic ${result.epicKey}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return result;
|
|
146
|
+
} catch (error: any) {
|
|
147
|
+
return {
|
|
148
|
+
success: false,
|
|
149
|
+
action: 'error',
|
|
150
|
+
error: error.message
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Helper functions (similar to GitHub sync)
|
|
156
|
+
|
|
157
|
+
async function findSpecWeaveRoot(specPath: string): Promise<string> {
|
|
158
|
+
let currentDir = path.dirname(specPath);
|
|
159
|
+
|
|
160
|
+
while (true) {
|
|
161
|
+
const specweaveDir = path.join(currentDir, '.specweave');
|
|
162
|
+
try {
|
|
163
|
+
await fs.access(specweaveDir);
|
|
164
|
+
return currentDir;
|
|
165
|
+
} catch {
|
|
166
|
+
const parentDir = path.dirname(currentDir);
|
|
167
|
+
if (parentDir === currentDir) {
|
|
168
|
+
throw new Error('.specweave directory not found');
|
|
169
|
+
}
|
|
170
|
+
currentDir = parentDir;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function buildTaskMapping(increments: any[]): any {
|
|
176
|
+
if (increments.length === 0) return undefined;
|
|
177
|
+
|
|
178
|
+
const firstIncrement = increments[0];
|
|
179
|
+
const tasks = firstIncrement.tasks.map((task: TaskInfo) => ({
|
|
180
|
+
id: task.id,
|
|
181
|
+
title: task.title,
|
|
182
|
+
userStories: task.userStories
|
|
183
|
+
}));
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
incrementId: firstIncrement.id,
|
|
187
|
+
tasks,
|
|
188
|
+
tasksUrl: `tasks.md` // JIRA doesn't support external links in same way
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function findArchitectureDocs(rootDir: string, specId: string): Promise<any[]> {
|
|
193
|
+
const docs: any[] = [];
|
|
194
|
+
const archDir = path.join(rootDir, '.specweave/docs/internal/architecture');
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
const adrDir = path.join(archDir, 'adr');
|
|
198
|
+
try {
|
|
199
|
+
const adrs = await fs.readdir(adrDir);
|
|
200
|
+
const relatedAdrs = adrs.filter(file => file.includes(specId.replace('spec-', '')));
|
|
201
|
+
|
|
202
|
+
for (const adr of relatedAdrs) {
|
|
203
|
+
docs.push({
|
|
204
|
+
type: 'adr',
|
|
205
|
+
path: path.join(adrDir, adr),
|
|
206
|
+
title: adr.replace('.md', '').replace(/-/g, ' ')
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
} catch {}
|
|
210
|
+
} catch {}
|
|
211
|
+
|
|
212
|
+
return docs;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function findExistingEpic(client: JiraClientV2, specId: string): Promise<any | null> {
|
|
216
|
+
try {
|
|
217
|
+
const epics = await client.searchEpics(`summary ~ "[${specId}]"`);
|
|
218
|
+
return epics[0] || null;
|
|
219
|
+
} catch {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import * as fs from "fs-extra";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as yaml from "yaml";
|
|
4
|
+
class JiraEpicSync {
|
|
5
|
+
constructor(client, specsDir, projectKey) {
|
|
6
|
+
this.client = client;
|
|
7
|
+
this.specsDir = specsDir;
|
|
8
|
+
this.projectKey = projectKey;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Sync Epic folder to JIRA (Epic + Stories)
|
|
12
|
+
*/
|
|
13
|
+
async syncEpicToJira(epicId) {
|
|
14
|
+
console.log(`
|
|
15
|
+
\u{1F504} Syncing Epic ${epicId} to JIRA...`);
|
|
16
|
+
const epicFolder = await this.findEpicFolder(epicId);
|
|
17
|
+
if (!epicFolder) {
|
|
18
|
+
throw new Error(`Epic ${epicId} not found in ${this.specsDir}`);
|
|
19
|
+
}
|
|
20
|
+
const readmePath = path.join(epicFolder, "README.md");
|
|
21
|
+
const epicData = await this.parseEpicReadme(readmePath);
|
|
22
|
+
console.log(` \u{1F4E6} Epic: ${epicData.title}`);
|
|
23
|
+
console.log(` \u{1F4CA} Increments: ${epicData.total_increments}`);
|
|
24
|
+
let epicKey = epicData.external_tools.jira.key;
|
|
25
|
+
let epicUrl = epicData.external_tools.jira.url;
|
|
26
|
+
if (!epicKey) {
|
|
27
|
+
console.log(` \u{1F680} Creating JIRA Epic...`);
|
|
28
|
+
const epic = await this.createEpic(epicData);
|
|
29
|
+
epicKey = epic.key;
|
|
30
|
+
epicUrl = epic.url;
|
|
31
|
+
console.log(` \u2705 Created Epic ${epicKey}`);
|
|
32
|
+
await this.updateEpicReadme(readmePath, {
|
|
33
|
+
type: "epic",
|
|
34
|
+
key: epicKey,
|
|
35
|
+
url: epicUrl
|
|
36
|
+
});
|
|
37
|
+
} else {
|
|
38
|
+
console.log(` \u267B\uFE0F Updating existing Epic ${epicKey}...`);
|
|
39
|
+
await this.updateEpic(epicKey, epicData);
|
|
40
|
+
console.log(` \u2705 Updated Epic ${epicKey}`);
|
|
41
|
+
}
|
|
42
|
+
let storiesCreated = 0;
|
|
43
|
+
let storiesUpdated = 0;
|
|
44
|
+
console.log(`
|
|
45
|
+
\u{1F4DD} Syncing ${epicData.increments.length} increments...`);
|
|
46
|
+
for (const increment of epicData.increments) {
|
|
47
|
+
const incrementFile = path.join(epicFolder, `${increment.id}.md`);
|
|
48
|
+
if (!await fs.pathExists(incrementFile)) {
|
|
49
|
+
console.log(` \u26A0\uFE0F Increment file not found: ${increment.id}.md`);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const incrementData = await this.parseIncrementFile(incrementFile);
|
|
53
|
+
const existingStory = increment.external.jira;
|
|
54
|
+
if (!existingStory) {
|
|
55
|
+
const storyKey = await this.createStory(
|
|
56
|
+
epicData.id,
|
|
57
|
+
incrementData,
|
|
58
|
+
epicKey
|
|
59
|
+
);
|
|
60
|
+
storiesCreated++;
|
|
61
|
+
console.log(` \u2705 Created Story ${storyKey} for ${increment.id}`);
|
|
62
|
+
await this.updateIncrementExternalLink(
|
|
63
|
+
readmePath,
|
|
64
|
+
incrementFile,
|
|
65
|
+
increment.id,
|
|
66
|
+
storyKey
|
|
67
|
+
);
|
|
68
|
+
} else {
|
|
69
|
+
await this.updateStory(
|
|
70
|
+
epicData.id,
|
|
71
|
+
existingStory,
|
|
72
|
+
incrementData,
|
|
73
|
+
epicKey
|
|
74
|
+
);
|
|
75
|
+
storiesUpdated++;
|
|
76
|
+
console.log(` \u267B\uFE0F Updated Story ${existingStory} for ${increment.id}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
console.log(`
|
|
80
|
+
\u2705 Epic sync complete!`);
|
|
81
|
+
console.log(` Epic: ${epicUrl}`);
|
|
82
|
+
console.log(` Stories created: ${storiesCreated}`);
|
|
83
|
+
console.log(` Stories updated: ${storiesUpdated}`);
|
|
84
|
+
return {
|
|
85
|
+
epicKey,
|
|
86
|
+
epicUrl,
|
|
87
|
+
storiesCreated,
|
|
88
|
+
storiesUpdated
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Find Epic folder by ID (FS-001 or just 001)
|
|
93
|
+
*/
|
|
94
|
+
async findEpicFolder(epicId) {
|
|
95
|
+
const normalizedId = epicId.startsWith("FS-") ? epicId : `FS-${epicId.padStart(3, "0")}`;
|
|
96
|
+
const folders = await fs.readdir(this.specsDir);
|
|
97
|
+
for (const folder of folders) {
|
|
98
|
+
if (folder.startsWith(normalizedId)) {
|
|
99
|
+
return path.join(this.specsDir, folder);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Parse Epic README.md to extract frontmatter
|
|
106
|
+
*/
|
|
107
|
+
async parseEpicReadme(readmePath) {
|
|
108
|
+
const content = await fs.readFile(readmePath, "utf-8");
|
|
109
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
110
|
+
if (!match) {
|
|
111
|
+
throw new Error("Epic README.md missing YAML frontmatter");
|
|
112
|
+
}
|
|
113
|
+
const frontmatter = yaml.parse(match[1]);
|
|
114
|
+
return frontmatter;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Parse increment file to extract title and overview
|
|
118
|
+
*/
|
|
119
|
+
async parseIncrementFile(filePath) {
|
|
120
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
121
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
122
|
+
let frontmatter = { id: "", epic: "" };
|
|
123
|
+
let bodyContent = content;
|
|
124
|
+
if (match) {
|
|
125
|
+
frontmatter = yaml.parse(match[1]);
|
|
126
|
+
bodyContent = content.slice(match[0].length).trim();
|
|
127
|
+
}
|
|
128
|
+
const titleMatch = bodyContent.match(/^#\s+(.+)$/m);
|
|
129
|
+
const title = titleMatch ? titleMatch[1].trim() : frontmatter.id || path.basename(filePath, ".md");
|
|
130
|
+
const overviewMatch = bodyContent.match(/^#[^\n]+\n+([^\n]+)/);
|
|
131
|
+
const overview = overviewMatch ? overviewMatch[1].trim() : "No overview available";
|
|
132
|
+
return {
|
|
133
|
+
id: frontmatter.id,
|
|
134
|
+
title,
|
|
135
|
+
overview,
|
|
136
|
+
content: bodyContent,
|
|
137
|
+
frontmatter
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Create JIRA Epic
|
|
142
|
+
*/
|
|
143
|
+
async createEpic(epic) {
|
|
144
|
+
const summary = `[${epic.id}] ${epic.title}`;
|
|
145
|
+
const description = `Epic: ${epic.title}
|
|
146
|
+
|
|
147
|
+
Progress: ${epic.completed_increments}/${epic.total_increments} increments (${epic.progress})
|
|
148
|
+
|
|
149
|
+
Priority: ${epic.priority}
|
|
150
|
+
Status: ${epic.status}`;
|
|
151
|
+
const issueData = {
|
|
152
|
+
issueType: "Epic",
|
|
153
|
+
summary,
|
|
154
|
+
description,
|
|
155
|
+
priority: this.mapPriorityToJira(epic.priority),
|
|
156
|
+
labels: ["epic-sync", epic.id.toLowerCase()]
|
|
157
|
+
};
|
|
158
|
+
const issue = await this.client.createIssue(issueData, this.projectKey);
|
|
159
|
+
return {
|
|
160
|
+
key: issue.key,
|
|
161
|
+
url: issue.self.replace("/rest/api/3/issue/", "/browse/")
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Update JIRA Epic
|
|
166
|
+
*/
|
|
167
|
+
async updateEpic(epicKey, epic) {
|
|
168
|
+
const summary = `[${epic.id}] ${epic.title}`;
|
|
169
|
+
const description = `Epic: ${epic.title}
|
|
170
|
+
|
|
171
|
+
Progress: ${epic.completed_increments}/${epic.total_increments} increments (${epic.progress})
|
|
172
|
+
|
|
173
|
+
Priority: ${epic.priority}
|
|
174
|
+
Status: ${epic.status}`;
|
|
175
|
+
await this.client.updateIssue({
|
|
176
|
+
key: epicKey,
|
|
177
|
+
summary,
|
|
178
|
+
description,
|
|
179
|
+
priority: this.mapPriorityToJira(epic.priority),
|
|
180
|
+
labels: ["epic-sync", epic.id.toLowerCase()]
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Create JIRA Story for increment
|
|
185
|
+
*/
|
|
186
|
+
async createStory(epicId, increment, epicKey) {
|
|
187
|
+
const summary = `[${epicId}] ${increment.title}`;
|
|
188
|
+
const description = `${increment.overview}
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
**Increment**: ${increment.id}
|
|
193
|
+
**Epic**: ${epicId} (${epicKey})
|
|
194
|
+
|
|
195
|
+
\u{1F916} Auto-created by SpecWeave Epic Sync`;
|
|
196
|
+
const issueData = {
|
|
197
|
+
issueType: "Story",
|
|
198
|
+
summary,
|
|
199
|
+
description,
|
|
200
|
+
epicKey,
|
|
201
|
+
// Link to Epic via Epic Link field
|
|
202
|
+
labels: ["increment", "epic-sync"]
|
|
203
|
+
};
|
|
204
|
+
const issue = await this.client.createIssue(issueData, this.projectKey);
|
|
205
|
+
return issue.key;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Update JIRA Story for increment
|
|
209
|
+
*/
|
|
210
|
+
async updateStory(epicId, storyKey, increment, epicKey) {
|
|
211
|
+
const summary = `[${epicId}] ${increment.title}`;
|
|
212
|
+
const description = `${increment.overview}
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
**Increment**: ${increment.id}
|
|
217
|
+
**Epic**: ${epicId} (${epicKey})
|
|
218
|
+
|
|
219
|
+
\u{1F916} Auto-updated by SpecWeave Epic Sync`;
|
|
220
|
+
await this.client.updateIssue({
|
|
221
|
+
key: storyKey,
|
|
222
|
+
summary,
|
|
223
|
+
description,
|
|
224
|
+
labels: ["increment", "epic-sync"]
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Map SpecWeave priority to JIRA priority
|
|
229
|
+
*/
|
|
230
|
+
mapPriorityToJira(priority) {
|
|
231
|
+
const map = {
|
|
232
|
+
P0: "Highest",
|
|
233
|
+
P1: "High",
|
|
234
|
+
P2: "Medium",
|
|
235
|
+
P3: "Low"
|
|
236
|
+
};
|
|
237
|
+
return map[priority] || "Medium";
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Update Epic README.md with JIRA Epic key
|
|
241
|
+
*/
|
|
242
|
+
async updateEpicReadme(readmePath, jira) {
|
|
243
|
+
const content = await fs.readFile(readmePath, "utf-8");
|
|
244
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
245
|
+
if (!match) {
|
|
246
|
+
throw new Error("Epic README.md missing YAML frontmatter");
|
|
247
|
+
}
|
|
248
|
+
const frontmatter = yaml.parse(match[1]);
|
|
249
|
+
frontmatter.external_tools.jira = jira;
|
|
250
|
+
const newFrontmatter = yaml.stringify(frontmatter);
|
|
251
|
+
const newContent = content.replace(
|
|
252
|
+
/^---\n[\s\S]*?\n---/,
|
|
253
|
+
`---
|
|
254
|
+
${newFrontmatter}---`
|
|
255
|
+
);
|
|
256
|
+
await fs.writeFile(readmePath, newContent, "utf-8");
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Update increment external link in both Epic README and increment file
|
|
260
|
+
*/
|
|
261
|
+
async updateIncrementExternalLink(readmePath, incrementFile, incrementId, storyKey) {
|
|
262
|
+
const storyUrl = `https://${this.client["credentials"].domain}/browse/${storyKey}`;
|
|
263
|
+
const readmeContent = await fs.readFile(readmePath, "utf-8");
|
|
264
|
+
const readmeMatch = readmeContent.match(/^---\n([\s\S]*?)\n---/);
|
|
265
|
+
if (readmeMatch) {
|
|
266
|
+
const frontmatter = yaml.parse(readmeMatch[1]);
|
|
267
|
+
const increment = frontmatter.increments.find(
|
|
268
|
+
(inc) => inc.id === incrementId
|
|
269
|
+
);
|
|
270
|
+
if (increment) {
|
|
271
|
+
increment.external.jira = storyKey;
|
|
272
|
+
const newFrontmatter = yaml.stringify(frontmatter);
|
|
273
|
+
const newContent = readmeContent.replace(
|
|
274
|
+
/^---\n[\s\S]*?\n---/,
|
|
275
|
+
`---
|
|
276
|
+
${newFrontmatter}---`
|
|
277
|
+
);
|
|
278
|
+
await fs.writeFile(readmePath, newContent, "utf-8");
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const incrementContent = await fs.readFile(incrementFile, "utf-8");
|
|
282
|
+
const incrementMatch = incrementContent.match(/^---\n([\s\S]*?)\n---/);
|
|
283
|
+
if (incrementMatch) {
|
|
284
|
+
const frontmatter = yaml.parse(incrementMatch[1]);
|
|
285
|
+
if (!frontmatter.external) {
|
|
286
|
+
frontmatter.external = {};
|
|
287
|
+
}
|
|
288
|
+
frontmatter.external.jira = {
|
|
289
|
+
story: storyKey,
|
|
290
|
+
url: storyUrl
|
|
291
|
+
};
|
|
292
|
+
const newFrontmatter = yaml.stringify(frontmatter);
|
|
293
|
+
const newContent = incrementContent.replace(
|
|
294
|
+
/^---\n[\s\S]*?\n---/,
|
|
295
|
+
`---
|
|
296
|
+
${newFrontmatter}---`
|
|
297
|
+
);
|
|
298
|
+
await fs.writeFile(incrementFile, newContent, "utf-8");
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
export {
|
|
303
|
+
JiraEpicSync
|
|
304
|
+
};
|