@relipa/ai-flow-kit 0.2.0-beta.0 → 0.2.0-beta.1
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/bin/aiflow.js +56 -0
- package/custom/rules/output-language.md +36 -0
- package/custom/skills/automation-testing/SKILL.md +1 -1
- package/custom/skills/ba-skills/skill-ba-build-business-rules-v1.md +2 -0
- package/custom/skills/ba-skills/skill-ba-initial-analysis-v1.md +2 -0
- package/custom/skills/ba-skills/skill-ba-mermaid-flowchart-v1.md +2 -0
- package/custom/skills/ba-skills/skill-ba-qna-v1.md +2 -0
- package/custom/skills/ba-skills/skill-ba-ui-prototype-v1.md +2 -0
- package/custom/skills/ba-skills/skill-ba-write-uc-spec-v1.md +2 -0
- package/custom/skills/coverage-check/SKILL.md +2 -0
- package/custom/skills/create-system-requirement/SKILL.md +11 -7
- package/custom/skills/deploy-model/SKILL.md +2 -0
- package/custom/skills/design-experiment/SKILL.md +2 -0
- package/custom/skills/evaluate-model/SKILL.md +2 -0
- package/custom/skills/evidence-aggregation/SKILL.md +2 -0
- package/custom/skills/execute-flow/SKILL.md +2 -0
- package/custom/skills/explore-data/SKILL.md +2 -0
- package/custom/skills/frame-ml-problem/SKILL.md +2 -0
- package/custom/skills/gate-review/SKILL.md +2 -0
- package/custom/skills/generate-spec/SKILL.md +2 -0
- package/custom/skills/generate-test-report/SKILL.md +2 -0
- package/custom/skills/generate-testcase/SKILL.md +2 -0
- package/custom/skills/impact-analysis/SKILL.md +2 -0
- package/custom/skills/improve-algorithm/SKILL.md +2 -0
- package/custom/skills/ingest-data/SKILL.md +116 -2
- package/custom/skills/log-bug/SKILL.md +2 -0
- package/custom/skills/pr-impact-analysis/SKILL.md +2 -0
- package/custom/skills/read-study-requirement/SKILL.md +13 -7
- package/custom/skills/report-customer/SKILL.md +6 -2
- package/custom/skills/retest-orchestration/SKILL.md +2 -0
- package/custom/skills/review-plan/SKILL.md +2 -0
- package/custom/skills/script-sync/SKILL.md +1 -1
- package/custom/skills/test-analysis/SKILL.md +2 -0
- package/custom/skills/test-skills/rules/qa-writing-standards.md +2 -2
- package/custom/skills/train-model/SKILL.md +2 -0
- package/custom/templates/shared/create-testcase-workflow.md +32 -1
- package/custom/templates/shared/gate-workflow.md +15 -0
- package/docs/common/CHANGELOG.md +13 -0
- package/package.json +1 -1
- package/scripts/create-score-excel.js +4 -111
- package/scripts/init.js +24 -1
- package/scripts/link-resolver.js +93 -0
- package/scripts/ticket-writer.js +247 -0
- package/scripts/use.js +141 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Vấn đề 5 (docs/internal/PM Workflow_v1.0.md) — turns an AI-proposed, PM-approved task list
|
|
3
|
+
// (born out of `ingest-data` Gate 3) into real tickets on Backlog/Jira.
|
|
4
|
+
//
|
|
5
|
+
// Read access (fetch-links, listing projects/issueTypes/priorities) reuses the existing
|
|
6
|
+
// read-only credentials already configured for `ak use`/`ak fetch-links`. Creating a ticket is a
|
|
7
|
+
// WRITE action and requires a SEPARATE credential (`BACKLOG_API_KEY_WRITE` / `JIRA_API_TOKEN_WRITE`)
|
|
8
|
+
// — same "distinct WRITE key" policy the PM Workflow doc calls for, kept apart from the read key so
|
|
9
|
+
// a compromised/misconfigured read key can never create tickets.
|
|
10
|
+
|
|
11
|
+
const fs = require('fs-extra');
|
|
12
|
+
const chalk = require('chalk');
|
|
13
|
+
const useCommand = require('./use');
|
|
14
|
+
const linkResolver = require('./link-resolver');
|
|
15
|
+
|
|
16
|
+
const {
|
|
17
|
+
loadCredentials,
|
|
18
|
+
resolveBacklogCredentials,
|
|
19
|
+
resolveJiraCredentials,
|
|
20
|
+
saveMcpCredentials,
|
|
21
|
+
} = useCommand;
|
|
22
|
+
|
|
23
|
+
const {
|
|
24
|
+
fetchBacklogIssueTypes,
|
|
25
|
+
fetchBacklogPriorities,
|
|
26
|
+
createBacklogIssue,
|
|
27
|
+
createJiraIssue,
|
|
28
|
+
} = linkResolver;
|
|
29
|
+
|
|
30
|
+
// Only these keys can be written via `ak credentials set` — dedicated adapter credentials
|
|
31
|
+
// (BACKLOG_API_KEY, JIRA_API_TOKEN, ...) still go through `ak init --adapter` on purpose.
|
|
32
|
+
const SETTABLE_CREDENTIAL_KEYS = [
|
|
33
|
+
'BACKLOG_API_KEY_WRITE',
|
|
34
|
+
'JIRA_API_TOKEN_WRITE',
|
|
35
|
+
'JIRA_EMAIL_WRITE',
|
|
36
|
+
'BACKLOG_DEFAULT_PROJECT_ID',
|
|
37
|
+
'BACKLOG_DEFAULT_PROJECT_KEY',
|
|
38
|
+
'JIRA_DEFAULT_PROJECT_KEY',
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
function jiraFullDomain(domain) {
|
|
42
|
+
return domain.includes('.') ? domain : `${domain}.atlassian.net`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function missingCredResult(target, field, message) {
|
|
46
|
+
return { ok: false, error: 'missing-write-credentials', target, field, message };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function resolveBacklogWrite(creds) {
|
|
50
|
+
const read = resolveBacklogCredentials(creds);
|
|
51
|
+
if (!read) {
|
|
52
|
+
return missingCredResult(
|
|
53
|
+
'backlog', 'BACKLOG_API_KEY / BACKLOG_SPACE_KEY',
|
|
54
|
+
'Chưa cấu hình credentials đọc Backlog. Chạy `ak init --adapter backlog` trước.'
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
const apiKeyWrite = process.env.BACKLOG_API_KEY_WRITE || creds.BACKLOG_API_KEY_WRITE;
|
|
58
|
+
if (!apiKeyWrite) {
|
|
59
|
+
return missingCredResult(
|
|
60
|
+
'backlog', 'BACKLOG_API_KEY_WRITE',
|
|
61
|
+
'Cần API key Backlog có quyền tạo issue (khác key đọc hiện tại). Cung cấp API key này, mình sẽ lưu lại và tạo ticket ngay.'
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
return { ok: true, domain: read.domain, apiKeyRead: read.apiKey, apiKeyWrite };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function resolveJiraWrite(creds) {
|
|
68
|
+
const read = resolveJiraCredentials(creds);
|
|
69
|
+
if (!read) {
|
|
70
|
+
return missingCredResult(
|
|
71
|
+
'jira', 'JIRA_API_TOKEN / JIRA_EMAIL / JIRA_DOMAIN',
|
|
72
|
+
'Chưa cấu hình credentials đọc Jira. Chạy `ak init --adapter jira` trước.'
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
const apiTokenWrite = process.env.JIRA_API_TOKEN_WRITE || creds.JIRA_API_TOKEN_WRITE;
|
|
76
|
+
if (!apiTokenWrite) {
|
|
77
|
+
return missingCredResult(
|
|
78
|
+
'jira', 'JIRA_API_TOKEN_WRITE',
|
|
79
|
+
'Cần Jira API token có quyền tạo issue (khác token đọc hiện tại). Cung cấp token này, mình sẽ lưu lại và tạo ticket ngay.'
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
const emailWrite = process.env.JIRA_EMAIL_WRITE || creds.JIRA_EMAIL_WRITE || read.email;
|
|
83
|
+
return { ok: true, domain: jiraFullDomain(read.domain), emailWrite, apiTokenWrite };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function pickBacklogDefaults(domain, apiKeyRead, projectId) {
|
|
87
|
+
const [issueTypes, priorities] = await Promise.all([
|
|
88
|
+
fetchBacklogIssueTypes(domain, apiKeyRead, projectId),
|
|
89
|
+
fetchBacklogPriorities(domain, apiKeyRead),
|
|
90
|
+
]);
|
|
91
|
+
if (!issueTypes || !issueTypes.length) throw new Error(`Project ${projectId} không có issueType nào — không thể tạo issue.`);
|
|
92
|
+
const defaultIssueTypeId = issueTypes[0].id;
|
|
93
|
+
const normal = (priorities || []).find((p) => /normal|trung bình/i.test(p.name));
|
|
94
|
+
const defaultPriorityId = (normal || (priorities || [])[0] || {}).id;
|
|
95
|
+
if (!defaultPriorityId) throw new Error('Không lấy được danh sách priority từ Backlog.');
|
|
96
|
+
return { defaultIssueTypeId, defaultPriorityId };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* `ak tasks create-tickets <file.json> [--json]`
|
|
101
|
+
*
|
|
102
|
+
* Input file shape:
|
|
103
|
+
* {
|
|
104
|
+
* "target": "backlog" | "jira",
|
|
105
|
+
* "projectId": "12345", // optional — Backlog only, overrides BACKLOG_DEFAULT_PROJECT_ID
|
|
106
|
+
* "projectKey": "PROJ", // optional — Jira only, overrides JIRA_DEFAULT_PROJECT_KEY
|
|
107
|
+
* "tasks": [
|
|
108
|
+
* { "type": "spec|coding|test|other", "title": "...", "description": "..." }
|
|
109
|
+
* ]
|
|
110
|
+
* }
|
|
111
|
+
*
|
|
112
|
+
* `description` is expected to already be fully composed by the AI per the ticket template in
|
|
113
|
+
* PM Workflow_v1.0.md §6.1 (Nội dung task + Nguồn tham chiếu) — this command posts it as-is.
|
|
114
|
+
*/
|
|
115
|
+
async function createTicketsCommand(inputFile, options = {}) {
|
|
116
|
+
const emit = (obj) => {
|
|
117
|
+
if (options.json) { process.stdout.write(JSON.stringify(obj)); return; }
|
|
118
|
+
if (obj.ok === false) {
|
|
119
|
+
console.log(chalk.red(`✗ ${obj.message || obj.error}`));
|
|
120
|
+
} else {
|
|
121
|
+
console.log(chalk.green(`✓ ${obj.results.filter(r => r.ok).length}/${obj.results.length} ticket(s) created on ${obj.target}.`));
|
|
122
|
+
for (const r of obj.results) {
|
|
123
|
+
console.log(r.ok ? ` ${chalk.green('✓')} ${r.ticketId} ${chalk.gray(r.title)}` : ` ${chalk.red('✗')} ${chalk.gray(r.title)} — ${r.error}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
console.log(JSON.stringify(obj));
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
let input;
|
|
130
|
+
try {
|
|
131
|
+
input = await fs.readJson(inputFile);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
emit({ ok: false, error: 'invalid-input-file', message: `Không đọc được ${inputFile}: ${err.message}` });
|
|
134
|
+
process.exitCode = 1;
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const target = input.target;
|
|
139
|
+
const tasks = input.tasks || [];
|
|
140
|
+
if (!['backlog', 'jira'].includes(target)) {
|
|
141
|
+
emit({ ok: false, error: 'invalid-target', message: `target phải là "backlog" hoặc "jira", nhận được: ${target}` });
|
|
142
|
+
process.exitCode = 1;
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (!tasks.length) {
|
|
146
|
+
emit({ ok: false, error: 'empty-tasks', message: 'File input không có task nào trong "tasks".' });
|
|
147
|
+
process.exitCode = 1;
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const creds = await loadCredentials();
|
|
152
|
+
|
|
153
|
+
if (target === 'backlog') {
|
|
154
|
+
const write = await resolveBacklogWrite(creds);
|
|
155
|
+
if (!write.ok) { emit(write); process.exitCode = 2; return; }
|
|
156
|
+
|
|
157
|
+
const projectId = input.projectId || creds.BACKLOG_DEFAULT_PROJECT_ID;
|
|
158
|
+
if (!projectId) {
|
|
159
|
+
emit({
|
|
160
|
+
ok: false, error: 'missing-project',
|
|
161
|
+
message: 'Chưa xác định projectId đích trên Backlog. Chạy `ak backlog-projects` để xem danh sách, rồi `ak backlog-set-default-project <id> <key>`.',
|
|
162
|
+
});
|
|
163
|
+
process.exitCode = 2;
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let defaults;
|
|
168
|
+
try {
|
|
169
|
+
defaults = await pickBacklogDefaults(write.domain, write.apiKeyRead, projectId);
|
|
170
|
+
} catch (err) {
|
|
171
|
+
emit({ ok: false, error: 'project-lookup-failed', message: err.message });
|
|
172
|
+
process.exitCode = 1;
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const results = [];
|
|
177
|
+
for (const task of tasks) {
|
|
178
|
+
try {
|
|
179
|
+
const created = await createBacklogIssue(write.domain, write.apiKeyWrite, {
|
|
180
|
+
projectId,
|
|
181
|
+
summary: task.title,
|
|
182
|
+
description: task.description,
|
|
183
|
+
issueTypeId: task.issueTypeId || defaults.defaultIssueTypeId,
|
|
184
|
+
priorityId: task.priorityId || defaults.defaultPriorityId,
|
|
185
|
+
});
|
|
186
|
+
results.push({ type: task.type, title: task.title, ok: true, ticketId: created.ticketId, url: created.url });
|
|
187
|
+
} catch (err) {
|
|
188
|
+
results.push({ type: task.type, title: task.title, ok: false, error: err.message });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
emit({ ok: true, target, projectId, results });
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// target === 'jira'
|
|
196
|
+
const write = await resolveJiraWrite(creds);
|
|
197
|
+
if (!write.ok) { emit(write); process.exitCode = 2; return; }
|
|
198
|
+
|
|
199
|
+
const projectKey = input.projectKey || creds.JIRA_DEFAULT_PROJECT_KEY;
|
|
200
|
+
if (!projectKey) {
|
|
201
|
+
emit({
|
|
202
|
+
ok: false, error: 'missing-project',
|
|
203
|
+
message: 'Chưa xác định project đích trên Jira. Chạy `ak jira-projects` để xem danh sách, rồi `ak jira-set-default-project <key>`.',
|
|
204
|
+
});
|
|
205
|
+
process.exitCode = 2;
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const results = [];
|
|
210
|
+
for (const task of tasks) {
|
|
211
|
+
try {
|
|
212
|
+
const created = await createJiraIssue(write.domain, write.emailWrite, write.apiTokenWrite, {
|
|
213
|
+
projectKey,
|
|
214
|
+
summary: task.title,
|
|
215
|
+
description: task.description,
|
|
216
|
+
issueTypeName: task.issueType || 'Task',
|
|
217
|
+
});
|
|
218
|
+
results.push({ type: task.type, title: task.title, ok: true, ticketId: created.ticketId, url: created.url });
|
|
219
|
+
} catch (err) {
|
|
220
|
+
results.push({ type: task.type, title: task.title, ok: false, error: err.message });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
emit({ ok: true, target, projectKey, results });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* `ak credentials set <KEY> <VALUE>` — lets the AI persist a WRITE key the user pasted in chat
|
|
228
|
+
* (after being asked, per the missing-write-credentials error above) without a bespoke command
|
|
229
|
+
* per key. Restricted to an allowlist — dedicated adapter read-credentials still go through
|
|
230
|
+
* `ak init --adapter`, this is only for the pieces that flow introduced.
|
|
231
|
+
*/
|
|
232
|
+
async function setCredentialCommand(key, value) {
|
|
233
|
+
if (!SETTABLE_CREDENTIAL_KEYS.includes(key)) {
|
|
234
|
+
console.log(chalk.red(`✗ Unknown credential key: ${key}.`));
|
|
235
|
+
console.log(chalk.gray(` Allowed: ${SETTABLE_CREDENTIAL_KEYS.join(', ')}`));
|
|
236
|
+
process.exitCode = 1;
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
await saveMcpCredentials({ [key]: value });
|
|
240
|
+
console.log(chalk.green(`✓ Saved ${key} to ~/.aiflow/credentials.json`));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
module.exports = {
|
|
244
|
+
createTicketsCommand,
|
|
245
|
+
setCredentialCommand,
|
|
246
|
+
SETTABLE_CREDENTIAL_KEYS,
|
|
247
|
+
};
|
package/scripts/use.js
CHANGED
|
@@ -360,6 +360,14 @@ function backlogGet(url) {
|
|
|
360
360
|
});
|
|
361
361
|
}
|
|
362
362
|
|
|
363
|
+
/**
|
|
364
|
+
* Fetch the list of projects in the Backlog space — used to let the user pick a default
|
|
365
|
+
* project for tasks created manually (no ticket to derive a project from).
|
|
366
|
+
*/
|
|
367
|
+
function fetchBacklogProjects(domain, apiKey) {
|
|
368
|
+
return backlogGet(`https://${domain}/api/v2/projects?apiKey=${apiKey}`);
|
|
369
|
+
}
|
|
370
|
+
|
|
363
371
|
/**
|
|
364
372
|
* Fetch issue detail from Backlog REST API
|
|
365
373
|
*/
|
|
@@ -584,6 +592,64 @@ function fetchJiraIssue(domain, email, apiToken, issueKey) {
|
|
|
584
592
|
});
|
|
585
593
|
}
|
|
586
594
|
|
|
595
|
+
/**
|
|
596
|
+
* `ak jira-projects [--json]` — lists the projects visible to the configured Jira account, so the
|
|
597
|
+
* user can pick a default project for tasks created from Meeting Minutes/QnA (see Vấn đề 5.1 in
|
|
598
|
+
* PM Workflow_v1.0.md). Mirrors `listBacklogProjectsCommand` above.
|
|
599
|
+
*/
|
|
600
|
+
function fetchJiraProjects(domain, email, apiToken) {
|
|
601
|
+
return new Promise((resolve, reject) => {
|
|
602
|
+
const auth = Buffer.from(`${email}:${apiToken}`).toString("base64");
|
|
603
|
+
const url = `https://${domain}.atlassian.net/rest/api/3/project`;
|
|
604
|
+
https
|
|
605
|
+
.get(url, { headers: { Authorization: `Basic ${auth}`, Accept: "application/json" } }, (res) => {
|
|
606
|
+
let data = "";
|
|
607
|
+
res.on("data", (chunk) => (data += chunk));
|
|
608
|
+
res.on("end", () => {
|
|
609
|
+
if (res.statusCode !== 200) {
|
|
610
|
+
reject(new Error(`HTTP ${res.statusCode}: ${data}`));
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
try { resolve(JSON.parse(data)); }
|
|
614
|
+
catch (e) { reject(new Error("Invalid JSON response from Jira")); }
|
|
615
|
+
});
|
|
616
|
+
})
|
|
617
|
+
.on("error", reject);
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
async function listJiraProjectsCommand(options = {}) {
|
|
622
|
+
const creds = await loadCredentials();
|
|
623
|
+
const resolved = resolveJiraCredentials(creds);
|
|
624
|
+
if (!resolved) {
|
|
625
|
+
const msg = "Jira credentials not set. Run `aiflow init --adapter jira` first.";
|
|
626
|
+
if (options.json) { process.stdout.write(JSON.stringify({ ok: false, error: msg })); return; }
|
|
627
|
+
console.log(chalk.yellow(`⚠ ${msg}`));
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
try {
|
|
631
|
+
const projects = await fetchJiraProjects(resolved.domain, resolved.email, resolved.apiToken);
|
|
632
|
+
const list = projects.map((p) => ({ id: p.id, key: p.key, name: p.name }));
|
|
633
|
+
if (options.json) {
|
|
634
|
+
process.stdout.write(JSON.stringify({ ok: true, projects: list, defaultProjectKey: creds.JIRA_DEFAULT_PROJECT_KEY ?? null }));
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
console.log(chalk.cyan(`\nJira projects (${list.length}):`));
|
|
638
|
+
for (const p of list) {
|
|
639
|
+
const isDefault = p.key === creds.JIRA_DEFAULT_PROJECT_KEY;
|
|
640
|
+
console.log(` ${isDefault ? chalk.green("●") : " "} ${p.key} ${chalk.gray(p.name)} ${chalk.gray(`(id: ${p.id})`)}`);
|
|
641
|
+
}
|
|
642
|
+
} catch (err) {
|
|
643
|
+
if (options.json) { process.stdout.write(JSON.stringify({ ok: false, error: err.message })); return; }
|
|
644
|
+
console.log(chalk.red(`✗ Could not fetch Jira projects: ${err.message}`));
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
async function setDefaultJiraProjectCommand(key) {
|
|
649
|
+
await saveMcpCredentials({ JIRA_DEFAULT_PROJECT_KEY: key });
|
|
650
|
+
console.log(chalk.green(`✓ Default Jira project set to ${key}.`));
|
|
651
|
+
}
|
|
652
|
+
|
|
587
653
|
/**
|
|
588
654
|
* Fetch ALL comments of an issue from Jira REST API.
|
|
589
655
|
*/
|
|
@@ -787,11 +853,20 @@ async function manualContext(prefillId = "", options = {}) {
|
|
|
787
853
|
// Task type — pre-select existing value if available
|
|
788
854
|
const taskType = await promptForTaskType(existing.taskType, options.taskType);
|
|
789
855
|
|
|
856
|
+
// Stamp the default Backlog project (`ak backlog-set-default-project`), if one was ever set —
|
|
857
|
+
// manual tasks have no ticket to derive a project from otherwise. Kept for later use: syncing a
|
|
858
|
+
// locally-created task up to Backlog as a new issue needs to know which project it belongs to.
|
|
859
|
+
const creds = await loadCredentials();
|
|
860
|
+
const projectId = existing.projectId ?? creds.BACKLOG_DEFAULT_PROJECT_ID ?? null;
|
|
861
|
+
const projectKey = existing.projectKey ?? creds.BACKLOG_DEFAULT_PROJECT_KEY ?? null;
|
|
862
|
+
|
|
790
863
|
const context = {
|
|
791
864
|
taskId,
|
|
792
865
|
taskType,
|
|
793
866
|
title,
|
|
794
867
|
description,
|
|
868
|
+
projectId,
|
|
869
|
+
projectKey,
|
|
795
870
|
status: existing.status || "In Progress",
|
|
796
871
|
mode: existing.mode || "auto",
|
|
797
872
|
acceptanceCriteria: existing.acceptanceCriteria || [],
|
|
@@ -967,6 +1042,63 @@ async function loadCredentials() {
|
|
|
967
1042
|
return {};
|
|
968
1043
|
}
|
|
969
1044
|
|
|
1045
|
+
/** Always the global file (`~/.aiflow/credentials.json`) — same path `loadCredentials` reads
|
|
1046
|
+
* first — so a value written here is picked up by every later `loadCredentials()` call. */
|
|
1047
|
+
function globalCredentialsPath() {
|
|
1048
|
+
return path.join(os.homedir(), ".aiflow", "credentials.json");
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
/** Merges `patch` into the saved `.mcp` credentials block, creating the file if needed. */
|
|
1052
|
+
async function saveMcpCredentials(patch) {
|
|
1053
|
+
const file = globalCredentialsPath();
|
|
1054
|
+
await fs.ensureDir(path.dirname(file));
|
|
1055
|
+
const data = (await fs.pathExists(file)) ? await fs.readJson(file).catch(() => ({})) : {};
|
|
1056
|
+
data.mcp = { ...(data.mcp || {}), ...patch };
|
|
1057
|
+
await fs.writeJson(file, data, { spaces: 2 });
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
/**
|
|
1061
|
+
* `ak backlog-projects [--json]` — lists the projects in the configured Backlog space, so the
|
|
1062
|
+
* user (or a headless caller like the VS Code extension) can pick one as the default project for
|
|
1063
|
+
* manually-created tasks (see `manualContext` below, which stamps that default onto new tasks).
|
|
1064
|
+
*/
|
|
1065
|
+
async function listBacklogProjectsCommand(options = {}) {
|
|
1066
|
+
const creds = await loadCredentials();
|
|
1067
|
+
const resolved = resolveBacklogCredentials(creds);
|
|
1068
|
+
if (!resolved) {
|
|
1069
|
+
const msg = "Backlog credentials not set. Run `aiflow init --adapter backlog` first.";
|
|
1070
|
+
if (options.json) { process.stdout.write(JSON.stringify({ ok: false, error: msg })); return; }
|
|
1071
|
+
console.log(chalk.yellow(`⚠ ${msg}`));
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
try {
|
|
1075
|
+
const projects = await fetchBacklogProjects(resolved.domain, resolved.apiKey);
|
|
1076
|
+
const list = projects.map((p) => ({ id: p.id, projectKey: p.projectKey, name: p.name }));
|
|
1077
|
+
if (options.json) {
|
|
1078
|
+
process.stdout.write(JSON.stringify({ ok: true, projects: list, defaultProjectId: creds.BACKLOG_DEFAULT_PROJECT_ID ?? null }));
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
console.log(chalk.cyan(`\nBacklog projects (${list.length}):`));
|
|
1082
|
+
for (const p of list) {
|
|
1083
|
+
const isDefault = String(p.id) === String(creds.BACKLOG_DEFAULT_PROJECT_ID);
|
|
1084
|
+
console.log(` ${isDefault ? chalk.green("●") : " "} ${p.projectKey} ${chalk.gray(p.name)} ${chalk.gray(`(id: ${p.id})`)}`);
|
|
1085
|
+
}
|
|
1086
|
+
} catch (err) {
|
|
1087
|
+
if (options.json) { process.stdout.write(JSON.stringify({ ok: false, error: err.message })); return; }
|
|
1088
|
+
console.log(chalk.red(`✗ Could not fetch Backlog projects: ${err.message}`));
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
/**
|
|
1093
|
+
* `ak backlog-set-default-project <id> <key>` — saves the chosen default project, so future
|
|
1094
|
+
* manually-created tasks (`ak use --manual`) get stamped with it automatically. Non-interactive —
|
|
1095
|
+
* safe to spawn without a TTY (see New-ex-architecture_v1.0.md §1.1 for why that distinction matters).
|
|
1096
|
+
*/
|
|
1097
|
+
async function setDefaultBacklogProjectCommand(id, key) {
|
|
1098
|
+
await saveMcpCredentials({ BACKLOG_DEFAULT_PROJECT_ID: id, BACKLOG_DEFAULT_PROJECT_KEY: key });
|
|
1099
|
+
console.log(chalk.green(`✓ Default Backlog project set to ${key} (id: ${id}).`));
|
|
1100
|
+
}
|
|
1101
|
+
|
|
970
1102
|
// ──────────────────────────────────────────────────────────────
|
|
971
1103
|
// Context persistence
|
|
972
1104
|
// ──────────────────────────────────────────────────────────────
|
|
@@ -1353,3 +1485,12 @@ module.exports.detectTaskTypeFromString = detectTaskTypeFromString;
|
|
|
1353
1485
|
module.exports.manualContext = manualContext;
|
|
1354
1486
|
module.exports.promptForTaskType = promptForTaskType;
|
|
1355
1487
|
module.exports.previewTarget = previewTarget;
|
|
1488
|
+
module.exports.resolveBacklogCredentials = resolveBacklogCredentials;
|
|
1489
|
+
module.exports.fetchBacklogProjects = fetchBacklogProjects;
|
|
1490
|
+
module.exports.listBacklogProjectsCommand = listBacklogProjectsCommand;
|
|
1491
|
+
module.exports.setDefaultBacklogProjectCommand = setDefaultBacklogProjectCommand;
|
|
1492
|
+
module.exports.resolveJiraCredentials = resolveJiraCredentials;
|
|
1493
|
+
module.exports.fetchJiraProjects = fetchJiraProjects;
|
|
1494
|
+
module.exports.listJiraProjectsCommand = listJiraProjectsCommand;
|
|
1495
|
+
module.exports.setDefaultJiraProjectCommand = setDefaultJiraProjectCommand;
|
|
1496
|
+
module.exports.saveMcpCredentials = saveMcpCredentials;
|