specweave 0.13.6 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +189 -0
- package/dist/cli/commands/init.js +1 -1
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/status-line.d.ts +14 -0
- package/dist/cli/commands/status-line.d.ts.map +1 -0
- package/dist/cli/commands/status-line.js +75 -0
- package/dist/cli/commands/status-line.js.map +1 -0
- package/dist/core/status-line/status-line-manager.d.ts +62 -0
- package/dist/core/status-line/status-line-manager.d.ts.map +1 -0
- package/dist/core/status-line/status-line-manager.js +169 -0
- package/dist/core/status-line/status-line-manager.js.map +1 -0
- package/dist/core/status-line/types.d.ts +50 -0
- package/dist/core/status-line/types.d.ts.map +1 -0
- package/dist/core/status-line/types.js +17 -0
- package/dist/core/status-line/types.js.map +1 -0
- package/dist/utils/project-mapper.d.ts +74 -0
- package/dist/utils/project-mapper.d.ts.map +1 -0
- package/dist/utils/project-mapper.js +273 -0
- package/dist/utils/project-mapper.js.map +1 -0
- package/dist/utils/spec-splitter.d.ts +68 -0
- package/dist/utils/spec-splitter.d.ts.map +1 -0
- package/dist/utils/spec-splitter.js +314 -0
- package/dist/utils/spec-splitter.js.map +1 -0
- package/package.json +1 -1
- package/plugins/specweave/hooks/lib/update-status-line.sh +138 -0
- package/plugins/specweave/hooks/post-task-completion.sh +10 -0
- package/plugins/specweave/skills/multi-project-spec-mapper/SKILL.md +399 -0
- package/plugins/specweave-ado/lib/ado-multi-project-sync.js +453 -0
- package/plugins/specweave-ado/lib/ado-multi-project-sync.ts +633 -0
- package/plugins/specweave-docs/skills/docusaurus/SKILL.md +17 -3
- package/plugins/specweave-docs-preview/commands/preview.md +29 -4
- package/plugins/specweave-github/lib/github-multi-project-sync.js +340 -0
- package/plugins/specweave-github/lib/github-multi-project-sync.ts +461 -0
- package/plugins/specweave-jira/lib/jira-multi-project-sync.js +244 -0
- package/plugins/specweave-jira/lib/jira-multi-project-sync.ts +358 -0
|
@@ -7,6 +7,19 @@ description: Launch interactive documentation preview server with Docusaurus. Au
|
|
|
7
7
|
|
|
8
8
|
Launch an interactive Docusaurus development server to preview your SpecWeave living documentation.
|
|
9
9
|
|
|
10
|
+
## 🎨 Beautiful Docusaurus Experience
|
|
11
|
+
|
|
12
|
+
**This command ALWAYS uses Docusaurus** - not basic file serving! You get:
|
|
13
|
+
- ✅ Auto-generated navigation from folder structure
|
|
14
|
+
- ✅ Search functionality (instant local search)
|
|
15
|
+
- ✅ Beautiful theming (light/dark mode)
|
|
16
|
+
- ✅ Mermaid diagram rendering
|
|
17
|
+
- ✅ Hot reload (edit markdown, see changes instantly)
|
|
18
|
+
- ✅ Professional typography and layout
|
|
19
|
+
- ✅ Mobile responsive design
|
|
20
|
+
|
|
21
|
+
**Never settle for basic markdown rendering when you can have this!**
|
|
22
|
+
|
|
10
23
|
## Your Task
|
|
11
24
|
|
|
12
25
|
Execute the following workflow to launch the documentation preview server:
|
|
@@ -45,13 +58,25 @@ const docsConfig = config.documentation?.preview || {
|
|
|
45
58
|
};
|
|
46
59
|
```
|
|
47
60
|
|
|
48
|
-
### Step 3: Check if Documentation Preview is Enabled
|
|
61
|
+
### Step 3: Check if Documentation Preview is Enabled (Auto-Enable if Needed)
|
|
49
62
|
|
|
50
63
|
```typescript
|
|
51
64
|
if (!docsConfig.enabled) {
|
|
52
|
-
console.log('📚 Documentation preview is disabled
|
|
53
|
-
console.log('
|
|
54
|
-
|
|
65
|
+
console.log('📚 Documentation preview is currently disabled.');
|
|
66
|
+
console.log(' Enabling it now for beautiful Docusaurus experience...\n');
|
|
67
|
+
|
|
68
|
+
// Auto-enable the feature
|
|
69
|
+
config.documentation = config.documentation || {};
|
|
70
|
+
config.documentation.preview = {
|
|
71
|
+
...docsConfig,
|
|
72
|
+
enabled: true,
|
|
73
|
+
autoInstall: true
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Save updated config
|
|
77
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
78
|
+
|
|
79
|
+
console.log('✅ Documentation preview enabled!\n');
|
|
55
80
|
}
|
|
56
81
|
```
|
|
57
82
|
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { Octokit } from "@octokit/rest";
|
|
2
|
+
import { getPrimaryProject } from "../../../src/utils/project-mapper.js";
|
|
3
|
+
import { parseSpecFile } from "../../../src/utils/spec-splitter.js";
|
|
4
|
+
class GitHubMultiProjectSync {
|
|
5
|
+
constructor(config) {
|
|
6
|
+
this.config = config;
|
|
7
|
+
this.octokit = new Octokit({ auth: config.token });
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Sync spec to appropriate GitHub repos based on project mapping
|
|
11
|
+
*
|
|
12
|
+
* @param specPath Path to spec file
|
|
13
|
+
* @returns Array of sync results
|
|
14
|
+
*/
|
|
15
|
+
async syncSpec(specPath) {
|
|
16
|
+
const results = [];
|
|
17
|
+
const parsedSpec = await parseSpecFile(specPath);
|
|
18
|
+
const isMasterNested = !!this.config.masterRepo && !!this.config.nestedRepos;
|
|
19
|
+
if (isMasterNested) {
|
|
20
|
+
results.push(...await this.syncMasterNested(parsedSpec));
|
|
21
|
+
} else if (this.config.repos) {
|
|
22
|
+
results.push(...await this.syncMultipleRepos(parsedSpec));
|
|
23
|
+
} else {
|
|
24
|
+
throw new Error("Invalid config: Must specify repos[] or masterRepo+nestedRepos[]");
|
|
25
|
+
}
|
|
26
|
+
return results;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Pattern 1: Sync to multiple repos (simple)
|
|
30
|
+
*
|
|
31
|
+
* Each project → separate repo
|
|
32
|
+
* - FE user stories → company/frontend-web
|
|
33
|
+
* - BE user stories → company/backend-api
|
|
34
|
+
* - MOBILE user stories → company/mobile-app
|
|
35
|
+
*/
|
|
36
|
+
async syncMultipleRepos(parsedSpec) {
|
|
37
|
+
const results = [];
|
|
38
|
+
const projectStories = /* @__PURE__ */ new Map();
|
|
39
|
+
for (const userStory of parsedSpec.userStories) {
|
|
40
|
+
const primaryProject = getPrimaryProject(userStory);
|
|
41
|
+
if (primaryProject) {
|
|
42
|
+
const existing = projectStories.get(primaryProject.projectId) || [];
|
|
43
|
+
existing.push(userStory);
|
|
44
|
+
projectStories.set(primaryProject.projectId, existing);
|
|
45
|
+
} else {
|
|
46
|
+
console.warn(`\u26A0\uFE0F No confident project match for ${userStory.id} - skipping`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
for (const [projectId, stories] of projectStories.entries()) {
|
|
50
|
+
const repo = this.findRepoForProject(projectId);
|
|
51
|
+
if (!repo) {
|
|
52
|
+
console.warn(`\u26A0\uFE0F No GitHub repo configured for project ${projectId} - skipping`);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
for (const story of stories) {
|
|
56
|
+
const result = await this.createOrUpdateIssue(repo, story, projectId);
|
|
57
|
+
results.push(result);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return results;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Pattern 2: Sync to master + nested repos (advanced)
|
|
64
|
+
*
|
|
65
|
+
* - Master repo (epic-level): High-level overview
|
|
66
|
+
* - Nested repos (task-level): Detailed implementation
|
|
67
|
+
*
|
|
68
|
+
* Example:
|
|
69
|
+
* Master (company/master-project):
|
|
70
|
+
* Epic #10: User Authentication
|
|
71
|
+
* → Links to: frontend-web#42, backend-api#15, mobile-app#8
|
|
72
|
+
*
|
|
73
|
+
* Nested (company/frontend-web):
|
|
74
|
+
* Issue #42: Implement Login UI
|
|
75
|
+
* Task 1: Create login component
|
|
76
|
+
* Task 2: Add form validation
|
|
77
|
+
*/
|
|
78
|
+
async syncMasterNested(parsedSpec) {
|
|
79
|
+
const results = [];
|
|
80
|
+
if (!this.config.masterRepo || !this.config.nestedRepos) {
|
|
81
|
+
throw new Error("Master+nested mode requires masterRepo and nestedRepos");
|
|
82
|
+
}
|
|
83
|
+
const epicResult = await this.createEpicInMasterRepo(parsedSpec);
|
|
84
|
+
results.push(epicResult);
|
|
85
|
+
const projectStories = /* @__PURE__ */ new Map();
|
|
86
|
+
for (const userStory of parsedSpec.userStories) {
|
|
87
|
+
const primaryProject = getPrimaryProject(userStory);
|
|
88
|
+
if (primaryProject) {
|
|
89
|
+
const existing = projectStories.get(primaryProject.projectId) || [];
|
|
90
|
+
existing.push(userStory);
|
|
91
|
+
projectStories.set(primaryProject.projectId, existing);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
for (const [projectId, stories] of projectStories.entries()) {
|
|
95
|
+
const repo = this.findNestedRepoForProject(projectId);
|
|
96
|
+
if (!repo) {
|
|
97
|
+
console.warn(`\u26A0\uFE0F No nested repo for project ${projectId} - skipping`);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
for (const story of stories) {
|
|
101
|
+
const result = await this.createIssueInNestedRepo(repo, story, projectId, epicResult.issueNumber);
|
|
102
|
+
results.push(result);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (this.config.crossLinking) {
|
|
106
|
+
await this.updateEpicWithLinks(epicResult.issueNumber, results.filter((r) => r.repo !== this.config.masterRepo));
|
|
107
|
+
}
|
|
108
|
+
return results;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Create epic issue in master repo
|
|
112
|
+
*/
|
|
113
|
+
async createEpicInMasterRepo(parsedSpec) {
|
|
114
|
+
const title = `Epic: ${parsedSpec.metadata.title}`;
|
|
115
|
+
const body = `# ${parsedSpec.metadata.title}
|
|
116
|
+
|
|
117
|
+
**Status**: ${parsedSpec.metadata.status}
|
|
118
|
+
**Priority**: ${parsedSpec.metadata.priority}
|
|
119
|
+
**Estimated Effort**: ${parsedSpec.metadata.estimatedEffort || parsedSpec.metadata.estimated_effort}
|
|
120
|
+
|
|
121
|
+
## Executive Summary
|
|
122
|
+
|
|
123
|
+
${parsedSpec.executiveSummary}
|
|
124
|
+
|
|
125
|
+
## User Stories (${parsedSpec.userStories.length} total)
|
|
126
|
+
|
|
127
|
+
${parsedSpec.userStories.map((s, i) => `${i + 1}. ${s.id}: ${s.title}`).join("\n")}
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
\u{1F4CA} **This is a high-level epic** - detailed implementation tracked in nested repos
|
|
132
|
+
\u{1F517} **Links to implementation issues** (will be added automatically)
|
|
133
|
+
|
|
134
|
+
\u{1F916} Auto-generated by SpecWeave
|
|
135
|
+
`;
|
|
136
|
+
const response = await this.octokit.issues.create({
|
|
137
|
+
owner: this.config.owner,
|
|
138
|
+
repo: this.config.masterRepo,
|
|
139
|
+
title,
|
|
140
|
+
body,
|
|
141
|
+
labels: ["epic", "specweave"]
|
|
142
|
+
});
|
|
143
|
+
return {
|
|
144
|
+
project: "MASTER",
|
|
145
|
+
repo: this.config.masterRepo,
|
|
146
|
+
issueNumber: response.data.number,
|
|
147
|
+
url: response.data.html_url,
|
|
148
|
+
action: "created"
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Create issue in nested repo + link to epic
|
|
153
|
+
*/
|
|
154
|
+
async createIssueInNestedRepo(repo, userStory, projectId, epicNumber) {
|
|
155
|
+
const title = `${userStory.id}: ${userStory.title}`;
|
|
156
|
+
let body = `# ${userStory.title}
|
|
157
|
+
|
|
158
|
+
${userStory.description}
|
|
159
|
+
|
|
160
|
+
## Acceptance Criteria
|
|
161
|
+
|
|
162
|
+
${userStory.acceptanceCriteria.map((ac, i) => `- [ ] ${ac}`).join("\n")}
|
|
163
|
+
`;
|
|
164
|
+
if (userStory.technicalContext) {
|
|
165
|
+
body += `
|
|
166
|
+
## Technical Context
|
|
167
|
+
|
|
168
|
+
${userStory.technicalContext}
|
|
169
|
+
`;
|
|
170
|
+
}
|
|
171
|
+
if (this.config.crossLinking && epicNumber) {
|
|
172
|
+
body += `
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
\u{1F4CA} Part of Epic: ${this.config.owner}/${this.config.masterRepo}#${epicNumber}
|
|
176
|
+
`;
|
|
177
|
+
}
|
|
178
|
+
body += `
|
|
179
|
+
\u{1F916} Auto-generated by SpecWeave
|
|
180
|
+
`;
|
|
181
|
+
const response = await this.octokit.issues.create({
|
|
182
|
+
owner: this.config.owner,
|
|
183
|
+
repo,
|
|
184
|
+
title,
|
|
185
|
+
body,
|
|
186
|
+
labels: ["story", "specweave", projectId.toLowerCase()]
|
|
187
|
+
});
|
|
188
|
+
return {
|
|
189
|
+
project: projectId,
|
|
190
|
+
repo,
|
|
191
|
+
issueNumber: response.data.number,
|
|
192
|
+
url: response.data.html_url,
|
|
193
|
+
action: "created"
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Create or update issue (for simple multi-repo pattern)
|
|
198
|
+
*/
|
|
199
|
+
async createOrUpdateIssue(repo, userStory, projectId) {
|
|
200
|
+
const title = `${userStory.id}: ${userStory.title}`;
|
|
201
|
+
const body = `# ${userStory.title}
|
|
202
|
+
|
|
203
|
+
${userStory.description}
|
|
204
|
+
|
|
205
|
+
## Acceptance Criteria
|
|
206
|
+
|
|
207
|
+
${userStory.acceptanceCriteria.map((ac, i) => `- [ ] ${ac}`).join("\n")}
|
|
208
|
+
|
|
209
|
+
${userStory.technicalContext ? `
|
|
210
|
+
## Technical Context
|
|
211
|
+
|
|
212
|
+
${userStory.technicalContext}
|
|
213
|
+
` : ""}
|
|
214
|
+
|
|
215
|
+
\u{1F916} Auto-generated by SpecWeave
|
|
216
|
+
`;
|
|
217
|
+
const existingIssues = await this.octokit.issues.listForRepo({
|
|
218
|
+
owner: this.config.owner,
|
|
219
|
+
repo,
|
|
220
|
+
labels: "specweave",
|
|
221
|
+
state: "all"
|
|
222
|
+
});
|
|
223
|
+
const existing = existingIssues.data.find((issue) => issue.title === title);
|
|
224
|
+
if (existing) {
|
|
225
|
+
const response = await this.octokit.issues.update({
|
|
226
|
+
owner: this.config.owner,
|
|
227
|
+
repo,
|
|
228
|
+
issue_number: existing.number,
|
|
229
|
+
body
|
|
230
|
+
});
|
|
231
|
+
return {
|
|
232
|
+
project: projectId,
|
|
233
|
+
repo,
|
|
234
|
+
issueNumber: response.data.number,
|
|
235
|
+
url: response.data.html_url,
|
|
236
|
+
action: "updated"
|
|
237
|
+
};
|
|
238
|
+
} else {
|
|
239
|
+
const response = await this.octokit.issues.create({
|
|
240
|
+
owner: this.config.owner,
|
|
241
|
+
repo,
|
|
242
|
+
title,
|
|
243
|
+
body,
|
|
244
|
+
labels: ["specweave", projectId.toLowerCase()]
|
|
245
|
+
});
|
|
246
|
+
return {
|
|
247
|
+
project: projectId,
|
|
248
|
+
repo,
|
|
249
|
+
issueNumber: response.data.number,
|
|
250
|
+
url: response.data.html_url,
|
|
251
|
+
action: "created"
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Update epic with links to nested issues
|
|
257
|
+
*/
|
|
258
|
+
async updateEpicWithLinks(epicNumber, nestedResults) {
|
|
259
|
+
if (!this.config.masterRepo) return;
|
|
260
|
+
const linksSection = `
|
|
261
|
+
|
|
262
|
+
## Implementation Issues
|
|
263
|
+
|
|
264
|
+
${nestedResults.map((r) => `- ${r.project}: ${this.config.owner}/${r.repo}#${r.issueNumber} - ${r.url}`).join("\n")}
|
|
265
|
+
`;
|
|
266
|
+
const epic = await this.octokit.issues.get({
|
|
267
|
+
owner: this.config.owner,
|
|
268
|
+
repo: this.config.masterRepo,
|
|
269
|
+
issue_number: epicNumber
|
|
270
|
+
});
|
|
271
|
+
const updatedBody = epic.data.body + linksSection;
|
|
272
|
+
await this.octokit.issues.update({
|
|
273
|
+
owner: this.config.owner,
|
|
274
|
+
repo: this.config.masterRepo,
|
|
275
|
+
issue_number: epicNumber,
|
|
276
|
+
body: updatedBody
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Find GitHub repo for project ID
|
|
281
|
+
*
|
|
282
|
+
* Maps project IDs to repo names:
|
|
283
|
+
* - FE → frontend-web
|
|
284
|
+
* - BE → backend-api
|
|
285
|
+
* - MOBILE → mobile-app
|
|
286
|
+
*/
|
|
287
|
+
findRepoForProject(projectId) {
|
|
288
|
+
if (!this.config.repos) return void 0;
|
|
289
|
+
let match = this.config.repos.find((repo) => repo.toLowerCase().includes(projectId.toLowerCase()));
|
|
290
|
+
if (!match) {
|
|
291
|
+
const fuzzyMap = {
|
|
292
|
+
FE: ["frontend", "web", "ui", "client"],
|
|
293
|
+
BE: ["backend", "api", "server"],
|
|
294
|
+
MOBILE: ["mobile", "app", "ios", "android"],
|
|
295
|
+
INFRA: ["infra", "infrastructure", "devops", "platform"]
|
|
296
|
+
};
|
|
297
|
+
const keywords = fuzzyMap[projectId] || [];
|
|
298
|
+
match = this.config.repos.find(
|
|
299
|
+
(repo) => keywords.some((keyword) => repo.toLowerCase().includes(keyword))
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
return match;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Find nested repo for project ID (same logic as findRepoForProject)
|
|
306
|
+
*/
|
|
307
|
+
findNestedRepoForProject(projectId) {
|
|
308
|
+
if (!this.config.nestedRepos) return void 0;
|
|
309
|
+
const tempConfig = { ...this.config, repos: this.config.nestedRepos };
|
|
310
|
+
const tempSync = new GitHubMultiProjectSync(tempConfig);
|
|
311
|
+
return tempSync.findRepoForProject(projectId);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
function formatSyncResults(results) {
|
|
315
|
+
const lines = [];
|
|
316
|
+
lines.push("\u{1F4CA} GitHub Multi-Project Sync Results:\n");
|
|
317
|
+
const byProject = /* @__PURE__ */ new Map();
|
|
318
|
+
for (const result of results) {
|
|
319
|
+
const existing = byProject.get(result.project) || [];
|
|
320
|
+
existing.push(result);
|
|
321
|
+
byProject.set(result.project, existing);
|
|
322
|
+
}
|
|
323
|
+
for (const [project, projectResults] of byProject.entries()) {
|
|
324
|
+
lines.push(`
|
|
325
|
+
**${project}**:`);
|
|
326
|
+
for (const result of projectResults) {
|
|
327
|
+
const icon = result.action === "created" ? "\u2705" : result.action === "updated" ? "\u{1F504}" : "\u23ED\uFE0F";
|
|
328
|
+
lines.push(` ${icon} ${result.repo}#${result.issueNumber} (${result.action})`);
|
|
329
|
+
lines.push(` ${result.url}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
lines.push(`
|
|
333
|
+
\u2705 Total: ${results.length} issues synced
|
|
334
|
+
`);
|
|
335
|
+
return lines.join("\n");
|
|
336
|
+
}
|
|
337
|
+
export {
|
|
338
|
+
GitHubMultiProjectSync,
|
|
339
|
+
formatSyncResults
|
|
340
|
+
};
|