@supaku/agentfactory-cli 0.7.5 → 0.7.7
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/dist/src/__tests__/subpath-exports.test.js +10 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -0
- package/dist/src/lib/linear-runner.d.ts +33 -0
- package/dist/src/lib/linear-runner.d.ts.map +1 -0
- package/dist/src/lib/linear-runner.js +681 -0
- package/dist/src/lib/worker-runner.d.ts.map +1 -1
- package/dist/src/lib/worker-runner.js +20 -1
- package/dist/src/linear.d.ts +37 -0
- package/dist/src/linear.d.ts.map +1 -0
- package/dist/src/linear.js +118 -0
- package/package.json +11 -5
|
@@ -39,4 +39,14 @@ describe('@supaku/agentfactory-cli subpath exports', () => {
|
|
|
39
39
|
expect(mod.runLogAnalyzer).toBeDefined();
|
|
40
40
|
expect(typeof mod.runLogAnalyzer).toBe('function');
|
|
41
41
|
});
|
|
42
|
+
it('exports runLinear from ./linear', async () => {
|
|
43
|
+
const mod = await import('../lib/linear-runner.js');
|
|
44
|
+
expect(mod.runLinear).toBeDefined();
|
|
45
|
+
expect(typeof mod.runLinear).toBe('function');
|
|
46
|
+
});
|
|
47
|
+
it('exports parseLinearArgs from ./linear', async () => {
|
|
48
|
+
const mod = await import('../lib/linear-runner.js');
|
|
49
|
+
expect(mod.parseLinearArgs).toBeDefined();
|
|
50
|
+
expect(typeof mod.parseLinearArgs).toBe('function');
|
|
51
|
+
});
|
|
42
52
|
});
|
package/dist/src/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";AACA;;;;;GAKG;AAEH,QAAA,MAAM,OAAO,QAAkB,CAAA;AAE/B,iBAAS,SAAS,IAAI,IAAI,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";AACA;;;;;GAKG;AAEH,QAAA,MAAM,OAAO,QAAkB,CAAA;AAE/B,iBAAS,SAAS,IAAI,IAAI,CAqBzB"}
|
package/dist/src/index.js
CHANGED
|
@@ -21,6 +21,7 @@ Commands:
|
|
|
21
21
|
cleanup Clean up orphaned git worktrees
|
|
22
22
|
queue-admin Manage Redis work queue and sessions
|
|
23
23
|
analyze-logs Analyze agent session logs for errors
|
|
24
|
+
linear Linear issue tracker operations
|
|
24
25
|
help Show this help message
|
|
25
26
|
|
|
26
27
|
Run 'agentfactory <command> --help' for command-specific options.
|
|
@@ -47,6 +48,9 @@ switch (command) {
|
|
|
47
48
|
case 'analyze-logs':
|
|
48
49
|
import('./analyze-logs');
|
|
49
50
|
break;
|
|
51
|
+
case 'linear':
|
|
52
|
+
import('./linear');
|
|
53
|
+
break;
|
|
50
54
|
case 'help':
|
|
51
55
|
case '--help':
|
|
52
56
|
case '-h':
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linear CLI Runner — process-agnostic Linear operations.
|
|
3
|
+
*
|
|
4
|
+
* All 15 command implementations extracted from packages/core/src/linear-cli.ts.
|
|
5
|
+
* This module does NOT call process.exit, read process.argv, or load dotenv.
|
|
6
|
+
*/
|
|
7
|
+
export interface LinearRunnerConfig {
|
|
8
|
+
command: string;
|
|
9
|
+
args: Record<string, string | string[] | boolean>;
|
|
10
|
+
positionalArgs: string[];
|
|
11
|
+
apiKey?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface LinearRunnerResult {
|
|
14
|
+
output: unknown;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Parse CLI arguments into a structured object.
|
|
18
|
+
*
|
|
19
|
+
* Supports:
|
|
20
|
+
* - `--key value` pairs
|
|
21
|
+
* - JSON array values: `--labels '["Bug", "Feature"]'`
|
|
22
|
+
* - Comma-separated values for array fields: `--labels "Bug,Feature"`
|
|
23
|
+
* - Boolean flags: `--dry-run` (value = "true")
|
|
24
|
+
*
|
|
25
|
+
* Returns the command (first non-flag arg), named args, and positional args.
|
|
26
|
+
*/
|
|
27
|
+
export declare function parseLinearArgs(argv: string[]): {
|
|
28
|
+
command: string | undefined;
|
|
29
|
+
args: Record<string, string | string[] | boolean>;
|
|
30
|
+
positionalArgs: string[];
|
|
31
|
+
};
|
|
32
|
+
export declare function runLinear(config: LinearRunnerConfig): Promise<LinearRunnerResult>;
|
|
33
|
+
//# sourceMappingURL=linear-runner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"linear-runner.d.ts","sourceRoot":"","sources":["../../../src/lib/linear-runner.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAUH,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC,CAAA;IACjD,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,OAAO,CAAA;CAChB;AAOD;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG;IAC/C,OAAO,EAAE,MAAM,GAAG,SAAS,CAAA;IAC3B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC,CAAA;IACjD,cAAc,EAAE,MAAM,EAAE,CAAA;CACzB,CA2CA;AAymBD,wBAAsB,SAAS,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CA2MvF"}
|
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linear CLI Runner — process-agnostic Linear operations.
|
|
3
|
+
*
|
|
4
|
+
* All 15 command implementations extracted from packages/core/src/linear-cli.ts.
|
|
5
|
+
* This module does NOT call process.exit, read process.argv, or load dotenv.
|
|
6
|
+
*/
|
|
7
|
+
import { createLinearAgentClient } from '@supaku/agentfactory-linear';
|
|
8
|
+
import { checkPRDeploymentStatus, formatDeploymentStatus, } from '@supaku/agentfactory';
|
|
9
|
+
// ── Arg parsing ────────────────────────────────────────────────────
|
|
10
|
+
/** Fields that should be split on commas to create arrays */
|
|
11
|
+
const ARRAY_FIELDS = new Set(['labels']);
|
|
12
|
+
/**
|
|
13
|
+
* Parse CLI arguments into a structured object.
|
|
14
|
+
*
|
|
15
|
+
* Supports:
|
|
16
|
+
* - `--key value` pairs
|
|
17
|
+
* - JSON array values: `--labels '["Bug", "Feature"]'`
|
|
18
|
+
* - Comma-separated values for array fields: `--labels "Bug,Feature"`
|
|
19
|
+
* - Boolean flags: `--dry-run` (value = "true")
|
|
20
|
+
*
|
|
21
|
+
* Returns the command (first non-flag arg), named args, and positional args.
|
|
22
|
+
*/
|
|
23
|
+
export function parseLinearArgs(argv) {
|
|
24
|
+
const command = argv[0] && !argv[0].startsWith('--') ? argv[0] : undefined;
|
|
25
|
+
const rest = command ? argv.slice(1) : argv;
|
|
26
|
+
const args = {};
|
|
27
|
+
const positionalArgs = [];
|
|
28
|
+
for (let i = 0; i < rest.length; i++) {
|
|
29
|
+
const arg = rest[i];
|
|
30
|
+
if (arg.startsWith('--')) {
|
|
31
|
+
const key = arg.slice(2);
|
|
32
|
+
const value = rest[i + 1];
|
|
33
|
+
if (value && !value.startsWith('--')) {
|
|
34
|
+
// Support JSON array format: --labels '["Bug", "Feature"]'
|
|
35
|
+
if (value.startsWith('[') && value.endsWith(']')) {
|
|
36
|
+
try {
|
|
37
|
+
const parsed = JSON.parse(value);
|
|
38
|
+
if (Array.isArray(parsed)) {
|
|
39
|
+
args[key] = parsed;
|
|
40
|
+
i++;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Not valid JSON, fall through to normal handling
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Only split on comma for known array fields
|
|
49
|
+
if (ARRAY_FIELDS.has(key) && value.includes(',')) {
|
|
50
|
+
args[key] = value.split(',').map((v) => v.trim());
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
args[key] = value;
|
|
54
|
+
}
|
|
55
|
+
i++;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
args[key] = true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
positionalArgs.push(arg);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { command, args, positionalArgs };
|
|
66
|
+
}
|
|
67
|
+
// ── Command implementations ────────────────────────────────────────
|
|
68
|
+
async function getIssue(client, issueId) {
|
|
69
|
+
const issue = await client.getIssue(issueId);
|
|
70
|
+
const state = await issue.state;
|
|
71
|
+
const team = await issue.team;
|
|
72
|
+
const project = await issue.project;
|
|
73
|
+
const labels = await issue.labels();
|
|
74
|
+
return {
|
|
75
|
+
id: issue.id,
|
|
76
|
+
identifier: issue.identifier,
|
|
77
|
+
title: issue.title,
|
|
78
|
+
description: issue.description,
|
|
79
|
+
url: issue.url,
|
|
80
|
+
status: state?.name,
|
|
81
|
+
team: team?.name,
|
|
82
|
+
project: project?.name,
|
|
83
|
+
labels: labels.nodes.map((l) => l.name),
|
|
84
|
+
createdAt: issue.createdAt,
|
|
85
|
+
updatedAt: issue.updatedAt,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
async function createIssue(client, options) {
|
|
89
|
+
const team = await client.getTeam(options.team);
|
|
90
|
+
const createPayload = {
|
|
91
|
+
teamId: team.id,
|
|
92
|
+
title: options.title,
|
|
93
|
+
};
|
|
94
|
+
if (options.description) {
|
|
95
|
+
createPayload.description = options.description;
|
|
96
|
+
}
|
|
97
|
+
if (options.parentId) {
|
|
98
|
+
createPayload.parentId = options.parentId;
|
|
99
|
+
}
|
|
100
|
+
if (options.project) {
|
|
101
|
+
const projects = await client.linearClient.projects({
|
|
102
|
+
filter: { name: { eq: options.project } },
|
|
103
|
+
});
|
|
104
|
+
if (projects.nodes.length > 0) {
|
|
105
|
+
createPayload.projectId = projects.nodes[0].id;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (options.state) {
|
|
109
|
+
const statuses = await client.getTeamStatuses(team.id);
|
|
110
|
+
const stateId = statuses[options.state];
|
|
111
|
+
if (stateId) {
|
|
112
|
+
createPayload.stateId = stateId;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (options.labels && options.labels.length > 0) {
|
|
116
|
+
const allLabels = await client.linearClient.issueLabels();
|
|
117
|
+
const labelIds = [];
|
|
118
|
+
for (const labelName of options.labels) {
|
|
119
|
+
const label = allLabels.nodes.find((l) => l.name.toLowerCase() === labelName.toLowerCase());
|
|
120
|
+
if (label) {
|
|
121
|
+
labelIds.push(label.id);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (labelIds.length > 0) {
|
|
125
|
+
createPayload.labelIds = labelIds;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const payload = await client.linearClient.createIssue(createPayload);
|
|
129
|
+
if (!payload.success) {
|
|
130
|
+
throw new Error('Failed to create issue');
|
|
131
|
+
}
|
|
132
|
+
const issue = await payload.issue;
|
|
133
|
+
if (!issue) {
|
|
134
|
+
throw new Error('Issue created but not returned');
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
id: issue.id,
|
|
138
|
+
identifier: issue.identifier,
|
|
139
|
+
title: issue.title,
|
|
140
|
+
url: issue.url,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
async function updateIssue(client, issueId, options) {
|
|
144
|
+
const issue = await client.getIssue(issueId);
|
|
145
|
+
const team = await issue.team;
|
|
146
|
+
const updateData = {};
|
|
147
|
+
if (options.title) {
|
|
148
|
+
updateData.title = options.title;
|
|
149
|
+
}
|
|
150
|
+
if (options.description) {
|
|
151
|
+
updateData.description = options.description;
|
|
152
|
+
}
|
|
153
|
+
if (options.state && team) {
|
|
154
|
+
const statuses = await client.getTeamStatuses(team.id);
|
|
155
|
+
const stateId = statuses[options.state];
|
|
156
|
+
if (stateId) {
|
|
157
|
+
updateData.stateId = stateId;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (options.labels && options.labels.length > 0) {
|
|
161
|
+
const allLabels = await client.linearClient.issueLabels();
|
|
162
|
+
const labelIds = [];
|
|
163
|
+
for (const labelName of options.labels) {
|
|
164
|
+
const label = allLabels.nodes.find((l) => l.name.toLowerCase() === labelName.toLowerCase());
|
|
165
|
+
if (label) {
|
|
166
|
+
labelIds.push(label.id);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
updateData.labelIds = labelIds;
|
|
170
|
+
}
|
|
171
|
+
const updatedIssue = await client.updateIssue(issue.id, updateData);
|
|
172
|
+
const state = await updatedIssue.state;
|
|
173
|
+
return {
|
|
174
|
+
id: updatedIssue.id,
|
|
175
|
+
identifier: updatedIssue.identifier,
|
|
176
|
+
title: updatedIssue.title,
|
|
177
|
+
status: state?.name,
|
|
178
|
+
url: updatedIssue.url,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
async function listComments(client, issueId) {
|
|
182
|
+
const comments = await client.getIssueComments(issueId);
|
|
183
|
+
return comments.map((c) => ({
|
|
184
|
+
id: c.id,
|
|
185
|
+
body: c.body,
|
|
186
|
+
createdAt: c.createdAt,
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
async function createComment(client, issueId, body) {
|
|
190
|
+
const comment = await client.createComment(issueId, body);
|
|
191
|
+
return {
|
|
192
|
+
id: comment.id,
|
|
193
|
+
body: comment.body,
|
|
194
|
+
createdAt: comment.createdAt,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
async function addRelation(client, issueId, relatedIssueId, relationType) {
|
|
198
|
+
const result = await client.createIssueRelation({
|
|
199
|
+
issueId,
|
|
200
|
+
relatedIssueId,
|
|
201
|
+
type: relationType,
|
|
202
|
+
});
|
|
203
|
+
return {
|
|
204
|
+
success: result.success,
|
|
205
|
+
relationId: result.relationId,
|
|
206
|
+
issueId,
|
|
207
|
+
relatedIssueId,
|
|
208
|
+
type: relationType,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
async function listRelations(client, issueId) {
|
|
212
|
+
const result = await client.getIssueRelations(issueId);
|
|
213
|
+
return {
|
|
214
|
+
issueId,
|
|
215
|
+
relations: result.relations.map((r) => ({
|
|
216
|
+
id: r.id,
|
|
217
|
+
type: r.type,
|
|
218
|
+
relatedIssue: r.relatedIssueIdentifier ?? r.relatedIssueId,
|
|
219
|
+
createdAt: r.createdAt,
|
|
220
|
+
})),
|
|
221
|
+
inverseRelations: result.inverseRelations.map((r) => ({
|
|
222
|
+
id: r.id,
|
|
223
|
+
type: r.type,
|
|
224
|
+
sourceIssue: r.issueIdentifier ?? r.issueId,
|
|
225
|
+
createdAt: r.createdAt,
|
|
226
|
+
})),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
async function removeRelation(client, relationId) {
|
|
230
|
+
const result = await client.deleteIssueRelation(relationId);
|
|
231
|
+
return {
|
|
232
|
+
success: result.success,
|
|
233
|
+
relationId,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
async function listBacklogIssues(client, projectName) {
|
|
237
|
+
const projects = await client.linearClient.projects({
|
|
238
|
+
filter: { name: { eqIgnoreCase: projectName } },
|
|
239
|
+
});
|
|
240
|
+
if (projects.nodes.length === 0) {
|
|
241
|
+
throw new Error(`Project not found: ${projectName}`);
|
|
242
|
+
}
|
|
243
|
+
const project = projects.nodes[0];
|
|
244
|
+
const issues = await client.linearClient.issues({
|
|
245
|
+
filter: {
|
|
246
|
+
project: { id: { eq: project.id } },
|
|
247
|
+
state: { name: { eqIgnoreCase: 'Backlog' } },
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
const results = [];
|
|
251
|
+
for (const issue of issues.nodes) {
|
|
252
|
+
const state = await issue.state;
|
|
253
|
+
const labels = await issue.labels();
|
|
254
|
+
results.push({
|
|
255
|
+
id: issue.id,
|
|
256
|
+
identifier: issue.identifier,
|
|
257
|
+
title: issue.title,
|
|
258
|
+
description: issue.description,
|
|
259
|
+
url: issue.url,
|
|
260
|
+
priority: issue.priority,
|
|
261
|
+
status: state?.name,
|
|
262
|
+
labels: labels.nodes.map((l) => l.name),
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
results.sort((a, b) => {
|
|
266
|
+
const aPriority = a.priority || 5;
|
|
267
|
+
const bPriority = b.priority || 5;
|
|
268
|
+
return aPriority - bPriority;
|
|
269
|
+
});
|
|
270
|
+
return results;
|
|
271
|
+
}
|
|
272
|
+
async function getBlockingIssues(client, issueId) {
|
|
273
|
+
const relations = await client.getIssueRelations(issueId);
|
|
274
|
+
const blockingIssues = [];
|
|
275
|
+
for (const relation of relations.inverseRelations) {
|
|
276
|
+
if (relation.type === 'blocks') {
|
|
277
|
+
const blockingIssue = await client.getIssue(relation.issueId);
|
|
278
|
+
const state = await blockingIssue.state;
|
|
279
|
+
const statusName = state?.name ?? 'Unknown';
|
|
280
|
+
if (statusName !== 'Accepted') {
|
|
281
|
+
blockingIssues.push({
|
|
282
|
+
identifier: blockingIssue.identifier,
|
|
283
|
+
title: blockingIssue.title,
|
|
284
|
+
status: statusName,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return blockingIssues;
|
|
290
|
+
}
|
|
291
|
+
async function listUnblockedBacklogIssues(client, projectName) {
|
|
292
|
+
const projects = await client.linearClient.projects({
|
|
293
|
+
filter: { name: { eqIgnoreCase: projectName } },
|
|
294
|
+
});
|
|
295
|
+
if (projects.nodes.length === 0) {
|
|
296
|
+
throw new Error(`Project not found: ${projectName}`);
|
|
297
|
+
}
|
|
298
|
+
const project = projects.nodes[0];
|
|
299
|
+
const issues = await client.linearClient.issues({
|
|
300
|
+
filter: {
|
|
301
|
+
project: { id: { eq: project.id } },
|
|
302
|
+
state: { name: { eqIgnoreCase: 'Backlog' } },
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
const results = [];
|
|
306
|
+
for (const issue of issues.nodes) {
|
|
307
|
+
const blockingIssues = await getBlockingIssues(client, issue.id);
|
|
308
|
+
const state = await issue.state;
|
|
309
|
+
const labels = await issue.labels();
|
|
310
|
+
results.push({
|
|
311
|
+
id: issue.id,
|
|
312
|
+
identifier: issue.identifier,
|
|
313
|
+
title: issue.title,
|
|
314
|
+
description: issue.description,
|
|
315
|
+
url: issue.url,
|
|
316
|
+
priority: issue.priority,
|
|
317
|
+
status: state?.name,
|
|
318
|
+
labels: labels.nodes.map((l) => l.name),
|
|
319
|
+
blocked: blockingIssues.length > 0,
|
|
320
|
+
blockedBy: blockingIssues,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
const unblockedResults = results.filter((r) => !r.blocked);
|
|
324
|
+
unblockedResults.sort((a, b) => {
|
|
325
|
+
const aPriority = a.priority || 5;
|
|
326
|
+
const bPriority = b.priority || 5;
|
|
327
|
+
return aPriority - bPriority;
|
|
328
|
+
});
|
|
329
|
+
return unblockedResults;
|
|
330
|
+
}
|
|
331
|
+
async function checkBlocked(client, issueId) {
|
|
332
|
+
const blockingIssues = await getBlockingIssues(client, issueId);
|
|
333
|
+
return {
|
|
334
|
+
issueId,
|
|
335
|
+
blocked: blockingIssues.length > 0,
|
|
336
|
+
blockedBy: blockingIssues,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
async function listSubIssues(client, issueId) {
|
|
340
|
+
const graph = await client.getSubIssueGraph(issueId);
|
|
341
|
+
return {
|
|
342
|
+
parentId: graph.parentId,
|
|
343
|
+
parentIdentifier: graph.parentIdentifier,
|
|
344
|
+
subIssueCount: graph.subIssues.length,
|
|
345
|
+
subIssues: graph.subIssues.map((node) => ({
|
|
346
|
+
id: node.issue.id,
|
|
347
|
+
identifier: node.issue.identifier,
|
|
348
|
+
title: node.issue.title,
|
|
349
|
+
status: node.issue.status,
|
|
350
|
+
priority: node.issue.priority,
|
|
351
|
+
labels: node.issue.labels,
|
|
352
|
+
url: node.issue.url,
|
|
353
|
+
blockedBy: node.blockedBy,
|
|
354
|
+
blocks: node.blocks,
|
|
355
|
+
})),
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
async function listSubIssueStatuses(client, issueId) {
|
|
359
|
+
const statuses = await client.getSubIssueStatuses(issueId);
|
|
360
|
+
return {
|
|
361
|
+
parentIssue: issueId,
|
|
362
|
+
subIssueCount: statuses.length,
|
|
363
|
+
subIssues: statuses,
|
|
364
|
+
allFinishedOrLater: statuses.every((s) => ['Finished', 'Delivered', 'Accepted', 'Canceled'].includes(s.status)),
|
|
365
|
+
incomplete: statuses.filter((s) => !['Finished', 'Delivered', 'Accepted', 'Canceled'].includes(s.status)),
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
async function updateSubIssue(client, issueId, options) {
|
|
369
|
+
const issue = await client.getIssue(issueId);
|
|
370
|
+
if (options.state) {
|
|
371
|
+
await client.updateIssueStatus(issue.id, options.state);
|
|
372
|
+
}
|
|
373
|
+
if (options.comment) {
|
|
374
|
+
await client.createComment(issue.id, options.comment);
|
|
375
|
+
}
|
|
376
|
+
const updatedIssue = await client.getIssue(issueId);
|
|
377
|
+
const state = await updatedIssue.state;
|
|
378
|
+
return {
|
|
379
|
+
id: updatedIssue.id,
|
|
380
|
+
identifier: updatedIssue.identifier,
|
|
381
|
+
title: updatedIssue.title,
|
|
382
|
+
status: state?.name,
|
|
383
|
+
url: updatedIssue.url,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
async function checkDeployment(prNumber, format = 'json') {
|
|
387
|
+
const result = await checkPRDeploymentStatus(prNumber);
|
|
388
|
+
if (!result) {
|
|
389
|
+
throw new Error(`Could not get deployment status for PR #${prNumber}. Make sure the PR exists and you have access to it.`);
|
|
390
|
+
}
|
|
391
|
+
if (format === 'markdown') {
|
|
392
|
+
return formatDeploymentStatus(result);
|
|
393
|
+
}
|
|
394
|
+
return result;
|
|
395
|
+
}
|
|
396
|
+
async function createBlocker(client, options) {
|
|
397
|
+
// 1. Fetch source issue to resolve team/project
|
|
398
|
+
const sourceIssue = await client.getIssue(options.sourceIssueId);
|
|
399
|
+
const sourceTeam = await sourceIssue.team;
|
|
400
|
+
const sourceProject = await sourceIssue.project;
|
|
401
|
+
const teamName = options.team ?? sourceTeam?.name;
|
|
402
|
+
if (!teamName) {
|
|
403
|
+
throw new Error('Could not resolve team from source issue. Provide --team explicitly.');
|
|
404
|
+
}
|
|
405
|
+
const team = await client.getTeam(teamName);
|
|
406
|
+
const projectName = options.project ?? sourceProject?.name;
|
|
407
|
+
// 2. Deduplicate: check for existing Icebox issues with same title + "Needs Human" label
|
|
408
|
+
if (projectName) {
|
|
409
|
+
const projects = await client.linearClient.projects({
|
|
410
|
+
filter: { name: { eqIgnoreCase: projectName } },
|
|
411
|
+
});
|
|
412
|
+
if (projects.nodes.length > 0) {
|
|
413
|
+
const existingIssues = await client.linearClient.issues({
|
|
414
|
+
filter: {
|
|
415
|
+
project: { id: { eq: projects.nodes[0].id } },
|
|
416
|
+
state: { name: { eqIgnoreCase: 'Icebox' } },
|
|
417
|
+
labels: { name: { eqIgnoreCase: 'Needs Human' } },
|
|
418
|
+
},
|
|
419
|
+
});
|
|
420
|
+
const duplicate = existingIssues.nodes.find((i) => i.title.toLowerCase() === options.title.toLowerCase());
|
|
421
|
+
if (duplicate) {
|
|
422
|
+
// Add a +1 comment to the existing issue
|
|
423
|
+
await client.createComment(duplicate.id, `+1 — Also needed by ${sourceIssue.identifier}`);
|
|
424
|
+
return {
|
|
425
|
+
id: duplicate.id,
|
|
426
|
+
identifier: duplicate.identifier,
|
|
427
|
+
title: duplicate.title,
|
|
428
|
+
url: duplicate.url,
|
|
429
|
+
sourceIssue: sourceIssue.identifier,
|
|
430
|
+
relation: 'blocks',
|
|
431
|
+
deduplicated: true,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
// 3. Create the blocker issue
|
|
437
|
+
const createPayload = {
|
|
438
|
+
teamId: team.id,
|
|
439
|
+
title: options.title,
|
|
440
|
+
};
|
|
441
|
+
// Description with source reference
|
|
442
|
+
const descParts = [];
|
|
443
|
+
if (options.description) {
|
|
444
|
+
descParts.push(options.description);
|
|
445
|
+
}
|
|
446
|
+
descParts.push(`\n---\n*Source issue: ${sourceIssue.identifier}*`);
|
|
447
|
+
createPayload.description = descParts.join('\n\n');
|
|
448
|
+
// Set state to Icebox
|
|
449
|
+
const statuses = await client.getTeamStatuses(team.id);
|
|
450
|
+
const iceboxStateId = statuses['Icebox'];
|
|
451
|
+
if (iceboxStateId) {
|
|
452
|
+
createPayload.stateId = iceboxStateId;
|
|
453
|
+
}
|
|
454
|
+
// Set "Needs Human" label
|
|
455
|
+
const allLabels = await client.linearClient.issueLabels();
|
|
456
|
+
const needsHumanLabel = allLabels.nodes.find((l) => l.name.toLowerCase() === 'needs human');
|
|
457
|
+
if (needsHumanLabel) {
|
|
458
|
+
createPayload.labelIds = [needsHumanLabel.id];
|
|
459
|
+
}
|
|
460
|
+
// Set project
|
|
461
|
+
if (projectName) {
|
|
462
|
+
const projects = await client.linearClient.projects({
|
|
463
|
+
filter: { name: { eqIgnoreCase: projectName } },
|
|
464
|
+
});
|
|
465
|
+
if (projects.nodes.length > 0) {
|
|
466
|
+
createPayload.projectId = projects.nodes[0].id;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
const payload = await client.linearClient.createIssue(createPayload);
|
|
470
|
+
if (!payload.success) {
|
|
471
|
+
throw new Error('Failed to create blocker issue');
|
|
472
|
+
}
|
|
473
|
+
const blockerIssue = await payload.issue;
|
|
474
|
+
if (!blockerIssue) {
|
|
475
|
+
throw new Error('Blocker issue created but not returned');
|
|
476
|
+
}
|
|
477
|
+
// 4. Create blocking relation: blocker blocks source
|
|
478
|
+
await client.createIssueRelation({
|
|
479
|
+
issueId: blockerIssue.id,
|
|
480
|
+
relatedIssueId: sourceIssue.id,
|
|
481
|
+
type: 'blocks',
|
|
482
|
+
});
|
|
483
|
+
// 5. Post comment on source issue
|
|
484
|
+
await client.createComment(sourceIssue.id, `\u{1F6A7} Human blocker created: [${blockerIssue.identifier}](${blockerIssue.url}) — ${options.title}`);
|
|
485
|
+
// 6. Optionally assign
|
|
486
|
+
if (options.assignee) {
|
|
487
|
+
const users = await client.linearClient.users({
|
|
488
|
+
filter: {
|
|
489
|
+
or: [
|
|
490
|
+
{ name: { eqIgnoreCase: options.assignee } },
|
|
491
|
+
{ email: { eq: options.assignee } },
|
|
492
|
+
],
|
|
493
|
+
},
|
|
494
|
+
});
|
|
495
|
+
if (users.nodes.length > 0) {
|
|
496
|
+
await client.linearClient.updateIssue(blockerIssue.id, {
|
|
497
|
+
assigneeId: users.nodes[0].id,
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return {
|
|
502
|
+
id: blockerIssue.id,
|
|
503
|
+
identifier: blockerIssue.identifier,
|
|
504
|
+
title: blockerIssue.title,
|
|
505
|
+
url: blockerIssue.url,
|
|
506
|
+
sourceIssue: sourceIssue.identifier,
|
|
507
|
+
relation: 'blocks',
|
|
508
|
+
deduplicated: false,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
// ── Commands that don't require LINEAR_API_KEY ─────────────────────
|
|
512
|
+
const NO_API_KEY_COMMANDS = new Set(['check-deployment']);
|
|
513
|
+
// ── Main runner ────────────────────────────────────────────────────
|
|
514
|
+
export async function runLinear(config) {
|
|
515
|
+
const { command, args, positionalArgs, apiKey } = config;
|
|
516
|
+
// Lazy client — only created for commands that need it
|
|
517
|
+
let _client = null;
|
|
518
|
+
function client() {
|
|
519
|
+
if (!_client) {
|
|
520
|
+
if (!apiKey) {
|
|
521
|
+
throw new Error('LINEAR_API_KEY environment variable is required');
|
|
522
|
+
}
|
|
523
|
+
_client = createLinearAgentClient({ apiKey });
|
|
524
|
+
}
|
|
525
|
+
return _client;
|
|
526
|
+
}
|
|
527
|
+
// Validate API key for commands that need it
|
|
528
|
+
if (!NO_API_KEY_COMMANDS.has(command) && !apiKey) {
|
|
529
|
+
throw new Error('LINEAR_API_KEY environment variable is required');
|
|
530
|
+
}
|
|
531
|
+
// Helper: get first positional or error
|
|
532
|
+
function requirePositional(name) {
|
|
533
|
+
const val = positionalArgs[0];
|
|
534
|
+
if (!val || val.startsWith('--')) {
|
|
535
|
+
throw new Error(`Missing required argument: <${name}>`);
|
|
536
|
+
}
|
|
537
|
+
return val;
|
|
538
|
+
}
|
|
539
|
+
// Parse sub-options from the original args (for commands that re-parse after positional)
|
|
540
|
+
function subArgs() {
|
|
541
|
+
return args;
|
|
542
|
+
}
|
|
543
|
+
let output;
|
|
544
|
+
switch (command) {
|
|
545
|
+
case 'get-issue': {
|
|
546
|
+
const issueId = requirePositional('issue-id');
|
|
547
|
+
output = await getIssue(client(), issueId);
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
550
|
+
case 'create-issue': {
|
|
551
|
+
if (!args.title || !args.team) {
|
|
552
|
+
throw new Error('Usage: af-linear create-issue --title "Title" --team "Team" [--description "..."] [--project "..."] [--labels "Label1,Label2"] [--state "Backlog"] [--parentId "..."]');
|
|
553
|
+
}
|
|
554
|
+
output = await createIssue(client(), {
|
|
555
|
+
title: args.title,
|
|
556
|
+
team: args.team,
|
|
557
|
+
description: args.description,
|
|
558
|
+
project: args.project,
|
|
559
|
+
labels: args.labels,
|
|
560
|
+
state: args.state,
|
|
561
|
+
parentId: args.parentId,
|
|
562
|
+
});
|
|
563
|
+
break;
|
|
564
|
+
}
|
|
565
|
+
case 'update-issue': {
|
|
566
|
+
const issueId = requirePositional('issue-id');
|
|
567
|
+
const opts = subArgs();
|
|
568
|
+
output = await updateIssue(client(), issueId, {
|
|
569
|
+
title: opts.title,
|
|
570
|
+
description: opts.description,
|
|
571
|
+
state: opts.state,
|
|
572
|
+
labels: opts.labels,
|
|
573
|
+
});
|
|
574
|
+
break;
|
|
575
|
+
}
|
|
576
|
+
case 'list-comments': {
|
|
577
|
+
const issueId = requirePositional('issue-id');
|
|
578
|
+
output = await listComments(client(), issueId);
|
|
579
|
+
break;
|
|
580
|
+
}
|
|
581
|
+
case 'create-comment': {
|
|
582
|
+
const issueId = requirePositional('issue-id');
|
|
583
|
+
if (!args.body) {
|
|
584
|
+
throw new Error('Usage: af-linear create-comment <issue-id> --body "Comment text"');
|
|
585
|
+
}
|
|
586
|
+
output = await createComment(client(), issueId, args.body);
|
|
587
|
+
break;
|
|
588
|
+
}
|
|
589
|
+
case 'list-backlog-issues': {
|
|
590
|
+
if (!args.project) {
|
|
591
|
+
throw new Error('Usage: af-linear list-backlog-issues --project "ProjectName"');
|
|
592
|
+
}
|
|
593
|
+
output = await listBacklogIssues(client(), args.project);
|
|
594
|
+
break;
|
|
595
|
+
}
|
|
596
|
+
case 'list-unblocked-backlog': {
|
|
597
|
+
if (!args.project) {
|
|
598
|
+
throw new Error('Usage: af-linear list-unblocked-backlog --project "ProjectName"');
|
|
599
|
+
}
|
|
600
|
+
output = await listUnblockedBacklogIssues(client(), args.project);
|
|
601
|
+
break;
|
|
602
|
+
}
|
|
603
|
+
case 'check-blocked': {
|
|
604
|
+
const issueId = requirePositional('issue-id');
|
|
605
|
+
output = await checkBlocked(client(), issueId);
|
|
606
|
+
break;
|
|
607
|
+
}
|
|
608
|
+
case 'add-relation': {
|
|
609
|
+
const issueId = positionalArgs[0];
|
|
610
|
+
const relatedIssueId = positionalArgs[1];
|
|
611
|
+
const relationType = args.type;
|
|
612
|
+
if (!issueId ||
|
|
613
|
+
issueId.startsWith('--') ||
|
|
614
|
+
!relatedIssueId ||
|
|
615
|
+
relatedIssueId.startsWith('--') ||
|
|
616
|
+
!relationType ||
|
|
617
|
+
!['related', 'blocks', 'duplicate'].includes(relationType)) {
|
|
618
|
+
throw new Error('Usage: af-linear add-relation <issue-id> <related-issue-id> --type <related|blocks|duplicate>');
|
|
619
|
+
}
|
|
620
|
+
output = await addRelation(client(), issueId, relatedIssueId, relationType);
|
|
621
|
+
break;
|
|
622
|
+
}
|
|
623
|
+
case 'list-relations': {
|
|
624
|
+
const issueId = requirePositional('issue-id');
|
|
625
|
+
output = await listRelations(client(), issueId);
|
|
626
|
+
break;
|
|
627
|
+
}
|
|
628
|
+
case 'remove-relation': {
|
|
629
|
+
const relationId = requirePositional('relation-id');
|
|
630
|
+
output = await removeRelation(client(), relationId);
|
|
631
|
+
break;
|
|
632
|
+
}
|
|
633
|
+
case 'list-sub-issues': {
|
|
634
|
+
const issueId = requirePositional('issue-id');
|
|
635
|
+
output = await listSubIssues(client(), issueId);
|
|
636
|
+
break;
|
|
637
|
+
}
|
|
638
|
+
case 'list-sub-issue-statuses': {
|
|
639
|
+
const issueId = requirePositional('issue-id');
|
|
640
|
+
output = await listSubIssueStatuses(client(), issueId);
|
|
641
|
+
break;
|
|
642
|
+
}
|
|
643
|
+
case 'update-sub-issue': {
|
|
644
|
+
const issueId = requirePositional('issue-id');
|
|
645
|
+
const opts = subArgs();
|
|
646
|
+
output = await updateSubIssue(client(), issueId, {
|
|
647
|
+
state: opts.state,
|
|
648
|
+
comment: opts.comment,
|
|
649
|
+
});
|
|
650
|
+
break;
|
|
651
|
+
}
|
|
652
|
+
case 'check-deployment': {
|
|
653
|
+
const prArg = requirePositional('pr-number');
|
|
654
|
+
const prNumber = parseInt(prArg, 10);
|
|
655
|
+
if (isNaN(prNumber)) {
|
|
656
|
+
throw new Error('PR number must be a valid integer');
|
|
657
|
+
}
|
|
658
|
+
const format = args.format || 'json';
|
|
659
|
+
output = await checkDeployment(prNumber, format);
|
|
660
|
+
break;
|
|
661
|
+
}
|
|
662
|
+
case 'create-blocker': {
|
|
663
|
+
const sourceIssueId = requirePositional('source-issue-id');
|
|
664
|
+
if (!args.title) {
|
|
665
|
+
throw new Error('Usage: af-linear create-blocker <source-issue-id> --title "Title" [--description "..."] [--team "..."] [--project "..."] [--assignee "user@email.com"]');
|
|
666
|
+
}
|
|
667
|
+
output = await createBlocker(client(), {
|
|
668
|
+
title: args.title,
|
|
669
|
+
sourceIssueId,
|
|
670
|
+
description: args.description,
|
|
671
|
+
team: args.team,
|
|
672
|
+
project: args.project,
|
|
673
|
+
assignee: args.assignee,
|
|
674
|
+
});
|
|
675
|
+
break;
|
|
676
|
+
}
|
|
677
|
+
default:
|
|
678
|
+
throw new Error(`Unknown command: ${command}`);
|
|
679
|
+
}
|
|
680
|
+
return { output };
|
|
681
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worker-runner.d.ts","sourceRoot":"","sources":["../../../src/lib/worker-runner.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAmBH,MAAM,WAAW,kBAAkB;IACjC,0BAA0B;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,iCAAiC;IACjC,MAAM,EAAE,MAAM,CAAA;IACd,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,6CAA6C;IAC7C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,mDAAmD;IACnD,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,gFAAgF;IAChF,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,iDAAiD;IACjD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,uDAAuD;IACvD,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;CACpB;AAwED;;;;;GAKG;AACH,wBAAsB,SAAS,CAC7B,MAAM,EAAE,kBAAkB,EAC1B,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"worker-runner.d.ts","sourceRoot":"","sources":["../../../src/lib/worker-runner.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAmBH,MAAM,WAAW,kBAAkB;IACjC,0BAA0B;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,iCAAiC;IACjC,MAAM,EAAE,MAAM,CAAA;IACd,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,6CAA6C;IAC7C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,mDAAmD;IACnD,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,gFAAgF;IAChF,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,iDAAiD;IACjD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,uDAAuD;IACvD,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;CACpB;AAwED;;;;;GAKG;AACH,wBAAsB,SAAS,CAC7B,MAAM,EAAE,kBAAkB,EAC1B,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,IAAI,CAAC,CAw5Bf"}
|
|
@@ -54,6 +54,7 @@ export async function runWorker(config, signal) {
|
|
|
54
54
|
let shutdownInProgress = false;
|
|
55
55
|
let consecutiveHeartbeatFailures = 0;
|
|
56
56
|
let reregistrationInProgress = false;
|
|
57
|
+
let claimFailureCount = 0;
|
|
57
58
|
const activeOrchestrators = new Map();
|
|
58
59
|
// Logger — will be re-created after registration with worker context
|
|
59
60
|
let log = createLogger({}, { showTimestamp: true });
|
|
@@ -241,6 +242,10 @@ export async function runWorker(config, signal) {
|
|
|
241
242
|
});
|
|
242
243
|
if (result.data) {
|
|
243
244
|
consecutiveHeartbeatFailures = 0;
|
|
245
|
+
if (claimFailureCount > 0) {
|
|
246
|
+
log.info('Claim race summary since last heartbeat', { claimFailures: claimFailureCount });
|
|
247
|
+
claimFailureCount = 0;
|
|
248
|
+
}
|
|
244
249
|
log.debug('Heartbeat acknowledged', {
|
|
245
250
|
activeCount,
|
|
246
251
|
pendingWorkCount: result.data.pendingWorkCount,
|
|
@@ -608,6 +613,19 @@ export async function runWorker(config, signal) {
|
|
|
608
613
|
workerShortId = registration.workerId.substring(0, 8);
|
|
609
614
|
// Update logger with worker context
|
|
610
615
|
log = createLogger({ workerId, workerShortId }, { showTimestamp: true });
|
|
616
|
+
// Auto-inherit projects from server if not explicitly configured
|
|
617
|
+
if (!config.projects?.length) {
|
|
618
|
+
try {
|
|
619
|
+
const serverConfig = await apiRequest('/api/config');
|
|
620
|
+
if (serverConfig?.projects?.length) {
|
|
621
|
+
config.projects = serverConfig.projects;
|
|
622
|
+
log.info('Auto-inherited projects from server', { projects: config.projects });
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
catch {
|
|
626
|
+
log.debug('Could not fetch server config, using no project filter');
|
|
627
|
+
}
|
|
628
|
+
}
|
|
611
629
|
// Set up heartbeat
|
|
612
630
|
heartbeatTimer = setInterval(() => sendHeartbeat(), registration.heartbeatInterval);
|
|
613
631
|
// Send initial heartbeat
|
|
@@ -644,7 +662,8 @@ export async function runWorker(config, signal) {
|
|
|
644
662
|
}
|
|
645
663
|
}
|
|
646
664
|
else {
|
|
647
|
-
|
|
665
|
+
claimFailureCount++;
|
|
666
|
+
log.debug(`Failed to claim work: ${item.issueIdentifier}`);
|
|
648
667
|
}
|
|
649
668
|
}
|
|
650
669
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* AgentFactory Linear CLI
|
|
4
|
+
*
|
|
5
|
+
* Thin wrapper around the linear runner.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* af-linear <command> [options]
|
|
9
|
+
*
|
|
10
|
+
* Commands:
|
|
11
|
+
* get-issue <id> Get issue details
|
|
12
|
+
* create-issue Create a new issue
|
|
13
|
+
* update-issue <id> Update an existing issue
|
|
14
|
+
* list-comments <issueId> List comments on an issue
|
|
15
|
+
* create-comment <issueId> Create a comment on an issue
|
|
16
|
+
* list-backlog-issues List backlog issues for a project
|
|
17
|
+
* list-unblocked-backlog List unblocked backlog issues
|
|
18
|
+
* check-blocked <id> Check if an issue is blocked
|
|
19
|
+
* add-relation <id> <id> Create relation between issues
|
|
20
|
+
* list-relations <id> List relations for an issue
|
|
21
|
+
* remove-relation <id> Remove a relation by ID
|
|
22
|
+
* list-sub-issues <id> List sub-issues of a parent issue
|
|
23
|
+
* list-sub-issue-statuses <id> List sub-issue statuses (lightweight)
|
|
24
|
+
* update-sub-issue <id> Update sub-issue status with comment
|
|
25
|
+
* check-deployment <PR> Check Vercel deployment status for a PR
|
|
26
|
+
* create-blocker <source-id> Create a human-needed blocker issue
|
|
27
|
+
*
|
|
28
|
+
* Array Values:
|
|
29
|
+
* --labels accepts comma-separated: --labels "Bug,Feature"
|
|
30
|
+
* For values with commas, use JSON: --labels '["Bug", "UI, UX"]'
|
|
31
|
+
* Text fields (--description, --title, --body) preserve commas.
|
|
32
|
+
*
|
|
33
|
+
* Environment:
|
|
34
|
+
* LINEAR_API_KEY Required API key for authentication
|
|
35
|
+
*/
|
|
36
|
+
export {};
|
|
37
|
+
//# sourceMappingURL=linear.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"linear.d.ts","sourceRoot":"","sources":["../../src/linear.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* AgentFactory Linear CLI
|
|
4
|
+
*
|
|
5
|
+
* Thin wrapper around the linear runner.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* af-linear <command> [options]
|
|
9
|
+
*
|
|
10
|
+
* Commands:
|
|
11
|
+
* get-issue <id> Get issue details
|
|
12
|
+
* create-issue Create a new issue
|
|
13
|
+
* update-issue <id> Update an existing issue
|
|
14
|
+
* list-comments <issueId> List comments on an issue
|
|
15
|
+
* create-comment <issueId> Create a comment on an issue
|
|
16
|
+
* list-backlog-issues List backlog issues for a project
|
|
17
|
+
* list-unblocked-backlog List unblocked backlog issues
|
|
18
|
+
* check-blocked <id> Check if an issue is blocked
|
|
19
|
+
* add-relation <id> <id> Create relation between issues
|
|
20
|
+
* list-relations <id> List relations for an issue
|
|
21
|
+
* remove-relation <id> Remove a relation by ID
|
|
22
|
+
* list-sub-issues <id> List sub-issues of a parent issue
|
|
23
|
+
* list-sub-issue-statuses <id> List sub-issue statuses (lightweight)
|
|
24
|
+
* update-sub-issue <id> Update sub-issue status with comment
|
|
25
|
+
* check-deployment <PR> Check Vercel deployment status for a PR
|
|
26
|
+
* create-blocker <source-id> Create a human-needed blocker issue
|
|
27
|
+
*
|
|
28
|
+
* Array Values:
|
|
29
|
+
* --labels accepts comma-separated: --labels "Bug,Feature"
|
|
30
|
+
* For values with commas, use JSON: --labels '["Bug", "UI, UX"]'
|
|
31
|
+
* Text fields (--description, --title, --body) preserve commas.
|
|
32
|
+
*
|
|
33
|
+
* Environment:
|
|
34
|
+
* LINEAR_API_KEY Required API key for authentication
|
|
35
|
+
*/
|
|
36
|
+
import path from 'path';
|
|
37
|
+
import { config } from 'dotenv';
|
|
38
|
+
// Load environment variables from .env.local
|
|
39
|
+
config({ path: path.resolve(process.cwd(), '.env.local') });
|
|
40
|
+
import { runLinear, parseLinearArgs } from './lib/linear-runner.js';
|
|
41
|
+
function printHelp() {
|
|
42
|
+
console.log(`
|
|
43
|
+
AgentFactory Linear CLI — Linear issue tracker operations
|
|
44
|
+
|
|
45
|
+
Usage:
|
|
46
|
+
af-linear <command> [options]
|
|
47
|
+
|
|
48
|
+
Commands:
|
|
49
|
+
get-issue <id> Get issue details
|
|
50
|
+
create-issue Create a new issue
|
|
51
|
+
update-issue <id> Update an existing issue
|
|
52
|
+
list-comments <issueId> List comments on an issue
|
|
53
|
+
create-comment <issueId> Create a comment on an issue
|
|
54
|
+
list-backlog-issues List backlog issues for a project
|
|
55
|
+
list-unblocked-backlog List unblocked backlog issues
|
|
56
|
+
check-blocked <id> Check if an issue is blocked
|
|
57
|
+
add-relation <id> <id> Create relation between issues
|
|
58
|
+
list-relations <id> List relations for an issue
|
|
59
|
+
remove-relation <id> Remove a relation by ID
|
|
60
|
+
list-sub-issues <id> List sub-issues of a parent issue
|
|
61
|
+
list-sub-issue-statuses <id> List sub-issue statuses (lightweight)
|
|
62
|
+
update-sub-issue <id> Update sub-issue status with comment
|
|
63
|
+
check-deployment <PR> Check Vercel deployment status for a PR
|
|
64
|
+
create-blocker <source-id> Create a human-needed blocker issue
|
|
65
|
+
help Show this help message
|
|
66
|
+
|
|
67
|
+
Options:
|
|
68
|
+
--help, -h Show this help message
|
|
69
|
+
|
|
70
|
+
Array Values:
|
|
71
|
+
--labels accepts comma-separated: --labels "Bug,Feature"
|
|
72
|
+
For values with commas, use JSON: --labels '["Bug", "UI, UX"]'
|
|
73
|
+
|
|
74
|
+
Environment:
|
|
75
|
+
LINEAR_API_KEY Required API key for authentication
|
|
76
|
+
LINEAR_ACCESS_TOKEN Alternative to LINEAR_API_KEY
|
|
77
|
+
|
|
78
|
+
Examples:
|
|
79
|
+
af-linear get-issue PROJ-123
|
|
80
|
+
af-linear create-issue --title "Add auth" --team "Engineering" --project "Backend"
|
|
81
|
+
af-linear update-issue PROJ-123 --state "Finished"
|
|
82
|
+
af-linear list-backlog-issues --project "MyProject"
|
|
83
|
+
af-linear check-deployment 42
|
|
84
|
+
`);
|
|
85
|
+
}
|
|
86
|
+
async function main() {
|
|
87
|
+
const { command, args, positionalArgs } = parseLinearArgs(process.argv.slice(2));
|
|
88
|
+
if (!command || command === 'help' || args['help'] || args['h']) {
|
|
89
|
+
printHelp();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const apiKey = process.env.LINEAR_API_KEY || process.env.LINEAR_ACCESS_TOKEN;
|
|
93
|
+
const result = await runLinear({
|
|
94
|
+
command,
|
|
95
|
+
args,
|
|
96
|
+
positionalArgs,
|
|
97
|
+
apiKey,
|
|
98
|
+
});
|
|
99
|
+
// check-deployment with markdown format outputs a string, not JSON
|
|
100
|
+
if (typeof result.output === 'string') {
|
|
101
|
+
console.log(result.output);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
console.log(JSON.stringify(result.output, null, 2));
|
|
105
|
+
}
|
|
106
|
+
// Exit with error code if deployment check failed
|
|
107
|
+
if (command === 'check-deployment' &&
|
|
108
|
+
typeof result.output === 'object' &&
|
|
109
|
+
result.output !== null &&
|
|
110
|
+
'anyFailed' in result.output &&
|
|
111
|
+
result.output.anyFailed) {
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
main().catch((error) => {
|
|
116
|
+
console.error('Error:', error instanceof Error ? error.message : error);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supaku/agentfactory-cli",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "CLI tools for AgentFactory — local orchestrator, remote worker, queue admin",
|
|
6
6
|
"author": "Supaku (https://supaku.com)",
|
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
"af-worker-fleet": "./dist/src/worker-fleet.js",
|
|
34
34
|
"af-cleanup": "./dist/src/cleanup.js",
|
|
35
35
|
"af-queue-admin": "./dist/src/queue-admin.js",
|
|
36
|
-
"af-analyze-logs": "./dist/src/analyze-logs.js"
|
|
36
|
+
"af-analyze-logs": "./dist/src/analyze-logs.js",
|
|
37
|
+
"af-linear": "./dist/src/linear.js"
|
|
37
38
|
},
|
|
38
39
|
"main": "./dist/src/index.js",
|
|
39
40
|
"module": "./dist/src/index.js",
|
|
@@ -73,6 +74,11 @@
|
|
|
73
74
|
"types": "./dist/src/lib/analyze-logs-runner.d.ts",
|
|
74
75
|
"import": "./dist/src/lib/analyze-logs-runner.js",
|
|
75
76
|
"default": "./dist/src/lib/analyze-logs-runner.js"
|
|
77
|
+
},
|
|
78
|
+
"./linear": {
|
|
79
|
+
"types": "./dist/src/lib/linear-runner.d.ts",
|
|
80
|
+
"import": "./dist/src/lib/linear-runner.js",
|
|
81
|
+
"default": "./dist/src/lib/linear-runner.js"
|
|
76
82
|
}
|
|
77
83
|
},
|
|
78
84
|
"files": [
|
|
@@ -82,9 +88,9 @@
|
|
|
82
88
|
],
|
|
83
89
|
"dependencies": {
|
|
84
90
|
"dotenv": "^17.2.3",
|
|
85
|
-
"@supaku/agentfactory
|
|
86
|
-
"@supaku/agentfactory-server": "0.7.
|
|
87
|
-
"@supaku/agentfactory": "0.7.
|
|
91
|
+
"@supaku/agentfactory": "0.7.7",
|
|
92
|
+
"@supaku/agentfactory-server": "0.7.7",
|
|
93
|
+
"@supaku/agentfactory-linear": "0.7.7"
|
|
88
94
|
},
|
|
89
95
|
"devDependencies": {
|
|
90
96
|
"@types/node": "^22.5.4",
|