@renseiai/agentfactory 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +125 -0
- package/dist/src/config/index.d.ts +3 -0
- package/dist/src/config/index.d.ts.map +1 -0
- package/dist/src/config/index.js +1 -0
- package/dist/src/config/repository-config.d.ts +44 -0
- package/dist/src/config/repository-config.d.ts.map +1 -0
- package/dist/src/config/repository-config.js +88 -0
- package/dist/src/config/repository-config.test.d.ts +2 -0
- package/dist/src/config/repository-config.test.d.ts.map +1 -0
- package/dist/src/config/repository-config.test.js +249 -0
- package/dist/src/deployment/deployment-checker.d.ts +110 -0
- package/dist/src/deployment/deployment-checker.d.ts.map +1 -0
- package/dist/src/deployment/deployment-checker.js +242 -0
- package/dist/src/deployment/index.d.ts +3 -0
- package/dist/src/deployment/index.d.ts.map +1 -0
- package/dist/src/deployment/index.js +2 -0
- package/dist/src/frontend/index.d.ts +2 -0
- package/dist/src/frontend/index.d.ts.map +1 -0
- package/dist/src/frontend/index.js +1 -0
- package/dist/src/frontend/types.d.ts +106 -0
- package/dist/src/frontend/types.d.ts.map +1 -0
- package/dist/src/frontend/types.js +11 -0
- package/dist/src/governor/decision-engine.d.ts +52 -0
- package/dist/src/governor/decision-engine.d.ts.map +1 -0
- package/dist/src/governor/decision-engine.js +220 -0
- package/dist/src/governor/decision-engine.test.d.ts +2 -0
- package/dist/src/governor/decision-engine.test.d.ts.map +1 -0
- package/dist/src/governor/decision-engine.test.js +629 -0
- package/dist/src/governor/event-bus.d.ts +43 -0
- package/dist/src/governor/event-bus.d.ts.map +1 -0
- package/dist/src/governor/event-bus.js +8 -0
- package/dist/src/governor/event-deduplicator.d.ts +43 -0
- package/dist/src/governor/event-deduplicator.d.ts.map +1 -0
- package/dist/src/governor/event-deduplicator.js +53 -0
- package/dist/src/governor/event-driven-governor.d.ts +131 -0
- package/dist/src/governor/event-driven-governor.d.ts.map +1 -0
- package/dist/src/governor/event-driven-governor.js +379 -0
- package/dist/src/governor/event-driven-governor.test.d.ts +2 -0
- package/dist/src/governor/event-driven-governor.test.d.ts.map +1 -0
- package/dist/src/governor/event-driven-governor.test.js +673 -0
- package/dist/src/governor/event-types.d.ts +78 -0
- package/dist/src/governor/event-types.d.ts.map +1 -0
- package/dist/src/governor/event-types.js +32 -0
- package/dist/src/governor/governor-types.d.ts +82 -0
- package/dist/src/governor/governor-types.d.ts.map +1 -0
- package/dist/src/governor/governor-types.js +21 -0
- package/dist/src/governor/governor.d.ts +100 -0
- package/dist/src/governor/governor.d.ts.map +1 -0
- package/dist/src/governor/governor.js +262 -0
- package/dist/src/governor/governor.test.d.ts +2 -0
- package/dist/src/governor/governor.test.d.ts.map +1 -0
- package/dist/src/governor/governor.test.js +514 -0
- package/dist/src/governor/human-touchpoints.d.ts +131 -0
- package/dist/src/governor/human-touchpoints.d.ts.map +1 -0
- package/dist/src/governor/human-touchpoints.js +251 -0
- package/dist/src/governor/human-touchpoints.test.d.ts +2 -0
- package/dist/src/governor/human-touchpoints.test.d.ts.map +1 -0
- package/dist/src/governor/human-touchpoints.test.js +366 -0
- package/dist/src/governor/in-memory-event-bus.d.ts +29 -0
- package/dist/src/governor/in-memory-event-bus.d.ts.map +1 -0
- package/dist/src/governor/in-memory-event-bus.js +79 -0
- package/dist/src/governor/index.d.ts +14 -0
- package/dist/src/governor/index.d.ts.map +1 -0
- package/dist/src/governor/index.js +13 -0
- package/dist/src/governor/override-parser.d.ts +60 -0
- package/dist/src/governor/override-parser.d.ts.map +1 -0
- package/dist/src/governor/override-parser.js +98 -0
- package/dist/src/governor/override-parser.test.d.ts +2 -0
- package/dist/src/governor/override-parser.test.d.ts.map +1 -0
- package/dist/src/governor/override-parser.test.js +312 -0
- package/dist/src/governor/platform-adapter.d.ts +69 -0
- package/dist/src/governor/platform-adapter.d.ts.map +1 -0
- package/dist/src/governor/platform-adapter.js +11 -0
- package/dist/src/governor/processing-state.d.ts +66 -0
- package/dist/src/governor/processing-state.d.ts.map +1 -0
- package/dist/src/governor/processing-state.js +43 -0
- package/dist/src/governor/processing-state.test.d.ts +2 -0
- package/dist/src/governor/processing-state.test.d.ts.map +1 -0
- package/dist/src/governor/processing-state.test.js +96 -0
- package/dist/src/governor/top-of-funnel.d.ts +118 -0
- package/dist/src/governor/top-of-funnel.d.ts.map +1 -0
- package/dist/src/governor/top-of-funnel.js +168 -0
- package/dist/src/governor/top-of-funnel.test.d.ts +2 -0
- package/dist/src/governor/top-of-funnel.test.d.ts.map +1 -0
- package/dist/src/governor/top-of-funnel.test.js +331 -0
- package/dist/src/index.d.ts +11 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +10 -0
- package/dist/src/linear-cli.d.ts +38 -0
- package/dist/src/linear-cli.d.ts.map +1 -0
- package/dist/src/linear-cli.js +674 -0
- package/dist/src/logger.d.ts +117 -0
- package/dist/src/logger.d.ts.map +1 -0
- package/dist/src/logger.js +430 -0
- package/dist/src/manifest/generate.d.ts +20 -0
- package/dist/src/manifest/generate.d.ts.map +1 -0
- package/dist/src/manifest/generate.js +65 -0
- package/dist/src/manifest/index.d.ts +4 -0
- package/dist/src/manifest/index.d.ts.map +1 -0
- package/dist/src/manifest/index.js +2 -0
- package/dist/src/manifest/route-manifest.d.ts +34 -0
- package/dist/src/manifest/route-manifest.d.ts.map +1 -0
- package/dist/src/manifest/route-manifest.js +148 -0
- package/dist/src/orchestrator/activity-emitter.d.ts +119 -0
- package/dist/src/orchestrator/activity-emitter.d.ts.map +1 -0
- package/dist/src/orchestrator/activity-emitter.js +306 -0
- package/dist/src/orchestrator/api-activity-emitter.d.ts +167 -0
- package/dist/src/orchestrator/api-activity-emitter.d.ts.map +1 -0
- package/dist/src/orchestrator/api-activity-emitter.js +417 -0
- package/dist/src/orchestrator/heartbeat-writer.d.ts +57 -0
- package/dist/src/orchestrator/heartbeat-writer.d.ts.map +1 -0
- package/dist/src/orchestrator/heartbeat-writer.js +137 -0
- package/dist/src/orchestrator/index.d.ts +20 -0
- package/dist/src/orchestrator/index.d.ts.map +1 -0
- package/dist/src/orchestrator/index.js +22 -0
- package/dist/src/orchestrator/log-analyzer.d.ts +160 -0
- package/dist/src/orchestrator/log-analyzer.d.ts.map +1 -0
- package/dist/src/orchestrator/log-analyzer.js +572 -0
- package/dist/src/orchestrator/log-config.d.ts +39 -0
- package/dist/src/orchestrator/log-config.d.ts.map +1 -0
- package/dist/src/orchestrator/log-config.js +45 -0
- package/dist/src/orchestrator/orchestrator.d.ts +316 -0
- package/dist/src/orchestrator/orchestrator.d.ts.map +1 -0
- package/dist/src/orchestrator/orchestrator.js +3290 -0
- package/dist/src/orchestrator/parse-work-result.d.ts +16 -0
- package/dist/src/orchestrator/parse-work-result.d.ts.map +1 -0
- package/dist/src/orchestrator/parse-work-result.js +135 -0
- package/dist/src/orchestrator/parse-work-result.test.d.ts +2 -0
- package/dist/src/orchestrator/parse-work-result.test.d.ts.map +1 -0
- package/dist/src/orchestrator/parse-work-result.test.js +234 -0
- package/dist/src/orchestrator/progress-logger.d.ts +72 -0
- package/dist/src/orchestrator/progress-logger.d.ts.map +1 -0
- package/dist/src/orchestrator/progress-logger.js +135 -0
- package/dist/src/orchestrator/session-logger.d.ts +159 -0
- package/dist/src/orchestrator/session-logger.d.ts.map +1 -0
- package/dist/src/orchestrator/session-logger.js +275 -0
- package/dist/src/orchestrator/state-recovery.d.ts +96 -0
- package/dist/src/orchestrator/state-recovery.d.ts.map +1 -0
- package/dist/src/orchestrator/state-recovery.js +302 -0
- package/dist/src/orchestrator/state-types.d.ts +165 -0
- package/dist/src/orchestrator/state-types.d.ts.map +1 -0
- package/dist/src/orchestrator/state-types.js +7 -0
- package/dist/src/orchestrator/stream-parser.d.ts +151 -0
- package/dist/src/orchestrator/stream-parser.d.ts.map +1 -0
- package/dist/src/orchestrator/stream-parser.js +137 -0
- package/dist/src/orchestrator/types.d.ts +232 -0
- package/dist/src/orchestrator/types.d.ts.map +1 -0
- package/dist/src/orchestrator/types.js +4 -0
- package/dist/src/orchestrator/validate-git-remote.test.d.ts +2 -0
- package/dist/src/orchestrator/validate-git-remote.test.d.ts.map +1 -0
- package/dist/src/orchestrator/validate-git-remote.test.js +61 -0
- package/dist/src/providers/a2a-auth.d.ts +81 -0
- package/dist/src/providers/a2a-auth.d.ts.map +1 -0
- package/dist/src/providers/a2a-auth.js +188 -0
- package/dist/src/providers/a2a-auth.test.d.ts +2 -0
- package/dist/src/providers/a2a-auth.test.d.ts.map +1 -0
- package/dist/src/providers/a2a-auth.test.js +232 -0
- package/dist/src/providers/a2a-provider.d.ts +254 -0
- package/dist/src/providers/a2a-provider.d.ts.map +1 -0
- package/dist/src/providers/a2a-provider.integration.test.d.ts +9 -0
- package/dist/src/providers/a2a-provider.integration.test.d.ts.map +1 -0
- package/dist/src/providers/a2a-provider.integration.test.js +665 -0
- package/dist/src/providers/a2a-provider.js +811 -0
- package/dist/src/providers/a2a-provider.test.d.ts +2 -0
- package/dist/src/providers/a2a-provider.test.d.ts.map +1 -0
- package/dist/src/providers/a2a-provider.test.js +681 -0
- package/dist/src/providers/amp-provider.d.ts +20 -0
- package/dist/src/providers/amp-provider.d.ts.map +1 -0
- package/dist/src/providers/amp-provider.js +24 -0
- package/dist/src/providers/claude-provider.d.ts +18 -0
- package/dist/src/providers/claude-provider.d.ts.map +1 -0
- package/dist/src/providers/claude-provider.js +437 -0
- package/dist/src/providers/codex-provider.d.ts +133 -0
- package/dist/src/providers/codex-provider.d.ts.map +1 -0
- package/dist/src/providers/codex-provider.js +381 -0
- package/dist/src/providers/codex-provider.test.d.ts +2 -0
- package/dist/src/providers/codex-provider.test.d.ts.map +1 -0
- package/dist/src/providers/codex-provider.test.js +387 -0
- package/dist/src/providers/index.d.ts +44 -0
- package/dist/src/providers/index.d.ts.map +1 -0
- package/dist/src/providers/index.js +85 -0
- package/dist/src/providers/spring-ai-provider.d.ts +90 -0
- package/dist/src/providers/spring-ai-provider.d.ts.map +1 -0
- package/dist/src/providers/spring-ai-provider.integration.test.d.ts +13 -0
- package/dist/src/providers/spring-ai-provider.integration.test.d.ts.map +1 -0
- package/dist/src/providers/spring-ai-provider.integration.test.js +351 -0
- package/dist/src/providers/spring-ai-provider.js +317 -0
- package/dist/src/providers/spring-ai-provider.test.d.ts +2 -0
- package/dist/src/providers/spring-ai-provider.test.d.ts.map +1 -0
- package/dist/src/providers/spring-ai-provider.test.js +200 -0
- package/dist/src/providers/types.d.ts +165 -0
- package/dist/src/providers/types.d.ts.map +1 -0
- package/dist/src/providers/types.js +13 -0
- package/dist/src/templates/adapters.d.ts +51 -0
- package/dist/src/templates/adapters.d.ts.map +1 -0
- package/dist/src/templates/adapters.js +104 -0
- package/dist/src/templates/adapters.test.d.ts +2 -0
- package/dist/src/templates/adapters.test.d.ts.map +1 -0
- package/dist/src/templates/adapters.test.js +165 -0
- package/dist/src/templates/agent-definition.d.ts +85 -0
- package/dist/src/templates/agent-definition.d.ts.map +1 -0
- package/dist/src/templates/agent-definition.js +97 -0
- package/dist/src/templates/agent-definition.test.d.ts +2 -0
- package/dist/src/templates/agent-definition.test.d.ts.map +1 -0
- package/dist/src/templates/agent-definition.test.js +209 -0
- package/dist/src/templates/index.d.ts +14 -0
- package/dist/src/templates/index.d.ts.map +1 -0
- package/dist/src/templates/index.js +11 -0
- package/dist/src/templates/loader.d.ts +41 -0
- package/dist/src/templates/loader.d.ts.map +1 -0
- package/dist/src/templates/loader.js +114 -0
- package/dist/src/templates/registry.d.ts +80 -0
- package/dist/src/templates/registry.d.ts.map +1 -0
- package/dist/src/templates/registry.js +177 -0
- package/dist/src/templates/registry.test.d.ts +2 -0
- package/dist/src/templates/registry.test.d.ts.map +1 -0
- package/dist/src/templates/registry.test.js +198 -0
- package/dist/src/templates/renderer.d.ts +29 -0
- package/dist/src/templates/renderer.d.ts.map +1 -0
- package/dist/src/templates/renderer.js +35 -0
- package/dist/src/templates/strategy-templates.test.d.ts +2 -0
- package/dist/src/templates/strategy-templates.test.d.ts.map +1 -0
- package/dist/src/templates/strategy-templates.test.js +619 -0
- package/dist/src/templates/types.d.ts +233 -0
- package/dist/src/templates/types.d.ts.map +1 -0
- package/dist/src/templates/types.js +127 -0
- package/dist/src/templates/types.test.d.ts +2 -0
- package/dist/src/templates/types.test.d.ts.map +1 -0
- package/dist/src/templates/types.test.js +232 -0
- package/dist/src/tools/index.d.ts +6 -0
- package/dist/src/tools/index.d.ts.map +1 -0
- package/dist/src/tools/index.js +3 -0
- package/dist/src/tools/linear-runner.d.ts +34 -0
- package/dist/src/tools/linear-runner.d.ts.map +1 -0
- package/dist/src/tools/linear-runner.js +700 -0
- package/dist/src/tools/plugins/linear.d.ts +9 -0
- package/dist/src/tools/plugins/linear.d.ts.map +1 -0
- package/dist/src/tools/plugins/linear.js +138 -0
- package/dist/src/tools/registry.d.ts +9 -0
- package/dist/src/tools/registry.d.ts.map +1 -0
- package/dist/src/tools/registry.js +18 -0
- package/dist/src/tools/types.d.ts +18 -0
- package/dist/src/tools/types.d.ts.map +1 -0
- package/dist/src/tools/types.js +1 -0
- package/package.json +78 -0
|
@@ -0,0 +1,700 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linear CLI Runner — process-agnostic Linear operations.
|
|
3
|
+
*
|
|
4
|
+
* All 16 command implementations. This module does NOT call process.exit,
|
|
5
|
+
* read process.argv, or load dotenv. Shared by both the CLI entry point
|
|
6
|
+
* and the in-process tool plugin.
|
|
7
|
+
*/
|
|
8
|
+
import { readFileSync } from 'node:fs';
|
|
9
|
+
import { createLinearAgentClient, getDefaultTeamName } from '@renseiai/agentfactory-linear';
|
|
10
|
+
import { checkPRDeploymentStatus, formatDeploymentStatus, } from '../deployment/index.js';
|
|
11
|
+
// ── Arg parsing ────────────────────────────────────────────────────
|
|
12
|
+
/** Fields that should be split on commas to create arrays */
|
|
13
|
+
const ARRAY_FIELDS = new Set(['labels']);
|
|
14
|
+
/**
|
|
15
|
+
* Parse CLI arguments into a structured object.
|
|
16
|
+
*
|
|
17
|
+
* Supports:
|
|
18
|
+
* - `--key value` pairs
|
|
19
|
+
* - JSON array values: `--labels '["Bug", "Feature"]'`
|
|
20
|
+
* - Comma-separated values for array fields: `--labels "Bug,Feature"`
|
|
21
|
+
* - Boolean flags: `--dry-run` (value = "true")
|
|
22
|
+
*
|
|
23
|
+
* Returns the command (first non-flag arg), named args, and positional args.
|
|
24
|
+
*/
|
|
25
|
+
export function parseLinearArgs(argv) {
|
|
26
|
+
const command = argv[0] && !argv[0].startsWith('--') ? argv[0] : undefined;
|
|
27
|
+
const rest = command ? argv.slice(1) : argv;
|
|
28
|
+
const args = {};
|
|
29
|
+
const positionalArgs = [];
|
|
30
|
+
for (let i = 0; i < rest.length; i++) {
|
|
31
|
+
const arg = rest[i];
|
|
32
|
+
if (arg.startsWith('--')) {
|
|
33
|
+
const key = arg.slice(2);
|
|
34
|
+
const value = rest[i + 1];
|
|
35
|
+
if (value && !value.startsWith('--')) {
|
|
36
|
+
// Support JSON array format: --labels '["Bug", "Feature"]'
|
|
37
|
+
if (value.startsWith('[') && value.endsWith(']')) {
|
|
38
|
+
try {
|
|
39
|
+
const parsed = JSON.parse(value);
|
|
40
|
+
if (Array.isArray(parsed)) {
|
|
41
|
+
args[key] = parsed;
|
|
42
|
+
i++;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// Not valid JSON, fall through to normal handling
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// Only split on comma for known array fields
|
|
51
|
+
if (ARRAY_FIELDS.has(key) && value.includes(',')) {
|
|
52
|
+
args[key] = value.split(',').map((v) => v.trim());
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
args[key] = value;
|
|
56
|
+
}
|
|
57
|
+
i++;
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
args[key] = true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
positionalArgs.push(arg);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { command, args, positionalArgs };
|
|
68
|
+
}
|
|
69
|
+
// ── Command implementations ────────────────────────────────────────
|
|
70
|
+
async function getIssue(client, issueId) {
|
|
71
|
+
const issue = await client.getIssue(issueId);
|
|
72
|
+
const state = await issue.state;
|
|
73
|
+
const team = await issue.team;
|
|
74
|
+
const project = await issue.project;
|
|
75
|
+
const labels = await issue.labels();
|
|
76
|
+
return {
|
|
77
|
+
id: issue.id,
|
|
78
|
+
identifier: issue.identifier,
|
|
79
|
+
title: issue.title,
|
|
80
|
+
description: issue.description,
|
|
81
|
+
url: issue.url,
|
|
82
|
+
status: state?.name,
|
|
83
|
+
team: team?.name,
|
|
84
|
+
project: project?.name,
|
|
85
|
+
labels: labels.nodes.map((l) => l.name),
|
|
86
|
+
createdAt: issue.createdAt,
|
|
87
|
+
updatedAt: issue.updatedAt,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
async function createIssue(client, options) {
|
|
91
|
+
const team = await client.getTeam(options.team);
|
|
92
|
+
const createPayload = {
|
|
93
|
+
teamId: team.id,
|
|
94
|
+
title: options.title,
|
|
95
|
+
};
|
|
96
|
+
if (options.description) {
|
|
97
|
+
createPayload.description = options.description;
|
|
98
|
+
}
|
|
99
|
+
if (options.parentId) {
|
|
100
|
+
createPayload.parentId = options.parentId;
|
|
101
|
+
}
|
|
102
|
+
if (options.project) {
|
|
103
|
+
const projects = await client.linearClient.projects({
|
|
104
|
+
filter: { name: { eq: options.project } },
|
|
105
|
+
});
|
|
106
|
+
if (projects.nodes.length > 0) {
|
|
107
|
+
createPayload.projectId = projects.nodes[0].id;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (options.state) {
|
|
111
|
+
const statuses = await client.getTeamStatuses(team.id);
|
|
112
|
+
const stateId = statuses[options.state];
|
|
113
|
+
if (stateId) {
|
|
114
|
+
createPayload.stateId = stateId;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (options.labels && options.labels.length > 0) {
|
|
118
|
+
const allLabels = await client.linearClient.issueLabels();
|
|
119
|
+
const labelIds = [];
|
|
120
|
+
for (const labelName of options.labels) {
|
|
121
|
+
const label = allLabels.nodes.find((l) => l.name.toLowerCase() === labelName.toLowerCase());
|
|
122
|
+
if (label) {
|
|
123
|
+
labelIds.push(label.id);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (labelIds.length > 0) {
|
|
127
|
+
createPayload.labelIds = labelIds;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const payload = await client.linearClient.createIssue(createPayload);
|
|
131
|
+
if (!payload.success) {
|
|
132
|
+
throw new Error('Failed to create issue');
|
|
133
|
+
}
|
|
134
|
+
const issue = await payload.issue;
|
|
135
|
+
if (!issue) {
|
|
136
|
+
throw new Error('Issue created but not returned');
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
id: issue.id,
|
|
140
|
+
identifier: issue.identifier,
|
|
141
|
+
title: issue.title,
|
|
142
|
+
url: issue.url,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
async function updateIssue(client, issueId, options) {
|
|
146
|
+
const issue = await client.getIssue(issueId);
|
|
147
|
+
const team = await issue.team;
|
|
148
|
+
const updateData = {};
|
|
149
|
+
if (options.title) {
|
|
150
|
+
updateData.title = options.title;
|
|
151
|
+
}
|
|
152
|
+
if (options.description) {
|
|
153
|
+
updateData.description = options.description;
|
|
154
|
+
}
|
|
155
|
+
if (options.state && team) {
|
|
156
|
+
const statuses = await client.getTeamStatuses(team.id);
|
|
157
|
+
const stateId = statuses[options.state];
|
|
158
|
+
if (stateId) {
|
|
159
|
+
updateData.stateId = stateId;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (options.labels && options.labels.length > 0) {
|
|
163
|
+
const allLabels = await client.linearClient.issueLabels();
|
|
164
|
+
const labelIds = [];
|
|
165
|
+
for (const labelName of options.labels) {
|
|
166
|
+
const label = allLabels.nodes.find((l) => l.name.toLowerCase() === labelName.toLowerCase());
|
|
167
|
+
if (label) {
|
|
168
|
+
labelIds.push(label.id);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
updateData.labelIds = labelIds;
|
|
172
|
+
}
|
|
173
|
+
const updatedIssue = await client.updateIssue(issue.id, updateData);
|
|
174
|
+
const state = await updatedIssue.state;
|
|
175
|
+
return {
|
|
176
|
+
id: updatedIssue.id,
|
|
177
|
+
identifier: updatedIssue.identifier,
|
|
178
|
+
title: updatedIssue.title,
|
|
179
|
+
status: state?.name,
|
|
180
|
+
url: updatedIssue.url,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
async function listComments(client, issueId) {
|
|
184
|
+
const comments = await client.getIssueComments(issueId);
|
|
185
|
+
return comments.map((c) => ({
|
|
186
|
+
id: c.id,
|
|
187
|
+
body: c.body,
|
|
188
|
+
createdAt: c.createdAt,
|
|
189
|
+
}));
|
|
190
|
+
}
|
|
191
|
+
async function createComment(client, issueId, body) {
|
|
192
|
+
const comment = await client.createComment(issueId, body);
|
|
193
|
+
return {
|
|
194
|
+
id: comment.id,
|
|
195
|
+
body: comment.body,
|
|
196
|
+
createdAt: comment.createdAt,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
async function addRelation(client, issueId, relatedIssueId, relationType) {
|
|
200
|
+
const result = await client.createIssueRelation({
|
|
201
|
+
issueId,
|
|
202
|
+
relatedIssueId,
|
|
203
|
+
type: relationType,
|
|
204
|
+
});
|
|
205
|
+
return {
|
|
206
|
+
success: result.success,
|
|
207
|
+
relationId: result.relationId,
|
|
208
|
+
issueId,
|
|
209
|
+
relatedIssueId,
|
|
210
|
+
type: relationType,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
async function listRelations(client, issueId) {
|
|
214
|
+
const result = await client.getIssueRelations(issueId);
|
|
215
|
+
return {
|
|
216
|
+
issueId,
|
|
217
|
+
relations: result.relations.map((r) => ({
|
|
218
|
+
id: r.id,
|
|
219
|
+
type: r.type,
|
|
220
|
+
relatedIssue: r.relatedIssueIdentifier ?? r.relatedIssueId,
|
|
221
|
+
createdAt: r.createdAt,
|
|
222
|
+
})),
|
|
223
|
+
inverseRelations: result.inverseRelations.map((r) => ({
|
|
224
|
+
id: r.id,
|
|
225
|
+
type: r.type,
|
|
226
|
+
sourceIssue: r.issueIdentifier ?? r.issueId,
|
|
227
|
+
createdAt: r.createdAt,
|
|
228
|
+
})),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
async function removeRelation(client, relationId) {
|
|
232
|
+
const result = await client.deleteIssueRelation(relationId);
|
|
233
|
+
return {
|
|
234
|
+
success: result.success,
|
|
235
|
+
relationId,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
async function listBacklogIssues(client, projectName) {
|
|
239
|
+
const projects = await client.linearClient.projects({
|
|
240
|
+
filter: { name: { eqIgnoreCase: projectName } },
|
|
241
|
+
});
|
|
242
|
+
if (projects.nodes.length === 0) {
|
|
243
|
+
throw new Error(`Project not found: ${projectName}`);
|
|
244
|
+
}
|
|
245
|
+
const project = projects.nodes[0];
|
|
246
|
+
const issues = await client.linearClient.issues({
|
|
247
|
+
filter: {
|
|
248
|
+
project: { id: { eq: project.id } },
|
|
249
|
+
state: { name: { eqIgnoreCase: 'Backlog' } },
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
const results = [];
|
|
253
|
+
for (const issue of issues.nodes) {
|
|
254
|
+
const state = await issue.state;
|
|
255
|
+
const labels = await issue.labels();
|
|
256
|
+
results.push({
|
|
257
|
+
id: issue.id,
|
|
258
|
+
identifier: issue.identifier,
|
|
259
|
+
title: issue.title,
|
|
260
|
+
description: issue.description,
|
|
261
|
+
url: issue.url,
|
|
262
|
+
priority: issue.priority,
|
|
263
|
+
status: state?.name,
|
|
264
|
+
labels: labels.nodes.map((l) => l.name),
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
results.sort((a, b) => {
|
|
268
|
+
const aPriority = a.priority || 5;
|
|
269
|
+
const bPriority = b.priority || 5;
|
|
270
|
+
return aPriority - bPriority;
|
|
271
|
+
});
|
|
272
|
+
return results;
|
|
273
|
+
}
|
|
274
|
+
async function getBlockingIssues(client, issueId) {
|
|
275
|
+
const relations = await client.getIssueRelations(issueId);
|
|
276
|
+
const blockingIssues = [];
|
|
277
|
+
for (const relation of relations.inverseRelations) {
|
|
278
|
+
if (relation.type === 'blocks') {
|
|
279
|
+
const blockingIssue = await client.getIssue(relation.issueId);
|
|
280
|
+
const state = await blockingIssue.state;
|
|
281
|
+
const statusName = state?.name ?? 'Unknown';
|
|
282
|
+
if (statusName !== 'Accepted') {
|
|
283
|
+
blockingIssues.push({
|
|
284
|
+
identifier: blockingIssue.identifier,
|
|
285
|
+
title: blockingIssue.title,
|
|
286
|
+
status: statusName,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return blockingIssues;
|
|
292
|
+
}
|
|
293
|
+
async function listUnblockedBacklogIssues(client, projectName) {
|
|
294
|
+
const projects = await client.linearClient.projects({
|
|
295
|
+
filter: { name: { eqIgnoreCase: projectName } },
|
|
296
|
+
});
|
|
297
|
+
if (projects.nodes.length === 0) {
|
|
298
|
+
throw new Error(`Project not found: ${projectName}`);
|
|
299
|
+
}
|
|
300
|
+
const project = projects.nodes[0];
|
|
301
|
+
const issues = await client.linearClient.issues({
|
|
302
|
+
filter: {
|
|
303
|
+
project: { id: { eq: project.id } },
|
|
304
|
+
state: { name: { eqIgnoreCase: 'Backlog' } },
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
const results = [];
|
|
308
|
+
for (const issue of issues.nodes) {
|
|
309
|
+
const blockingIssues = await getBlockingIssues(client, issue.id);
|
|
310
|
+
const state = await issue.state;
|
|
311
|
+
const labels = await issue.labels();
|
|
312
|
+
results.push({
|
|
313
|
+
id: issue.id,
|
|
314
|
+
identifier: issue.identifier,
|
|
315
|
+
title: issue.title,
|
|
316
|
+
description: issue.description,
|
|
317
|
+
url: issue.url,
|
|
318
|
+
priority: issue.priority,
|
|
319
|
+
status: state?.name,
|
|
320
|
+
labels: labels.nodes.map((l) => l.name),
|
|
321
|
+
blocked: blockingIssues.length > 0,
|
|
322
|
+
blockedBy: blockingIssues,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
const unblockedResults = results.filter((r) => !r.blocked);
|
|
326
|
+
unblockedResults.sort((a, b) => {
|
|
327
|
+
const aPriority = a.priority || 5;
|
|
328
|
+
const bPriority = b.priority || 5;
|
|
329
|
+
return aPriority - bPriority;
|
|
330
|
+
});
|
|
331
|
+
return unblockedResults;
|
|
332
|
+
}
|
|
333
|
+
async function checkBlocked(client, issueId) {
|
|
334
|
+
const blockingIssues = await getBlockingIssues(client, issueId);
|
|
335
|
+
return {
|
|
336
|
+
issueId,
|
|
337
|
+
blocked: blockingIssues.length > 0,
|
|
338
|
+
blockedBy: blockingIssues,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
async function listSubIssues(client, issueId) {
|
|
342
|
+
const graph = await client.getSubIssueGraph(issueId);
|
|
343
|
+
return {
|
|
344
|
+
parentId: graph.parentId,
|
|
345
|
+
parentIdentifier: graph.parentIdentifier,
|
|
346
|
+
subIssueCount: graph.subIssues.length,
|
|
347
|
+
subIssues: graph.subIssues.map((node) => ({
|
|
348
|
+
id: node.issue.id,
|
|
349
|
+
identifier: node.issue.identifier,
|
|
350
|
+
title: node.issue.title,
|
|
351
|
+
status: node.issue.status,
|
|
352
|
+
priority: node.issue.priority,
|
|
353
|
+
labels: node.issue.labels,
|
|
354
|
+
url: node.issue.url,
|
|
355
|
+
blockedBy: node.blockedBy,
|
|
356
|
+
blocks: node.blocks,
|
|
357
|
+
})),
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
async function listSubIssueStatuses(client, issueId) {
|
|
361
|
+
const statuses = await client.getSubIssueStatuses(issueId);
|
|
362
|
+
return {
|
|
363
|
+
parentIssue: issueId,
|
|
364
|
+
subIssueCount: statuses.length,
|
|
365
|
+
subIssues: statuses,
|
|
366
|
+
allFinishedOrLater: statuses.every((s) => ['Finished', 'Delivered', 'Accepted', 'Canceled'].includes(s.status)),
|
|
367
|
+
incomplete: statuses.filter((s) => !['Finished', 'Delivered', 'Accepted', 'Canceled'].includes(s.status)),
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
async function updateSubIssue(client, issueId, options) {
|
|
371
|
+
const issue = await client.getIssue(issueId);
|
|
372
|
+
if (options.state) {
|
|
373
|
+
await client.updateIssueStatus(issue.id, options.state);
|
|
374
|
+
}
|
|
375
|
+
if (options.comment) {
|
|
376
|
+
await client.createComment(issue.id, options.comment);
|
|
377
|
+
}
|
|
378
|
+
const updatedIssue = await client.getIssue(issueId);
|
|
379
|
+
const state = await updatedIssue.state;
|
|
380
|
+
return {
|
|
381
|
+
id: updatedIssue.id,
|
|
382
|
+
identifier: updatedIssue.identifier,
|
|
383
|
+
title: updatedIssue.title,
|
|
384
|
+
status: state?.name,
|
|
385
|
+
url: updatedIssue.url,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
async function checkDeployment(prNumber, format = 'json') {
|
|
389
|
+
const result = await checkPRDeploymentStatus(prNumber);
|
|
390
|
+
if (!result) {
|
|
391
|
+
throw new Error(`Could not get deployment status for PR #${prNumber}. Make sure the PR exists and you have access to it.`);
|
|
392
|
+
}
|
|
393
|
+
if (format === 'markdown') {
|
|
394
|
+
return formatDeploymentStatus(result);
|
|
395
|
+
}
|
|
396
|
+
return result;
|
|
397
|
+
}
|
|
398
|
+
async function createBlocker(client, options) {
|
|
399
|
+
// 1. Fetch source issue to resolve team/project
|
|
400
|
+
const sourceIssue = await client.getIssue(options.sourceIssueId);
|
|
401
|
+
const sourceTeam = await sourceIssue.team;
|
|
402
|
+
const sourceProject = await sourceIssue.project;
|
|
403
|
+
const teamName = options.team ?? sourceTeam?.key;
|
|
404
|
+
if (!teamName) {
|
|
405
|
+
throw new Error('Could not resolve team from source issue. Provide --team explicitly.');
|
|
406
|
+
}
|
|
407
|
+
const team = await client.getTeam(teamName);
|
|
408
|
+
const projectName = options.project ?? sourceProject?.name;
|
|
409
|
+
// 2. Deduplicate: check for existing Icebox issues with same title + "Needs Human" label
|
|
410
|
+
if (projectName) {
|
|
411
|
+
const projects = await client.linearClient.projects({
|
|
412
|
+
filter: { name: { eqIgnoreCase: projectName } },
|
|
413
|
+
});
|
|
414
|
+
if (projects.nodes.length > 0) {
|
|
415
|
+
const existingIssues = await client.linearClient.issues({
|
|
416
|
+
filter: {
|
|
417
|
+
project: { id: { eq: projects.nodes[0].id } },
|
|
418
|
+
state: { name: { eqIgnoreCase: 'Icebox' } },
|
|
419
|
+
labels: { name: { eqIgnoreCase: 'Needs Human' } },
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
const duplicate = existingIssues.nodes.find((i) => i.title.toLowerCase() === options.title.toLowerCase());
|
|
423
|
+
if (duplicate) {
|
|
424
|
+
// Add a +1 comment to the existing issue
|
|
425
|
+
await client.createComment(duplicate.id, `+1 — Also needed by ${sourceIssue.identifier}`);
|
|
426
|
+
return {
|
|
427
|
+
id: duplicate.id,
|
|
428
|
+
identifier: duplicate.identifier,
|
|
429
|
+
title: duplicate.title,
|
|
430
|
+
url: duplicate.url,
|
|
431
|
+
sourceIssue: sourceIssue.identifier,
|
|
432
|
+
relation: 'blocks',
|
|
433
|
+
deduplicated: true,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
// 3. Create the blocker issue
|
|
439
|
+
const createPayload = {
|
|
440
|
+
teamId: team.id,
|
|
441
|
+
title: options.title,
|
|
442
|
+
};
|
|
443
|
+
// Description with source reference
|
|
444
|
+
const descParts = [];
|
|
445
|
+
if (options.description) {
|
|
446
|
+
descParts.push(options.description);
|
|
447
|
+
}
|
|
448
|
+
descParts.push(`\n---\n*Source issue: ${sourceIssue.identifier}*`);
|
|
449
|
+
createPayload.description = descParts.join('\n\n');
|
|
450
|
+
// Set state to Icebox
|
|
451
|
+
const statuses = await client.getTeamStatuses(team.id);
|
|
452
|
+
const iceboxStateId = statuses['Icebox'];
|
|
453
|
+
if (iceboxStateId) {
|
|
454
|
+
createPayload.stateId = iceboxStateId;
|
|
455
|
+
}
|
|
456
|
+
// Set "Needs Human" label
|
|
457
|
+
const allLabels = await client.linearClient.issueLabels();
|
|
458
|
+
const needsHumanLabel = allLabels.nodes.find((l) => l.name.toLowerCase() === 'needs human');
|
|
459
|
+
if (needsHumanLabel) {
|
|
460
|
+
createPayload.labelIds = [needsHumanLabel.id];
|
|
461
|
+
}
|
|
462
|
+
// Set project
|
|
463
|
+
if (projectName) {
|
|
464
|
+
const projects = await client.linearClient.projects({
|
|
465
|
+
filter: { name: { eqIgnoreCase: projectName } },
|
|
466
|
+
});
|
|
467
|
+
if (projects.nodes.length > 0) {
|
|
468
|
+
createPayload.projectId = projects.nodes[0].id;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
const payload = await client.linearClient.createIssue(createPayload);
|
|
472
|
+
if (!payload.success) {
|
|
473
|
+
throw new Error('Failed to create blocker issue');
|
|
474
|
+
}
|
|
475
|
+
const blockerIssue = await payload.issue;
|
|
476
|
+
if (!blockerIssue) {
|
|
477
|
+
throw new Error('Blocker issue created but not returned');
|
|
478
|
+
}
|
|
479
|
+
// 4. Create blocking relation: blocker blocks source
|
|
480
|
+
await client.createIssueRelation({
|
|
481
|
+
issueId: blockerIssue.id,
|
|
482
|
+
relatedIssueId: sourceIssue.id,
|
|
483
|
+
type: 'blocks',
|
|
484
|
+
});
|
|
485
|
+
// 5. Post comment on source issue
|
|
486
|
+
await client.createComment(sourceIssue.id, `\u{1F6A7} Human blocker created: [${blockerIssue.identifier}](${blockerIssue.url}) — ${options.title}`);
|
|
487
|
+
// 6. Optionally assign
|
|
488
|
+
if (options.assignee) {
|
|
489
|
+
const users = await client.linearClient.users({
|
|
490
|
+
filter: {
|
|
491
|
+
or: [
|
|
492
|
+
{ name: { eqIgnoreCase: options.assignee } },
|
|
493
|
+
{ email: { eq: options.assignee } },
|
|
494
|
+
],
|
|
495
|
+
},
|
|
496
|
+
});
|
|
497
|
+
if (users.nodes.length > 0) {
|
|
498
|
+
await client.linearClient.updateIssue(blockerIssue.id, {
|
|
499
|
+
assigneeId: users.nodes[0].id,
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return {
|
|
504
|
+
id: blockerIssue.id,
|
|
505
|
+
identifier: blockerIssue.identifier,
|
|
506
|
+
title: blockerIssue.title,
|
|
507
|
+
url: blockerIssue.url,
|
|
508
|
+
sourceIssue: sourceIssue.identifier,
|
|
509
|
+
relation: 'blocks',
|
|
510
|
+
deduplicated: false,
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
// ── File-based arg helpers ──────────────────────────────────────────
|
|
514
|
+
/**
|
|
515
|
+
* Resolve a text value that may come from a `--foo-file` flag.
|
|
516
|
+
* If `fooFile` is provided, reads the file content and returns it.
|
|
517
|
+
* Otherwise returns `foo` as-is.
|
|
518
|
+
*/
|
|
519
|
+
function resolveFileArg(value, filePath) {
|
|
520
|
+
if (filePath && typeof filePath === 'string') {
|
|
521
|
+
return readFileSync(filePath, 'utf-8');
|
|
522
|
+
}
|
|
523
|
+
return value;
|
|
524
|
+
}
|
|
525
|
+
// ── Commands that don't require LINEAR_API_KEY ─────────────────────
|
|
526
|
+
const NO_API_KEY_COMMANDS = new Set(['check-deployment']);
|
|
527
|
+
// ── Main runner ────────────────────────────────────────────────────
|
|
528
|
+
export async function runLinear(config) {
|
|
529
|
+
const { command, args, positionalArgs, apiKey } = config;
|
|
530
|
+
// Lazy client — only created for commands that need it
|
|
531
|
+
let _client = null;
|
|
532
|
+
function client() {
|
|
533
|
+
if (!_client) {
|
|
534
|
+
if (!apiKey) {
|
|
535
|
+
throw new Error('LINEAR_API_KEY environment variable is required');
|
|
536
|
+
}
|
|
537
|
+
_client = createLinearAgentClient({ apiKey });
|
|
538
|
+
}
|
|
539
|
+
return _client;
|
|
540
|
+
}
|
|
541
|
+
// Validate API key for commands that need it
|
|
542
|
+
if (!NO_API_KEY_COMMANDS.has(command) && !apiKey) {
|
|
543
|
+
throw new Error('LINEAR_API_KEY environment variable is required');
|
|
544
|
+
}
|
|
545
|
+
// Helper: get first positional or error
|
|
546
|
+
function requirePositional(name) {
|
|
547
|
+
const val = positionalArgs[0];
|
|
548
|
+
if (!val || val.startsWith('--')) {
|
|
549
|
+
throw new Error(`Missing required argument: <${name}>`);
|
|
550
|
+
}
|
|
551
|
+
return val;
|
|
552
|
+
}
|
|
553
|
+
// Parse sub-options from the original args (for commands that re-parse after positional)
|
|
554
|
+
function subArgs() {
|
|
555
|
+
return args;
|
|
556
|
+
}
|
|
557
|
+
let output;
|
|
558
|
+
switch (command) {
|
|
559
|
+
case 'get-issue': {
|
|
560
|
+
const issueId = requirePositional('issue-id');
|
|
561
|
+
output = await getIssue(client(), issueId);
|
|
562
|
+
break;
|
|
563
|
+
}
|
|
564
|
+
case 'create-issue': {
|
|
565
|
+
const teamArg = args.team ?? (getDefaultTeamName() || undefined);
|
|
566
|
+
if (!args.title || !teamArg) {
|
|
567
|
+
throw new Error('Usage: af-linear create-issue --title "Title" --team "Team" [--description "..."] [--project "..."] [--labels "Label1,Label2"] [--state "Backlog"] [--parentId "..."]\n' +
|
|
568
|
+
'Tip: Set LINEAR_TEAM_NAME env var to provide a default team.');
|
|
569
|
+
}
|
|
570
|
+
const createDescription = resolveFileArg(args.description, args['description-file']);
|
|
571
|
+
output = await createIssue(client(), {
|
|
572
|
+
title: args.title,
|
|
573
|
+
team: teamArg,
|
|
574
|
+
description: createDescription,
|
|
575
|
+
project: args.project,
|
|
576
|
+
labels: args.labels,
|
|
577
|
+
state: args.state,
|
|
578
|
+
parentId: args.parentId,
|
|
579
|
+
});
|
|
580
|
+
break;
|
|
581
|
+
}
|
|
582
|
+
case 'update-issue': {
|
|
583
|
+
const issueId = requirePositional('issue-id');
|
|
584
|
+
const opts = subArgs();
|
|
585
|
+
const updateDescription = resolveFileArg(opts.description, opts['description-file']);
|
|
586
|
+
output = await updateIssue(client(), issueId, {
|
|
587
|
+
title: opts.title,
|
|
588
|
+
description: updateDescription,
|
|
589
|
+
state: opts.state,
|
|
590
|
+
labels: opts.labels,
|
|
591
|
+
});
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
594
|
+
case 'list-comments': {
|
|
595
|
+
const issueId = requirePositional('issue-id');
|
|
596
|
+
output = await listComments(client(), issueId);
|
|
597
|
+
break;
|
|
598
|
+
}
|
|
599
|
+
case 'create-comment': {
|
|
600
|
+
const issueId = requirePositional('issue-id');
|
|
601
|
+
const commentBody = resolveFileArg(args.body, args['body-file']);
|
|
602
|
+
if (!commentBody) {
|
|
603
|
+
throw new Error('Usage: af-linear create-comment <issue-id> --body "Comment text" or --body-file /path/to/file');
|
|
604
|
+
}
|
|
605
|
+
output = await createComment(client(), issueId, commentBody);
|
|
606
|
+
break;
|
|
607
|
+
}
|
|
608
|
+
case 'list-backlog-issues': {
|
|
609
|
+
if (!args.project) {
|
|
610
|
+
throw new Error('Usage: af-linear list-backlog-issues --project "ProjectName"');
|
|
611
|
+
}
|
|
612
|
+
output = await listBacklogIssues(client(), args.project);
|
|
613
|
+
break;
|
|
614
|
+
}
|
|
615
|
+
case 'list-unblocked-backlog': {
|
|
616
|
+
if (!args.project) {
|
|
617
|
+
throw new Error('Usage: af-linear list-unblocked-backlog --project "ProjectName"');
|
|
618
|
+
}
|
|
619
|
+
output = await listUnblockedBacklogIssues(client(), args.project);
|
|
620
|
+
break;
|
|
621
|
+
}
|
|
622
|
+
case 'check-blocked': {
|
|
623
|
+
const issueId = requirePositional('issue-id');
|
|
624
|
+
output = await checkBlocked(client(), issueId);
|
|
625
|
+
break;
|
|
626
|
+
}
|
|
627
|
+
case 'add-relation': {
|
|
628
|
+
const issueId = positionalArgs[0];
|
|
629
|
+
const relatedIssueId = positionalArgs[1];
|
|
630
|
+
const relationType = args.type;
|
|
631
|
+
if (!issueId ||
|
|
632
|
+
issueId.startsWith('--') ||
|
|
633
|
+
!relatedIssueId ||
|
|
634
|
+
relatedIssueId.startsWith('--') ||
|
|
635
|
+
!relationType ||
|
|
636
|
+
!['related', 'blocks', 'duplicate'].includes(relationType)) {
|
|
637
|
+
throw new Error('Usage: af-linear add-relation <issue-id> <related-issue-id> --type <related|blocks|duplicate>');
|
|
638
|
+
}
|
|
639
|
+
output = await addRelation(client(), issueId, relatedIssueId, relationType);
|
|
640
|
+
break;
|
|
641
|
+
}
|
|
642
|
+
case 'list-relations': {
|
|
643
|
+
const issueId = requirePositional('issue-id');
|
|
644
|
+
output = await listRelations(client(), issueId);
|
|
645
|
+
break;
|
|
646
|
+
}
|
|
647
|
+
case 'remove-relation': {
|
|
648
|
+
const relationId = requirePositional('relation-id');
|
|
649
|
+
output = await removeRelation(client(), relationId);
|
|
650
|
+
break;
|
|
651
|
+
}
|
|
652
|
+
case 'list-sub-issues': {
|
|
653
|
+
const issueId = requirePositional('issue-id');
|
|
654
|
+
output = await listSubIssues(client(), issueId);
|
|
655
|
+
break;
|
|
656
|
+
}
|
|
657
|
+
case 'list-sub-issue-statuses': {
|
|
658
|
+
const issueId = requirePositional('issue-id');
|
|
659
|
+
output = await listSubIssueStatuses(client(), issueId);
|
|
660
|
+
break;
|
|
661
|
+
}
|
|
662
|
+
case 'update-sub-issue': {
|
|
663
|
+
const issueId = requirePositional('issue-id');
|
|
664
|
+
const opts = subArgs();
|
|
665
|
+
output = await updateSubIssue(client(), issueId, {
|
|
666
|
+
state: opts.state,
|
|
667
|
+
comment: opts.comment,
|
|
668
|
+
});
|
|
669
|
+
break;
|
|
670
|
+
}
|
|
671
|
+
case 'check-deployment': {
|
|
672
|
+
const prArg = requirePositional('pr-number');
|
|
673
|
+
const prNumber = parseInt(prArg, 10);
|
|
674
|
+
if (isNaN(prNumber)) {
|
|
675
|
+
throw new Error('PR number must be a valid integer');
|
|
676
|
+
}
|
|
677
|
+
const format = args.format || 'json';
|
|
678
|
+
output = await checkDeployment(prNumber, format);
|
|
679
|
+
break;
|
|
680
|
+
}
|
|
681
|
+
case 'create-blocker': {
|
|
682
|
+
const sourceIssueId = requirePositional('source-issue-id');
|
|
683
|
+
if (!args.title) {
|
|
684
|
+
throw new Error('Usage: af-linear create-blocker <source-issue-id> --title "Title" [--description "..."] [--team "..."] [--project "..."] [--assignee "user@email.com"]');
|
|
685
|
+
}
|
|
686
|
+
output = await createBlocker(client(), {
|
|
687
|
+
title: args.title,
|
|
688
|
+
sourceIssueId,
|
|
689
|
+
description: args.description,
|
|
690
|
+
team: args.team,
|
|
691
|
+
project: args.project,
|
|
692
|
+
assignee: args.assignee,
|
|
693
|
+
});
|
|
694
|
+
break;
|
|
695
|
+
}
|
|
696
|
+
default:
|
|
697
|
+
throw new Error(`Unknown command: ${command}`);
|
|
698
|
+
}
|
|
699
|
+
return { output };
|
|
700
|
+
}
|