@agentteams/cli 0.1.22
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/.agentteams/config.json +8 -0
- package/.agentteams/convention.md +100 -0
- package/.agentteams/platform/co-action-guide.md +144 -0
- package/.agentteams/platform/completion-report-guide.md +146 -0
- package/.agentteams/platform/convention-authoring-guide.md +112 -0
- package/.agentteams/platform/convention-ud-guide.md +59 -0
- package/.agentteams/platform/plan-guide.md +180 -0
- package/.agentteams/platform/plan-template.md +201 -0
- package/.agentteams/platform/post-mortem-guide.md +92 -0
- package/CODE_OF_CONDUCT.md +36 -0
- package/CONTRIBUTING.md +39 -0
- package/LICENSE +201 -0
- package/README.md +405 -0
- package/SECURITY.md +27 -0
- package/dist/api/coaction.d.ts +17 -0
- package/dist/api/coaction.d.ts.map +1 -0
- package/dist/api/coaction.js +102 -0
- package/dist/api/coaction.js.map +1 -0
- package/dist/api/comment.d.ts +13 -0
- package/dist/api/comment.d.ts.map +1 -0
- package/dist/api/comment.js +33 -0
- package/dist/api/comment.js.map +1 -0
- package/dist/api/feedback.d.ts +7 -0
- package/dist/api/feedback.d.ts.map +1 -0
- package/dist/api/feedback.js +6 -0
- package/dist/api/feedback.js.map +1 -0
- package/dist/api/linear.d.ts +5 -0
- package/dist/api/linear.d.ts.map +1 -0
- package/dist/api/linear.js +27 -0
- package/dist/api/linear.js.map +1 -0
- package/dist/api/member.d.ts +16 -0
- package/dist/api/member.d.ts.map +1 -0
- package/dist/api/member.js +6 -0
- package/dist/api/member.js.map +1 -0
- package/dist/api/plan.d.ts +51 -0
- package/dist/api/plan.d.ts.map +1 -0
- package/dist/api/plan.js +80 -0
- package/dist/api/plan.js.map +1 -0
- package/dist/api/postmortem.d.ts +6 -0
- package/dist/api/postmortem.d.ts.map +1 -0
- package/dist/api/postmortem.js +33 -0
- package/dist/api/postmortem.js.map +1 -0
- package/dist/api/report.d.ts +6 -0
- package/dist/api/report.d.ts.map +1 -0
- package/dist/api/report.js +33 -0
- package/dist/api/report.js.map +1 -0
- package/dist/api/search.d.ts +2 -0
- package/dist/api/search.d.ts.map +1 -0
- package/dist/api/search.js +20 -0
- package/dist/api/search.js.map +1 -0
- package/dist/api/status.d.ts +12 -0
- package/dist/api/status.d.ts.map +1 -0
- package/dist/api/status.js +33 -0
- package/dist/api/status.js.map +1 -0
- package/dist/commands/agentConfig.d.ts +4 -0
- package/dist/commands/agentConfig.d.ts.map +1 -0
- package/dist/commands/agentConfig.js +41 -0
- package/dist/commands/agentConfig.js.map +1 -0
- package/dist/commands/agentConfigCommand.d.ts +2 -0
- package/dist/commands/agentConfigCommand.d.ts.map +1 -0
- package/dist/commands/agentConfigCommand.js +20 -0
- package/dist/commands/agentConfigCommand.js.map +1 -0
- package/dist/commands/coaction.d.ts +2 -0
- package/dist/commands/coaction.d.ts.map +1 -0
- package/dist/commands/coaction.js +405 -0
- package/dist/commands/coaction.js.map +1 -0
- package/dist/commands/comment.d.ts +2 -0
- package/dist/commands/comment.d.ts.map +1 -0
- package/dist/commands/comment.js +63 -0
- package/dist/commands/comment.js.map +1 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +30 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/convention.d.ts +35 -0
- package/dist/commands/convention.d.ts.map +1 -0
- package/dist/commands/convention.js +680 -0
- package/dist/commands/convention.js.map +1 -0
- package/dist/commands/conventionRouter.d.ts +3 -0
- package/dist/commands/conventionRouter.d.ts.map +1 -0
- package/dist/commands/conventionRouter.js +46 -0
- package/dist/commands/conventionRouter.js.map +1 -0
- package/dist/commands/dependency.d.ts +4 -0
- package/dist/commands/dependency.d.ts.map +1 -0
- package/dist/commands/dependency.js +41 -0
- package/dist/commands/dependency.js.map +1 -0
- package/dist/commands/dependencyCommand.d.ts +2 -0
- package/dist/commands/dependencyCommand.d.ts.map +1 -0
- package/dist/commands/dependencyCommand.js +27 -0
- package/dist/commands/dependencyCommand.js.map +1 -0
- package/dist/commands/feedback.d.ts +2 -0
- package/dist/commands/feedback.d.ts.map +1 -0
- package/dist/commands/feedback.js +38 -0
- package/dist/commands/feedback.js.map +1 -0
- package/dist/commands/index.d.ts +2 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +128 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/init.d.ts +22 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +293 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/linear.d.ts +2 -0
- package/dist/commands/linear.d.ts.map +1 -0
- package/dist/commands/linear.js +41 -0
- package/dist/commands/linear.js.map +1 -0
- package/dist/commands/plan.d.ts +12 -0
- package/dist/commands/plan.d.ts.map +1 -0
- package/dist/commands/plan.js +626 -0
- package/dist/commands/plan.js.map +1 -0
- package/dist/commands/postmortem.d.ts +2 -0
- package/dist/commands/postmortem.d.ts.map +1 -0
- package/dist/commands/postmortem.js +157 -0
- package/dist/commands/postmortem.js.map +1 -0
- package/dist/commands/report.d.ts +2 -0
- package/dist/commands/report.d.ts.map +1 -0
- package/dist/commands/report.js +213 -0
- package/dist/commands/report.js.map +1 -0
- package/dist/commands/search.d.ts +2 -0
- package/dist/commands/search.d.ts.map +1 -0
- package/dist/commands/search.js +29 -0
- package/dist/commands/search.js.map +1 -0
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +60 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +750 -0
- package/dist/index.js.map +1 -0
- package/dist/types/index.d.ts +237 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +6 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/atomicWrite.d.ts +2 -0
- package/dist/utils/atomicWrite.d.ts.map +1 -0
- package/dist/utils/atomicWrite.js +7 -0
- package/dist/utils/atomicWrite.js.map +1 -0
- package/dist/utils/authServer.d.ts +18 -0
- package/dist/utils/authServer.d.ts.map +1 -0
- package/dist/utils/authServer.js +215 -0
- package/dist/utils/authServer.js.map +1 -0
- package/dist/utils/config.d.ts +31 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +113 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/env.d.ts +7 -0
- package/dist/utils/env.d.ts.map +1 -0
- package/dist/utils/env.js +19 -0
- package/dist/utils/env.js.map +1 -0
- package/dist/utils/errors.d.ts +6 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +112 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/formatter.d.ts +2 -0
- package/dist/utils/formatter.d.ts.map +1 -0
- package/dist/utils/formatter.js +55 -0
- package/dist/utils/formatter.js.map +1 -0
- package/dist/utils/git.d.ts +21 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +44 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/httpClient.d.ts +3 -0
- package/dist/utils/httpClient.d.ts.map +1 -0
- package/dist/utils/httpClient.js +45 -0
- package/dist/utils/httpClient.js.map +1 -0
- package/dist/utils/httpHeaders.d.ts +3 -0
- package/dist/utils/httpHeaders.d.ts.map +1 -0
- package/dist/utils/httpHeaders.js +11 -0
- package/dist/utils/httpHeaders.js.map +1 -0
- package/dist/utils/initOutput.d.ts +3 -0
- package/dist/utils/initOutput.d.ts.map +1 -0
- package/dist/utils/initOutput.js +45 -0
- package/dist/utils/initOutput.js.map +1 -0
- package/dist/utils/legacyCompat.d.ts +3 -0
- package/dist/utils/legacyCompat.d.ts.map +1 -0
- package/dist/utils/legacyCompat.js +20 -0
- package/dist/utils/legacyCompat.js.map +1 -0
- package/dist/utils/outputPolicy.d.ts +12 -0
- package/dist/utils/outputPolicy.d.ts.map +1 -0
- package/dist/utils/outputPolicy.js +171 -0
- package/dist/utils/outputPolicy.js.map +1 -0
- package/dist/utils/parsers.d.ts +14 -0
- package/dist/utils/parsers.d.ts.map +1 -0
- package/dist/utils/parsers.js +78 -0
- package/dist/utils/parsers.js.map +1 -0
- package/dist/utils/planFormat.d.ts +17 -0
- package/dist/utils/planFormat.d.ts.map +1 -0
- package/dist/utils/planFormat.js +81 -0
- package/dist/utils/planFormat.js.map +1 -0
- package/dist/utils/spinner.d.ts +6 -0
- package/dist/utils/spinner.d.ts.map +1 -0
- package/dist/utils/spinner.js +34 -0
- package/dist/utils/spinner.js.map +1 -0
- package/dist/utils/updateCheck.d.ts +19 -0
- package/dist/utils/updateCheck.d.ts.map +1 -0
- package/dist/utils/updateCheck.js +103 -0
- package/dist/utils/updateCheck.js.map +1 -0
- package/package.json +55 -0
- package/rlarua-agentteams-cli-0.1.8.tgz +0 -0
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync, rmSync, readdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { checkConventionFreshness } from './convention.js';
|
|
4
|
+
import { findProjectConfig } from '../utils/config.js';
|
|
5
|
+
import { collectGitMetrics } from '../utils/git.js';
|
|
6
|
+
import { withSpinner, printFileInfo } from '../utils/spinner.js';
|
|
7
|
+
import { formatPlanWithDependenciesText, mergePlanWithDependencies, normalizeDependencies } from '../utils/planFormat.js';
|
|
8
|
+
import { ensureUrlProtocol, interpretEscapes, stripFrontmatter, toNonEmptyString, toNonNegativeInteger, toPositiveInteger, toSafeFileName, deleteIfTempFile, } from '../utils/parsers.js';
|
|
9
|
+
import { assignPlan, createPlan, deletePlan, finishPlanLifecycle, getPlan, getPlanDependencies, getPlanStatus, linkOriginIssue, listOriginIssues, listPlans, patchPlanStatus, startPlanLifecycle, unlinkOriginIssue, updatePlan, } from '../api/plan.js';
|
|
10
|
+
function findProjectRoot() {
|
|
11
|
+
const configPath = findProjectConfig(process.cwd());
|
|
12
|
+
if (!configPath)
|
|
13
|
+
return null;
|
|
14
|
+
return resolve(configPath, '..', '..');
|
|
15
|
+
}
|
|
16
|
+
function formatFreshnessChangeLabel(change) {
|
|
17
|
+
const target = (change.title && change.title.trim().length > 0)
|
|
18
|
+
? change.title.trim()
|
|
19
|
+
: (change.fileName && change.fileName.trim().length > 0)
|
|
20
|
+
? change.fileName.trim()
|
|
21
|
+
: change.id;
|
|
22
|
+
if (change.type === 'new')
|
|
23
|
+
return `new: ${target}`;
|
|
24
|
+
if (change.type === 'deleted')
|
|
25
|
+
return `deleted: ${target}`;
|
|
26
|
+
return `updated: ${target}`;
|
|
27
|
+
}
|
|
28
|
+
export function buildFreshnessNoticeLines(freshness) {
|
|
29
|
+
const lines = ['⚠ Updated conventions found:'];
|
|
30
|
+
if (freshness.platformGuidesChanged) {
|
|
31
|
+
lines.push(' - platform guides (shared)');
|
|
32
|
+
}
|
|
33
|
+
for (const change of freshness.conventionChanges) {
|
|
34
|
+
lines.push(` - ${formatFreshnessChangeLabel(change)}`);
|
|
35
|
+
}
|
|
36
|
+
return lines;
|
|
37
|
+
}
|
|
38
|
+
async function runFreshnessCheckSilent(apiUrl, projectId, headers) {
|
|
39
|
+
const projectRoot = findProjectRoot();
|
|
40
|
+
if (!projectRoot)
|
|
41
|
+
return;
|
|
42
|
+
try {
|
|
43
|
+
const freshness = await checkConventionFreshness(apiUrl, projectId, headers, projectRoot);
|
|
44
|
+
const hasChanges = freshness.platformGuidesChanged || freshness.conventionChanges.length > 0;
|
|
45
|
+
if (!hasChanges)
|
|
46
|
+
return;
|
|
47
|
+
const noticeLines = buildFreshnessNoticeLines(freshness);
|
|
48
|
+
for (const line of noticeLines) {
|
|
49
|
+
process.stderr.write(`${line}\n`);
|
|
50
|
+
}
|
|
51
|
+
process.stderr.write('Run agentteams convention download to sync latest conventions.\n');
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
void error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export function buildUniquePlanRunbookFileName(title, planId, existingFileNames) {
|
|
58
|
+
const idPrefix = planId.slice(0, 8);
|
|
59
|
+
const safeName = toSafeFileName(title) || 'plan';
|
|
60
|
+
const baseName = `${safeName}-${idPrefix}`;
|
|
61
|
+
const used = new Set(existingFileNames.map((name) => name.toLowerCase()));
|
|
62
|
+
let fileName = `${baseName}.md`;
|
|
63
|
+
let sequence = 2;
|
|
64
|
+
while (used.has(fileName.toLowerCase())) {
|
|
65
|
+
fileName = `${baseName}-${sequence}.md`;
|
|
66
|
+
sequence += 1;
|
|
67
|
+
}
|
|
68
|
+
return fileName;
|
|
69
|
+
}
|
|
70
|
+
function minimalPlanRefactorChecklistTemplate() {
|
|
71
|
+
return [
|
|
72
|
+
'## Refactor Checklist',
|
|
73
|
+
'- Define current pain points and target behavior',
|
|
74
|
+
'- Identify impacted modules and side effects',
|
|
75
|
+
'- Keep API/schema contracts backward-compatible',
|
|
76
|
+
'- Add or update related tests',
|
|
77
|
+
'- Run verification (`npm test`, `npm run build`) and record outcomes',
|
|
78
|
+
'',
|
|
79
|
+
].join('\n');
|
|
80
|
+
}
|
|
81
|
+
function minimalPlanQuickTemplate() {
|
|
82
|
+
return [
|
|
83
|
+
'## TL;DR',
|
|
84
|
+
'- Goal: {what will be done}',
|
|
85
|
+
'- Out of scope: {what will NOT be done}',
|
|
86
|
+
'- Done when: {how we verify completion}',
|
|
87
|
+
'',
|
|
88
|
+
'## Tasks',
|
|
89
|
+
'- Implement the change',
|
|
90
|
+
'- Update or add tests',
|
|
91
|
+
'- Run verification (`npm test`, `npm run build`) and record outcomes',
|
|
92
|
+
'',
|
|
93
|
+
].join('\n');
|
|
94
|
+
}
|
|
95
|
+
function resolvePlanTemplate(template) {
|
|
96
|
+
if (template === undefined || template === null)
|
|
97
|
+
return undefined;
|
|
98
|
+
const value = String(template).trim();
|
|
99
|
+
if (value.length === 0)
|
|
100
|
+
return undefined;
|
|
101
|
+
if (value === 'refactor-minimal')
|
|
102
|
+
return minimalPlanRefactorChecklistTemplate();
|
|
103
|
+
if (value === 'quick-minimal')
|
|
104
|
+
return minimalPlanQuickTemplate();
|
|
105
|
+
throw new Error(`Unsupported plan template: ${value}. Only 'refactor-minimal' and 'quick-minimal' are supported.`);
|
|
106
|
+
}
|
|
107
|
+
export async function executePlanCommand(apiUrl, projectId, headers, action, options) {
|
|
108
|
+
switch (action) {
|
|
109
|
+
case 'list': {
|
|
110
|
+
await runFreshnessCheckSilent(apiUrl, projectId, headers);
|
|
111
|
+
const params = {};
|
|
112
|
+
if (options.title)
|
|
113
|
+
params.title = options.title;
|
|
114
|
+
if (options.search)
|
|
115
|
+
params.search = options.search;
|
|
116
|
+
if (options.status)
|
|
117
|
+
params.status = options.status;
|
|
118
|
+
if (options.type)
|
|
119
|
+
params.type = options.type;
|
|
120
|
+
if (options.assignedTo)
|
|
121
|
+
params.assignedTo = options.assignedTo;
|
|
122
|
+
const page = toPositiveInteger(options.page);
|
|
123
|
+
const pageSize = toPositiveInteger(options.pageSize);
|
|
124
|
+
if (page !== undefined)
|
|
125
|
+
params.page = page;
|
|
126
|
+
if (pageSize !== undefined)
|
|
127
|
+
params.pageSize = pageSize;
|
|
128
|
+
return listPlans(apiUrl, projectId, headers, params);
|
|
129
|
+
}
|
|
130
|
+
case 'get': {
|
|
131
|
+
if (!options.id)
|
|
132
|
+
throw new Error('--id is required for plan get');
|
|
133
|
+
await runFreshnessCheckSilent(apiUrl, projectId, headers);
|
|
134
|
+
const response = await getPlan(apiUrl, projectId, headers, options.id);
|
|
135
|
+
if (options.includeDeps) {
|
|
136
|
+
const depsResponse = await getPlanDependencies(apiUrl, projectId, headers, options.id);
|
|
137
|
+
const dependencies = normalizeDependencies(depsResponse);
|
|
138
|
+
const mergedPlan = mergePlanWithDependencies(response, dependencies);
|
|
139
|
+
if (options.format === 'text') {
|
|
140
|
+
return formatPlanWithDependenciesText(mergedPlan.data, dependencies);
|
|
141
|
+
}
|
|
142
|
+
return mergedPlan;
|
|
143
|
+
}
|
|
144
|
+
return response;
|
|
145
|
+
}
|
|
146
|
+
case 'show': {
|
|
147
|
+
if (!options.id)
|
|
148
|
+
throw new Error('--id is required for plan show');
|
|
149
|
+
await runFreshnessCheckSilent(apiUrl, projectId, headers);
|
|
150
|
+
const response = await getPlan(apiUrl, projectId, headers, options.id);
|
|
151
|
+
if (options.includeDeps) {
|
|
152
|
+
const depsResponse = await getPlanDependencies(apiUrl, projectId, headers, options.id);
|
|
153
|
+
const dependencies = normalizeDependencies(depsResponse);
|
|
154
|
+
const mergedPlan = mergePlanWithDependencies(response, dependencies);
|
|
155
|
+
if (options.format === 'text') {
|
|
156
|
+
return formatPlanWithDependenciesText(mergedPlan.data, dependencies);
|
|
157
|
+
}
|
|
158
|
+
return mergedPlan;
|
|
159
|
+
}
|
|
160
|
+
return response;
|
|
161
|
+
}
|
|
162
|
+
case 'status': {
|
|
163
|
+
if (!options.id)
|
|
164
|
+
throw new Error('--id is required for plan status');
|
|
165
|
+
return getPlanStatus(apiUrl, projectId, headers, options.id);
|
|
166
|
+
}
|
|
167
|
+
case 'set-status': {
|
|
168
|
+
if (!options.id)
|
|
169
|
+
throw new Error('--id is required for plan set-status');
|
|
170
|
+
if (!options.status)
|
|
171
|
+
throw new Error('--status is required for plan set-status');
|
|
172
|
+
return patchPlanStatus(apiUrl, projectId, headers, options.id, options.status);
|
|
173
|
+
}
|
|
174
|
+
case 'start': {
|
|
175
|
+
if (!options.id)
|
|
176
|
+
throw new Error('--id is required for plan start');
|
|
177
|
+
const assignAgent = options.agent
|
|
178
|
+
?? options.defaultCreatedBy;
|
|
179
|
+
if (!assignAgent) {
|
|
180
|
+
throw new Error('No agent available for assignment. Set AGENTTEAMS_AGENT_NAME or pass --agent.');
|
|
181
|
+
}
|
|
182
|
+
const startGitInfo = options.git === false ? {} : collectGitMetrics();
|
|
183
|
+
const body = {
|
|
184
|
+
assignedTo: assignAgent,
|
|
185
|
+
};
|
|
186
|
+
if (options.task) {
|
|
187
|
+
body.task = options.task;
|
|
188
|
+
}
|
|
189
|
+
if (startGitInfo.commitHash) {
|
|
190
|
+
body.startCommit = startGitInfo.commitHash;
|
|
191
|
+
}
|
|
192
|
+
if (startGitInfo.branchName) {
|
|
193
|
+
body.startBranch = startGitInfo.branchName;
|
|
194
|
+
}
|
|
195
|
+
const result = await withSpinner('Starting plan...', () => startPlanLifecycle(apiUrl, projectId, headers, options.id, body), 'Plan started');
|
|
196
|
+
process.stderr.write(`\n Hint: Run 'agentteams plan download --id ${options.id}' to save the plan locally.\n`);
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
case 'finish': {
|
|
200
|
+
if (!options.id)
|
|
201
|
+
throw new Error('--id is required for plan finish');
|
|
202
|
+
let reportContent;
|
|
203
|
+
if (options.reportFile) {
|
|
204
|
+
const reportFilePath = resolve(options.reportFile);
|
|
205
|
+
if (!existsSync(reportFilePath)) {
|
|
206
|
+
throw new Error(`File not found: ${options.reportFile}`);
|
|
207
|
+
}
|
|
208
|
+
reportContent = readFileSync(reportFilePath, 'utf-8');
|
|
209
|
+
printFileInfo(options.reportFile, reportContent);
|
|
210
|
+
}
|
|
211
|
+
const includeCompletionReport = typeof reportContent === 'string' && reportContent.trim().length > 0;
|
|
212
|
+
const body = {};
|
|
213
|
+
if (options.task) {
|
|
214
|
+
body.task = options.task;
|
|
215
|
+
}
|
|
216
|
+
if (includeCompletionReport) {
|
|
217
|
+
// Fetch plan to get startCommit for accurate diff range
|
|
218
|
+
let planStartCommit;
|
|
219
|
+
if (options.git !== false) {
|
|
220
|
+
try {
|
|
221
|
+
const planResponse = await getPlan(apiUrl, projectId, headers, options.id);
|
|
222
|
+
planStartCommit = planResponse?.data?.startCommit ?? undefined;
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
// Plan fetch failure is non-blocking; fall back to HEAD~1 diff
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const autoGitMetrics = options.git === false
|
|
229
|
+
? {}
|
|
230
|
+
: collectGitMetrics(undefined, { startCommit: planStartCommit });
|
|
231
|
+
const commitHash = toNonEmptyString(options.commitHash) ?? autoGitMetrics.commitHash;
|
|
232
|
+
const branchName = toNonEmptyString(options.branchName) ?? autoGitMetrics.branchName;
|
|
233
|
+
const filesModified = toNonNegativeInteger(options.filesModified) ?? autoGitMetrics.filesModified;
|
|
234
|
+
const linesAdded = toNonNegativeInteger(options.linesAdded) ?? autoGitMetrics.linesAdded;
|
|
235
|
+
const linesDeleted = toNonNegativeInteger(options.linesDeleted) ?? autoGitMetrics.linesDeleted;
|
|
236
|
+
const durationSeconds = toNonNegativeInteger(options.durationSeconds);
|
|
237
|
+
const commitStart = toNonEmptyString(options.commitStart) ?? planStartCommit;
|
|
238
|
+
const commitEnd = toNonEmptyString(options.commitEnd) ?? autoGitMetrics.commitHash;
|
|
239
|
+
const pullRequestId = toNonEmptyString(options.pullRequestId);
|
|
240
|
+
const qualityScore = toNonNegativeInteger(options.qualityScore);
|
|
241
|
+
const reportStatus = toNonEmptyString(options.reportStatus);
|
|
242
|
+
const reportTitle = typeof options.reportTitle === 'string' && options.reportTitle.trim().length > 0
|
|
243
|
+
? options.reportTitle.trim()
|
|
244
|
+
: (() => { throw new Error('--report-title is required when attaching a completion report'); })();
|
|
245
|
+
body.completionReport = {
|
|
246
|
+
title: reportTitle,
|
|
247
|
+
content: reportContent.trim(),
|
|
248
|
+
};
|
|
249
|
+
if (reportStatus !== undefined)
|
|
250
|
+
body.completionReport.status = reportStatus;
|
|
251
|
+
if (qualityScore !== undefined)
|
|
252
|
+
body.completionReport.qualityScore = qualityScore;
|
|
253
|
+
if (commitHash !== undefined)
|
|
254
|
+
body.completionReport.commitHash = commitHash;
|
|
255
|
+
if (branchName !== undefined)
|
|
256
|
+
body.completionReport.branchName = branchName;
|
|
257
|
+
if (filesModified !== undefined)
|
|
258
|
+
body.completionReport.filesModified = filesModified;
|
|
259
|
+
if (linesAdded !== undefined)
|
|
260
|
+
body.completionReport.linesAdded = linesAdded;
|
|
261
|
+
if (linesDeleted !== undefined)
|
|
262
|
+
body.completionReport.linesDeleted = linesDeleted;
|
|
263
|
+
if (durationSeconds !== undefined)
|
|
264
|
+
body.completionReport.durationSeconds = durationSeconds;
|
|
265
|
+
if (commitStart !== undefined)
|
|
266
|
+
body.completionReport.commitStart = commitStart;
|
|
267
|
+
if (commitEnd !== undefined)
|
|
268
|
+
body.completionReport.commitEnd = commitEnd;
|
|
269
|
+
if (pullRequestId !== undefined)
|
|
270
|
+
body.completionReport.pullRequestId = pullRequestId;
|
|
271
|
+
}
|
|
272
|
+
const finishResult = await withSpinner('Finishing plan...', () => finishPlanLifecycle(apiUrl, projectId, headers, options.id, body), 'Plan finished');
|
|
273
|
+
if (options.reportFile)
|
|
274
|
+
deleteIfTempFile(options.reportFile);
|
|
275
|
+
return finishResult;
|
|
276
|
+
}
|
|
277
|
+
case 'create': {
|
|
278
|
+
if (!options.title)
|
|
279
|
+
throw new Error('--title is required for plan create');
|
|
280
|
+
let content = options.content;
|
|
281
|
+
const hasExplicitContent = typeof options.content === 'string' && options.content.trim().length > 0;
|
|
282
|
+
const hasExplicitFile = typeof options.file === 'string' && options.file.trim().length > 0;
|
|
283
|
+
const templateContent = resolvePlanTemplate(options.template);
|
|
284
|
+
if (!content && !options.file && templateContent) {
|
|
285
|
+
content = templateContent;
|
|
286
|
+
}
|
|
287
|
+
if ((hasExplicitContent || hasExplicitFile) && templateContent) {
|
|
288
|
+
process.stderr.write('[warn] plan create: --template is ignored because --content/--file was provided.\n');
|
|
289
|
+
}
|
|
290
|
+
if (options.file) {
|
|
291
|
+
const filePath = resolve(options.file);
|
|
292
|
+
if (!existsSync(filePath)) {
|
|
293
|
+
throw new Error(`File not found: ${options.file}`);
|
|
294
|
+
}
|
|
295
|
+
content = stripFrontmatter(readFileSync(filePath, 'utf-8'));
|
|
296
|
+
printFileInfo(options.file, content);
|
|
297
|
+
}
|
|
298
|
+
if (typeof content === 'string' && options.interpretEscapes) {
|
|
299
|
+
content = interpretEscapes(content);
|
|
300
|
+
}
|
|
301
|
+
if (!content || content.trim().length === 0) {
|
|
302
|
+
throw new Error('--content, --file, or --template is required for plan create');
|
|
303
|
+
}
|
|
304
|
+
if (options.status && options.status !== 'DRAFT') {
|
|
305
|
+
process.stderr.write(`[warn] plan create: --status ${options.status} is ignored. Plans are always created as DRAFT.\n`);
|
|
306
|
+
}
|
|
307
|
+
const createResult = await withSpinner('Creating plan...', () => createPlan(apiUrl, projectId, headers, {
|
|
308
|
+
title: options.title,
|
|
309
|
+
content,
|
|
310
|
+
type: options.type,
|
|
311
|
+
priority: options.priority ?? 'MEDIUM',
|
|
312
|
+
repositoryId: options.repositoryId ?? options.defaultRepositoryId,
|
|
313
|
+
status: 'DRAFT',
|
|
314
|
+
}), 'Plan created');
|
|
315
|
+
// --origin-issue flag: link origin issues after plan creation
|
|
316
|
+
const originIssueFlags = Array.isArray(options.originIssue)
|
|
317
|
+
? options.originIssue
|
|
318
|
+
: options.originIssue ? [options.originIssue] : [];
|
|
319
|
+
if (originIssueFlags.length > 0 && createResult?.data?.id) {
|
|
320
|
+
const createdPlanId = createResult.data.id;
|
|
321
|
+
for (const raw of originIssueFlags) {
|
|
322
|
+
// Format: PROVIDER:EXTERNAL_ID:URL[:TITLE]
|
|
323
|
+
// Use first colon to get provider, second to get externalId, rest is URL[:TITLE]
|
|
324
|
+
const firstColon = raw.indexOf(':');
|
|
325
|
+
const secondColon = raw.indexOf(':', firstColon + 1);
|
|
326
|
+
if (firstColon < 0 || secondColon < 0) {
|
|
327
|
+
process.stderr.write(`[warn] Skipping invalid --origin-issue: "${raw}" (expected provider:externalId:externalUrl[:title])\n`);
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
const provider = raw.substring(0, firstColon);
|
|
331
|
+
const externalId = raw.substring(firstColon + 1, secondColon);
|
|
332
|
+
const remainder = raw.substring(secondColon + 1);
|
|
333
|
+
// Title is optional, separated by the last colon that's NOT part of a URL path
|
|
334
|
+
// URL always contains "://" so find the scheme separator, then look for trailing :title
|
|
335
|
+
let externalUrl;
|
|
336
|
+
let externalTitle;
|
|
337
|
+
const schemeEnd = remainder.indexOf('://');
|
|
338
|
+
if (schemeEnd >= 0) {
|
|
339
|
+
// Find last colon after the scheme
|
|
340
|
+
const afterScheme = schemeEnd + 3;
|
|
341
|
+
const lastColon = remainder.lastIndexOf(':');
|
|
342
|
+
if (lastColon > afterScheme) {
|
|
343
|
+
externalUrl = remainder.substring(0, lastColon);
|
|
344
|
+
externalTitle = remainder.substring(lastColon + 1) || undefined;
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
externalUrl = remainder;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
externalUrl = remainder;
|
|
352
|
+
}
|
|
353
|
+
try {
|
|
354
|
+
await linkOriginIssue(apiUrl, projectId, headers, createdPlanId, {
|
|
355
|
+
provider: provider.toUpperCase(),
|
|
356
|
+
externalId,
|
|
357
|
+
externalUrl: ensureUrlProtocol(externalUrl),
|
|
358
|
+
externalTitle,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
catch (err) {
|
|
362
|
+
// 409 CONFLICT = already linked, skip silently
|
|
363
|
+
if (err?.response?.status !== 409) {
|
|
364
|
+
process.stderr.write(`[warn] Failed to link origin issue (${provider}:${externalId}): ${err?.message ?? err}\n`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return createResult;
|
|
370
|
+
}
|
|
371
|
+
case 'update': {
|
|
372
|
+
if (!options.id)
|
|
373
|
+
throw new Error('--id is required for plan update');
|
|
374
|
+
const body = {};
|
|
375
|
+
if (options.title)
|
|
376
|
+
body.title = options.title;
|
|
377
|
+
if (options.file) {
|
|
378
|
+
const filePath = resolve(options.file);
|
|
379
|
+
if (!existsSync(filePath)) {
|
|
380
|
+
throw new Error(`File not found: ${options.file}`);
|
|
381
|
+
}
|
|
382
|
+
body.content = stripFrontmatter(readFileSync(filePath, 'utf-8'));
|
|
383
|
+
printFileInfo(options.file, body.content);
|
|
384
|
+
}
|
|
385
|
+
else if (options.content) {
|
|
386
|
+
body.content = options.content;
|
|
387
|
+
if (typeof body.content === 'string' && options.interpretEscapes) {
|
|
388
|
+
body.content = interpretEscapes(body.content);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (options.status)
|
|
392
|
+
body.status = options.status;
|
|
393
|
+
if (options.type)
|
|
394
|
+
body.type = options.type;
|
|
395
|
+
if (options.priority)
|
|
396
|
+
body.priority = options.priority;
|
|
397
|
+
return withSpinner('Updating plan...', () => updatePlan(apiUrl, projectId, headers, options.id, body), 'Plan updated');
|
|
398
|
+
}
|
|
399
|
+
case 'delete': {
|
|
400
|
+
if (!options.id)
|
|
401
|
+
throw new Error('--id is required for plan delete');
|
|
402
|
+
await deletePlan(apiUrl, projectId, headers, options.id);
|
|
403
|
+
return { message: `Plan ${options.id} deleted successfully` };
|
|
404
|
+
}
|
|
405
|
+
case 'assign': {
|
|
406
|
+
if (!options.id)
|
|
407
|
+
throw new Error('--id is required for plan assign');
|
|
408
|
+
if (!options.agent)
|
|
409
|
+
throw new Error('--agent is required for plan assign');
|
|
410
|
+
return assignPlan(apiUrl, projectId, headers, options.id, options.agent);
|
|
411
|
+
}
|
|
412
|
+
case 'download': {
|
|
413
|
+
if (!options.id)
|
|
414
|
+
throw new Error('--id is required for plan download');
|
|
415
|
+
const projectRoot = findProjectRoot();
|
|
416
|
+
if (!projectRoot) {
|
|
417
|
+
throw new Error("Project root not found. Run 'agentteams init' first.");
|
|
418
|
+
}
|
|
419
|
+
const result = await withSpinner('Downloading plan...', async () => {
|
|
420
|
+
const response = await getPlan(apiUrl, projectId, headers, options.id);
|
|
421
|
+
const plan = response.data;
|
|
422
|
+
const activePlanDir = join(projectRoot, '.agentteams', 'cli', 'active-plan');
|
|
423
|
+
if (!existsSync(activePlanDir)) {
|
|
424
|
+
mkdirSync(activePlanDir, { recursive: true });
|
|
425
|
+
}
|
|
426
|
+
const existingFiles = readdirSync(activePlanDir).filter((name) => name.endsWith('.md'));
|
|
427
|
+
const fileName = buildUniquePlanRunbookFileName(plan.title, plan.id, existingFiles);
|
|
428
|
+
const filePath = join(activePlanDir, fileName);
|
|
429
|
+
const frontmatter = [
|
|
430
|
+
'---',
|
|
431
|
+
`planId: ${plan.id}`,
|
|
432
|
+
`title: ${plan.title}`,
|
|
433
|
+
`status: ${plan.status}`,
|
|
434
|
+
`priority: ${plan.priority}`,
|
|
435
|
+
plan.webUrl ? `webUrl: ${plan.webUrl}` : null,
|
|
436
|
+
`downloadedAt: ${new Date().toISOString()}`,
|
|
437
|
+
'---',
|
|
438
|
+
].filter(Boolean).join('\n');
|
|
439
|
+
const markdown = plan.contentMarkdown ?? '';
|
|
440
|
+
writeFileSync(filePath, `${frontmatter}\n\n${markdown}`, 'utf-8');
|
|
441
|
+
return {
|
|
442
|
+
message: `Plan downloaded to ${fileName}`,
|
|
443
|
+
filePath: `.agentteams/cli/active-plan/${fileName}`,
|
|
444
|
+
};
|
|
445
|
+
}, 'Plan downloaded');
|
|
446
|
+
return result;
|
|
447
|
+
}
|
|
448
|
+
case 'cleanup': {
|
|
449
|
+
const projectRoot = findProjectRoot();
|
|
450
|
+
if (!projectRoot) {
|
|
451
|
+
throw new Error("Project root not found. Run 'agentteams init' first.");
|
|
452
|
+
}
|
|
453
|
+
const activePlanDir = join(projectRoot, '.agentteams', 'cli', 'active-plan');
|
|
454
|
+
if (!existsSync(activePlanDir)) {
|
|
455
|
+
return { message: 'No active-plan directory found.', deletedFiles: [] };
|
|
456
|
+
}
|
|
457
|
+
const deletedFiles = await withSpinner('Cleaning up plan files...', async () => {
|
|
458
|
+
const allFiles = readdirSync(activePlanDir).filter((f) => f.endsWith('.md'));
|
|
459
|
+
const deleted = [];
|
|
460
|
+
if (options.id) {
|
|
461
|
+
for (const file of allFiles) {
|
|
462
|
+
const content = readFileSync(join(activePlanDir, file), 'utf-8');
|
|
463
|
+
const match = content.match(/^planId:\s*(.+)$/m);
|
|
464
|
+
if (match && match[1].trim() === options.id) {
|
|
465
|
+
rmSync(join(activePlanDir, file));
|
|
466
|
+
deleted.push(file);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
for (const file of allFiles) {
|
|
472
|
+
rmSync(join(activePlanDir, file));
|
|
473
|
+
deleted.push(file);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return deleted;
|
|
477
|
+
}, 'Cleaned up plan files');
|
|
478
|
+
return {
|
|
479
|
+
message: deletedFiles.length > 0
|
|
480
|
+
? `Deleted ${deletedFiles.length} file(s).`
|
|
481
|
+
: 'No matching files found.',
|
|
482
|
+
deletedFiles,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
case 'quick': {
|
|
486
|
+
if (!options.title)
|
|
487
|
+
throw new Error('--title is required for plan quick');
|
|
488
|
+
const assignAgent = options.agent
|
|
489
|
+
?? options.defaultCreatedBy;
|
|
490
|
+
if (!assignAgent) {
|
|
491
|
+
throw new Error('No agent available for assignment. Set AGENTTEAMS_AGENT_NAME or pass --agent.');
|
|
492
|
+
}
|
|
493
|
+
// Resolve plan content: --content > --file > template fallback
|
|
494
|
+
let planContent = undefined;
|
|
495
|
+
const hasQuickContent = typeof options.content === 'string' && options.content.trim().length > 0;
|
|
496
|
+
const hasQuickFile = typeof options.file === 'string' && options.file.trim().length > 0;
|
|
497
|
+
if (hasQuickContent) {
|
|
498
|
+
planContent = options.content;
|
|
499
|
+
}
|
|
500
|
+
else if (hasQuickFile) {
|
|
501
|
+
const filePath = resolve(options.file);
|
|
502
|
+
if (!existsSync(filePath)) {
|
|
503
|
+
throw new Error(`File not found: ${options.file}`);
|
|
504
|
+
}
|
|
505
|
+
planContent = stripFrontmatter(readFileSync(filePath, 'utf-8'));
|
|
506
|
+
printFileInfo(options.file, planContent);
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
throw new Error('--content or --file is required for plan quick. Provide the actual work description instead of using a template.');
|
|
510
|
+
}
|
|
511
|
+
if (typeof planContent === 'string' && options.interpretEscapes) {
|
|
512
|
+
planContent = interpretEscapes(planContent);
|
|
513
|
+
}
|
|
514
|
+
const priority = options.priority ?? 'LOW';
|
|
515
|
+
// 1. Create plan
|
|
516
|
+
const createResult = await withSpinner('Creating quick plan...', () => createPlan(apiUrl, projectId, headers, {
|
|
517
|
+
title: options.title,
|
|
518
|
+
content: planContent,
|
|
519
|
+
type: options.type,
|
|
520
|
+
priority,
|
|
521
|
+
repositoryId: options.repositoryId ?? options.defaultRepositoryId,
|
|
522
|
+
status: 'DRAFT',
|
|
523
|
+
}), 'Plan created');
|
|
524
|
+
const planId = createResult?.data?.id;
|
|
525
|
+
if (!planId) {
|
|
526
|
+
throw new Error('Failed to create plan: no plan ID returned.');
|
|
527
|
+
}
|
|
528
|
+
// 2. Start plan
|
|
529
|
+
await withSpinner('Starting plan...', () => startPlanLifecycle(apiUrl, projectId, headers, planId, { assignedTo: assignAgent }), 'Plan started');
|
|
530
|
+
// 3. Finish plan (no completion report for quick plans)
|
|
531
|
+
const finishResult = await withSpinner('Finishing plan...', () => finishPlanLifecycle(apiUrl, projectId, headers, planId, {}), 'Plan finished');
|
|
532
|
+
return {
|
|
533
|
+
message: `Quick plan completed (${planId})`,
|
|
534
|
+
planId,
|
|
535
|
+
create: createResult,
|
|
536
|
+
finish: finishResult,
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
case 'link-issue': {
|
|
540
|
+
const planId = toNonEmptyString(options.id);
|
|
541
|
+
if (!planId)
|
|
542
|
+
throw new Error('--id is required for plan link-issue');
|
|
543
|
+
const provider = toNonEmptyString(options.provider)?.toUpperCase();
|
|
544
|
+
if (!provider)
|
|
545
|
+
throw new Error('--provider is required for plan link-issue');
|
|
546
|
+
const externalId = toNonEmptyString(options.externalId);
|
|
547
|
+
if (!externalId)
|
|
548
|
+
throw new Error('--external-id is required for plan link-issue');
|
|
549
|
+
const externalUrl = toNonEmptyString(options.externalUrl);
|
|
550
|
+
if (!externalUrl)
|
|
551
|
+
throw new Error('--external-url is required for plan link-issue');
|
|
552
|
+
if (!['GITHUB', 'GITLAB', 'LINEAR'].includes(provider)) {
|
|
553
|
+
throw new Error('--provider must be one of: GITHUB, GITLAB, LINEAR');
|
|
554
|
+
}
|
|
555
|
+
const body = { provider, externalId, externalUrl: ensureUrlProtocol(externalUrl) };
|
|
556
|
+
if (options.title)
|
|
557
|
+
body.externalTitle = options.title;
|
|
558
|
+
if (options.metadata) {
|
|
559
|
+
try {
|
|
560
|
+
body.metadata = JSON.parse(options.metadata);
|
|
561
|
+
}
|
|
562
|
+
catch {
|
|
563
|
+
throw new Error('--metadata must be valid JSON');
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return linkOriginIssue(apiUrl, projectId, headers, planId, body);
|
|
567
|
+
}
|
|
568
|
+
case 'unlink-issue': {
|
|
569
|
+
const planId = toNonEmptyString(options.id);
|
|
570
|
+
if (!planId)
|
|
571
|
+
throw new Error('--id is required for plan unlink-issue');
|
|
572
|
+
const issueId = toNonEmptyString(options.issueId);
|
|
573
|
+
if (!issueId)
|
|
574
|
+
throw new Error('--issue-id is required for plan unlink-issue');
|
|
575
|
+
return unlinkOriginIssue(apiUrl, projectId, headers, planId, issueId);
|
|
576
|
+
}
|
|
577
|
+
case 'list-issues': {
|
|
578
|
+
const planId = toNonEmptyString(options.id);
|
|
579
|
+
if (!planId)
|
|
580
|
+
throw new Error('--id is required for plan list-issues');
|
|
581
|
+
return listOriginIssues(apiUrl, projectId, headers, planId);
|
|
582
|
+
}
|
|
583
|
+
case 'issue': {
|
|
584
|
+
// Shorter alias for link-issue, designed for agent convenience
|
|
585
|
+
const planId = toNonEmptyString(options.id);
|
|
586
|
+
if (!planId)
|
|
587
|
+
throw new Error('--id is required for plan issue');
|
|
588
|
+
const provider = toNonEmptyString(options.provider)?.toUpperCase();
|
|
589
|
+
if (!provider)
|
|
590
|
+
throw new Error('--provider is required for plan issue');
|
|
591
|
+
const externalId = toNonEmptyString(options.externalId);
|
|
592
|
+
if (!externalId)
|
|
593
|
+
throw new Error('--external-id is required for plan issue');
|
|
594
|
+
const externalUrl = toNonEmptyString(options.externalUrl);
|
|
595
|
+
if (!externalUrl)
|
|
596
|
+
throw new Error('--external-url is required for plan issue');
|
|
597
|
+
if (!['GITHUB', 'GITLAB', 'LINEAR'].includes(provider)) {
|
|
598
|
+
throw new Error('--provider must be one of: GITHUB, GITLAB, LINEAR');
|
|
599
|
+
}
|
|
600
|
+
const body = { provider, externalId, externalUrl: ensureUrlProtocol(externalUrl) };
|
|
601
|
+
if (options.title)
|
|
602
|
+
body.externalTitle = options.title;
|
|
603
|
+
if (options.metadata) {
|
|
604
|
+
try {
|
|
605
|
+
body.metadata = JSON.parse(options.metadata);
|
|
606
|
+
}
|
|
607
|
+
catch {
|
|
608
|
+
throw new Error('--metadata must be valid JSON');
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
try {
|
|
612
|
+
return await linkOriginIssue(apiUrl, projectId, headers, planId, body);
|
|
613
|
+
}
|
|
614
|
+
catch (err) {
|
|
615
|
+
// 409 CONFLICT = already linked, return success message
|
|
616
|
+
if (err?.response?.status === 409) {
|
|
617
|
+
return { message: 'Origin issue already linked (skipped)' };
|
|
618
|
+
}
|
|
619
|
+
throw err;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
default:
|
|
623
|
+
throw new Error(`Unknown action: ${action}`);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
//# sourceMappingURL=plan.js.map
|