agile-context-engineering 0.2.2 → 0.5.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-plugin/plugin.json +10 -0
- package/CHANGELOG.md +82 -0
- package/README.md +27 -18
- package/agents/ace-product-owner.md +1 -1
- package/agents/ace-technical-application-architect.md +28 -0
- package/agents/ace-wiki-mapper.md +144 -29
- package/bin/install.js +67 -63
- package/hooks/ace-check-update.js +17 -9
- package/package.json +7 -5
- package/shared/lib/ace-core.js +308 -0
- package/shared/lib/ace-core.test.js +308 -0
- package/shared/lib/ace-github.js +753 -0
- package/shared/lib/ace-story.js +400 -0
- package/shared/lib/ace-story.test.js +250 -0
- package/{agile-context-engineering → shared}/utils/ui-formatting.md +299 -299
- package/skills/execute-story/SKILL.md +110 -0
- package/skills/execute-story/script.js +305 -0
- package/skills/execute-story/script.test.js +261 -0
- package/skills/execute-story/walkthrough-template.xml +255 -0
- package/{agile-context-engineering/workflows/execute-story.xml → skills/execute-story/workflow.xml} +83 -9
- package/skills/help/SKILL.md +69 -0
- package/skills/help/script.js +318 -0
- package/skills/help/script.test.js +183 -0
- package/{agile-context-engineering/workflows/help.xml → skills/help/workflow.xml} +8 -8
- package/skills/init-coding-standards/SKILL.md +72 -0
- package/{agile-context-engineering/templates/wiki/coding-standards.xml → skills/init-coding-standards/coding-standards-template.xml} +38 -0
- package/skills/init-coding-standards/script.js +59 -0
- package/skills/init-coding-standards/script.test.js +70 -0
- package/{agile-context-engineering/workflows/init-coding-standards.xml → skills/init-coding-standards/workflow.xml} +4 -9
- package/skills/map-cross-cutting/SKILL.md +89 -0
- package/skills/map-cross-cutting/workflow.xml +330 -0
- package/skills/map-guide/SKILL.md +89 -0
- package/skills/map-guide/workflow.xml +320 -0
- package/skills/map-pattern/SKILL.md +89 -0
- package/skills/map-pattern/workflow.xml +331 -0
- package/skills/map-story/SKILL.md +127 -0
- package/skills/map-story/templates/guide.xml +137 -0
- package/skills/map-story/templates/pattern.xml +159 -0
- package/skills/map-story/templates/system-cross-cutting.xml +197 -0
- package/skills/map-story/templates/walkthrough.xml +255 -0
- package/{agile-context-engineering/workflows/map-story.xml → skills/map-story/workflow.xml} +258 -9
- package/skills/map-subsystem/SKILL.md +111 -0
- package/skills/map-subsystem/script.js +60 -0
- package/skills/map-subsystem/script.test.js +68 -0
- package/skills/map-subsystem/templates/decizions.xml +115 -0
- package/skills/map-subsystem/templates/guide.xml +137 -0
- package/{agile-context-engineering/templates/wiki → skills/map-subsystem/templates}/module-discovery.xml +3 -3
- package/skills/map-subsystem/templates/pattern.xml +159 -0
- package/skills/map-subsystem/templates/system-cross-cutting.xml +197 -0
- package/skills/map-subsystem/templates/system.xml +381 -0
- package/skills/map-subsystem/templates/walkthrough.xml +255 -0
- package/{agile-context-engineering/workflows/map-subsystem.xml → skills/map-subsystem/workflow.xml} +17 -21
- package/skills/map-sys-doc/SKILL.md +90 -0
- package/skills/map-sys-doc/system.xml +381 -0
- package/skills/map-sys-doc/workflow.xml +336 -0
- package/skills/map-system/SKILL.md +85 -0
- package/skills/map-system/script.js +84 -0
- package/skills/map-system/script.test.js +73 -0
- package/skills/map-system/templates/wiki-readme.xml +297 -0
- package/{agile-context-engineering/workflows/map-system.xml → skills/map-system/workflow.xml} +11 -16
- package/skills/map-walkthrough/SKILL.md +92 -0
- package/skills/map-walkthrough/walkthrough.xml +255 -0
- package/skills/map-walkthrough/workflow.xml +457 -0
- package/skills/plan-backlog/SKILL.md +75 -0
- package/skills/plan-backlog/script.js +136 -0
- package/skills/plan-backlog/script.test.js +83 -0
- package/{agile-context-engineering/workflows/plan-backlog.xml → skills/plan-backlog/workflow.xml} +13 -21
- package/skills/plan-feature/SKILL.md +76 -0
- package/skills/plan-feature/script.js +148 -0
- package/skills/plan-feature/script.test.js +80 -0
- package/{agile-context-engineering/workflows/plan-feature.xml → skills/plan-feature/workflow.xml} +21 -29
- package/skills/plan-product-vision/SKILL.md +75 -0
- package/skills/plan-product-vision/script.js +60 -0
- package/skills/plan-product-vision/script.test.js +69 -0
- package/{agile-context-engineering/workflows/plan-product-vision.xml → skills/plan-product-vision/workflow.xml} +4 -9
- package/skills/plan-story/SKILL.md +116 -0
- package/skills/plan-story/script.js +326 -0
- package/skills/plan-story/script.test.js +240 -0
- package/skills/plan-story/story-template.xml +451 -0
- package/{agile-context-engineering/workflows/plan-story.xml → skills/plan-story/workflow.xml} +1285 -909
- package/skills/research-external-solution/SKILL.md +107 -0
- package/skills/research-external-solution/script.js +238 -0
- package/skills/research-external-solution/script.test.js +134 -0
- package/{agile-context-engineering/workflows/research-external-solution.xml → skills/research-external-solution/workflow.xml} +4 -6
- package/skills/research-integration-solution/SKILL.md +98 -0
- package/{agile-context-engineering/templates/product/story-integration-solution.xml → skills/research-integration-solution/integration-solution-template.xml} +1 -0
- package/skills/research-integration-solution/script.js +231 -0
- package/skills/research-integration-solution/script.test.js +134 -0
- package/{agile-context-engineering/workflows/research-integration-solution.xml → skills/research-integration-solution/workflow.xml} +4 -5
- package/skills/research-story-wiki/SKILL.md +92 -0
- package/skills/research-story-wiki/script.js +231 -0
- package/skills/research-story-wiki/script.test.js +138 -0
- package/{agile-context-engineering/templates/product/story-wiki.xml → skills/research-story-wiki/story-wiki-template.xml} +4 -0
- package/{agile-context-engineering/workflows/research-story-wiki.xml → skills/research-story-wiki/workflow.xml} +5 -6
- package/skills/research-technical-solution/SKILL.md +103 -0
- package/skills/research-technical-solution/script.js +231 -0
- package/skills/research-technical-solution/script.test.js +134 -0
- package/{agile-context-engineering/workflows/research-technical-solution.xml → skills/research-technical-solution/workflow.xml} +4 -5
- package/skills/review-story/SKILL.md +100 -0
- package/skills/review-story/script.js +257 -0
- package/skills/review-story/script.test.js +169 -0
- package/skills/review-story/story-template.xml +451 -0
- package/{agile-context-engineering/workflows/review-story.xml → skills/review-story/workflow.xml} +1 -3
- package/skills/update/SKILL.md +53 -0
- package/{agile-context-engineering/workflows/update.xml → skills/update/workflow.xml} +237 -207
- package/agile-context-engineering/src/ace-tools.js +0 -2881
- package/agile-context-engineering/src/ace-tools.test.js +0 -1089
- package/agile-context-engineering/templates/_command.md +0 -54
- package/agile-context-engineering/templates/_workflow.xml +0 -17
- package/agile-context-engineering/templates/config.json +0 -0
- package/agile-context-engineering/templates/product/integration-solution.xml +0 -0
- package/agile-context-engineering/templates/wiki/wiki-readme.xml +0 -276
- package/commands/ace/execute-story.md +0 -137
- package/commands/ace/help.md +0 -93
- package/commands/ace/init-coding-standards.md +0 -83
- package/commands/ace/map-story.md +0 -156
- package/commands/ace/map-subsystem.md +0 -138
- package/commands/ace/map-system.md +0 -92
- package/commands/ace/plan-backlog.md +0 -83
- package/commands/ace/plan-feature.md +0 -89
- package/commands/ace/plan-product-vision.md +0 -81
- package/commands/ace/plan-story.md +0 -145
- package/commands/ace/research-external-solution.md +0 -138
- package/commands/ace/research-integration-solution.md +0 -135
- package/commands/ace/research-story-wiki.md +0 -116
- package/commands/ace/research-technical-solution.md +0 -147
- package/commands/ace/review-story.md +0 -109
- package/commands/ace/update.md +0 -54
- /package/{agile-context-engineering → shared}/utils/questioning.xml +0 -0
- /package/{agile-context-engineering/templates/product/story.xml → skills/execute-story/story-template.xml} +0 -0
- /package/{agile-context-engineering/templates/wiki → skills/map-cross-cutting}/system-cross-cutting.xml +0 -0
- /package/{agile-context-engineering/templates/wiki → skills/map-guide}/guide.xml +0 -0
- /package/{agile-context-engineering/templates/wiki → skills/map-pattern}/pattern.xml +0 -0
- /package/{agile-context-engineering/templates/wiki → skills/map-story/templates}/decizions.xml +0 -0
- /package/{agile-context-engineering/templates/wiki → skills/map-story/templates}/system.xml +0 -0
- /package/{agile-context-engineering/templates/wiki → skills/map-story/templates}/tech-debt-index.xml +0 -0
- /package/{agile-context-engineering/templates/wiki → skills/map-subsystem/templates}/subsystem-architecture.xml +0 -0
- /package/{agile-context-engineering/templates/wiki → skills/map-subsystem/templates}/subsystem-structure.xml +0 -0
- /package/{agile-context-engineering/templates/wiki → skills/map-system/templates}/system-architecture.xml +0 -0
- /package/{agile-context-engineering/templates/wiki → skills/map-system/templates}/system-structure.xml +0 -0
- /package/{agile-context-engineering/templates/wiki → skills/map-system/templates}/testing-framework.xml +0 -0
- /package/{agile-context-engineering/templates/product/product-backlog.xml → skills/plan-backlog/product-backlog-template.xml} +0 -0
- /package/{agile-context-engineering/templates/product/feature.xml → skills/plan-feature/feature-template.xml} +0 -0
- /package/{agile-context-engineering/templates/product/product-vision.xml → skills/plan-product-vision/product-vision-template.xml} +0 -0
- /package/{agile-context-engineering/templates/product/external-solution.xml → skills/research-external-solution/external-solution-template.xml} +0 -0
- /package/{agile-context-engineering/templates/product/story-technical-solution.xml → skills/research-technical-solution/technical-solution-template.xml} +0 -0
|
@@ -0,0 +1,753 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ACE GitHub — GitHub integration operations shared across ACE skills.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from ace-tools.js monolith. Contains: project context resolution
|
|
5
|
+
* and story/feature GitHub sync.
|
|
6
|
+
*
|
|
7
|
+
* Usage: const { syncStory, resolveProjectContext } = require('./ace-github');
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const { execSync } = require('child_process');
|
|
14
|
+
const {
|
|
15
|
+
safeReadFile, parseKeyValueArgs, execCommand, output, error,
|
|
16
|
+
} = require('./ace-core');
|
|
17
|
+
const {
|
|
18
|
+
extractStoryMetadata, extractIssueNumberFromFile,
|
|
19
|
+
} = require('./ace-story');
|
|
20
|
+
|
|
21
|
+
// ─── Project Context ─────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolve project ID and field definitions for a GitHub Project.
|
|
25
|
+
* Returns { project_id, fields } where fields maps field names to { id, type, options? }.
|
|
26
|
+
*/
|
|
27
|
+
function resolveProjectContext(owner, project, cwd) {
|
|
28
|
+
const projectListRaw = execCommand(
|
|
29
|
+
`gh project list --owner ${owner} --format json --limit 20`,
|
|
30
|
+
cwd
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
let project_id = null;
|
|
34
|
+
if (projectListRaw) {
|
|
35
|
+
try {
|
|
36
|
+
const parsed = JSON.parse(projectListRaw);
|
|
37
|
+
const projects = parsed.projects || parsed || [];
|
|
38
|
+
const match = projects.find(p => String(p.number) === String(project));
|
|
39
|
+
if (match) project_id = match.id;
|
|
40
|
+
} catch {}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const fieldsRaw = execCommand(
|
|
44
|
+
`gh project field-list ${project} --owner ${owner} --format json`,
|
|
45
|
+
cwd
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const fields = {};
|
|
49
|
+
if (fieldsRaw) {
|
|
50
|
+
try {
|
|
51
|
+
const parsed = JSON.parse(fieldsRaw);
|
|
52
|
+
const fieldList = parsed.fields || parsed || [];
|
|
53
|
+
for (const field of fieldList) {
|
|
54
|
+
const entry = { id: field.id, type: field.type };
|
|
55
|
+
if (field.options) {
|
|
56
|
+
entry.options = {};
|
|
57
|
+
for (const opt of field.options) {
|
|
58
|
+
entry.options[opt.name] = opt.id;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
fields[field.name] = entry;
|
|
62
|
+
}
|
|
63
|
+
} catch {}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { project_id, fields };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ─── Story Sync ──────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Sync story and feature body + project status to GitHub.
|
|
73
|
+
* Updates issue body via --body-file and project status via GraphQL.
|
|
74
|
+
*
|
|
75
|
+
* Required args: repo=owner/name story_file=path
|
|
76
|
+
* Optional args: feature_file=path owner=org project=number
|
|
77
|
+
*/
|
|
78
|
+
function syncStory(cwd, raw, extraArgs) {
|
|
79
|
+
const params = parseKeyValueArgs(extraArgs);
|
|
80
|
+
const repo = params.repo;
|
|
81
|
+
const storyFile = params.story_file;
|
|
82
|
+
|
|
83
|
+
if (!repo || !storyFile) {
|
|
84
|
+
error('sync-github requires: repo=owner/name story_file=path');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const result = {
|
|
88
|
+
story: { number: null, updated: false, status_synced: false, error: null },
|
|
89
|
+
feature: { number: null, updated: false, status_synced: false, error: null },
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Resolve project context for status updates (optional)
|
|
93
|
+
const owner = params.owner;
|
|
94
|
+
const project = params.project;
|
|
95
|
+
let projectCtx = null;
|
|
96
|
+
|
|
97
|
+
if (owner && project) {
|
|
98
|
+
projectCtx = resolveProjectContext(owner, project, cwd);
|
|
99
|
+
if (!projectCtx.project_id) {
|
|
100
|
+
process.stderr.write(` ! Could not resolve GitHub Project #${project}. Status updates skipped.\n`);
|
|
101
|
+
projectCtx = null;
|
|
102
|
+
} else if (!projectCtx.fields.Status) {
|
|
103
|
+
process.stderr.write(' ! GitHub Project has no Status field. Status updates skipped.\n');
|
|
104
|
+
projectCtx = null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Helper: update project status for a single issue
|
|
109
|
+
function syncProjectStatus(issueNumber, filePath, label) {
|
|
110
|
+
if (!projectCtx) return false;
|
|
111
|
+
|
|
112
|
+
const content = safeReadFile(filePath);
|
|
113
|
+
if (!content) return false;
|
|
114
|
+
|
|
115
|
+
const metadata = extractStoryMetadata(content);
|
|
116
|
+
const localStatus = metadata.status;
|
|
117
|
+
if (!localStatus) {
|
|
118
|
+
process.stderr.write(` — ${label} has no Status field. Skipping project status update.\n`);
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const statusField = projectCtx.fields.Status;
|
|
123
|
+
const statusOptionId = statusField.options?.[localStatus];
|
|
124
|
+
if (!statusOptionId) {
|
|
125
|
+
process.stderr.write(` ! GitHub Project has no status option "${localStatus}". Skipping status update for ${label}.\n`);
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Look up project item ID via GraphQL
|
|
130
|
+
const repoParts = repo.split('/');
|
|
131
|
+
const repoOwner = repoParts[0];
|
|
132
|
+
const repoName = repoParts[1] || repoParts[0];
|
|
133
|
+
const itemQuery = `query { repository(owner: \\"${repoOwner}\\", name: \\"${repoName}\\") { issue(number: ${issueNumber}) { projectItems(first: 10) { nodes { id project { id } } } } } }`;
|
|
134
|
+
const itemResult = execCommand(
|
|
135
|
+
`gh api graphql -f query="${itemQuery}"`,
|
|
136
|
+
cwd
|
|
137
|
+
);
|
|
138
|
+
let itemId = null;
|
|
139
|
+
if (itemResult) {
|
|
140
|
+
try {
|
|
141
|
+
const parsed = JSON.parse(itemResult);
|
|
142
|
+
const nodes = parsed.data?.repository?.issue?.projectItems?.nodes || [];
|
|
143
|
+
const match = nodes.find(n => n.project?.id === projectCtx.project_id);
|
|
144
|
+
itemId = match?.id || null;
|
|
145
|
+
} catch {}
|
|
146
|
+
}
|
|
147
|
+
if (!itemId) {
|
|
148
|
+
process.stderr.write(` ! ${label} #${issueNumber} not found in GitHub Project. Skipping status update.\n`);
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const statusOk = execCommand(
|
|
153
|
+
`gh project item-edit --project-id ${projectCtx.project_id} --id ${itemId} --field-id ${statusField.id} --single-select-option-id ${statusOptionId}`,
|
|
154
|
+
cwd
|
|
155
|
+
);
|
|
156
|
+
if (statusOk !== null) {
|
|
157
|
+
process.stderr.write(` + Updated ${label} #${issueNumber} project status → "${localStatus}".\n`);
|
|
158
|
+
return true;
|
|
159
|
+
} else {
|
|
160
|
+
process.stderr.write(` x FAILED to update ${label} #${issueNumber} project status.\n`);
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Sync story issue
|
|
166
|
+
const storyPath = path.isAbsolute(storyFile) ? storyFile : path.join(cwd, storyFile);
|
|
167
|
+
const storyIssue = extractIssueNumberFromFile(cwd, storyFile);
|
|
168
|
+
|
|
169
|
+
if (!storyIssue) {
|
|
170
|
+
result.story.error = 'No GitHub issue linked';
|
|
171
|
+
process.stderr.write(' — Story has no GitHub issue linked. Skipping.\n');
|
|
172
|
+
} else {
|
|
173
|
+
result.story.number = storyIssue;
|
|
174
|
+
const safePath = storyPath.replace(/\\/g, '/');
|
|
175
|
+
try {
|
|
176
|
+
execSync(`gh issue edit ${storyIssue} --repo ${repo} --body-file "${safePath}"`, {
|
|
177
|
+
cwd, shell: 'bash', stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf-8', timeout: 30000,
|
|
178
|
+
});
|
|
179
|
+
result.story.updated = true;
|
|
180
|
+
process.stderr.write(` + Updated GitHub story issue #${storyIssue}.\n`);
|
|
181
|
+
} catch (e) {
|
|
182
|
+
result.story.error = (e.stderr || e.message || 'unknown error').trim();
|
|
183
|
+
process.stderr.write(` x FAILED to update GitHub story issue #${storyIssue}.\n`);
|
|
184
|
+
process.stderr.write(` Error: ${result.story.error}\n`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (result.story.updated) {
|
|
188
|
+
result.story.status_synced = syncProjectStatus(storyIssue, storyPath, 'Story');
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Sync feature issue
|
|
193
|
+
const featureFile = params.feature_file;
|
|
194
|
+
if (featureFile) {
|
|
195
|
+
const featurePath = path.isAbsolute(featureFile) ? featureFile : path.join(cwd, featureFile);
|
|
196
|
+
const featureIssue = extractIssueNumberFromFile(cwd, featureFile);
|
|
197
|
+
|
|
198
|
+
if (!featureIssue) {
|
|
199
|
+
result.feature.error = 'No GitHub issue linked';
|
|
200
|
+
process.stderr.write(' — Feature has no GitHub issue linked. Skipping.\n');
|
|
201
|
+
} else {
|
|
202
|
+
result.feature.number = featureIssue;
|
|
203
|
+
const safePath = featurePath.replace(/\\/g, '/');
|
|
204
|
+
try {
|
|
205
|
+
execSync(`gh issue edit ${featureIssue} --repo ${repo} --body-file "${safePath}"`, {
|
|
206
|
+
cwd, shell: 'bash', stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf-8', timeout: 30000,
|
|
207
|
+
});
|
|
208
|
+
result.feature.updated = true;
|
|
209
|
+
process.stderr.write(` + Updated GitHub feature issue #${featureIssue}.\n`);
|
|
210
|
+
} catch (e) {
|
|
211
|
+
result.feature.error = (e.stderr || e.message || 'unknown error').trim();
|
|
212
|
+
process.stderr.write(` x FAILED to update GitHub feature issue #${featureIssue}.\n`);
|
|
213
|
+
process.stderr.write(` Error: ${result.feature.error}\n`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (result.feature.updated) {
|
|
217
|
+
result.feature.status_synced = syncProjectStatus(featureIssue, featurePath, 'Feature');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
output(result, raw);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ─── Resolve Fields ─────────────────────────────────────────────────────────
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Resolve native issue types and project field definitions.
|
|
229
|
+
* Required args: repo=owner/name owner=org project=number
|
|
230
|
+
* Outputs: { issue_types, project_id, fields }
|
|
231
|
+
*/
|
|
232
|
+
function resolveFields(cwd, raw, extraArgs) {
|
|
233
|
+
const params = parseKeyValueArgs(extraArgs);
|
|
234
|
+
const repo = params.repo;
|
|
235
|
+
const owner = params.owner;
|
|
236
|
+
const project = params.project;
|
|
237
|
+
|
|
238
|
+
if (!repo || !owner || !project) {
|
|
239
|
+
error('resolve-fields requires: repo=owner/name owner=org project=number');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const repoName = repo.split('/')[1];
|
|
243
|
+
|
|
244
|
+
// Resolve native issue types via GraphQL
|
|
245
|
+
const issueTypes = {};
|
|
246
|
+
const typeQuery = `query { repository(owner: \\"${owner}\\", name: \\"${repoName}\\") { issueTypes(first: 10) { nodes { id name } } } }`;
|
|
247
|
+
const typeResult = execCommand(
|
|
248
|
+
`gh api graphql -f query="${typeQuery}"`,
|
|
249
|
+
cwd
|
|
250
|
+
);
|
|
251
|
+
if (typeResult) {
|
|
252
|
+
try {
|
|
253
|
+
const parsed = JSON.parse(typeResult);
|
|
254
|
+
const nodes = parsed.data?.repository?.issueTypes?.nodes || [];
|
|
255
|
+
for (const node of nodes) {
|
|
256
|
+
issueTypes[node.name] = node.id;
|
|
257
|
+
}
|
|
258
|
+
} catch {}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Resolve project context (project_id + fields)
|
|
262
|
+
const ctx = resolveProjectContext(owner, project, cwd);
|
|
263
|
+
|
|
264
|
+
output({ issue_types: issueTypes, project_id: ctx.project_id, fields: ctx.fields }, raw);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ─── Create Issue ───────────────────────────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Create a GitHub issue, set native type, add to project, and set project fields.
|
|
271
|
+
*
|
|
272
|
+
* Required args: type, title, repo, owner, project, project_id, type_id
|
|
273
|
+
* Body args (one of): body=... OR body_file=path
|
|
274
|
+
* Optional: status_field_id, status_option_id, priority_field_id, priority_option_id,
|
|
275
|
+
* estimate_field_id, estimate, parent, milestone
|
|
276
|
+
* Outputs: { number, url, item_id, type_set, status_set, priority_set, estimate_set,
|
|
277
|
+
* parent_set, milestone_set }
|
|
278
|
+
*/
|
|
279
|
+
function createIssue(cwd, raw, extraArgs) {
|
|
280
|
+
const params = parseKeyValueArgs(extraArgs);
|
|
281
|
+
const type = params.type;
|
|
282
|
+
const title = params.title;
|
|
283
|
+
let body = params.body || '';
|
|
284
|
+
const bodyFile = params.body_file;
|
|
285
|
+
const repo = params.repo;
|
|
286
|
+
const owner = params.owner;
|
|
287
|
+
const project = params.project;
|
|
288
|
+
const projectId = params.project_id;
|
|
289
|
+
const typeId = params.type_id;
|
|
290
|
+
|
|
291
|
+
// Read body from file if provided
|
|
292
|
+
if (bodyFile) {
|
|
293
|
+
const filePath = path.isAbsolute(bodyFile) ? bodyFile : path.join(cwd, bodyFile);
|
|
294
|
+
const content = safeReadFile(filePath);
|
|
295
|
+
if (content !== null) body = content;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (!type || !title || !repo || !owner || !project || !projectId || !typeId) {
|
|
299
|
+
error('create-issue requires: type, title, repo, owner, project, project_id, type_id');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const prefixedTitle = `[${type}] ${title}`;
|
|
303
|
+
|
|
304
|
+
// Write body to temp file to avoid shell escaping issues
|
|
305
|
+
const tmpFile = path.join(os.tmpdir(), `ace-issue-${Date.now()}.md`);
|
|
306
|
+
try {
|
|
307
|
+
fs.writeFileSync(tmpFile, body, 'utf-8');
|
|
308
|
+
} catch (e) {
|
|
309
|
+
error(`Failed to write temp body file: ${e.message}`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const safeTmpFile = tmpFile.replace(/\\/g, '/');
|
|
313
|
+
|
|
314
|
+
// Create issue via gh CLI
|
|
315
|
+
let issueUrl = null;
|
|
316
|
+
try {
|
|
317
|
+
issueUrl = execSync(
|
|
318
|
+
`gh issue create --repo ${repo} --title "${prefixedTitle.replace(/"/g, '\\"')}" --body-file "${safeTmpFile}"`,
|
|
319
|
+
{ cwd, shell: 'bash', stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf-8', timeout: 30000 }
|
|
320
|
+
).trim();
|
|
321
|
+
} catch (e) {
|
|
322
|
+
try { fs.unlinkSync(tmpFile); } catch {}
|
|
323
|
+
error(`Failed to create issue: ${(e.stderr || e.message || '').trim()}`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
try { fs.unlinkSync(tmpFile); } catch {}
|
|
327
|
+
|
|
328
|
+
// Extract issue number from URL
|
|
329
|
+
const number = issueUrl ? parseInt(issueUrl.split('/').pop(), 10) : null;
|
|
330
|
+
|
|
331
|
+
const result = {
|
|
332
|
+
number,
|
|
333
|
+
url: issueUrl,
|
|
334
|
+
item_id: null,
|
|
335
|
+
type_set: false,
|
|
336
|
+
status_set: false,
|
|
337
|
+
priority_set: false,
|
|
338
|
+
estimate_set: false,
|
|
339
|
+
parent_set: false,
|
|
340
|
+
milestone_set: false,
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
if (!number) {
|
|
344
|
+
output(result, raw);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Set native issue type via GraphQL mutation
|
|
349
|
+
if (typeId) {
|
|
350
|
+
const repoName = repo.split('/')[1];
|
|
351
|
+
// First get the issue node ID
|
|
352
|
+
const issueQuery = `query { repository(owner: \\"${owner}\\", name: \\"${repoName}\\") { issue(number: ${number}) { id } } }`;
|
|
353
|
+
const issueResult = execCommand(`gh api graphql -f query="${issueQuery}"`, cwd);
|
|
354
|
+
let issueNodeId = null;
|
|
355
|
+
if (issueResult) {
|
|
356
|
+
try {
|
|
357
|
+
issueNodeId = JSON.parse(issueResult).data?.repository?.issue?.id;
|
|
358
|
+
} catch {}
|
|
359
|
+
}
|
|
360
|
+
if (issueNodeId) {
|
|
361
|
+
const typeMutation = `mutation { updateIssue(input: { id: \\"${issueNodeId}\\", issueTypeId: \\"${typeId}\\" }) { issue { id } } }`;
|
|
362
|
+
const typeOk = execCommand(`gh api graphql -f query="${typeMutation}"`, cwd);
|
|
363
|
+
result.type_set = typeOk !== null;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Add to project
|
|
368
|
+
const addResult = execCommand(
|
|
369
|
+
`gh project item-add ${project} --owner ${owner} --url ${issueUrl} --format json`,
|
|
370
|
+
cwd
|
|
371
|
+
);
|
|
372
|
+
if (addResult) {
|
|
373
|
+
try {
|
|
374
|
+
const parsed = JSON.parse(addResult);
|
|
375
|
+
result.item_id = parsed.id || null;
|
|
376
|
+
} catch {}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Set project fields if item was added
|
|
380
|
+
if (result.item_id) {
|
|
381
|
+
// Status (single-select)
|
|
382
|
+
if (params.status_field_id && params.status_option_id) {
|
|
383
|
+
const statusOk = execCommand(
|
|
384
|
+
`gh project item-edit --project-id ${projectId} --id ${result.item_id} --field-id ${params.status_field_id} --single-select-option-id ${params.status_option_id}`,
|
|
385
|
+
cwd
|
|
386
|
+
);
|
|
387
|
+
result.status_set = statusOk !== null;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Priority (single-select)
|
|
391
|
+
if (params.priority_field_id && params.priority_option_id) {
|
|
392
|
+
const priorityOk = execCommand(
|
|
393
|
+
`gh project item-edit --project-id ${projectId} --id ${result.item_id} --field-id ${params.priority_field_id} --single-select-option-id ${params.priority_option_id}`,
|
|
394
|
+
cwd
|
|
395
|
+
);
|
|
396
|
+
result.priority_set = priorityOk !== null;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Estimate (number)
|
|
400
|
+
if (params.estimate_field_id && params.estimate) {
|
|
401
|
+
const estimateOk = execCommand(
|
|
402
|
+
`gh project item-edit --project-id ${projectId} --id ${result.item_id} --field-id ${params.estimate_field_id} --number ${params.estimate}`,
|
|
403
|
+
cwd
|
|
404
|
+
);
|
|
405
|
+
result.estimate_set = estimateOk !== null;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Set parent via GraphQL addSubIssue mutation
|
|
410
|
+
if (params.parent) {
|
|
411
|
+
const repoName = repo.split('/')[1];
|
|
412
|
+
// Get parent issue node ID
|
|
413
|
+
const parentQuery = `query { repository(owner: \\"${owner}\\", name: \\"${repoName}\\") { issue(number: ${params.parent}) { id } } }`;
|
|
414
|
+
const parentResult = execCommand(`gh api graphql -f query="${parentQuery}"`, cwd);
|
|
415
|
+
let parentNodeId = null;
|
|
416
|
+
if (parentResult) {
|
|
417
|
+
try {
|
|
418
|
+
parentNodeId = JSON.parse(parentResult).data?.repository?.issue?.id;
|
|
419
|
+
} catch {}
|
|
420
|
+
}
|
|
421
|
+
// Get child issue node ID
|
|
422
|
+
const childQuery = `query { repository(owner: \\"${owner}\\", name: \\"${repoName}\\") { issue(number: ${number}) { id } } }`;
|
|
423
|
+
const childResult = execCommand(`gh api graphql -f query="${childQuery}"`, cwd);
|
|
424
|
+
let childNodeId = null;
|
|
425
|
+
if (childResult) {
|
|
426
|
+
try {
|
|
427
|
+
childNodeId = JSON.parse(childResult).data?.repository?.issue?.id;
|
|
428
|
+
} catch {}
|
|
429
|
+
}
|
|
430
|
+
if (parentNodeId && childNodeId) {
|
|
431
|
+
const subIssueMutation = `mutation { addSubIssue(input: { issueId: \\"${parentNodeId}\\", subIssueId: \\"${childNodeId}\\" }) { issue { id } } }`;
|
|
432
|
+
const parentOk = execCommand(`gh api graphql -f query="${subIssueMutation}"`, cwd);
|
|
433
|
+
result.parent_set = parentOk !== null;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Set milestone
|
|
438
|
+
if (params.milestone) {
|
|
439
|
+
const milestoneOk = execCommand(
|
|
440
|
+
`gh issue edit ${number} --repo ${repo} --milestone "${params.milestone}"`,
|
|
441
|
+
cwd
|
|
442
|
+
);
|
|
443
|
+
result.milestone_set = milestoneOk !== null;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
output(result, raw);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// ─── Update Issue ───────────────────────────────────────────────────────────
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Update an existing GitHub issue's title, body, and/or project fields.
|
|
453
|
+
*
|
|
454
|
+
* Required args: number, repo
|
|
455
|
+
* Optional: title, body, body_file, owner, project, project_id,
|
|
456
|
+
* status_field_id, status_option_id, priority_field_id, priority_option_id,
|
|
457
|
+
* estimate_field_id, estimate
|
|
458
|
+
* Outputs: { number, updated_title, updated_body, status_set, priority_set, estimate_set }
|
|
459
|
+
*/
|
|
460
|
+
function updateIssue(cwd, raw, extraArgs) {
|
|
461
|
+
const params = parseKeyValueArgs(extraArgs);
|
|
462
|
+
const number = params.number;
|
|
463
|
+
const repo = params.repo;
|
|
464
|
+
const title = params.title;
|
|
465
|
+
let body = params.body;
|
|
466
|
+
const bodyFile = params.body_file;
|
|
467
|
+
|
|
468
|
+
if (!number || !repo) {
|
|
469
|
+
error('update-issue requires: number, repo');
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const result = {
|
|
473
|
+
number: parseInt(number, 10),
|
|
474
|
+
updated_title: false,
|
|
475
|
+
updated_body: false,
|
|
476
|
+
status_set: false,
|
|
477
|
+
priority_set: false,
|
|
478
|
+
estimate_set: false,
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
// Build gh issue edit command parts
|
|
482
|
+
const editParts = [`gh issue edit ${number} --repo ${repo}`];
|
|
483
|
+
|
|
484
|
+
if (title) {
|
|
485
|
+
editParts.push(`--title "${title.replace(/"/g, '\\"')}"`);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
let tmpFile = null;
|
|
489
|
+
if (bodyFile) {
|
|
490
|
+
const filePath = path.isAbsolute(bodyFile) ? bodyFile : path.join(cwd, bodyFile);
|
|
491
|
+
const safePath = filePath.replace(/\\/g, '/');
|
|
492
|
+
editParts.push(`--body-file "${safePath}"`);
|
|
493
|
+
} else if (body) {
|
|
494
|
+
tmpFile = path.join(os.tmpdir(), `ace-issue-update-${Date.now()}.md`);
|
|
495
|
+
try {
|
|
496
|
+
fs.writeFileSync(tmpFile, body, 'utf-8');
|
|
497
|
+
const safeTmpFile = tmpFile.replace(/\\/g, '/');
|
|
498
|
+
editParts.push(`--body-file "${safeTmpFile}"`);
|
|
499
|
+
} catch (e) {
|
|
500
|
+
error(`Failed to write temp body file: ${e.message}`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Execute issue edit if we have title or body to update
|
|
505
|
+
if (title || bodyFile || body) {
|
|
506
|
+
try {
|
|
507
|
+
execSync(editParts.join(' '), {
|
|
508
|
+
cwd, shell: 'bash', stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf-8', timeout: 30000,
|
|
509
|
+
});
|
|
510
|
+
if (title) result.updated_title = true;
|
|
511
|
+
if (bodyFile || body) result.updated_body = true;
|
|
512
|
+
} catch (e) {
|
|
513
|
+
process.stderr.write(` x Failed to update issue #${number}: ${(e.stderr || e.message || '').trim()}\n`);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (tmpFile) {
|
|
518
|
+
try { fs.unlinkSync(tmpFile); } catch {}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Update project fields if provided
|
|
522
|
+
const projectId = params.project_id;
|
|
523
|
+
|
|
524
|
+
if (projectId && (params.status_field_id || params.priority_field_id || params.estimate_field_id)) {
|
|
525
|
+
// Find the project item ID for this issue
|
|
526
|
+
const repoParts = repo.split('/');
|
|
527
|
+
const repoOwner = repoParts[0];
|
|
528
|
+
const repoName = repoParts[1] || repoParts[0];
|
|
529
|
+
const itemQuery = `query { repository(owner: \\"${repoOwner}\\", name: \\"${repoName}\\") { issue(number: ${number}) { projectItems(first: 10) { nodes { id project { id } } } } } }`;
|
|
530
|
+
const itemResult = execCommand(`gh api graphql -f query="${itemQuery}"`, cwd);
|
|
531
|
+
let itemId = null;
|
|
532
|
+
if (itemResult) {
|
|
533
|
+
try {
|
|
534
|
+
const parsed = JSON.parse(itemResult);
|
|
535
|
+
const nodes = parsed.data?.repository?.issue?.projectItems?.nodes || [];
|
|
536
|
+
const match = nodes.find(n => n.project?.id === projectId);
|
|
537
|
+
itemId = match?.id || null;
|
|
538
|
+
} catch {}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (itemId) {
|
|
542
|
+
// Status (single-select)
|
|
543
|
+
if (params.status_field_id && params.status_option_id) {
|
|
544
|
+
const statusOk = execCommand(
|
|
545
|
+
`gh project item-edit --project-id ${projectId} --id ${itemId} --field-id ${params.status_field_id} --single-select-option-id ${params.status_option_id}`,
|
|
546
|
+
cwd
|
|
547
|
+
);
|
|
548
|
+
result.status_set = statusOk !== null;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Priority (single-select)
|
|
552
|
+
if (params.priority_field_id && params.priority_option_id) {
|
|
553
|
+
const priorityOk = execCommand(
|
|
554
|
+
`gh project item-edit --project-id ${projectId} --id ${itemId} --field-id ${params.priority_field_id} --single-select-option-id ${params.priority_option_id}`,
|
|
555
|
+
cwd
|
|
556
|
+
);
|
|
557
|
+
result.priority_set = priorityOk !== null;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Estimate (number)
|
|
561
|
+
if (params.estimate_field_id && params.estimate) {
|
|
562
|
+
const estimateOk = execCommand(
|
|
563
|
+
`gh project item-edit --project-id ${projectId} --id ${itemId} --field-id ${params.estimate_field_id} --number ${params.estimate}`,
|
|
564
|
+
cwd
|
|
565
|
+
);
|
|
566
|
+
result.estimate_set = estimateOk !== null;
|
|
567
|
+
}
|
|
568
|
+
} else {
|
|
569
|
+
process.stderr.write(` ! Issue #${number} not found in GitHub Project. Skipping field updates.\n`);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
output(result, raw);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// ─── Fetch Issues ───────────────────────────────────────────────────────────
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Fetch all epics and features from a GitHub Project via paginated GraphQL.
|
|
580
|
+
*
|
|
581
|
+
* Required args: repo=owner/name owner=org project=number
|
|
582
|
+
* Outputs: { epics: [...], features: [...], counts: { total, epics, features, skipped } }
|
|
583
|
+
*/
|
|
584
|
+
function fetchIssues(cwd, raw, extraArgs) {
|
|
585
|
+
const params = parseKeyValueArgs(extraArgs);
|
|
586
|
+
const repo = params.repo;
|
|
587
|
+
const owner = params.owner;
|
|
588
|
+
const project = params.project;
|
|
589
|
+
|
|
590
|
+
if (!repo || !owner || !project) {
|
|
591
|
+
error('fetch-issues requires: repo=owner/name owner=org project=number');
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Get project ID
|
|
595
|
+
const projectListRaw = execCommand(
|
|
596
|
+
`gh project list --owner ${owner} --format json --limit 20`,
|
|
597
|
+
cwd
|
|
598
|
+
);
|
|
599
|
+
let projectId = null;
|
|
600
|
+
if (projectListRaw) {
|
|
601
|
+
try {
|
|
602
|
+
const parsed = JSON.parse(projectListRaw);
|
|
603
|
+
const projects = parsed.projects || parsed || [];
|
|
604
|
+
const match = projects.find(p => String(p.number) === String(project));
|
|
605
|
+
if (match) projectId = match.id;
|
|
606
|
+
} catch {}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (!projectId) {
|
|
610
|
+
error(`Could not find GitHub Project #${project} for owner "${owner}".`);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Paginated GraphQL query to fetch all project items
|
|
614
|
+
const allItems = [];
|
|
615
|
+
let hasNextPage = true;
|
|
616
|
+
let cursor = null;
|
|
617
|
+
|
|
618
|
+
while (hasNextPage) {
|
|
619
|
+
const afterClause = cursor ? `, after: \\"${cursor}\\"` : '';
|
|
620
|
+
const query = `query {
|
|
621
|
+
node(id: \\"${projectId}\\") {
|
|
622
|
+
... on ProjectV2 {
|
|
623
|
+
items(first: 100${afterClause}) {
|
|
624
|
+
pageInfo { hasNextPage endCursor }
|
|
625
|
+
nodes {
|
|
626
|
+
id
|
|
627
|
+
fieldValues(first: 20) {
|
|
628
|
+
nodes {
|
|
629
|
+
... on ProjectV2ItemFieldTextValue { text field { ... on ProjectV2Field { name } } }
|
|
630
|
+
... on ProjectV2ItemFieldNumberValue { number field { ... on ProjectV2Field { name } } }
|
|
631
|
+
... on ProjectV2ItemFieldSingleSelectValue { name field { ... on ProjectV2SingleSelectField { name } } }
|
|
632
|
+
... on ProjectV2ItemFieldIterationValue { title field { ... on ProjectV2IterationField { name } } }
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
content {
|
|
636
|
+
... on Issue {
|
|
637
|
+
number
|
|
638
|
+
title
|
|
639
|
+
url
|
|
640
|
+
state
|
|
641
|
+
issueType { name }
|
|
642
|
+
parent { number title }
|
|
643
|
+
milestone { title }
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}`.replace(/\n/g, ' ').replace(/\s+/g, ' ');
|
|
651
|
+
|
|
652
|
+
const result = execCommand(`gh api graphql -f query="${query}"`, cwd);
|
|
653
|
+
if (!result) break;
|
|
654
|
+
|
|
655
|
+
try {
|
|
656
|
+
const parsed = JSON.parse(result);
|
|
657
|
+
const items = parsed.data?.node?.items;
|
|
658
|
+
if (!items) break;
|
|
659
|
+
|
|
660
|
+
allItems.push(...(items.nodes || []));
|
|
661
|
+
hasNextPage = items.pageInfo?.hasNextPage || false;
|
|
662
|
+
cursor = items.pageInfo?.endCursor || null;
|
|
663
|
+
} catch {
|
|
664
|
+
break;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Process items into epics and features
|
|
669
|
+
const epics = [];
|
|
670
|
+
const features = [];
|
|
671
|
+
let skipped = 0;
|
|
672
|
+
|
|
673
|
+
for (const item of allItems) {
|
|
674
|
+
const content = item.content;
|
|
675
|
+
if (!content || !content.number) {
|
|
676
|
+
skipped++;
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Determine type: native issueType first, then title prefix fallback
|
|
681
|
+
let itemType = null;
|
|
682
|
+
if (content.issueType?.name) {
|
|
683
|
+
const nativeType = content.issueType.name;
|
|
684
|
+
if (nativeType === 'Epic' || nativeType === 'Feature') {
|
|
685
|
+
itemType = nativeType;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
if (!itemType) {
|
|
689
|
+
if (content.title.startsWith('[Epic]')) {
|
|
690
|
+
itemType = 'Epic';
|
|
691
|
+
} else if (content.title.startsWith('[Feature]')) {
|
|
692
|
+
itemType = 'Feature';
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
if (!itemType) {
|
|
697
|
+
skipped++;
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Extract field values
|
|
702
|
+
const fieldValues = {};
|
|
703
|
+
const fvNodes = item.fieldValues?.nodes || [];
|
|
704
|
+
for (const fv of fvNodes) {
|
|
705
|
+
const fieldName = fv.field?.name;
|
|
706
|
+
if (!fieldName) continue;
|
|
707
|
+
if (fv.name !== undefined) fieldValues[fieldName] = fv.name; // single-select
|
|
708
|
+
else if (fv.number !== undefined) fieldValues[fieldName] = fv.number; // number
|
|
709
|
+
else if (fv.text !== undefined) fieldValues[fieldName] = fv.text; // text
|
|
710
|
+
else if (fv.title !== undefined) fieldValues[fieldName] = fv.title; // iteration
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const entry = {
|
|
714
|
+
number: content.number,
|
|
715
|
+
title: content.title,
|
|
716
|
+
status: fieldValues.Status || null,
|
|
717
|
+
priority: fieldValues.Priority || null,
|
|
718
|
+
estimate: fieldValues.Estimate || null,
|
|
719
|
+
sprint: fieldValues.Sprint || fieldValues.Iteration || null,
|
|
720
|
+
milestone: content.milestone?.title || null,
|
|
721
|
+
url: content.url,
|
|
722
|
+
state: content.state,
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
if (itemType === 'Epic') {
|
|
726
|
+
epics.push(entry);
|
|
727
|
+
} else {
|
|
728
|
+
entry.parent_number = content.parent?.number || null;
|
|
729
|
+
entry.parent_title = content.parent?.title || null;
|
|
730
|
+
features.push(entry);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
output({
|
|
735
|
+
epics,
|
|
736
|
+
features,
|
|
737
|
+
counts: {
|
|
738
|
+
total: allItems.length,
|
|
739
|
+
epics: epics.length,
|
|
740
|
+
features: features.length,
|
|
741
|
+
skipped,
|
|
742
|
+
},
|
|
743
|
+
}, raw);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
module.exports = {
|
|
747
|
+
resolveProjectContext,
|
|
748
|
+
syncStory,
|
|
749
|
+
resolveFields,
|
|
750
|
+
createIssue,
|
|
751
|
+
updateIssue,
|
|
752
|
+
fetchIssues,
|
|
753
|
+
};
|