specweave 0.28.17 → 0.28.20
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/dist/plugins/specweave-ado/lib/ado-board-resolver.d.ts +94 -0
- package/dist/plugins/specweave-ado/lib/ado-board-resolver.d.ts.map +1 -0
- package/dist/plugins/specweave-ado/lib/ado-board-resolver.js +219 -0
- package/dist/plugins/specweave-ado/lib/ado-board-resolver.js.map +1 -0
- package/dist/plugins/specweave-ado/lib/ado-spec-sync.d.ts +16 -0
- package/dist/plugins/specweave-ado/lib/ado-spec-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-ado/lib/ado-spec-sync.js +63 -3
- package/dist/plugins/specweave-ado/lib/ado-spec-sync.js.map +1 -1
- package/dist/plugins/specweave-ado/lib/ado-status-sync.d.ts +12 -3
- package/dist/plugins/specweave-ado/lib/ado-status-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-ado/lib/ado-status-sync.js +37 -3
- package/dist/plugins/specweave-ado/lib/ado-status-sync.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts +6 -11
- package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-feature-sync.js +6 -11
- package/dist/plugins/specweave-github/lib/github-feature-sync.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-increment-sync-cli.d.ts +21 -0
- package/dist/plugins/specweave-github/lib/github-increment-sync-cli.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-increment-sync-cli.js +445 -0
- package/dist/plugins/specweave-github/lib/github-increment-sync-cli.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-status-sync.d.ts +10 -0
- package/dist/plugins/specweave-github/lib/github-status-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-status-sync.js +40 -2
- package/dist/plugins/specweave-github/lib/github-status-sync.js.map +1 -1
- package/dist/plugins/specweave-github/lib/increment-issue-builder.d.ts +94 -0
- package/dist/plugins/specweave-github/lib/increment-issue-builder.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/increment-issue-builder.js +369 -0
- package/dist/plugins/specweave-github/lib/increment-issue-builder.js.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-board-resolver.d.ts +50 -0
- package/dist/plugins/specweave-jira/lib/jira-board-resolver.d.ts.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-board-resolver.js +84 -0
- package/dist/plugins/specweave-jira/lib/jira-board-resolver.js.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-spec-sync.d.ts +12 -0
- package/dist/plugins/specweave-jira/lib/jira-spec-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-spec-sync.js +57 -5
- package/dist/plugins/specweave-jira/lib/jira-spec-sync.js.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-status-sync.d.ts +5 -1
- package/dist/plugins/specweave-jira/lib/jira-status-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-status-sync.js +12 -4
- package/dist/plugins/specweave-jira/lib/jira-status-sync.js.map +1 -1
- package/dist/src/cli/commands/import-external.d.ts.map +1 -1
- package/dist/src/cli/commands/import-external.js +12 -7
- package/dist/src/cli/commands/import-external.js.map +1 -1
- package/dist/src/cli/helpers/init/external-import.d.ts.map +1 -1
- package/dist/src/cli/helpers/init/external-import.js +308 -36
- package/dist/src/cli/helpers/init/external-import.js.map +1 -1
- package/dist/src/cli/helpers/init/jira-ado-auto-detect.d.ts +115 -0
- package/dist/src/cli/helpers/init/jira-ado-auto-detect.d.ts.map +1 -0
- package/dist/src/cli/helpers/init/jira-ado-auto-detect.js +590 -0
- package/dist/src/cli/helpers/init/jira-ado-auto-detect.js.map +1 -0
- package/dist/src/cli/helpers/issue-tracker/ado-area-selection.d.ts +65 -0
- package/dist/src/cli/helpers/issue-tracker/ado-area-selection.d.ts.map +1 -0
- package/dist/src/cli/helpers/issue-tracker/ado-area-selection.js +278 -0
- package/dist/src/cli/helpers/issue-tracker/ado-area-selection.js.map +1 -0
- package/dist/src/cli/helpers/issue-tracker/jira-board-selection.d.ts +64 -0
- package/dist/src/cli/helpers/issue-tracker/jira-board-selection.d.ts.map +1 -0
- package/dist/src/cli/helpers/issue-tracker/jira-board-selection.js +251 -0
- package/dist/src/cli/helpers/issue-tracker/jira-board-selection.js.map +1 -0
- package/dist/src/config/types.d.ts +6 -6
- package/dist/src/core/ac-test-validator-cli.js +4 -1
- package/dist/src/core/ac-test-validator-cli.js.map +1 -1
- package/dist/src/core/ac-test-validator.d.ts.map +1 -1
- package/dist/src/core/ac-test-validator.js +4 -1
- package/dist/src/core/ac-test-validator.js.map +1 -1
- package/dist/src/core/background/index.d.ts +11 -0
- package/dist/src/core/background/index.d.ts.map +1 -0
- package/dist/src/core/background/index.js +11 -0
- package/dist/src/core/background/index.js.map +1 -0
- package/dist/src/core/background/job-manager.d.ts +65 -0
- package/dist/src/core/background/job-manager.d.ts.map +1 -0
- package/dist/src/core/background/job-manager.js +192 -0
- package/dist/src/core/background/job-manager.js.map +1 -0
- package/dist/src/core/background/types.d.ts +59 -0
- package/dist/src/core/background/types.d.ts.map +1 -0
- package/dist/src/core/background/types.js +8 -0
- package/dist/src/core/background/types.js.map +1 -0
- package/dist/src/core/repo-structure/multi-repo-configurator.d.ts +25 -0
- package/dist/src/core/repo-structure/multi-repo-configurator.d.ts.map +1 -0
- package/dist/src/core/repo-structure/multi-repo-configurator.js +614 -0
- package/dist/src/core/repo-structure/multi-repo-configurator.js.map +1 -0
- package/dist/src/core/repo-structure/repo-initializer.d.ts +40 -0
- package/dist/src/core/repo-structure/repo-initializer.d.ts.map +1 -0
- package/dist/src/core/repo-structure/repo-initializer.js +252 -0
- package/dist/src/core/repo-structure/repo-initializer.js.map +1 -0
- package/dist/src/core/repo-structure/repo-structure-manager.d.ts +3 -37
- package/dist/src/core/repo-structure/repo-structure-manager.d.ts.map +1 -1
- package/dist/src/core/repo-structure/repo-structure-manager.js +23 -803
- package/dist/src/core/repo-structure/repo-structure-manager.js.map +1 -1
- package/dist/src/core/types/increment-metadata.d.ts +75 -0
- package/dist/src/core/types/increment-metadata.d.ts.map +1 -1
- package/dist/src/core/types/spec-metadata.d.ts +2 -0
- package/dist/src/core/types/spec-metadata.d.ts.map +1 -1
- package/dist/src/core/types/sync-profile.d.ts +137 -5
- package/dist/src/core/types/sync-profile.d.ts.map +1 -1
- package/dist/src/core/types/sync-profile.js +63 -0
- package/dist/src/core/types/sync-profile.js.map +1 -1
- package/dist/src/importers/external-importer.d.ts +25 -0
- package/dist/src/importers/external-importer.d.ts.map +1 -1
- package/dist/src/importers/github-importer.d.ts.map +1 -1
- package/dist/src/importers/github-importer.js +5 -3
- package/dist/src/importers/github-importer.js.map +1 -1
- package/dist/src/importers/import-coordinator.d.ts +20 -0
- package/dist/src/importers/import-coordinator.d.ts.map +1 -1
- package/dist/src/importers/import-coordinator.js.map +1 -1
- package/dist/src/importers/item-converter.d.ts +51 -0
- package/dist/src/importers/item-converter.d.ts.map +1 -1
- package/dist/src/importers/item-converter.js +39 -12
- package/dist/src/importers/item-converter.js.map +1 -1
- package/dist/src/init/architecture/types.d.ts +2 -2
- package/dist/src/init/compliance/types.d.ts +1 -1
- package/dist/src/init/repo/types.d.ts +1 -1
- package/dist/src/living-docs/fs-id-allocator.d.ts +72 -3
- package/dist/src/living-docs/fs-id-allocator.d.ts.map +1 -1
- package/dist/src/living-docs/fs-id-allocator.js +142 -16
- package/dist/src/living-docs/fs-id-allocator.js.map +1 -1
- package/dist/src/locales/de/cli.json +14 -0
- package/dist/src/locales/es/cli.json +14 -0
- package/dist/src/locales/fr/cli.json +14 -0
- package/dist/src/locales/ja/cli.json +14 -0
- package/dist/src/locales/ko/cli.json +14 -0
- package/dist/src/locales/pt/cli.json +14 -0
- package/dist/src/locales/ru/cli.json +14 -0
- package/dist/src/locales/zh/cli.json +14 -0
- package/dist/src/utils/chalk-fallback.d.ts +38 -0
- package/dist/src/utils/chalk-fallback.d.ts.map +1 -0
- package/dist/src/utils/chalk-fallback.js +118 -0
- package/dist/src/utils/chalk-fallback.js.map +1 -0
- package/dist/src/utils/project-id-generator.d.ts +127 -0
- package/dist/src/utils/project-id-generator.d.ts.map +1 -0
- package/dist/src/utils/project-id-generator.js +228 -0
- package/dist/src/utils/project-id-generator.js.map +1 -0
- package/package.json +1 -1
- package/plugins/specweave/agents/pm/AGENT.md +202 -0
- package/plugins/specweave/commands/specweave-import-external.md +5 -3
- package/plugins/specweave/commands/specweave-jobs.md +160 -0
- package/plugins/specweave/commands/specweave-sync-docs.md +6 -2
- package/plugins/specweave/hooks/pre-task-completion.sh +35 -17
- package/plugins/specweave/lib/vendor/core/ac-test-validator-cli.d.ts +16 -0
- package/plugins/specweave/lib/vendor/core/ac-test-validator-cli.js +121 -0
- package/plugins/specweave/lib/vendor/core/ac-test-validator-cli.js.map +1 -0
- package/plugins/specweave/lib/vendor/core/ac-test-validator.d.ts +111 -0
- package/plugins/specweave/lib/vendor/core/ac-test-validator.js +295 -0
- package/plugins/specweave/lib/vendor/core/ac-test-validator.js.map +1 -0
- package/plugins/specweave/lib/vendor/core/types/increment-metadata.d.ts +75 -0
- package/plugins/specweave/lib/vendor/utils/chalk-fallback.d.ts +38 -0
- package/plugins/specweave/lib/vendor/utils/chalk-fallback.js +118 -0
- package/plugins/specweave/lib/vendor/utils/chalk-fallback.js.map +1 -0
- package/plugins/specweave/lib/vendor/utils/fs-native.d.ts +179 -0
- package/plugins/specweave/lib/vendor/utils/fs-native.js +319 -0
- package/plugins/specweave/lib/vendor/utils/fs-native.js.map +1 -0
- package/plugins/specweave/skills/code-reviewer/SKILL.md +1 -1
- package/plugins/specweave/skills/docs-updater/SKILL.md +61 -0
- package/plugins/specweave/skills/increment-planner/SKILL.md +10 -335
- package/plugins/specweave/skills/increment-planner/templates/metadata.json +13 -0
- package/plugins/specweave/skills/increment-planner/templates/plan.md +50 -0
- package/plugins/specweave/skills/increment-planner/templates/spec-multi-project.md +86 -0
- package/plugins/specweave/skills/increment-planner/templates/spec-single-project.md +50 -0
- package/plugins/specweave/skills/increment-planner/templates/tasks-multi-project.md +86 -0
- package/plugins/specweave/skills/increment-planner/templates/tasks-single-project.md +48 -0
- package/plugins/specweave-ado/commands/specweave-ado-import-areas.md +358 -0
- package/plugins/specweave-ado/lib/ado-spec-sync.js +59 -3
- package/plugins/specweave-ado/lib/ado-spec-sync.ts +72 -3
- package/plugins/specweave-ado/lib/ado-status-sync.js +35 -3
- package/plugins/specweave-ado/lib/ado-status-sync.ts +48 -4
- package/plugins/specweave-alternatives/skills/architecture-alternatives/SKILL.md +1 -0
- package/plugins/specweave-alternatives/skills/bmad-method/SKILL.md +1 -0
- package/plugins/specweave-core/skills/code-quality/SKILL.md +1 -0
- package/plugins/specweave-core/skills/design-patterns/SKILL.md +1 -0
- package/plugins/specweave-core/skills/software-architecture/SKILL.md +1 -0
- package/plugins/specweave-github/commands/specweave-github-cleanup-duplicates.md +14 -10
- package/plugins/specweave-github/commands/specweave-github-sync.md +57 -0
- package/plugins/specweave-github/hooks/.specweave/logs/hooks-debug.log +74 -0
- package/plugins/specweave-github/lib/github-feature-sync.ts +6 -11
- package/plugins/specweave-github/lib/github-increment-sync-cli.js +456 -0
- package/plugins/specweave-github/lib/github-increment-sync-cli.ts +588 -0
- package/plugins/specweave-github/lib/github-status-sync.js +37 -1
- package/plugins/specweave-github/lib/github-status-sync.ts +60 -4
- package/plugins/specweave-github/lib/increment-issue-builder.js +389 -0
- package/plugins/specweave-github/lib/increment-issue-builder.ts +502 -0
- package/plugins/specweave-github/skills/github-issue-standard/SKILL.md +19 -24
- package/plugins/specweave-infrastructure/agents/observability-engineer/AGENT.md +15 -23
- package/plugins/specweave-jira/commands/specweave-jira-import-boards.md +331 -0
- package/plugins/specweave-jira/lib/jira-spec-sync.js +53 -5
- package/plugins/specweave-jira/lib/jira-spec-sync.ts +87 -7
- package/plugins/specweave-jira/lib/jira-status-sync.js +9 -3
- package/plugins/specweave-jira/lib/jira-status-sync.ts +15 -6
- package/plugins/specweave-ml/agents/data-scientist/AGENT.md +16 -20
- package/plugins/specweave-ml/agents/ml-engineer/AGENT.md +18 -19
- package/plugins/specweave-ml/skills/{ml-pipeline-workflow → mlops-dag-builder}/SKILL.md +18 -14
- package/plugins/specweave-release/hooks/.specweave/logs/dora-tracking.log +111 -0
- package/plugins/specweave-ui/skills/browser-automation/SKILL.md +1 -1
- package/plugins/specweave-ui/skills/ui-testing/SKILL.md +10 -122
- package/dist/plugins/specweave-github/lib/epic-content-builder.d.ts +0 -70
- package/dist/plugins/specweave-github/lib/epic-content-builder.d.ts.map +0 -1
- package/dist/plugins/specweave-github/lib/epic-content-builder.js +0 -258
- package/dist/plugins/specweave-github/lib/epic-content-builder.js.map +0 -1
- package/dist/plugins/specweave-github/lib/github-epic-sync.d.ts +0 -83
- package/dist/plugins/specweave-github/lib/github-epic-sync.d.ts.map +0 -1
- package/dist/plugins/specweave-github/lib/github-epic-sync.js +0 -466
- package/dist/plugins/specweave-github/lib/github-epic-sync.js.map +0 -1
- package/plugins/specweave-github/lib/epic-content-builder.js +0 -265
- package/plugins/specweave-github/lib/epic-content-builder.ts +0 -376
- package/plugins/specweave-github/lib/github-epic-sync.js +0 -488
- package/plugins/specweave-github/lib/github-epic-sync.ts +0 -715
|
@@ -1,715 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GitHub Epic Sync - Hierarchical synchronization for Epic folder structure
|
|
3
|
-
*
|
|
4
|
-
* Architecture:
|
|
5
|
-
* - Epic (FS-001) → GitHub Milestone
|
|
6
|
-
* - Increment (0001-core-framework) → GitHub Issue (linked to Milestone)
|
|
7
|
-
*
|
|
8
|
-
* This implements the Universal Hierarchy architecture for GitHub.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { readdir, readFile, writeFile } from 'fs/promises';
|
|
12
|
-
import { existsSync } from 'fs';
|
|
13
|
-
import * as path from 'path';
|
|
14
|
-
import * as yaml from 'yaml';
|
|
15
|
-
import { GitHubClientV2 } from './github-client-v2.js';
|
|
16
|
-
import { execFileNoThrow } from '../../../src/utils/execFileNoThrow.js';
|
|
17
|
-
import { DuplicateDetector } from './duplicate-detector.js';
|
|
18
|
-
import { EpicContentBuilder } from './epic-content-builder.js';
|
|
19
|
-
|
|
20
|
-
interface EpicFrontmatter {
|
|
21
|
-
id: string;
|
|
22
|
-
title: string;
|
|
23
|
-
type: 'epic';
|
|
24
|
-
status: 'complete' | 'active' | 'planning' | 'archived';
|
|
25
|
-
priority: string;
|
|
26
|
-
created: string;
|
|
27
|
-
last_updated: string;
|
|
28
|
-
external_tools: {
|
|
29
|
-
github: {
|
|
30
|
-
type: 'milestone';
|
|
31
|
-
id: number | null;
|
|
32
|
-
url: string | null;
|
|
33
|
-
};
|
|
34
|
-
jira: {
|
|
35
|
-
type: 'epic';
|
|
36
|
-
key: string | null;
|
|
37
|
-
url: string | null;
|
|
38
|
-
};
|
|
39
|
-
ado: {
|
|
40
|
-
type: 'feature';
|
|
41
|
-
id: number | null;
|
|
42
|
-
url: string | null;
|
|
43
|
-
};
|
|
44
|
-
};
|
|
45
|
-
increments: Array<{
|
|
46
|
-
id: string;
|
|
47
|
-
status: string;
|
|
48
|
-
external: {
|
|
49
|
-
github: number | null;
|
|
50
|
-
jira: string | null;
|
|
51
|
-
ado: number | null;
|
|
52
|
-
};
|
|
53
|
-
}>;
|
|
54
|
-
total_increments: number;
|
|
55
|
-
completed_increments: number;
|
|
56
|
-
progress: string;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
interface IncrementFrontmatter {
|
|
60
|
-
id: string;
|
|
61
|
-
epic: string;
|
|
62
|
-
type?: string;
|
|
63
|
-
status?: string;
|
|
64
|
-
external?: {
|
|
65
|
-
github?: {
|
|
66
|
-
issue: number | null;
|
|
67
|
-
url: string | null;
|
|
68
|
-
};
|
|
69
|
-
jira?: {
|
|
70
|
-
story: string | null;
|
|
71
|
-
url: string | null;
|
|
72
|
-
};
|
|
73
|
-
ado?: {
|
|
74
|
-
user_story: number | null;
|
|
75
|
-
url: string | null;
|
|
76
|
-
};
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export class GitHubEpicSync {
|
|
81
|
-
private client: GitHubClientV2;
|
|
82
|
-
private specsDir: string;
|
|
83
|
-
|
|
84
|
-
constructor(client: GitHubClientV2, specsDir: string) {
|
|
85
|
-
this.client = client;
|
|
86
|
-
this.specsDir = specsDir;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Sync Epic folder to GitHub (Milestone + Issues)
|
|
91
|
-
*/
|
|
92
|
-
async syncEpicToGitHub(epicId: string): Promise<{
|
|
93
|
-
milestoneNumber: number;
|
|
94
|
-
milestoneUrl: string;
|
|
95
|
-
issuesCreated: number;
|
|
96
|
-
issuesUpdated: number;
|
|
97
|
-
duplicatesDetected: number;
|
|
98
|
-
}> {
|
|
99
|
-
console.log(`\n🔄 Syncing Epic ${epicId} to GitHub...`);
|
|
100
|
-
|
|
101
|
-
// 1. Load Epic FEATURE.md
|
|
102
|
-
const epicFolder = await this.findEpicFolder(epicId);
|
|
103
|
-
if (!epicFolder) {
|
|
104
|
-
throw new Error(`Epic ${epicId} not found in ${this.specsDir}`);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const readmePath = path.join(epicFolder, 'FEATURE.md');
|
|
108
|
-
const epicData = await this.parseEpicReadme(readmePath);
|
|
109
|
-
|
|
110
|
-
console.log(` 📦 Epic: ${epicData.title}`);
|
|
111
|
-
console.log(` 📊 Increments: ${epicData.total_increments}`);
|
|
112
|
-
|
|
113
|
-
// 2. Create or update GitHub Milestone
|
|
114
|
-
let milestoneNumber = epicData.external_tools.github.id;
|
|
115
|
-
let milestoneUrl = epicData.external_tools.github.url;
|
|
116
|
-
|
|
117
|
-
if (!milestoneNumber) {
|
|
118
|
-
console.log(` 🚀 Creating GitHub Milestone...`);
|
|
119
|
-
const milestone = await this.createMilestone(epicData);
|
|
120
|
-
milestoneNumber = milestone.number;
|
|
121
|
-
milestoneUrl = milestone.url;
|
|
122
|
-
console.log(` ✅ Created Milestone #${milestoneNumber}`);
|
|
123
|
-
|
|
124
|
-
// Update Epic README with Milestone ID
|
|
125
|
-
await this.updateEpicReadme(readmePath, {
|
|
126
|
-
type: 'milestone',
|
|
127
|
-
id: milestoneNumber,
|
|
128
|
-
url: milestoneUrl,
|
|
129
|
-
});
|
|
130
|
-
} else {
|
|
131
|
-
console.log(` ♻️ Updating existing Milestone #${milestoneNumber}...`);
|
|
132
|
-
await this.updateMilestone(milestoneNumber, epicData);
|
|
133
|
-
console.log(` ✅ Updated Milestone #${milestoneNumber}`);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// 3. Sync each increment as GitHub Issue (WITH DUPLICATE DETECTION!)
|
|
137
|
-
let issuesCreated = 0;
|
|
138
|
-
let issuesUpdated = 0;
|
|
139
|
-
let duplicatesDetected = 0;
|
|
140
|
-
|
|
141
|
-
console.log(`\n 📝 Syncing ${epicData.increments.length} increments...`);
|
|
142
|
-
|
|
143
|
-
for (const increment of epicData.increments) {
|
|
144
|
-
const incrementFile = path.join(epicFolder, `${increment.id}.md`);
|
|
145
|
-
if (!existsSync(incrementFile)) {
|
|
146
|
-
console.log(` ⚠️ Increment file not found: ${increment.id}.md`);
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const incrementData = await this.parseIncrementFile(incrementFile);
|
|
151
|
-
const existingIssue = increment.external.github;
|
|
152
|
-
|
|
153
|
-
if (!existingIssue) {
|
|
154
|
-
// NEW: Check GitHub FIRST before creating (duplicate detection!)
|
|
155
|
-
console.log(` 🔍 Checking GitHub for existing issue: ${increment.id}...`);
|
|
156
|
-
const githubIssue = await this.findExistingIssue(epicData.id, increment.id);
|
|
157
|
-
|
|
158
|
-
if (githubIssue) {
|
|
159
|
-
// Found existing issue! Re-link it instead of creating duplicate
|
|
160
|
-
console.log(` ♻️ Found existing Issue #${githubIssue} for ${increment.id} (self-healing)`);
|
|
161
|
-
await this.updateIncrementExternalLink(
|
|
162
|
-
readmePath,
|
|
163
|
-
incrementFile,
|
|
164
|
-
increment.id,
|
|
165
|
-
githubIssue
|
|
166
|
-
);
|
|
167
|
-
issuesUpdated++;
|
|
168
|
-
duplicatesDetected++;
|
|
169
|
-
} else {
|
|
170
|
-
// Truly new issue - create it
|
|
171
|
-
const issueNumber = await this.createIssue(
|
|
172
|
-
epicData.id,
|
|
173
|
-
incrementData,
|
|
174
|
-
milestoneNumber!
|
|
175
|
-
);
|
|
176
|
-
issuesCreated++;
|
|
177
|
-
console.log(` ✅ Created Issue #${issueNumber} for ${increment.id}`);
|
|
178
|
-
|
|
179
|
-
// Update Epic README and Increment file
|
|
180
|
-
await this.updateIncrementExternalLink(
|
|
181
|
-
readmePath,
|
|
182
|
-
incrementFile,
|
|
183
|
-
increment.id,
|
|
184
|
-
issueNumber
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
} else {
|
|
188
|
-
// Update existing issue
|
|
189
|
-
await this.updateIssue(
|
|
190
|
-
epicData.id,
|
|
191
|
-
existingIssue,
|
|
192
|
-
incrementData,
|
|
193
|
-
milestoneNumber!
|
|
194
|
-
);
|
|
195
|
-
issuesUpdated++;
|
|
196
|
-
console.log(` ♻️ Updated Issue #${existingIssue} for ${increment.id}`);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
console.log(`\n✅ Epic sync complete!`);
|
|
201
|
-
console.log(` Milestone: ${milestoneUrl}`);
|
|
202
|
-
console.log(` Issues created: ${issuesCreated}`);
|
|
203
|
-
console.log(` Issues updated: ${issuesUpdated}`);
|
|
204
|
-
if (duplicatesDetected > 0) {
|
|
205
|
-
console.log(` 🔗 Self-healed: ${duplicatesDetected} (found existing issues)`);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// 4. Post-sync validation: Check for duplicates
|
|
209
|
-
console.log(`\n🔍 Post-sync validation...`);
|
|
210
|
-
const validation = await this.validateSync(epicData.id);
|
|
211
|
-
|
|
212
|
-
if (validation.duplicatesFound > 0) {
|
|
213
|
-
console.warn(`\n⚠️ WARNING: ${validation.duplicatesFound} duplicate(s) detected!`);
|
|
214
|
-
console.warn(` This may indicate a previous sync created duplicates.`);
|
|
215
|
-
console.warn(` Run cleanup command to resolve:`);
|
|
216
|
-
console.warn(` /specweave-github:cleanup-duplicates ${epicData.id}`);
|
|
217
|
-
console.warn(`\n Duplicate groups:`);
|
|
218
|
-
for (const [title, numbers] of validation.duplicateGroups) {
|
|
219
|
-
console.warn(` - "${title}": Issues #${numbers.join(', #')}`);
|
|
220
|
-
}
|
|
221
|
-
} else {
|
|
222
|
-
console.log(` ✅ No duplicates found`);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
return {
|
|
226
|
-
milestoneNumber: milestoneNumber!,
|
|
227
|
-
milestoneUrl: milestoneUrl!,
|
|
228
|
-
issuesCreated,
|
|
229
|
-
issuesUpdated,
|
|
230
|
-
duplicatesDetected,
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Validate sync results - check for duplicate issues
|
|
236
|
-
*
|
|
237
|
-
* Searches GitHub for all issues with the Epic ID and detects duplicates
|
|
238
|
-
* (multiple issues with the same title).
|
|
239
|
-
*
|
|
240
|
-
* @param epicId - Epic ID (e.g., FS-031)
|
|
241
|
-
* @returns Validation result with duplicate count and groups
|
|
242
|
-
*/
|
|
243
|
-
private async validateSync(epicId: string): Promise<{
|
|
244
|
-
totalIssues: number;
|
|
245
|
-
duplicatesFound: number;
|
|
246
|
-
duplicateGroups: Array<[string, number[]]>;
|
|
247
|
-
}> {
|
|
248
|
-
try {
|
|
249
|
-
// Search for all issues with Epic ID in title
|
|
250
|
-
const titlePattern = `[${epicId}]`;
|
|
251
|
-
|
|
252
|
-
const result = await execFileNoThrow('gh', [
|
|
253
|
-
'issue',
|
|
254
|
-
'list',
|
|
255
|
-
'--search',
|
|
256
|
-
`"${titlePattern}" in:title`,
|
|
257
|
-
'--json',
|
|
258
|
-
'number,title,state',
|
|
259
|
-
'--limit',
|
|
260
|
-
'100', // Check up to 100 issues
|
|
261
|
-
'--state',
|
|
262
|
-
'all', // Include both open and closed
|
|
263
|
-
]);
|
|
264
|
-
|
|
265
|
-
if (result.exitCode !== 0 || !result.stdout) {
|
|
266
|
-
console.warn(` ⚠️ Validation failed: ${result.stderr || 'unknown error'}`);
|
|
267
|
-
return { totalIssues: 0, duplicatesFound: 0, duplicateGroups: [] };
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
const issues = JSON.parse(result.stdout) as Array<{
|
|
271
|
-
number: number;
|
|
272
|
-
title: string;
|
|
273
|
-
state: string;
|
|
274
|
-
}>;
|
|
275
|
-
|
|
276
|
-
// Group issues by title
|
|
277
|
-
const titleGroups = new Map<string, number[]>();
|
|
278
|
-
for (const issue of issues) {
|
|
279
|
-
const title = issue.title;
|
|
280
|
-
if (!titleGroups.has(title)) {
|
|
281
|
-
titleGroups.set(title, []);
|
|
282
|
-
}
|
|
283
|
-
titleGroups.get(title)!.push(issue.number);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// Find duplicates (titles with more than one issue)
|
|
287
|
-
const duplicateGroups: Array<[string, number[]]> = [];
|
|
288
|
-
for (const [title, numbers] of titleGroups.entries()) {
|
|
289
|
-
if (numbers.length > 1) {
|
|
290
|
-
duplicateGroups.push([title, numbers]);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
return {
|
|
295
|
-
totalIssues: issues.length,
|
|
296
|
-
duplicatesFound: duplicateGroups.length,
|
|
297
|
-
duplicateGroups,
|
|
298
|
-
};
|
|
299
|
-
} catch (error) {
|
|
300
|
-
console.warn(` ⚠️ Validation error: ${error}`);
|
|
301
|
-
return { totalIssues: 0, duplicatesFound: 0, duplicateGroups: [] };
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* Find existing GitHub issue for increment (duplicate detection!)
|
|
307
|
-
*
|
|
308
|
-
* Searches GitHub for issues matching the Epic ID and Increment ID.
|
|
309
|
-
* This prevents creating duplicates when frontmatter is lost/corrupted.
|
|
310
|
-
*
|
|
311
|
-
* @param epicId - Epic ID (e.g., FS-031)
|
|
312
|
-
* @param incrementId - Increment ID (e.g., 0031-feature-name)
|
|
313
|
-
* @returns GitHub issue number if found, null otherwise
|
|
314
|
-
*/
|
|
315
|
-
private async findExistingIssue(
|
|
316
|
-
epicId: string,
|
|
317
|
-
incrementId: string
|
|
318
|
-
): Promise<number | null> {
|
|
319
|
-
try {
|
|
320
|
-
// Search GitHub for issues with Epic ID in title
|
|
321
|
-
// Pattern: "[FS-031] Title" (new) or "[INC-0031] Title" (deprecated, legacy support only)
|
|
322
|
-
const titlePattern = `[${epicId}]`;
|
|
323
|
-
|
|
324
|
-
const result = await execFileNoThrow('gh', [
|
|
325
|
-
'issue',
|
|
326
|
-
'list',
|
|
327
|
-
'--search',
|
|
328
|
-
`"${titlePattern}" in:title`,
|
|
329
|
-
'--json',
|
|
330
|
-
'number,title,body',
|
|
331
|
-
'--limit',
|
|
332
|
-
'50', // Check up to 50 issues (should cover most Epics)
|
|
333
|
-
]);
|
|
334
|
-
|
|
335
|
-
if (result.exitCode !== 0 || !result.stdout) {
|
|
336
|
-
console.warn(` ⚠️ GitHub search failed: ${result.stderr || 'unknown error'}`);
|
|
337
|
-
return null;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
const issues = JSON.parse(result.stdout) as Array<{
|
|
341
|
-
number: number;
|
|
342
|
-
title: string;
|
|
343
|
-
body: string;
|
|
344
|
-
}>;
|
|
345
|
-
|
|
346
|
-
if (issues.length === 0) {
|
|
347
|
-
return null; // No issues found for this Epic
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// Find issue that mentions this increment ID in body
|
|
351
|
-
// Look for patterns like "**Increment**: 0031-feature-name"
|
|
352
|
-
for (const issue of issues) {
|
|
353
|
-
if (issue.body && issue.body.includes(`**Increment**: ${incrementId}`)) {
|
|
354
|
-
console.log(
|
|
355
|
-
` 🔗 Found existing issue #${issue.number} for ${incrementId}`
|
|
356
|
-
);
|
|
357
|
-
return issue.number;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// Fallback: Check if increment ID is in title
|
|
362
|
-
// Pattern: "[INC-0031-feature-name] Title"
|
|
363
|
-
for (const issue of issues) {
|
|
364
|
-
if (issue.title.toLowerCase().includes(incrementId.toLowerCase())) {
|
|
365
|
-
console.log(
|
|
366
|
-
` 🔗 Found existing issue #${issue.number} for ${incrementId} (title match)`
|
|
367
|
-
);
|
|
368
|
-
return issue.number;
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
return null; // No matching issue found
|
|
373
|
-
} catch (error) {
|
|
374
|
-
console.warn(` ⚠️ Error searching for existing issue: ${error}`);
|
|
375
|
-
return null; // Fail gracefully - continue with sync
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
/**
|
|
380
|
-
* Find Epic folder by ID (FS-001 or just 001)
|
|
381
|
-
*/
|
|
382
|
-
private async findEpicFolder(epicId: string): Promise<string | null> {
|
|
383
|
-
const normalizedId = epicId.startsWith('FS-')
|
|
384
|
-
? epicId
|
|
385
|
-
: `FS-${epicId.padStart(3, '0')}`;
|
|
386
|
-
|
|
387
|
-
const folders = await readdir(this.specsDir);
|
|
388
|
-
for (const folder of folders) {
|
|
389
|
-
if (folder.startsWith(normalizedId)) {
|
|
390
|
-
return path.join(this.specsDir, folder);
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
return null;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
/**
|
|
398
|
-
* Parse Epic FEATURE.md to extract frontmatter
|
|
399
|
-
*/
|
|
400
|
-
private async parseEpicReadme(
|
|
401
|
-
readmePath: string
|
|
402
|
-
): Promise<EpicFrontmatter> {
|
|
403
|
-
const content = await readFile(readmePath, 'utf-8');
|
|
404
|
-
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
405
|
-
|
|
406
|
-
if (!match) {
|
|
407
|
-
throw new Error('Epic FEATURE.md missing YAML frontmatter');
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
const frontmatter = yaml.parse(match[1]) as EpicFrontmatter;
|
|
411
|
-
return frontmatter;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
/**
|
|
415
|
-
* Parse increment file to extract title and overview
|
|
416
|
-
*/
|
|
417
|
-
private async parseIncrementFile(
|
|
418
|
-
filePath: string
|
|
419
|
-
): Promise<{
|
|
420
|
-
id: string;
|
|
421
|
-
title: string;
|
|
422
|
-
overview: string;
|
|
423
|
-
content: string;
|
|
424
|
-
frontmatter: IncrementFrontmatter;
|
|
425
|
-
}> {
|
|
426
|
-
const content = await readFile(filePath, 'utf-8');
|
|
427
|
-
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
428
|
-
|
|
429
|
-
let frontmatter: IncrementFrontmatter = { id: '', epic: '' };
|
|
430
|
-
let bodyContent = content;
|
|
431
|
-
|
|
432
|
-
if (match) {
|
|
433
|
-
frontmatter = yaml.parse(match[1]) as IncrementFrontmatter;
|
|
434
|
-
bodyContent = content.slice(match[0].length).trim();
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
// Extract title
|
|
438
|
-
const titleMatch = bodyContent.match(/^#\s+(.+)$/m);
|
|
439
|
-
const title = titleMatch
|
|
440
|
-
? titleMatch[1].trim()
|
|
441
|
-
: frontmatter.id || path.basename(filePath, '.md');
|
|
442
|
-
|
|
443
|
-
// Extract overview (first paragraph after title)
|
|
444
|
-
const overviewMatch = bodyContent.match(/^#[^\n]+\n+([^\n]+)/);
|
|
445
|
-
const overview = overviewMatch
|
|
446
|
-
? overviewMatch[1].trim()
|
|
447
|
-
: 'No overview available';
|
|
448
|
-
|
|
449
|
-
return {
|
|
450
|
-
id: frontmatter.id,
|
|
451
|
-
title,
|
|
452
|
-
overview,
|
|
453
|
-
content: bodyContent,
|
|
454
|
-
frontmatter,
|
|
455
|
-
};
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
/**
|
|
459
|
-
* Create GitHub Milestone
|
|
460
|
-
*/
|
|
461
|
-
private async createMilestone(epic: EpicFrontmatter): Promise<{
|
|
462
|
-
number: number;
|
|
463
|
-
url: string;
|
|
464
|
-
}> {
|
|
465
|
-
const title = `[${epic.id}] ${epic.title}`;
|
|
466
|
-
const description = `Epic: ${epic.title}\n\nProgress: ${epic.completed_increments}/${epic.total_increments} increments (${epic.progress})\n\nPriority: ${epic.priority}\nStatus: ${epic.status}`;
|
|
467
|
-
|
|
468
|
-
// Determine milestone state
|
|
469
|
-
const state = epic.status === 'complete' ? 'closed' : 'open';
|
|
470
|
-
|
|
471
|
-
// Use GitHub CLI to create milestone
|
|
472
|
-
const result = await execFileNoThrow('gh', [
|
|
473
|
-
'api',
|
|
474
|
-
'/repos/{owner}/{repo}/milestones',
|
|
475
|
-
'-X',
|
|
476
|
-
'POST',
|
|
477
|
-
'-f',
|
|
478
|
-
`title=${title}`,
|
|
479
|
-
'-f',
|
|
480
|
-
`description=${description}`,
|
|
481
|
-
'-f',
|
|
482
|
-
`state=${state}`,
|
|
483
|
-
]);
|
|
484
|
-
|
|
485
|
-
if (result.exitCode !== 0) {
|
|
486
|
-
throw new Error(
|
|
487
|
-
`Failed to create GitHub Milestone: ${result.stderr || result.stdout}`
|
|
488
|
-
);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
const milestone = JSON.parse(result.stdout);
|
|
492
|
-
return {
|
|
493
|
-
number: milestone.number,
|
|
494
|
-
url: milestone.html_url,
|
|
495
|
-
};
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
/**
|
|
499
|
-
* Update GitHub Milestone
|
|
500
|
-
*/
|
|
501
|
-
private async updateMilestone(
|
|
502
|
-
milestoneNumber: number,
|
|
503
|
-
epic: EpicFrontmatter
|
|
504
|
-
): Promise<void> {
|
|
505
|
-
const title = `[${epic.id}] ${epic.title}`;
|
|
506
|
-
const description = `Epic: ${epic.title}\n\nProgress: ${epic.completed_increments}/${epic.total_increments} increments (${epic.progress})\n\nPriority: ${epic.priority}\nStatus: ${epic.status}`;
|
|
507
|
-
const state = epic.status === 'complete' ? 'closed' : 'open';
|
|
508
|
-
|
|
509
|
-
const result = await execFileNoThrow('gh', [
|
|
510
|
-
'api',
|
|
511
|
-
`/repos/{owner}/{repo}/milestones/${milestoneNumber}`,
|
|
512
|
-
'-X',
|
|
513
|
-
'PATCH',
|
|
514
|
-
'-f',
|
|
515
|
-
`title=${title}`,
|
|
516
|
-
'-f',
|
|
517
|
-
`description=${description}`,
|
|
518
|
-
'-f',
|
|
519
|
-
`state=${state}`,
|
|
520
|
-
]);
|
|
521
|
-
|
|
522
|
-
if (result.exitCode !== 0) {
|
|
523
|
-
throw new Error(
|
|
524
|
-
`Failed to update GitHub Milestone: ${result.stderr || result.stdout}`
|
|
525
|
-
);
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
/**
|
|
530
|
-
* Create GitHub Issue for Epic with hierarchical content (US → Tasks)
|
|
531
|
-
*/
|
|
532
|
-
private async createIssue(
|
|
533
|
-
epicId: string,
|
|
534
|
-
increment: {
|
|
535
|
-
id: string;
|
|
536
|
-
title: string;
|
|
537
|
-
overview: string;
|
|
538
|
-
content: string;
|
|
539
|
-
},
|
|
540
|
-
milestoneNumber: number
|
|
541
|
-
): Promise<number> {
|
|
542
|
-
const title = `[${epicId}] ${increment.title}`;
|
|
543
|
-
|
|
544
|
-
// Build hierarchical issue body using EpicContentBuilder
|
|
545
|
-
const epicFolder = await this.findEpicFolder(epicId);
|
|
546
|
-
if (!epicFolder) {
|
|
547
|
-
throw new Error(`Epic folder not found for ${epicId}`);
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
const contentBuilder = new EpicContentBuilder(
|
|
551
|
-
epicFolder,
|
|
552
|
-
path.dirname(this.specsDir) // Project root
|
|
553
|
-
);
|
|
554
|
-
|
|
555
|
-
const body = await contentBuilder.buildIssueBody();
|
|
556
|
-
|
|
557
|
-
try {
|
|
558
|
-
// Use DuplicateDetector for full 3-phase protection
|
|
559
|
-
const result = await DuplicateDetector.createWithProtection({
|
|
560
|
-
title,
|
|
561
|
-
body,
|
|
562
|
-
titlePattern: `[${epicId}]`,
|
|
563
|
-
incrementId: increment.id, // For body matching
|
|
564
|
-
labels: ['increment', 'epic-sync'],
|
|
565
|
-
milestone: milestoneNumber.toString()
|
|
566
|
-
});
|
|
567
|
-
|
|
568
|
-
// Log duplicate detection results
|
|
569
|
-
if (result.wasReused) {
|
|
570
|
-
console.log(` ♻️ Reused existing issue #${result.issue.number} (duplicate prevention)`);
|
|
571
|
-
}
|
|
572
|
-
if (result.duplicatesFound > 0) {
|
|
573
|
-
console.log(` 🛡️ Duplicates: ${result.duplicatesFound} found, ${result.duplicatesClosed} closed`);
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
return result.issue.number;
|
|
577
|
-
|
|
578
|
-
} catch (error: any) {
|
|
579
|
-
throw new Error(`Failed to create GitHub Issue: ${error.message}`);
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
/**
|
|
584
|
-
* Update GitHub Issue for Epic with hierarchical content (US → Tasks)
|
|
585
|
-
*/
|
|
586
|
-
private async updateIssue(
|
|
587
|
-
epicId: string,
|
|
588
|
-
issueNumber: number,
|
|
589
|
-
increment: {
|
|
590
|
-
id: string;
|
|
591
|
-
title: string;
|
|
592
|
-
overview: string;
|
|
593
|
-
content: string;
|
|
594
|
-
},
|
|
595
|
-
milestoneNumber: number
|
|
596
|
-
): Promise<void> {
|
|
597
|
-
const title = `[${epicId}] ${increment.title}`;
|
|
598
|
-
|
|
599
|
-
// Build hierarchical issue body using EpicContentBuilder
|
|
600
|
-
const epicFolder = await this.findEpicFolder(epicId);
|
|
601
|
-
if (!epicFolder) {
|
|
602
|
-
throw new Error(`Epic folder not found for ${epicId}`);
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
const contentBuilder = new EpicContentBuilder(
|
|
606
|
-
epicFolder,
|
|
607
|
-
path.dirname(this.specsDir) // Project root
|
|
608
|
-
);
|
|
609
|
-
|
|
610
|
-
const body = await contentBuilder.buildIssueBody();
|
|
611
|
-
|
|
612
|
-
const result = await execFileNoThrow('gh', [
|
|
613
|
-
'issue',
|
|
614
|
-
'edit',
|
|
615
|
-
issueNumber.toString(),
|
|
616
|
-
'--title',
|
|
617
|
-
title,
|
|
618
|
-
'--body',
|
|
619
|
-
body,
|
|
620
|
-
'--milestone',
|
|
621
|
-
milestoneNumber.toString(),
|
|
622
|
-
]);
|
|
623
|
-
|
|
624
|
-
if (result.exitCode !== 0) {
|
|
625
|
-
throw new Error(
|
|
626
|
-
`Failed to update GitHub Issue: ${result.stderr || result.stdout}`
|
|
627
|
-
);
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
/**
|
|
632
|
-
* Update Epic FEATURE.md with GitHub Milestone ID
|
|
633
|
-
*/
|
|
634
|
-
private async updateEpicReadme(
|
|
635
|
-
readmePath: string,
|
|
636
|
-
github: { type: 'milestone'; id: number; url: string }
|
|
637
|
-
): Promise<void> {
|
|
638
|
-
const content = await readFile(readmePath, 'utf-8');
|
|
639
|
-
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
640
|
-
|
|
641
|
-
if (!match) {
|
|
642
|
-
throw new Error('Epic FEATURE.md missing YAML frontmatter');
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
const frontmatter = yaml.parse(match[1]) as EpicFrontmatter;
|
|
646
|
-
frontmatter.external_tools.github = github;
|
|
647
|
-
|
|
648
|
-
const newFrontmatter = yaml.stringify(frontmatter);
|
|
649
|
-
const newContent = content.replace(
|
|
650
|
-
/^---\n[\s\S]*?\n---/,
|
|
651
|
-
`---\n${newFrontmatter}---`
|
|
652
|
-
);
|
|
653
|
-
|
|
654
|
-
await writeFile(readmePath, newContent, 'utf-8');
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
/**
|
|
658
|
-
* Update increment external link in both Epic README and increment file
|
|
659
|
-
*/
|
|
660
|
-
private async updateIncrementExternalLink(
|
|
661
|
-
readmePath: string,
|
|
662
|
-
incrementFile: string,
|
|
663
|
-
incrementId: string,
|
|
664
|
-
issueNumber: number
|
|
665
|
-
): Promise<void> {
|
|
666
|
-
const issueUrl = `https://github.com/{owner}/{repo}/issues/${issueNumber}`;
|
|
667
|
-
|
|
668
|
-
// 1. Update Epic FEATURE.md
|
|
669
|
-
const readmeContent = await readFile(readmePath, 'utf-8');
|
|
670
|
-
const readmeMatch = readmeContent.match(/^---\n([\s\S]*?)\n---/);
|
|
671
|
-
|
|
672
|
-
if (readmeMatch) {
|
|
673
|
-
const frontmatter = yaml.parse(readmeMatch[1]) as EpicFrontmatter;
|
|
674
|
-
const increment = frontmatter.increments.find(
|
|
675
|
-
(inc) => inc.id === incrementId
|
|
676
|
-
);
|
|
677
|
-
|
|
678
|
-
if (increment) {
|
|
679
|
-
increment.external.github = issueNumber;
|
|
680
|
-
const newFrontmatter = yaml.stringify(frontmatter);
|
|
681
|
-
const newContent = readmeContent.replace(
|
|
682
|
-
/^---\n[\s\S]*?\n---/,
|
|
683
|
-
`---\n${newFrontmatter}---`
|
|
684
|
-
);
|
|
685
|
-
await writeFile(readmePath, newContent, 'utf-8');
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
// 2. Update increment file frontmatter
|
|
690
|
-
const incrementContent = await readFile(incrementFile, 'utf-8');
|
|
691
|
-
const incrementMatch = incrementContent.match(/^---\n([\s\S]*?)\n---/);
|
|
692
|
-
|
|
693
|
-
if (incrementMatch) {
|
|
694
|
-
const frontmatter = yaml.parse(
|
|
695
|
-
incrementMatch[1]
|
|
696
|
-
) as IncrementFrontmatter;
|
|
697
|
-
|
|
698
|
-
if (!frontmatter.external) {
|
|
699
|
-
frontmatter.external = {};
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
frontmatter.external.github = {
|
|
703
|
-
issue: issueNumber,
|
|
704
|
-
url: issueUrl,
|
|
705
|
-
};
|
|
706
|
-
|
|
707
|
-
const newFrontmatter = yaml.stringify(frontmatter);
|
|
708
|
-
const newContent = incrementContent.replace(
|
|
709
|
-
/^---\n[\s\S]*?\n---/,
|
|
710
|
-
`---\n${newFrontmatter}---`
|
|
711
|
-
);
|
|
712
|
-
await writeFile(incrementFile, newContent, 'utf-8');
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
}
|