specweave 1.0.235 → 1.0.239
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/README.md +89 -193
- package/dist/plugins/specweave-github/lib/github-ac-comment-poster.d.ts +37 -0
- package/dist/plugins/specweave-github/lib/github-ac-comment-poster.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-ac-comment-poster.js +176 -0
- package/dist/plugins/specweave-github/lib/github-ac-comment-poster.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-batch-sync.d.ts +36 -0
- package/dist/plugins/specweave-github/lib/github-batch-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-batch-sync.js +115 -0
- package/dist/plugins/specweave-github/lib/github-batch-sync.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-board-resolver-v2.d.ts +37 -0
- package/dist/plugins/specweave-github/lib/github-board-resolver-v2.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-board-resolver-v2.js +56 -0
- package/dist/plugins/specweave-github/lib/github-board-resolver-v2.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-conflict-resolver.d.ts +68 -0
- package/dist/plugins/specweave-github/lib/github-conflict-resolver.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-conflict-resolver.js +102 -0
- package/dist/plugins/specweave-github/lib/github-conflict-resolver.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-cross-repo-sync.d.ts +64 -0
- package/dist/plugins/specweave-github/lib/github-cross-repo-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-cross-repo-sync.js +162 -0
- package/dist/plugins/specweave-github/lib/github-cross-repo-sync.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-field-sync.d.ts +50 -0
- package/dist/plugins/specweave-github/lib/github-field-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-field-sync.js +107 -0
- package/dist/plugins/specweave-github/lib/github-field-sync.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-graphql-client.d.ts +53 -0
- package/dist/plugins/specweave-github/lib/github-graphql-client.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-graphql-client.js +138 -0
- package/dist/plugins/specweave-github/lib/github-graphql-client.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-issue-body-generator.d.ts +40 -0
- package/dist/plugins/specweave-github/lib/github-issue-body-generator.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-issue-body-generator.js +50 -0
- package/dist/plugins/specweave-github/lib/github-issue-body-generator.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-issue-body-parser.d.ts +30 -0
- package/dist/plugins/specweave-github/lib/github-issue-body-parser.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-issue-body-parser.js +75 -0
- package/dist/plugins/specweave-github/lib/github-issue-body-parser.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-pull-sync.d.ts +94 -0
- package/dist/plugins/specweave-github/lib/github-pull-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-pull-sync.js +232 -0
- package/dist/plugins/specweave-github/lib/github-pull-sync.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-push-sync.d.ts +50 -0
- package/dist/plugins/specweave-github/lib/github-push-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-push-sync.js +114 -0
- package/dist/plugins/specweave-github/lib/github-push-sync.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-rate-limiter.d.ts +53 -0
- package/dist/plugins/specweave-github/lib/github-rate-limiter.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-rate-limiter.js +109 -0
- package/dist/plugins/specweave-github/lib/github-rate-limiter.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-spec-frontmatter-updater.d.ts +21 -0
- package/dist/plugins/specweave-github/lib/github-spec-frontmatter-updater.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-spec-frontmatter-updater.js +161 -0
- package/dist/plugins/specweave-github/lib/github-spec-frontmatter-updater.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-sync-orchestrator.d.ts +46 -0
- package/dist/plugins/specweave-github/lib/github-sync-orchestrator.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-sync-orchestrator.js +99 -0
- package/dist/plugins/specweave-github/lib/github-sync-orchestrator.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-us-auto-closer.d.ts +43 -0
- package/dist/plugins/specweave-github/lib/github-us-auto-closer.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-us-auto-closer.js +153 -0
- package/dist/plugins/specweave-github/lib/github-us-auto-closer.js.map +1 -0
- package/dist/plugins/specweave-github/lib/index.d.ts +1 -4
- package/dist/plugins/specweave-github/lib/index.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/index.js +1 -4
- package/dist/plugins/specweave-github/lib/index.js.map +1 -1
- package/dist/plugins/specweave-testing/lib/playwright-ci-defaults.d.ts +7 -0
- package/dist/plugins/specweave-testing/lib/playwright-ci-defaults.d.ts.map +1 -0
- package/dist/plugins/specweave-testing/lib/playwright-ci-defaults.js +15 -0
- package/dist/plugins/specweave-testing/lib/playwright-ci-defaults.js.map +1 -0
- package/dist/plugins/specweave-testing/lib/playwright-cli-detector.d.ts +10 -0
- package/dist/plugins/specweave-testing/lib/playwright-cli-detector.d.ts.map +1 -0
- package/dist/plugins/specweave-testing/lib/playwright-cli-detector.js +36 -0
- package/dist/plugins/specweave-testing/lib/playwright-cli-detector.js.map +1 -0
- package/dist/plugins/specweave-testing/lib/playwright-cli-runner.d.ts +25 -0
- package/dist/plugins/specweave-testing/lib/playwright-cli-runner.d.ts.map +1 -0
- package/dist/plugins/specweave-testing/lib/playwright-cli-runner.js +57 -0
- package/dist/plugins/specweave-testing/lib/playwright-cli-runner.js.map +1 -0
- package/dist/plugins/specweave-testing/lib/playwright-routing.d.ts +7 -0
- package/dist/plugins/specweave-testing/lib/playwright-routing.d.ts.map +1 -0
- package/dist/plugins/specweave-testing/lib/playwright-routing.js +17 -0
- package/dist/plugins/specweave-testing/lib/playwright-routing.js.map +1 -0
- package/dist/src/cli/commands/auto.d.ts.map +1 -1
- package/dist/src/cli/commands/auto.js +1 -2
- package/dist/src/cli/commands/auto.js.map +1 -1
- package/dist/src/cli/commands/cancel-auto.js +1 -2
- package/dist/src/cli/commands/cancel-auto.js.map +1 -1
- package/dist/src/cli/commands/living-docs.js +2 -2
- package/dist/src/cli/commands/living-docs.js.map +1 -1
- package/dist/src/cli/commands/update.d.ts.map +1 -1
- package/dist/src/cli/commands/update.js +1 -2
- package/dist/src/cli/commands/update.js.map +1 -1
- package/dist/src/core/config/types.d.ts +8 -0
- package/dist/src/core/config/types.d.ts.map +1 -1
- package/dist/src/core/config/types.js +3 -0
- package/dist/src/core/config/types.js.map +1 -1
- package/dist/src/core/types/sync-profile.d.ts +72 -0
- package/dist/src/core/types/sync-profile.d.ts.map +1 -1
- package/dist/src/core/types/sync-profile.js +6 -0
- package/dist/src/core/types/sync-profile.js.map +1 -1
- package/package.json +2 -2
- package/plugins/specweave/hooks/hooks.json +2 -2
- package/plugins/specweave/hooks/startup-health-check.sh +1 -1
- package/plugins/specweave/hooks/stop-auto-v5.sh +166 -0
- package/plugins/specweave/hooks/user-prompt-submit.sh +10 -0
- package/plugins/specweave/hooks/v2/dispatchers/post-tool-use.sh +21 -1
- package/plugins/specweave/hooks/v2/dispatchers/session-start.sh +1 -1
- package/plugins/specweave/skills/auto/SKILL.md +71 -251
- package/plugins/specweave/skills/team-build/SKILL.md +370 -0
- package/plugins/specweave/skills/team-merge/SKILL.md +123 -0
- package/plugins/specweave/skills/team-orchestrate/SKILL.md +800 -0
- package/plugins/specweave/skills/team-status/SKILL.md +89 -0
- package/plugins/specweave-github/MULTI-PROJECT-SYNC-ARCHITECTURE.md +94 -8
- package/plugins/specweave-github/commands/sync.md +17 -3
- package/plugins/specweave-github/hooks/github-ac-sync-handler.sh +255 -0
- package/plugins/specweave-github/hooks/github-auto-create-handler.sh +455 -0
- package/plugins/specweave-github/lib/github-ac-comment-poster.js +150 -0
- package/plugins/specweave-github/lib/github-ac-comment-poster.ts +245 -0
- package/plugins/specweave-github/lib/github-batch-sync.js +93 -0
- package/plugins/specweave-github/lib/github-batch-sync.ts +152 -0
- package/plugins/specweave-github/lib/github-board-resolver-v2.js +47 -0
- package/plugins/specweave-github/lib/github-board-resolver-v2.ts +73 -0
- package/plugins/specweave-github/lib/github-conflict-resolver.js +90 -0
- package/plugins/specweave-github/lib/github-conflict-resolver.ts +154 -0
- package/plugins/specweave-github/lib/github-cross-repo-sync.js +168 -0
- package/plugins/specweave-github/lib/github-cross-repo-sync.ts +252 -0
- package/plugins/specweave-github/lib/github-field-sync.js +116 -0
- package/plugins/specweave-github/lib/github-field-sync.ts +165 -0
- package/plugins/specweave-github/lib/github-graphql-client.js +129 -0
- package/plugins/specweave-github/lib/github-graphql-client.ts +181 -0
- package/plugins/specweave-github/lib/github-issue-body-generator.js +30 -0
- package/plugins/specweave-github/lib/github-issue-body-generator.ts +76 -0
- package/plugins/specweave-github/lib/github-issue-body-parser.js +55 -0
- package/plugins/specweave-github/lib/github-issue-body-parser.ts +92 -0
- package/plugins/specweave-github/lib/github-pull-sync.js +185 -0
- package/plugins/specweave-github/lib/github-pull-sync.ts +343 -0
- package/plugins/specweave-github/lib/github-push-sync.js +119 -0
- package/plugins/specweave-github/lib/github-push-sync.ts +174 -0
- package/plugins/specweave-github/lib/github-rate-limiter.js +96 -0
- package/plugins/specweave-github/lib/github-rate-limiter.ts +143 -0
- package/plugins/specweave-github/lib/github-spec-frontmatter-updater.js +117 -0
- package/plugins/specweave-github/lib/github-spec-frontmatter-updater.ts +180 -0
- package/plugins/specweave-github/lib/github-sync-orchestrator.js +84 -0
- package/plugins/specweave-github/lib/github-sync-orchestrator.ts +156 -0
- package/plugins/specweave-github/lib/github-us-auto-closer.js +134 -0
- package/plugins/specweave-github/lib/github-us-auto-closer.ts +226 -0
- package/plugins/specweave-github/lib/index.js +1 -7
- package/plugins/specweave-github/lib/index.ts +1 -4
- package/plugins/specweave-github/skills/github-sync/SKILL.md +76 -4
- package/plugins/specweave-testing/commands/e2e-setup.md +18 -0
- package/plugins/specweave-testing/commands/ui-automate.md +2 -0
- package/plugins/specweave-testing/commands/ui-inspect.md +8 -0
- package/plugins/specweave-testing/lib/playwright-ci-defaults.d.ts +6 -0
- package/plugins/specweave-testing/lib/playwright-ci-defaults.js +14 -0
- package/plugins/specweave-testing/lib/playwright-ci-defaults.ts +24 -0
- package/plugins/specweave-testing/lib/playwright-cli-detector.js +33 -0
- package/plugins/specweave-testing/lib/playwright-cli-detector.ts +48 -0
- package/plugins/specweave-testing/lib/playwright-cli-runner.js +58 -0
- package/plugins/specweave-testing/lib/playwright-cli-runner.ts +80 -0
- package/plugins/specweave-testing/lib/playwright-routing.js +16 -0
- package/plugins/specweave-testing/lib/playwright-routing.ts +38 -0
- package/plugins/specweave-testing/skills/e2e-testing/SKILL.md +38 -0
- package/src/templates/CLAUDE.md.template +7 -0
- package/src/templates/config.json.template +9 -1
- package/dist/plugins/specweave-github/lib/subtask-sync.d.ts +0 -51
- package/dist/plugins/specweave-github/lib/subtask-sync.d.ts.map +0 -1
- package/dist/plugins/specweave-github/lib/subtask-sync.js +0 -147
- package/dist/plugins/specweave-github/lib/subtask-sync.js.map +0 -1
- package/dist/plugins/specweave-github/lib/task-parser.d.ts +0 -37
- package/dist/plugins/specweave-github/lib/task-parser.d.ts.map +0 -1
- package/dist/plugins/specweave-github/lib/task-parser.js +0 -211
- package/dist/plugins/specweave-github/lib/task-parser.js.map +0 -1
- package/dist/plugins/specweave-github/lib/task-sync.d.ts +0 -56
- package/dist/plugins/specweave-github/lib/task-sync.d.ts.map +0 -1
- package/dist/plugins/specweave-github/lib/task-sync.js +0 -375
- package/dist/plugins/specweave-github/lib/task-sync.js.map +0 -1
- package/plugins/specweave/hooks/validate-completion-conditions.sh +0 -474
- package/plugins/specweave-github/lib/subtask-sync.d.ts +0 -51
- package/plugins/specweave-github/lib/subtask-sync.d.ts.map +0 -1
- package/plugins/specweave-github/lib/subtask-sync.js +0 -154
- package/plugins/specweave-github/lib/subtask-sync.js.map +0 -1
- package/plugins/specweave-github/lib/subtask-sync.ts +0 -225
- package/plugins/specweave-github/lib/task-parser.d.js +0 -0
- package/plugins/specweave-github/lib/task-parser.d.ts +0 -37
- package/plugins/specweave-github/lib/task-parser.d.ts.map +0 -1
- package/plugins/specweave-github/lib/task-parser.js +0 -195
- package/plugins/specweave-github/lib/task-parser.js.map +0 -1
- package/plugins/specweave-github/lib/task-parser.ts +0 -246
- package/plugins/specweave-github/lib/task-sync.d.js +0 -0
- package/plugins/specweave-github/lib/task-sync.d.ts +0 -51
- package/plugins/specweave-github/lib/task-sync.d.ts.map +0 -1
- package/plugins/specweave-github/lib/task-sync.js +0 -415
- package/plugins/specweave-github/lib/task-sync.js.map +0 -1
- package/plugins/specweave-github/lib/task-sync.ts +0 -451
- package/plugins/specweave-github/skills/github-issue-tracker/SKILL.md +0 -496
- /package/plugins/specweave/hooks/{stop-auto.sh → _archive/stop-auto-v4-legacy.sh} +0 -0
- /package/plugins/{specweave-github/lib/subtask-sync.d.js → specweave-testing/lib/playwright-ci-defaults.d.js} +0 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Field Sync — Status and Priority field sync for Projects V2
|
|
3
|
+
*
|
|
4
|
+
* Syncs Status and Priority custom fields on Projects V2 items
|
|
5
|
+
* using configurable field mappings.
|
|
6
|
+
*
|
|
7
|
+
* @module github-field-sync
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { GitHubGraphQLClient } from './github-graphql-client.js';
|
|
11
|
+
|
|
12
|
+
export interface FieldSyncConfig {
|
|
13
|
+
projectId: string;
|
|
14
|
+
statusFieldMapping?: Record<string, string>;
|
|
15
|
+
priorityFieldMapping?: Record<string, string>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface FieldSyncItem {
|
|
19
|
+
itemId: string;
|
|
20
|
+
status?: string;
|
|
21
|
+
priority?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface FieldSyncResult {
|
|
25
|
+
updated: Array<{ itemId: string; field: string; value: string }>;
|
|
26
|
+
warnings: Array<{ itemId: string; field: string; message: string }>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface CachedField {
|
|
30
|
+
id: string;
|
|
31
|
+
name: string;
|
|
32
|
+
options: Map<string, string>; // option name → option ID
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const DEFAULT_STATUS_MAPPING: Record<string, string> = {
|
|
36
|
+
'planned': 'Todo',
|
|
37
|
+
'in-progress': 'In Progress',
|
|
38
|
+
'completed': 'Done',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const DEFAULT_PRIORITY_MAPPING: Record<string, string> = {
|
|
42
|
+
'P1': 'Urgent',
|
|
43
|
+
'P2': 'High',
|
|
44
|
+
'P3': 'Medium',
|
|
45
|
+
'P4': 'Low',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export class GitHubFieldSync {
|
|
49
|
+
private client: GitHubGraphQLClient;
|
|
50
|
+
private config: FieldSyncConfig;
|
|
51
|
+
private statusField?: CachedField;
|
|
52
|
+
private priorityField?: CachedField;
|
|
53
|
+
private fieldsLoaded = false;
|
|
54
|
+
|
|
55
|
+
constructor(client: GitHubGraphQLClient, config: FieldSyncConfig) {
|
|
56
|
+
this.client = client;
|
|
57
|
+
this.config = config;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Load project fields and cache field IDs and option IDs.
|
|
62
|
+
*/
|
|
63
|
+
async loadFields(): Promise<void> {
|
|
64
|
+
if (this.fieldsLoaded) return;
|
|
65
|
+
|
|
66
|
+
const fields = await this.client.getProjectFields(this.config.projectId);
|
|
67
|
+
|
|
68
|
+
for (const field of fields) {
|
|
69
|
+
if (field.name === 'Status' && field.options) {
|
|
70
|
+
this.statusField = {
|
|
71
|
+
id: field.id,
|
|
72
|
+
name: field.name,
|
|
73
|
+
options: new Map(field.options.map(o => [o.name, o.id])),
|
|
74
|
+
};
|
|
75
|
+
} else if (field.name === 'Priority' && field.options) {
|
|
76
|
+
this.priorityField = {
|
|
77
|
+
id: field.id,
|
|
78
|
+
name: field.name,
|
|
79
|
+
options: new Map(field.options.map(o => [o.name, o.id])),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
this.fieldsLoaded = true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Sync fields for one or more items.
|
|
89
|
+
* Auto-calls loadFields if not called yet.
|
|
90
|
+
*/
|
|
91
|
+
async syncItemFields(items: FieldSyncItem[]): Promise<FieldSyncResult> {
|
|
92
|
+
if (!this.fieldsLoaded) {
|
|
93
|
+
await this.loadFields();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const result: FieldSyncResult = { updated: [], warnings: [] };
|
|
97
|
+
|
|
98
|
+
for (const item of items) {
|
|
99
|
+
// Sync Status field
|
|
100
|
+
if (item.status !== undefined) {
|
|
101
|
+
await this.syncField(item, 'Status', item.status, this.statusField,
|
|
102
|
+
this.config.statusFieldMapping ?? DEFAULT_STATUS_MAPPING, result);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Sync Priority field
|
|
106
|
+
if (item.priority !== undefined) {
|
|
107
|
+
await this.syncField(item, 'Priority', item.priority, this.priorityField,
|
|
108
|
+
this.config.priorityFieldMapping ?? DEFAULT_PRIORITY_MAPPING, result);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private async syncField(
|
|
116
|
+
item: FieldSyncItem,
|
|
117
|
+
fieldName: string,
|
|
118
|
+
value: string,
|
|
119
|
+
cachedField: CachedField | undefined,
|
|
120
|
+
mapping: Record<string, string>,
|
|
121
|
+
result: FieldSyncResult,
|
|
122
|
+
): Promise<void> {
|
|
123
|
+
if (!cachedField) {
|
|
124
|
+
result.warnings.push({
|
|
125
|
+
itemId: item.itemId,
|
|
126
|
+
field: fieldName,
|
|
127
|
+
message: `${fieldName} field not found on project`,
|
|
128
|
+
});
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const mappedValue = mapping[value];
|
|
133
|
+
if (!mappedValue) {
|
|
134
|
+
result.warnings.push({
|
|
135
|
+
itemId: item.itemId,
|
|
136
|
+
field: fieldName,
|
|
137
|
+
message: `No mapping found for ${fieldName} value "${value}"`,
|
|
138
|
+
});
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const optionId = cachedField.options.get(mappedValue);
|
|
143
|
+
if (!optionId) {
|
|
144
|
+
result.warnings.push({
|
|
145
|
+
itemId: item.itemId,
|
|
146
|
+
field: fieldName,
|
|
147
|
+
message: `Option "${mappedValue}" not found on ${fieldName} field`,
|
|
148
|
+
});
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
await this.client.updateItemFieldValue(
|
|
153
|
+
this.config.projectId,
|
|
154
|
+
item.itemId,
|
|
155
|
+
cachedField.id,
|
|
156
|
+
{ singleSelectOptionId: optionId },
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
result.updated.push({
|
|
160
|
+
itemId: item.itemId,
|
|
161
|
+
field: fieldName,
|
|
162
|
+
value: mappedValue,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { execFile } from "child_process";
|
|
2
|
+
class GitHubGraphQLClient {
|
|
3
|
+
constructor(token) {
|
|
4
|
+
this.token = token;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Resolve an owner login (user or org) to a node ID.
|
|
8
|
+
* Tries user first, falls back to organization.
|
|
9
|
+
*/
|
|
10
|
+
async getOwnerNodeId(login) {
|
|
11
|
+
const userQuery = `query { user(login: "${login}") { id } }`;
|
|
12
|
+
const userResult = await this.executeGraphQL(userQuery);
|
|
13
|
+
if (userResult.data?.user && userResult.data.user.id) {
|
|
14
|
+
return userResult.data.user.id;
|
|
15
|
+
}
|
|
16
|
+
const orgQuery = `query { organization(login: "${login}") { id } }`;
|
|
17
|
+
const orgResult = await this.executeGraphQL(orgQuery);
|
|
18
|
+
if (orgResult.data?.organization && orgResult.data.organization.id) {
|
|
19
|
+
return orgResult.data.organization.id;
|
|
20
|
+
}
|
|
21
|
+
throw new Error(`Could not resolve to a User or Organization with the login of '${login}'.`);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Create a new GitHub Projects V2 board.
|
|
25
|
+
*/
|
|
26
|
+
async createProjectV2(ownerId, title) {
|
|
27
|
+
const query = `mutation {
|
|
28
|
+
createProjectV2(input: { ownerId: "${ownerId}", title: "${title}" }) {
|
|
29
|
+
projectV2 { id number }
|
|
30
|
+
}
|
|
31
|
+
}`;
|
|
32
|
+
const result = await this.executeGraphQL(query);
|
|
33
|
+
const project = result.data?.createProjectV2?.projectV2;
|
|
34
|
+
return { id: project.id, number: project.number };
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Add an issue (by node ID) to a Projects V2 board.
|
|
38
|
+
* Returns the project item ID. Idempotent.
|
|
39
|
+
*/
|
|
40
|
+
async addProjectV2Item(projectId, contentId) {
|
|
41
|
+
const query = `mutation {
|
|
42
|
+
addProjectV2ItemById(input: { projectId: "${projectId}", contentId: "${contentId}" }) {
|
|
43
|
+
item { id }
|
|
44
|
+
}
|
|
45
|
+
}`;
|
|
46
|
+
const result = await this.executeGraphQL(query);
|
|
47
|
+
const item = result.data?.addProjectV2ItemById?.item;
|
|
48
|
+
return item.id;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Update a field value on a Projects V2 item.
|
|
52
|
+
*/
|
|
53
|
+
async updateItemFieldValue(projectId, itemId, fieldId, value) {
|
|
54
|
+
const query = `mutation {
|
|
55
|
+
updateProjectV2ItemFieldValue(input: {
|
|
56
|
+
projectId: "${projectId}",
|
|
57
|
+
itemId: "${itemId}",
|
|
58
|
+
fieldId: "${fieldId}",
|
|
59
|
+
value: { singleSelectOptionId: "${value.singleSelectOptionId}" }
|
|
60
|
+
}) {
|
|
61
|
+
projectV2Item { id }
|
|
62
|
+
}
|
|
63
|
+
}`;
|
|
64
|
+
await this.executeGraphQL(query);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Get field definitions for a Projects V2 board.
|
|
68
|
+
*/
|
|
69
|
+
async getProjectFields(projectId) {
|
|
70
|
+
const query = `query {
|
|
71
|
+
node(id: "${projectId}") {
|
|
72
|
+
... on ProjectV2 {
|
|
73
|
+
fields(first: 50) {
|
|
74
|
+
nodes {
|
|
75
|
+
... on ProjectV2Field { id name dataType }
|
|
76
|
+
... on ProjectV2SingleSelectField {
|
|
77
|
+
id name dataType
|
|
78
|
+
options { id name }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}`;
|
|
85
|
+
const result = await this.executeGraphQL(query);
|
|
86
|
+
const fields = result.data?.node?.fields?.nodes || [];
|
|
87
|
+
return fields.map((f) => {
|
|
88
|
+
const field = { id: f.id, name: f.name };
|
|
89
|
+
if (f.options && f.options.length > 0) {
|
|
90
|
+
field.options = f.options;
|
|
91
|
+
}
|
|
92
|
+
return field;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Execute a GraphQL query via `gh api graphql`.
|
|
97
|
+
*/
|
|
98
|
+
executeGraphQL(query) {
|
|
99
|
+
return new Promise((resolve, reject) => {
|
|
100
|
+
const args = ["api", "graphql", "-f", `query=${query}`];
|
|
101
|
+
const opts = {};
|
|
102
|
+
if (this.token) {
|
|
103
|
+
opts.env = { ...process.env, GH_TOKEN: this.token };
|
|
104
|
+
}
|
|
105
|
+
execFile("gh", args, opts, (err, stdout, stderr) => {
|
|
106
|
+
if (err) {
|
|
107
|
+
reject(new Error(stderr || err.message));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
let parsed;
|
|
111
|
+
try {
|
|
112
|
+
parsed = JSON.parse(stdout);
|
|
113
|
+
} catch {
|
|
114
|
+
reject(new Error(`Failed to parse GraphQL response: ${stdout.slice(0, 200)}`));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (parsed.errors && parsed.errors.length > 0) {
|
|
118
|
+
const messages = parsed.errors.map((e) => e.message).join("; ");
|
|
119
|
+
reject(new Error(`GraphQL error: ${messages}`));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
resolve(parsed);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
export {
|
|
128
|
+
GitHubGraphQLClient
|
|
129
|
+
};
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub GraphQL Client for Projects V2
|
|
3
|
+
*
|
|
4
|
+
* Wraps `gh api graphql` CLI calls for GitHub Projects V2 operations.
|
|
5
|
+
* All calls use child_process.execFile for security (no shell interpolation).
|
|
6
|
+
*
|
|
7
|
+
* @module github-graphql-client
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { execFile } from 'child_process';
|
|
11
|
+
|
|
12
|
+
interface ProjectField {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
options?: Array<{ id: string; name: string }>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface GraphQLResponse {
|
|
19
|
+
data?: Record<string, unknown>;
|
|
20
|
+
errors?: Array<{ message: string; type?: string; path?: string[] }>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class GitHubGraphQLClient {
|
|
24
|
+
private token?: string;
|
|
25
|
+
|
|
26
|
+
constructor(token?: string) {
|
|
27
|
+
this.token = token;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Resolve an owner login (user or org) to a node ID.
|
|
32
|
+
* Tries user first, falls back to organization.
|
|
33
|
+
*/
|
|
34
|
+
async getOwnerNodeId(login: string): Promise<string> {
|
|
35
|
+
// Try user query first
|
|
36
|
+
const userQuery = `query { user(login: "${login}") { id } }`;
|
|
37
|
+
const userResult = await this.executeGraphQL(userQuery);
|
|
38
|
+
|
|
39
|
+
if (userResult.data?.user && (userResult.data.user as { id?: string }).id) {
|
|
40
|
+
return (userResult.data.user as { id: string }).id;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Fallback to organization
|
|
44
|
+
const orgQuery = `query { organization(login: "${login}") { id } }`;
|
|
45
|
+
const orgResult = await this.executeGraphQL(orgQuery);
|
|
46
|
+
|
|
47
|
+
if (orgResult.data?.organization && (orgResult.data.organization as { id?: string }).id) {
|
|
48
|
+
return (orgResult.data.organization as { id: string }).id;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
throw new Error(`Could not resolve to a User or Organization with the login of '${login}'.`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Create a new GitHub Projects V2 board.
|
|
56
|
+
*/
|
|
57
|
+
async createProjectV2(ownerId: string, title: string): Promise<{ id: string; number: number }> {
|
|
58
|
+
const query = `mutation {
|
|
59
|
+
createProjectV2(input: { ownerId: "${ownerId}", title: "${title}" }) {
|
|
60
|
+
projectV2 { id number }
|
|
61
|
+
}
|
|
62
|
+
}`;
|
|
63
|
+
|
|
64
|
+
const result = await this.executeGraphQL(query);
|
|
65
|
+
const project = (result.data?.createProjectV2 as { projectV2: { id: string; number: number } })?.projectV2;
|
|
66
|
+
return { id: project.id, number: project.number };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Add an issue (by node ID) to a Projects V2 board.
|
|
71
|
+
* Returns the project item ID. Idempotent.
|
|
72
|
+
*/
|
|
73
|
+
async addProjectV2Item(projectId: string, contentId: string): Promise<string> {
|
|
74
|
+
const query = `mutation {
|
|
75
|
+
addProjectV2ItemById(input: { projectId: "${projectId}", contentId: "${contentId}" }) {
|
|
76
|
+
item { id }
|
|
77
|
+
}
|
|
78
|
+
}`;
|
|
79
|
+
|
|
80
|
+
const result = await this.executeGraphQL(query);
|
|
81
|
+
const item = (result.data?.addProjectV2ItemById as { item: { id: string } })?.item;
|
|
82
|
+
return item.id;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Update a field value on a Projects V2 item.
|
|
87
|
+
*/
|
|
88
|
+
async updateItemFieldValue(
|
|
89
|
+
projectId: string,
|
|
90
|
+
itemId: string,
|
|
91
|
+
fieldId: string,
|
|
92
|
+
value: { singleSelectOptionId: string },
|
|
93
|
+
): Promise<void> {
|
|
94
|
+
const query = `mutation {
|
|
95
|
+
updateProjectV2ItemFieldValue(input: {
|
|
96
|
+
projectId: "${projectId}",
|
|
97
|
+
itemId: "${itemId}",
|
|
98
|
+
fieldId: "${fieldId}",
|
|
99
|
+
value: { singleSelectOptionId: "${value.singleSelectOptionId}" }
|
|
100
|
+
}) {
|
|
101
|
+
projectV2Item { id }
|
|
102
|
+
}
|
|
103
|
+
}`;
|
|
104
|
+
|
|
105
|
+
await this.executeGraphQL(query);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get field definitions for a Projects V2 board.
|
|
110
|
+
*/
|
|
111
|
+
async getProjectFields(projectId: string): Promise<ProjectField[]> {
|
|
112
|
+
const query = `query {
|
|
113
|
+
node(id: "${projectId}") {
|
|
114
|
+
... on ProjectV2 {
|
|
115
|
+
fields(first: 50) {
|
|
116
|
+
nodes {
|
|
117
|
+
... on ProjectV2Field { id name dataType }
|
|
118
|
+
... on ProjectV2SingleSelectField {
|
|
119
|
+
id name dataType
|
|
120
|
+
options { id name }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}`;
|
|
127
|
+
|
|
128
|
+
const result = await this.executeGraphQL(query);
|
|
129
|
+
const fields = (result.data?.node as { fields?: { nodes: Array<{
|
|
130
|
+
id: string;
|
|
131
|
+
name: string;
|
|
132
|
+
dataType: string;
|
|
133
|
+
options?: Array<{ id: string; name: string }>;
|
|
134
|
+
}> } })?.fields?.nodes || [];
|
|
135
|
+
|
|
136
|
+
return fields.map((f) => {
|
|
137
|
+
const field: ProjectField = { id: f.id, name: f.name };
|
|
138
|
+
if (f.options && f.options.length > 0) {
|
|
139
|
+
field.options = f.options;
|
|
140
|
+
}
|
|
141
|
+
return field;
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Execute a GraphQL query via `gh api graphql`.
|
|
147
|
+
*/
|
|
148
|
+
private executeGraphQL(query: string): Promise<GraphQLResponse> {
|
|
149
|
+
return new Promise((resolve, reject) => {
|
|
150
|
+
const args = ['api', 'graphql', '-f', `query=${query}`];
|
|
151
|
+
const opts: { env?: NodeJS.ProcessEnv } = {};
|
|
152
|
+
|
|
153
|
+
if (this.token) {
|
|
154
|
+
opts.env = { ...process.env, GH_TOKEN: this.token };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
execFile('gh', args, opts, (err, stdout, stderr) => {
|
|
158
|
+
if (err) {
|
|
159
|
+
reject(new Error(stderr || err.message));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let parsed: GraphQLResponse;
|
|
164
|
+
try {
|
|
165
|
+
parsed = JSON.parse(stdout);
|
|
166
|
+
} catch {
|
|
167
|
+
reject(new Error(`Failed to parse GraphQL response: ${stdout.slice(0, 200)}`));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (parsed.errors && parsed.errors.length > 0) {
|
|
172
|
+
const messages = parsed.errors.map((e) => e.message).join('; ');
|
|
173
|
+
reject(new Error(`GraphQL error: ${messages}`));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
resolve(parsed);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
function generateIssueBody(userStory) {
|
|
2
|
+
const lines = [];
|
|
3
|
+
lines.push("## Description");
|
|
4
|
+
lines.push("");
|
|
5
|
+
lines.push(userStory.description);
|
|
6
|
+
lines.push("");
|
|
7
|
+
lines.push(`**Priority**: ${userStory.priority}`);
|
|
8
|
+
lines.push("");
|
|
9
|
+
if (userStory.acceptanceCriteria.length > 0) {
|
|
10
|
+
lines.push("## Acceptance Criteria");
|
|
11
|
+
lines.push("");
|
|
12
|
+
lines.push("<!-- specweave:ac-start -->");
|
|
13
|
+
for (const ac of userStory.acceptanceCriteria) {
|
|
14
|
+
const checkbox = ac.completed ? "[x]" : "[ ]";
|
|
15
|
+
lines.push(`- ${checkbox} **${ac.id}**: ${ac.description}`);
|
|
16
|
+
}
|
|
17
|
+
lines.push("<!-- specweave:ac-end -->");
|
|
18
|
+
lines.push("");
|
|
19
|
+
}
|
|
20
|
+
const syncParts = [];
|
|
21
|
+
if (userStory.specId) {
|
|
22
|
+
syncParts.push(`spec=${userStory.specId}`);
|
|
23
|
+
}
|
|
24
|
+
syncParts.push(`us=${userStory.id}`);
|
|
25
|
+
lines.push(`<!-- specweave:sync ${syncParts.join(" ")} -->`);
|
|
26
|
+
return lines.join("\n");
|
|
27
|
+
}
|
|
28
|
+
export {
|
|
29
|
+
generateIssueBody
|
|
30
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Issue Body Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates GitHub issue body markdown from a User Story.
|
|
5
|
+
* Used by the spec-to-issue push sync to create rich issue content
|
|
6
|
+
* with AC checkboxes, priority badges, and sync markers.
|
|
7
|
+
*
|
|
8
|
+
* @module github-issue-body-generator
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface UserStoryInput {
|
|
12
|
+
/** User story ID, e.g., "US-001" */
|
|
13
|
+
id: string;
|
|
14
|
+
/** User story title */
|
|
15
|
+
title: string;
|
|
16
|
+
/** User story description */
|
|
17
|
+
description: string;
|
|
18
|
+
/** Priority, e.g., "P1" */
|
|
19
|
+
priority: string;
|
|
20
|
+
/** Acceptance criteria */
|
|
21
|
+
acceptanceCriteria: Array<{
|
|
22
|
+
id: string;
|
|
23
|
+
description: string;
|
|
24
|
+
completed: boolean;
|
|
25
|
+
}>;
|
|
26
|
+
/** Spec ID, e.g., "spec-001" */
|
|
27
|
+
specId?: string;
|
|
28
|
+
/** Local file path to the spec */
|
|
29
|
+
specPath?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Generate a GitHub issue body from a User Story input.
|
|
34
|
+
*
|
|
35
|
+
* Output format:
|
|
36
|
+
* - Description section
|
|
37
|
+
* - AC checkboxes between specweave markers
|
|
38
|
+
* - Priority badge
|
|
39
|
+
* - Sync footer marker
|
|
40
|
+
*/
|
|
41
|
+
export function generateIssueBody(userStory: UserStoryInput): string {
|
|
42
|
+
const lines: string[] = [];
|
|
43
|
+
|
|
44
|
+
// Description
|
|
45
|
+
lines.push('## Description');
|
|
46
|
+
lines.push('');
|
|
47
|
+
lines.push(userStory.description);
|
|
48
|
+
lines.push('');
|
|
49
|
+
|
|
50
|
+
// Priority badge
|
|
51
|
+
lines.push(`**Priority**: ${userStory.priority}`);
|
|
52
|
+
lines.push('');
|
|
53
|
+
|
|
54
|
+
// Acceptance Criteria with markers
|
|
55
|
+
if (userStory.acceptanceCriteria.length > 0) {
|
|
56
|
+
lines.push('## Acceptance Criteria');
|
|
57
|
+
lines.push('');
|
|
58
|
+
lines.push('<!-- specweave:ac-start -->');
|
|
59
|
+
for (const ac of userStory.acceptanceCriteria) {
|
|
60
|
+
const checkbox = ac.completed ? '[x]' : '[ ]';
|
|
61
|
+
lines.push(`- ${checkbox} **${ac.id}**: ${ac.description}`);
|
|
62
|
+
}
|
|
63
|
+
lines.push('<!-- specweave:ac-end -->');
|
|
64
|
+
lines.push('');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Sync footer marker
|
|
68
|
+
const syncParts: string[] = [];
|
|
69
|
+
if (userStory.specId) {
|
|
70
|
+
syncParts.push(`spec=${userStory.specId}`);
|
|
71
|
+
}
|
|
72
|
+
syncParts.push(`us=${userStory.id}`);
|
|
73
|
+
lines.push(`<!-- specweave:sync ${syncParts.join(' ')} -->`);
|
|
74
|
+
|
|
75
|
+
return lines.join('\n');
|
|
76
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const AC_CHECKBOX_RE = /^-\s+\[([ xX])\]\s+\*\*(?<acId>AC-[A-Z0-9]+-\d+)\*\*:\s*(?<desc>.+)$/;
|
|
2
|
+
const SYNC_MARKER_RE = /<!--\s*specweave:sync\s+(?:spec=(?<specId>[^\s]+)\s+)?us=(?<usId>[^\s]+)\s*-->/;
|
|
3
|
+
function parseIssueBody(body) {
|
|
4
|
+
const result = {
|
|
5
|
+
acceptanceCriteria: {}
|
|
6
|
+
};
|
|
7
|
+
if (!body || !body.trim()) {
|
|
8
|
+
return result;
|
|
9
|
+
}
|
|
10
|
+
const lines = body.split("\n");
|
|
11
|
+
let inAcSection = false;
|
|
12
|
+
let foundAcSection = false;
|
|
13
|
+
for (const line of lines) {
|
|
14
|
+
if (/^##\s+Acceptance Criteria/i.test(line)) {
|
|
15
|
+
inAcSection = true;
|
|
16
|
+
foundAcSection = true;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (inAcSection && /^##\s+/.test(line)) {
|
|
20
|
+
inAcSection = false;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (inAcSection) {
|
|
24
|
+
const match = line.match(AC_CHECKBOX_RE);
|
|
25
|
+
if (match?.groups) {
|
|
26
|
+
result.acceptanceCriteria[match.groups.acId] = {
|
|
27
|
+
checked: match[1] !== " ",
|
|
28
|
+
description: match.groups.desc.trim()
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (!foundAcSection) {
|
|
34
|
+
for (const line of lines) {
|
|
35
|
+
const match = line.match(AC_CHECKBOX_RE);
|
|
36
|
+
if (match?.groups) {
|
|
37
|
+
result.acceptanceCriteria[match.groups.acId] = {
|
|
38
|
+
checked: match[1] !== " ",
|
|
39
|
+
description: match.groups.desc.trim()
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const syncMatch = body.match(SYNC_MARKER_RE);
|
|
45
|
+
if (syncMatch?.groups?.usId) {
|
|
46
|
+
result.syncMarker = {
|
|
47
|
+
specId: syncMatch.groups.specId || "",
|
|
48
|
+
userStoryId: syncMatch.groups.usId
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
export {
|
|
54
|
+
parseIssueBody
|
|
55
|
+
};
|