@ebragas/linear-cli 0.9.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/README.md +211 -0
- package/dist/cli.js +2321 -0
- package/package.json +54 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,2321 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/cli.ts
|
|
27
|
+
var import_commander = require("commander");
|
|
28
|
+
var import_fs7 = require("fs");
|
|
29
|
+
var import_path4 = require("path");
|
|
30
|
+
|
|
31
|
+
// src/commands/auth.ts
|
|
32
|
+
var import_http = __toESM(require("http"));
|
|
33
|
+
var import_sdk = require("@linear/sdk");
|
|
34
|
+
|
|
35
|
+
// src/credentials.ts
|
|
36
|
+
var import_fs = require("fs");
|
|
37
|
+
var import_path = require("path");
|
|
38
|
+
var import_os = require("os");
|
|
39
|
+
var REQUIRED_FIELDS = [
|
|
40
|
+
"authMethod",
|
|
41
|
+
"clientId",
|
|
42
|
+
"clientSecret",
|
|
43
|
+
"accessToken",
|
|
44
|
+
"tokenExpiresAt",
|
|
45
|
+
"actorId",
|
|
46
|
+
"workspaceId",
|
|
47
|
+
"workspaceSlug"
|
|
48
|
+
];
|
|
49
|
+
function getCredentialsDir(opts) {
|
|
50
|
+
const dir = opts?.credentialsDir ?? process.env.LINEAR_AGENT_CREDENTIALS_DIR ?? (0, import_path.join)((0, import_os.homedir)(), ".linear", "credentials");
|
|
51
|
+
return dir.startsWith("~") ? (0, import_path.join)((0, import_os.homedir)(), dir.slice(2)) : (0, import_path.resolve)(dir);
|
|
52
|
+
}
|
|
53
|
+
function credentialsPath(agentId, credentialsDir) {
|
|
54
|
+
return (0, import_path.join)(credentialsDir, `${agentId}.json`);
|
|
55
|
+
}
|
|
56
|
+
function readCredentials(agentId, credentialsDir) {
|
|
57
|
+
const path = credentialsPath(agentId, credentialsDir);
|
|
58
|
+
let raw;
|
|
59
|
+
try {
|
|
60
|
+
raw = (0, import_fs.readFileSync)(path, "utf-8");
|
|
61
|
+
} catch {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Credentials not found for agent "${agentId}" at ${path}. Run "linear auth setup" first.`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
const data = JSON.parse(raw);
|
|
67
|
+
const missing = REQUIRED_FIELDS.filter(
|
|
68
|
+
(f) => data[f] === void 0 || data[f] === null
|
|
69
|
+
);
|
|
70
|
+
if (missing.length > 0) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`Credentials file missing required fields: ${missing.join(", ")}`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
return data;
|
|
76
|
+
}
|
|
77
|
+
function writeCredentials(agentId, credentialsDir, data) {
|
|
78
|
+
(0, import_fs.mkdirSync)(credentialsDir, { recursive: true, mode: 448 });
|
|
79
|
+
const path = credentialsPath(agentId, credentialsDir);
|
|
80
|
+
(0, import_fs.writeFileSync)(path, JSON.stringify(data, null, 2) + "\n", {
|
|
81
|
+
mode: 384
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
function deleteCredentials(agentId, credentialsDir) {
|
|
85
|
+
const path = credentialsPath(agentId, credentialsDir);
|
|
86
|
+
try {
|
|
87
|
+
const { unlinkSync } = require("fs");
|
|
88
|
+
unlinkSync(path);
|
|
89
|
+
} catch {
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// src/errors.ts
|
|
94
|
+
var CLIError = class extends Error {
|
|
95
|
+
constructor(message, exitCode, resolution) {
|
|
96
|
+
super(message);
|
|
97
|
+
this.exitCode = exitCode;
|
|
98
|
+
this.resolution = resolution;
|
|
99
|
+
this.name = this.constructor.name;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
var RateLimitError = class extends CLIError {
|
|
103
|
+
constructor(message, resetAt) {
|
|
104
|
+
super(message, 1, "Wait for rate limit reset or reduce request frequency.");
|
|
105
|
+
this.resetAt = resetAt;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
var AuthenticationError = class extends CLIError {
|
|
109
|
+
constructor(message) {
|
|
110
|
+
super(message, 2, 'Run "linear auth setup" to re-authenticate.');
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
var ForbiddenError = class extends CLIError {
|
|
114
|
+
constructor(message) {
|
|
115
|
+
super(
|
|
116
|
+
message,
|
|
117
|
+
3,
|
|
118
|
+
"The agent may have lost access. Check team permissions in Linear."
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
var ValidationError = class extends CLIError {
|
|
123
|
+
constructor(message, validOptions) {
|
|
124
|
+
const resolution = validOptions?.length ? `Valid options:
|
|
125
|
+
${validOptions.map((o) => ` - ${o}`).join("\n")}` : void 0;
|
|
126
|
+
super(message, 4, resolution);
|
|
127
|
+
this.validOptions = validOptions;
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
var NetworkError = class extends CLIError {
|
|
131
|
+
constructor(message) {
|
|
132
|
+
super(message, 5, "Check network connectivity and try again.");
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
var PartialSuccessError = class extends CLIError {
|
|
136
|
+
constructor(message, succeeded, failed) {
|
|
137
|
+
super(message, 6);
|
|
138
|
+
this.succeeded = succeeded;
|
|
139
|
+
this.failed = failed;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
function classifyError(err) {
|
|
143
|
+
if (err instanceof CLIError) return err;
|
|
144
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
145
|
+
const errObj = err;
|
|
146
|
+
const type = errObj?.type ?? errObj?.extensions?.code;
|
|
147
|
+
switch (type) {
|
|
148
|
+
case "RATELIMITED":
|
|
149
|
+
return new RateLimitError(message);
|
|
150
|
+
case "AUTHENTICATION_ERROR":
|
|
151
|
+
return new AuthenticationError(message);
|
|
152
|
+
case "FORBIDDEN":
|
|
153
|
+
return new ForbiddenError(message);
|
|
154
|
+
case "InvalidInputLinearError":
|
|
155
|
+
return new ValidationError(message);
|
|
156
|
+
default:
|
|
157
|
+
if (message.includes("ECONNREFUSED") || message.includes("ENOTFOUND") || message.includes("ETIMEDOUT") || message.includes("fetch failed")) {
|
|
158
|
+
return new NetworkError(message);
|
|
159
|
+
}
|
|
160
|
+
return new CLIError(message, 1);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/output.ts
|
|
165
|
+
function getFormat(formatFlag) {
|
|
166
|
+
if (formatFlag === "json" || formatFlag === "text") return formatFlag;
|
|
167
|
+
return process.stdout.isTTY ? "text" : "json";
|
|
168
|
+
}
|
|
169
|
+
function formatOutput(result, format) {
|
|
170
|
+
if (format === "json") {
|
|
171
|
+
return formatJson(result);
|
|
172
|
+
}
|
|
173
|
+
return formatText(result);
|
|
174
|
+
}
|
|
175
|
+
function formatJson(result) {
|
|
176
|
+
const { data, warnings } = result;
|
|
177
|
+
if (Array.isArray(data)) {
|
|
178
|
+
const obj = { results: data };
|
|
179
|
+
if (warnings?.length) obj.warnings = warnings;
|
|
180
|
+
return JSON.stringify(obj, null, 2);
|
|
181
|
+
}
|
|
182
|
+
if (warnings?.length) {
|
|
183
|
+
return JSON.stringify({ ...data, _warnings: warnings }, null, 2);
|
|
184
|
+
}
|
|
185
|
+
return JSON.stringify(data, null, 2);
|
|
186
|
+
}
|
|
187
|
+
function formatText(result) {
|
|
188
|
+
const { data, warnings } = result;
|
|
189
|
+
const lines = [];
|
|
190
|
+
if (Array.isArray(data)) {
|
|
191
|
+
for (const item of data) {
|
|
192
|
+
lines.push(formatRecord(item));
|
|
193
|
+
}
|
|
194
|
+
} else if (data && typeof data === "object") {
|
|
195
|
+
lines.push(formatRecord(data));
|
|
196
|
+
} else {
|
|
197
|
+
lines.push(String(data));
|
|
198
|
+
}
|
|
199
|
+
if (warnings?.length) {
|
|
200
|
+
lines.push("");
|
|
201
|
+
for (const w of warnings) {
|
|
202
|
+
lines.push(`Warning: ${w}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return lines.join("\n");
|
|
206
|
+
}
|
|
207
|
+
function formatRecord(item) {
|
|
208
|
+
if (!item || typeof item !== "object") return String(item);
|
|
209
|
+
const record = item;
|
|
210
|
+
return Object.entries(record).map(([key, value]) => {
|
|
211
|
+
if (value === null || value === void 0) return `${key}: -`;
|
|
212
|
+
if (typeof value === "object") return `${key}: ${JSON.stringify(value)}`;
|
|
213
|
+
return `${key}: ${value}`;
|
|
214
|
+
}).join("\n");
|
|
215
|
+
}
|
|
216
|
+
function printResult(result, format) {
|
|
217
|
+
console.log(formatOutput(result, format));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// src/commands/auth.ts
|
|
221
|
+
var TOKEN_URL = "https://api.linear.app/oauth/token";
|
|
222
|
+
var AUTHORIZE_URL = "https://linear.app/oauth/authorize";
|
|
223
|
+
var DEFAULT_SCOPES = "read,write,app:assignable,app:mentionable";
|
|
224
|
+
async function fetchToken(params) {
|
|
225
|
+
const response = await fetch(TOKEN_URL, {
|
|
226
|
+
method: "POST",
|
|
227
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
228
|
+
body: new URLSearchParams(params).toString()
|
|
229
|
+
});
|
|
230
|
+
if (!response.ok) {
|
|
231
|
+
const body = await response.text();
|
|
232
|
+
throw new AuthenticationError(
|
|
233
|
+
`Token request failed: ${response.status} ${response.statusText}
|
|
234
|
+
${body}`
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
return await response.json();
|
|
238
|
+
}
|
|
239
|
+
async function fetchViewerAndOrg(accessToken) {
|
|
240
|
+
const client = new import_sdk.LinearClient({ accessToken });
|
|
241
|
+
const viewer = await client.viewer;
|
|
242
|
+
const org = await client.organization;
|
|
243
|
+
return {
|
|
244
|
+
actorId: viewer.id,
|
|
245
|
+
name: viewer.name ?? viewer.id,
|
|
246
|
+
workspaceId: org.id,
|
|
247
|
+
workspaceSlug: org.urlKey
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
async function setupClientCredentials(opts) {
|
|
251
|
+
const tokenData = await fetchToken({
|
|
252
|
+
grant_type: "client_credentials",
|
|
253
|
+
client_id: opts.clientId,
|
|
254
|
+
client_secret: opts.clientSecret,
|
|
255
|
+
scope: opts.scopes
|
|
256
|
+
});
|
|
257
|
+
const { actorId, name, workspaceId, workspaceSlug } = await fetchViewerAndOrg(tokenData.access_token);
|
|
258
|
+
const credentials = {
|
|
259
|
+
authMethod: "client_credentials",
|
|
260
|
+
clientId: opts.clientId,
|
|
261
|
+
clientSecret: opts.clientSecret,
|
|
262
|
+
accessToken: tokenData.access_token,
|
|
263
|
+
refreshToken: null,
|
|
264
|
+
tokenExpiresAt: new Date(
|
|
265
|
+
Date.now() + tokenData.expires_in * 1e3
|
|
266
|
+
).toISOString(),
|
|
267
|
+
actorId,
|
|
268
|
+
workspaceId,
|
|
269
|
+
workspaceSlug
|
|
270
|
+
};
|
|
271
|
+
writeCredentials(opts.agent, opts.credentialsDir, credentials);
|
|
272
|
+
const format = getFormat(opts.format);
|
|
273
|
+
printResult(
|
|
274
|
+
{
|
|
275
|
+
data: {
|
|
276
|
+
status: "authenticated",
|
|
277
|
+
agent: opts.agent,
|
|
278
|
+
actorId,
|
|
279
|
+
name,
|
|
280
|
+
workspace: workspaceSlug,
|
|
281
|
+
expiresAt: credentials.tokenExpiresAt
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
format
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
async function setupOAuth(opts) {
|
|
288
|
+
const redirectUri = `http://localhost:${opts.port}/callback`;
|
|
289
|
+
const code = await new Promise((resolve3, reject) => {
|
|
290
|
+
const server = import_http.default.createServer((req, res) => {
|
|
291
|
+
const url = new URL(req.url ?? "/", `http://localhost:${opts.port}`);
|
|
292
|
+
if (url.pathname === "/callback") {
|
|
293
|
+
const code2 = url.searchParams.get("code");
|
|
294
|
+
const error = url.searchParams.get("error");
|
|
295
|
+
if (error) {
|
|
296
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
297
|
+
res.end(`<h1>Authorization failed</h1><p>${error}</p>`);
|
|
298
|
+
server.close();
|
|
299
|
+
reject(
|
|
300
|
+
new AuthenticationError(`OAuth authorization failed: ${error}`)
|
|
301
|
+
);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (code2) {
|
|
305
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
306
|
+
res.end(
|
|
307
|
+
"<h1>Authorization successful</h1><p>You can close this window.</p>"
|
|
308
|
+
);
|
|
309
|
+
server.close();
|
|
310
|
+
resolve3(code2);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
res.writeHead(404);
|
|
315
|
+
res.end("Not found");
|
|
316
|
+
});
|
|
317
|
+
server.listen(opts.port, () => {
|
|
318
|
+
const authUrl = new URL(AUTHORIZE_URL);
|
|
319
|
+
authUrl.searchParams.set("response_type", "code");
|
|
320
|
+
authUrl.searchParams.set("client_id", opts.clientId);
|
|
321
|
+
authUrl.searchParams.set("redirect_uri", redirectUri);
|
|
322
|
+
authUrl.searchParams.set("scope", opts.scopes);
|
|
323
|
+
authUrl.searchParams.set("actor", "app");
|
|
324
|
+
console.log(`
|
|
325
|
+
Open this URL in your browser:
|
|
326
|
+
${authUrl.toString()}
|
|
327
|
+
`);
|
|
328
|
+
console.log(`Waiting for callback on port ${opts.port}...`);
|
|
329
|
+
});
|
|
330
|
+
server.on("error", (err) => {
|
|
331
|
+
reject(
|
|
332
|
+
new AuthenticationError(
|
|
333
|
+
`Failed to start callback server: ${err.message}`
|
|
334
|
+
)
|
|
335
|
+
);
|
|
336
|
+
});
|
|
337
|
+
setTimeout(() => {
|
|
338
|
+
server.close();
|
|
339
|
+
reject(new AuthenticationError("OAuth callback timed out after 5 minutes"));
|
|
340
|
+
}, 5 * 60 * 1e3);
|
|
341
|
+
});
|
|
342
|
+
const tokenData = await fetchToken({
|
|
343
|
+
grant_type: "authorization_code",
|
|
344
|
+
code,
|
|
345
|
+
redirect_uri: redirectUri,
|
|
346
|
+
client_id: opts.clientId,
|
|
347
|
+
client_secret: opts.clientSecret
|
|
348
|
+
});
|
|
349
|
+
const { actorId, name, workspaceId, workspaceSlug } = await fetchViewerAndOrg(tokenData.access_token);
|
|
350
|
+
const credentials = {
|
|
351
|
+
authMethod: "oauth",
|
|
352
|
+
clientId: opts.clientId,
|
|
353
|
+
clientSecret: opts.clientSecret,
|
|
354
|
+
accessToken: tokenData.access_token,
|
|
355
|
+
refreshToken: tokenData.refresh_token ?? null,
|
|
356
|
+
tokenExpiresAt: new Date(
|
|
357
|
+
Date.now() + tokenData.expires_in * 1e3
|
|
358
|
+
).toISOString(),
|
|
359
|
+
actorId,
|
|
360
|
+
workspaceId,
|
|
361
|
+
workspaceSlug
|
|
362
|
+
};
|
|
363
|
+
writeCredentials(opts.agent, opts.credentialsDir, credentials);
|
|
364
|
+
const format = getFormat(opts.format);
|
|
365
|
+
printResult(
|
|
366
|
+
{
|
|
367
|
+
data: {
|
|
368
|
+
status: "authenticated",
|
|
369
|
+
agent: opts.agent,
|
|
370
|
+
actorId,
|
|
371
|
+
name,
|
|
372
|
+
workspace: workspaceSlug,
|
|
373
|
+
method: "oauth",
|
|
374
|
+
expiresAt: credentials.tokenExpiresAt
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
format
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
function registerAuthCommands(program2) {
|
|
381
|
+
const auth = program2.command("auth").description("Manage authentication and API tokens");
|
|
382
|
+
auth.command("setup").description("Authenticate an agent with Linear").requiredOption("--client-id <id>", "OAuth application client ID").requiredOption("--client-secret <secret>", "OAuth application client secret").option("--client-credentials", "Use client credentials grant (default)", true).option("--oauth", "Use OAuth authorization code flow").option("--port <port>", "Local callback server port (OAuth only)", "9876").option("--scopes <scopes>", "OAuth scopes", DEFAULT_SCOPES).action(async (opts, cmd) => {
|
|
383
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
384
|
+
const agent = globalOpts.agent;
|
|
385
|
+
if (!agent) {
|
|
386
|
+
console.error(
|
|
387
|
+
"Error: --agent is required (or set LINEAR_AGENT_ID env var)"
|
|
388
|
+
);
|
|
389
|
+
process.exit(4);
|
|
390
|
+
}
|
|
391
|
+
const credentialsDir = getCredentialsDir(globalOpts);
|
|
392
|
+
const format = globalOpts.format;
|
|
393
|
+
if (opts.oauth) {
|
|
394
|
+
await setupOAuth({
|
|
395
|
+
agent,
|
|
396
|
+
clientId: opts.clientId,
|
|
397
|
+
clientSecret: opts.clientSecret,
|
|
398
|
+
scopes: opts.scopes,
|
|
399
|
+
port: parseInt(opts.port, 10),
|
|
400
|
+
credentialsDir,
|
|
401
|
+
format
|
|
402
|
+
});
|
|
403
|
+
} else {
|
|
404
|
+
await setupClientCredentials({
|
|
405
|
+
agent,
|
|
406
|
+
clientId: opts.clientId,
|
|
407
|
+
clientSecret: opts.clientSecret,
|
|
408
|
+
scopes: opts.scopes,
|
|
409
|
+
credentialsDir,
|
|
410
|
+
format
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
auth.command("whoami").description("Verify token and print agent identity").action(async (_opts, cmd) => {
|
|
415
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
416
|
+
const agent = globalOpts.agent;
|
|
417
|
+
if (!agent) {
|
|
418
|
+
console.error(
|
|
419
|
+
"Error: --agent is required (or set LINEAR_AGENT_ID env var)"
|
|
420
|
+
);
|
|
421
|
+
process.exit(4);
|
|
422
|
+
}
|
|
423
|
+
const credentialsDir = getCredentialsDir(globalOpts);
|
|
424
|
+
const credentials = readCredentials(agent, credentialsDir);
|
|
425
|
+
const client = new import_sdk.LinearClient({
|
|
426
|
+
accessToken: credentials.accessToken
|
|
427
|
+
});
|
|
428
|
+
const viewer = await client.viewer;
|
|
429
|
+
const org = await client.organization;
|
|
430
|
+
const format = getFormat(globalOpts.format);
|
|
431
|
+
printResult(
|
|
432
|
+
{
|
|
433
|
+
data: {
|
|
434
|
+
agent,
|
|
435
|
+
actorId: credentials.actorId,
|
|
436
|
+
name: viewer.name ?? viewer.id,
|
|
437
|
+
email: viewer.email,
|
|
438
|
+
workspace: org.urlKey,
|
|
439
|
+
workspaceId: org.id,
|
|
440
|
+
authMethod: credentials.authMethod,
|
|
441
|
+
tokenExpiresAt: credentials.tokenExpiresAt
|
|
442
|
+
}
|
|
443
|
+
},
|
|
444
|
+
format
|
|
445
|
+
);
|
|
446
|
+
});
|
|
447
|
+
auth.command("refresh").description("Request a new token").action(async (_opts, cmd) => {
|
|
448
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
449
|
+
const agent = globalOpts.agent;
|
|
450
|
+
if (!agent) {
|
|
451
|
+
console.error(
|
|
452
|
+
"Error: --agent is required (or set LINEAR_AGENT_ID env var)"
|
|
453
|
+
);
|
|
454
|
+
process.exit(4);
|
|
455
|
+
}
|
|
456
|
+
const credentialsDir = getCredentialsDir(globalOpts);
|
|
457
|
+
const credentials = readCredentials(agent, credentialsDir);
|
|
458
|
+
const params = {
|
|
459
|
+
client_id: credentials.clientId,
|
|
460
|
+
client_secret: credentials.clientSecret
|
|
461
|
+
};
|
|
462
|
+
if (credentials.authMethod === "client_credentials") {
|
|
463
|
+
params.grant_type = "client_credentials";
|
|
464
|
+
params.scope = "read,write,app:assignable,app:mentionable";
|
|
465
|
+
} else {
|
|
466
|
+
params.grant_type = "refresh_token";
|
|
467
|
+
params.refresh_token = credentials.refreshToken ?? "";
|
|
468
|
+
}
|
|
469
|
+
const tokenData = await fetchToken(params);
|
|
470
|
+
const updated = {
|
|
471
|
+
...credentials,
|
|
472
|
+
accessToken: tokenData.access_token,
|
|
473
|
+
refreshToken: tokenData.refresh_token ?? credentials.refreshToken,
|
|
474
|
+
tokenExpiresAt: new Date(
|
|
475
|
+
Date.now() + tokenData.expires_in * 1e3
|
|
476
|
+
).toISOString()
|
|
477
|
+
};
|
|
478
|
+
writeCredentials(agent, credentialsDir, updated);
|
|
479
|
+
const format = getFormat(globalOpts.format);
|
|
480
|
+
printResult(
|
|
481
|
+
{
|
|
482
|
+
data: {
|
|
483
|
+
status: "refreshed",
|
|
484
|
+
agent,
|
|
485
|
+
expiresAt: updated.tokenExpiresAt
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
format
|
|
489
|
+
);
|
|
490
|
+
});
|
|
491
|
+
auth.command("revoke").description("Revoke token and delete credentials").action(async (_opts, cmd) => {
|
|
492
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
493
|
+
const agent = globalOpts.agent;
|
|
494
|
+
if (!agent) {
|
|
495
|
+
console.error(
|
|
496
|
+
"Error: --agent is required (or set LINEAR_AGENT_ID env var)"
|
|
497
|
+
);
|
|
498
|
+
process.exit(4);
|
|
499
|
+
}
|
|
500
|
+
const credentialsDir = getCredentialsDir(globalOpts);
|
|
501
|
+
let credentials;
|
|
502
|
+
try {
|
|
503
|
+
credentials = readCredentials(agent, credentialsDir);
|
|
504
|
+
} catch {
|
|
505
|
+
console.error(`No credentials found for agent "${agent}".`);
|
|
506
|
+
process.exit(4);
|
|
507
|
+
}
|
|
508
|
+
try {
|
|
509
|
+
const response = await fetch(
|
|
510
|
+
"https://api.linear.app/oauth/revoke",
|
|
511
|
+
{
|
|
512
|
+
method: "POST",
|
|
513
|
+
headers: {
|
|
514
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
515
|
+
Authorization: `Bearer ${credentials.accessToken}`
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
);
|
|
519
|
+
if (!response.ok) {
|
|
520
|
+
console.error(
|
|
521
|
+
`Warning: Token revocation returned ${response.status} (token may already be expired)`
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
} catch {
|
|
525
|
+
console.error(
|
|
526
|
+
"Warning: Could not reach Linear API to revoke token"
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
deleteCredentials(agent, credentialsDir);
|
|
530
|
+
const format = getFormat(globalOpts.format);
|
|
531
|
+
printResult(
|
|
532
|
+
{
|
|
533
|
+
data: {
|
|
534
|
+
status: "revoked",
|
|
535
|
+
agent
|
|
536
|
+
}
|
|
537
|
+
},
|
|
538
|
+
format
|
|
539
|
+
);
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// src/commands/issue.ts
|
|
544
|
+
var import_fs3 = require("fs");
|
|
545
|
+
|
|
546
|
+
// src/cache.ts
|
|
547
|
+
var import_fs2 = require("fs");
|
|
548
|
+
var import_path2 = require("path");
|
|
549
|
+
var TTL_MS = 24 * 60 * 60 * 1e3;
|
|
550
|
+
function cachePath(agentId, credentialsDir) {
|
|
551
|
+
return (0, import_path2.join)(credentialsDir, `${agentId}.cache.json`);
|
|
552
|
+
}
|
|
553
|
+
function readCache(agentId, credentialsDir) {
|
|
554
|
+
const path = cachePath(agentId, credentialsDir);
|
|
555
|
+
try {
|
|
556
|
+
const raw = (0, import_fs2.readFileSync)(path, "utf-8");
|
|
557
|
+
return JSON.parse(raw);
|
|
558
|
+
} catch {
|
|
559
|
+
return { teams: {} };
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
function writeCache(agentId, credentialsDir, cache) {
|
|
563
|
+
(0, import_fs2.mkdirSync)(credentialsDir, { recursive: true });
|
|
564
|
+
const path = cachePath(agentId, credentialsDir);
|
|
565
|
+
(0, import_fs2.writeFileSync)(path, JSON.stringify(cache, null, 2) + "\n");
|
|
566
|
+
}
|
|
567
|
+
function getTeamStates(cache, teamKey) {
|
|
568
|
+
const team = cache.teams[teamKey];
|
|
569
|
+
if (!team) return null;
|
|
570
|
+
const age = Date.now() - new Date(team.updatedAt).getTime();
|
|
571
|
+
if (age > TTL_MS) return null;
|
|
572
|
+
return team.states;
|
|
573
|
+
}
|
|
574
|
+
function setTeamStates(cache, teamKey, states) {
|
|
575
|
+
return {
|
|
576
|
+
...cache,
|
|
577
|
+
teams: {
|
|
578
|
+
...cache.teams,
|
|
579
|
+
[teamKey]: {
|
|
580
|
+
states,
|
|
581
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// src/resolvers.ts
|
|
588
|
+
var userCache = null;
|
|
589
|
+
function parseTeamKey(issueIdentifier) {
|
|
590
|
+
const match = issueIdentifier.match(/^([A-Z][A-Z0-9]*)-\d+$/);
|
|
591
|
+
if (!match) {
|
|
592
|
+
throw new ValidationError(
|
|
593
|
+
`Cannot parse team key from identifier "${issueIdentifier}". Expected format: TEAM-123`
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
return match[1];
|
|
597
|
+
}
|
|
598
|
+
async function resolveUser(value, credentials, client) {
|
|
599
|
+
if (value.toLowerCase() === "me") {
|
|
600
|
+
return credentials.actorId;
|
|
601
|
+
}
|
|
602
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
|
|
603
|
+
value
|
|
604
|
+
)) {
|
|
605
|
+
return value;
|
|
606
|
+
}
|
|
607
|
+
if (!userCache) {
|
|
608
|
+
userCache = /* @__PURE__ */ new Map();
|
|
609
|
+
const users = await client.users();
|
|
610
|
+
for (const u of users.nodes) {
|
|
611
|
+
if (u.name) userCache.set(u.name.toLowerCase(), u.id);
|
|
612
|
+
if (u.email) userCache.set(u.email.toLowerCase(), u.id);
|
|
613
|
+
if (u.displayName) userCache.set(u.displayName.toLowerCase(), u.id);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
const id = userCache.get(value.toLowerCase());
|
|
617
|
+
if (id) return id;
|
|
618
|
+
const validOptions = Array.from(userCache.keys());
|
|
619
|
+
throw new ValidationError(
|
|
620
|
+
`No user matching "${value}"`,
|
|
621
|
+
validOptions.slice(0, 20)
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
async function resolveState(name, teamKey, client, agentId, credentialsDir) {
|
|
625
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
|
|
626
|
+
name
|
|
627
|
+
)) {
|
|
628
|
+
return name;
|
|
629
|
+
}
|
|
630
|
+
let cache = readCache(agentId, credentialsDir);
|
|
631
|
+
let states = getTeamStates(cache, teamKey);
|
|
632
|
+
if (!states) {
|
|
633
|
+
states = await fetchTeamStates(teamKey, client);
|
|
634
|
+
cache = setTeamStates(cache, teamKey, states);
|
|
635
|
+
writeCache(agentId, credentialsDir, cache);
|
|
636
|
+
}
|
|
637
|
+
const nameLower = name.toLowerCase();
|
|
638
|
+
for (const [stateName, stateId] of Object.entries(states)) {
|
|
639
|
+
if (stateName.toLowerCase() === nameLower) return stateId;
|
|
640
|
+
}
|
|
641
|
+
throw new ValidationError(
|
|
642
|
+
`No workflow state "${name}" found for team ${teamKey}`,
|
|
643
|
+
Object.keys(states)
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
async function fetchTeamStates(teamKey, client) {
|
|
647
|
+
const teams = await client.teams({ filter: { key: { eq: teamKey } } });
|
|
648
|
+
const team = teams.nodes[0];
|
|
649
|
+
if (!team) {
|
|
650
|
+
throw new ValidationError(`Team "${teamKey}" not found`);
|
|
651
|
+
}
|
|
652
|
+
const workflowStates = await team.states();
|
|
653
|
+
const states = {};
|
|
654
|
+
for (const s of workflowStates.nodes) {
|
|
655
|
+
states[s.name] = s.id;
|
|
656
|
+
}
|
|
657
|
+
return states;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// src/client.ts
|
|
661
|
+
var import_sdk2 = require("@linear/sdk");
|
|
662
|
+
async function refreshToken(credentials, agentId, credentialsDir) {
|
|
663
|
+
const body = {
|
|
664
|
+
client_id: credentials.clientId,
|
|
665
|
+
client_secret: credentials.clientSecret
|
|
666
|
+
};
|
|
667
|
+
if (credentials.authMethod === "client_credentials") {
|
|
668
|
+
body.grant_type = "client_credentials";
|
|
669
|
+
body.scope = "read,write,app:assignable,app:mentionable";
|
|
670
|
+
} else {
|
|
671
|
+
body.grant_type = "refresh_token";
|
|
672
|
+
body.refresh_token = credentials.refreshToken ?? "";
|
|
673
|
+
}
|
|
674
|
+
const response = await fetch("https://api.linear.app/oauth/token", {
|
|
675
|
+
method: "POST",
|
|
676
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
677
|
+
body: new URLSearchParams(body).toString()
|
|
678
|
+
});
|
|
679
|
+
if (!response.ok) {
|
|
680
|
+
throw new AuthenticationError(
|
|
681
|
+
`Token refresh failed: ${response.status} ${response.statusText}`
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
const data = await response.json();
|
|
685
|
+
const expiresAt = new Date(
|
|
686
|
+
Date.now() + data.expires_in * 1e3
|
|
687
|
+
).toISOString();
|
|
688
|
+
const updated = {
|
|
689
|
+
...credentials,
|
|
690
|
+
accessToken: data.access_token,
|
|
691
|
+
tokenExpiresAt: expiresAt,
|
|
692
|
+
refreshToken: data.refresh_token ?? credentials.refreshToken
|
|
693
|
+
};
|
|
694
|
+
writeCredentials(agentId, credentialsDir, updated);
|
|
695
|
+
return updated;
|
|
696
|
+
}
|
|
697
|
+
function isRateLimited(err) {
|
|
698
|
+
const errObj = err;
|
|
699
|
+
const extensions = errObj?.extensions;
|
|
700
|
+
const type = errObj?.type ?? extensions?.code ?? "";
|
|
701
|
+
if (type === "RATELIMITED") {
|
|
702
|
+
const reset = extensions?.rateLimit;
|
|
703
|
+
return reset?.reset ?? true;
|
|
704
|
+
}
|
|
705
|
+
const errors = errObj?.errors;
|
|
706
|
+
if (errors?.some((e) => e.extensions?.code === "RATELIMITED")) {
|
|
707
|
+
return true;
|
|
708
|
+
}
|
|
709
|
+
return false;
|
|
710
|
+
}
|
|
711
|
+
function isAuthError(err) {
|
|
712
|
+
const errObj = err;
|
|
713
|
+
const type = errObj?.type ?? errObj?.extensions?.code;
|
|
714
|
+
if (type === "AUTHENTICATION_ERROR") return true;
|
|
715
|
+
const message = errObj?.message;
|
|
716
|
+
return message?.includes("AUTHENTICATION_ERROR") ?? false;
|
|
717
|
+
}
|
|
718
|
+
function isNetworkError(err) {
|
|
719
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
720
|
+
return message.includes("ECONNREFUSED") || message.includes("ENOTFOUND") || message.includes("ETIMEDOUT") || message.includes("fetch failed") || message.includes("network");
|
|
721
|
+
}
|
|
722
|
+
function sleep(ms) {
|
|
723
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
724
|
+
}
|
|
725
|
+
async function withRetry(fn, credentials, agentId, credentialsDir) {
|
|
726
|
+
const client = createClient(credentials);
|
|
727
|
+
try {
|
|
728
|
+
return await fn(client);
|
|
729
|
+
} catch (err) {
|
|
730
|
+
const rateLimitResult = isRateLimited(err);
|
|
731
|
+
if (rateLimitResult) {
|
|
732
|
+
if (typeof rateLimitResult === "number") {
|
|
733
|
+
const waitMs = Math.max(0, rateLimitResult - Date.now());
|
|
734
|
+
await sleep(Math.min(waitMs, 6e4));
|
|
735
|
+
} else {
|
|
736
|
+
await sleep(5e3);
|
|
737
|
+
}
|
|
738
|
+
try {
|
|
739
|
+
return await fn(client);
|
|
740
|
+
} catch {
|
|
741
|
+
throw new RateLimitError("Rate limited after retry");
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
if (isAuthError(err)) {
|
|
745
|
+
try {
|
|
746
|
+
const updated = await refreshToken(
|
|
747
|
+
credentials,
|
|
748
|
+
agentId,
|
|
749
|
+
credentialsDir
|
|
750
|
+
);
|
|
751
|
+
const newClient = createClient(updated);
|
|
752
|
+
return await fn(newClient);
|
|
753
|
+
} catch (refreshErr) {
|
|
754
|
+
if (refreshErr instanceof AuthenticationError) throw refreshErr;
|
|
755
|
+
throw new AuthenticationError(
|
|
756
|
+
"Token refresh failed. Run 'linear auth setup' to re-authenticate."
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
if (isNetworkError(err)) {
|
|
761
|
+
await sleep(2e3);
|
|
762
|
+
try {
|
|
763
|
+
return await fn(client);
|
|
764
|
+
} catch {
|
|
765
|
+
throw new NetworkError(
|
|
766
|
+
"Network error after retry. Check connectivity."
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
throw classifyError(err);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
function createClient(credentials) {
|
|
774
|
+
return new import_sdk2.LinearClient({ accessToken: credentials.accessToken });
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// src/context.ts
|
|
778
|
+
function requireAgent(globalOpts) {
|
|
779
|
+
const agent = globalOpts.agent;
|
|
780
|
+
if (!agent) {
|
|
781
|
+
console.error(
|
|
782
|
+
"Error: --agent is required (or set LINEAR_AGENT_ID env var)"
|
|
783
|
+
);
|
|
784
|
+
process.exit(4);
|
|
785
|
+
}
|
|
786
|
+
return agent;
|
|
787
|
+
}
|
|
788
|
+
async function runWithClient(globalOpts, fn) {
|
|
789
|
+
const agentId = requireAgent(globalOpts);
|
|
790
|
+
const credentialsDir = getCredentialsDir(globalOpts);
|
|
791
|
+
const credentials = readCredentials(agentId, credentialsDir);
|
|
792
|
+
return withRetry(
|
|
793
|
+
(client) => fn(client, { credentials, agentId, credentialsDir }),
|
|
794
|
+
credentials,
|
|
795
|
+
agentId,
|
|
796
|
+
credentialsDir
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// src/commands/issue.ts
|
|
801
|
+
var FREQUENCY_MAP = {
|
|
802
|
+
daily: "days",
|
|
803
|
+
weekly: "weeks",
|
|
804
|
+
monthly: "months",
|
|
805
|
+
yearly: "years"
|
|
806
|
+
};
|
|
807
|
+
function parseDate(value) {
|
|
808
|
+
const durationMatch = value.match(/^-P(\d+)D$/);
|
|
809
|
+
if (durationMatch) {
|
|
810
|
+
const days = parseInt(durationMatch[1], 10);
|
|
811
|
+
const date = new Date(Date.now() - days * 24 * 60 * 60 * 1e3);
|
|
812
|
+
return date.toISOString();
|
|
813
|
+
}
|
|
814
|
+
return new Date(value).toISOString();
|
|
815
|
+
}
|
|
816
|
+
async function createRelations(client, issueId, blocks, blockedBy, relatedTo) {
|
|
817
|
+
const succeeded = [];
|
|
818
|
+
const failed = [];
|
|
819
|
+
const warnings = [];
|
|
820
|
+
const relations = [];
|
|
821
|
+
for (const targetId of blocks) {
|
|
822
|
+
relations.push({
|
|
823
|
+
relatedIssueId: targetId,
|
|
824
|
+
type: "blocks",
|
|
825
|
+
label: `blocks ${targetId}`
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
for (const targetId of blockedBy) {
|
|
829
|
+
relations.push({
|
|
830
|
+
relatedIssueId: targetId,
|
|
831
|
+
type: "blocks",
|
|
832
|
+
label: `blocked-by ${targetId}`
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
for (const targetId of relatedTo) {
|
|
836
|
+
relations.push({
|
|
837
|
+
relatedIssueId: targetId,
|
|
838
|
+
type: "related",
|
|
839
|
+
label: `related-to ${targetId}`
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
for (const rel of relations) {
|
|
843
|
+
try {
|
|
844
|
+
if (rel.label.startsWith("blocked-by")) {
|
|
845
|
+
await client.createIssueRelation({
|
|
846
|
+
issueId: rel.relatedIssueId,
|
|
847
|
+
relatedIssueId: issueId,
|
|
848
|
+
type: rel.type
|
|
849
|
+
});
|
|
850
|
+
} else {
|
|
851
|
+
await client.createIssueRelation({
|
|
852
|
+
issueId,
|
|
853
|
+
relatedIssueId: rel.relatedIssueId,
|
|
854
|
+
type: rel.type
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
succeeded.push(rel.label);
|
|
858
|
+
} catch (err) {
|
|
859
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
860
|
+
failed.push(rel.label);
|
|
861
|
+
warnings.push(`Failed to create relation "${rel.label}": ${msg}`);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
return { succeeded, failed, warnings };
|
|
865
|
+
}
|
|
866
|
+
function collectArray(value, previous) {
|
|
867
|
+
return previous.concat([value]);
|
|
868
|
+
}
|
|
869
|
+
async function resolveLabels(client, labels) {
|
|
870
|
+
const labelIds = [];
|
|
871
|
+
for (const l of labels) {
|
|
872
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(l)) {
|
|
873
|
+
labelIds.push(l);
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
876
|
+
const result = await client.issueLabels({
|
|
877
|
+
filter: { name: { eqIgnoreCase: l } }
|
|
878
|
+
});
|
|
879
|
+
if (!result.nodes[0]) {
|
|
880
|
+
throw new ValidationError(`No label matching "${l}"`);
|
|
881
|
+
}
|
|
882
|
+
labelIds.push(result.nodes[0].id);
|
|
883
|
+
}
|
|
884
|
+
return labelIds;
|
|
885
|
+
}
|
|
886
|
+
async function resolveProject(client, project) {
|
|
887
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(project)) {
|
|
888
|
+
return project;
|
|
889
|
+
}
|
|
890
|
+
const projects = await client.projects({
|
|
891
|
+
filter: { name: { eqIgnoreCase: project } }
|
|
892
|
+
});
|
|
893
|
+
if (!projects.nodes[0]) {
|
|
894
|
+
throw new ValidationError(`No project matching "${project}"`);
|
|
895
|
+
}
|
|
896
|
+
return projects.nodes[0].id;
|
|
897
|
+
}
|
|
898
|
+
async function resolveTeam(client, team) {
|
|
899
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(team)) {
|
|
900
|
+
return team;
|
|
901
|
+
}
|
|
902
|
+
const teams = await client.teams({
|
|
903
|
+
filter: { name: { eqIgnoreCase: team } }
|
|
904
|
+
});
|
|
905
|
+
if (!teams.nodes[0]) {
|
|
906
|
+
throw new ValidationError(`No team matching "${team}"`);
|
|
907
|
+
}
|
|
908
|
+
return teams.nodes[0].id;
|
|
909
|
+
}
|
|
910
|
+
function registerIssueCommands(program2) {
|
|
911
|
+
const issue = program2.command("issue").description("Create, read, update, and search issues");
|
|
912
|
+
issue.command("list").description("List issues with filters").option("--assignee <user>", "Filter by assignee (name, email, ID, or 'me')").option("--delegate <agent>", "Filter by delegated agent").option("--state <state>", "Filter by workflow state name or type").option("--label <label>", "Filter by label name").option("--team <team>", "Filter by team name or ID").option("--project <project>", "Filter by project name or ID").option("--priority <priority>", "Filter by priority (0-4)").option("--query <text>", "Search title/description").option("--created-after <date>", "ISO-8601 date or duration (e.g., -P7D)").option("--updated-after <date>", "ISO-8601 date or duration").option("--limit <n>", "Max results (default: 50, max: 250)", "50").option("--include-archived", "Include archived issues").action(async (opts, cmd) => {
|
|
913
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
914
|
+
await runWithClient(globalOpts, async (client, { credentials }) => {
|
|
915
|
+
const format = getFormat(globalOpts.format);
|
|
916
|
+
const filter = {};
|
|
917
|
+
if (opts.assignee) {
|
|
918
|
+
const userId = await resolveUser(opts.assignee, credentials, client);
|
|
919
|
+
filter.assignee = { id: { eq: userId } };
|
|
920
|
+
}
|
|
921
|
+
if (opts.delegate) {
|
|
922
|
+
const userId = await resolveUser(opts.delegate, credentials, client);
|
|
923
|
+
filter.delegate = { id: { eq: userId } };
|
|
924
|
+
}
|
|
925
|
+
if (opts.state) {
|
|
926
|
+
filter.state = { name: { eqIgnoreCase: opts.state } };
|
|
927
|
+
}
|
|
928
|
+
if (opts.label) {
|
|
929
|
+
filter.labels = { name: { eqIgnoreCase: opts.label } };
|
|
930
|
+
}
|
|
931
|
+
if (opts.team) {
|
|
932
|
+
filter.team = { name: { eqIgnoreCase: opts.team } };
|
|
933
|
+
}
|
|
934
|
+
if (opts.project) {
|
|
935
|
+
filter.project = { name: { eqIgnoreCase: opts.project } };
|
|
936
|
+
}
|
|
937
|
+
if (opts.priority) {
|
|
938
|
+
filter.priority = { eq: parseInt(opts.priority, 10) };
|
|
939
|
+
}
|
|
940
|
+
if (opts.createdAfter) {
|
|
941
|
+
filter.createdAt = { gte: parseDate(opts.createdAfter) };
|
|
942
|
+
}
|
|
943
|
+
if (opts.updatedAfter) {
|
|
944
|
+
filter.updatedAt = { gte: parseDate(opts.updatedAfter) };
|
|
945
|
+
}
|
|
946
|
+
const limit = Math.min(parseInt(opts.limit, 10) || 50, 250);
|
|
947
|
+
const queryOpts = {
|
|
948
|
+
filter,
|
|
949
|
+
first: limit,
|
|
950
|
+
includeArchived: opts.includeArchived ?? false
|
|
951
|
+
};
|
|
952
|
+
if (opts.query) {
|
|
953
|
+
filter.or = [
|
|
954
|
+
{ title: { containsIgnoreCase: opts.query } },
|
|
955
|
+
{ description: { containsIgnoreCase: opts.query } }
|
|
956
|
+
];
|
|
957
|
+
}
|
|
958
|
+
const issues = await client.issues(queryOpts);
|
|
959
|
+
const results = await Promise.all(
|
|
960
|
+
issues.nodes.map(async (i) => {
|
|
961
|
+
const state = await i.state;
|
|
962
|
+
return {
|
|
963
|
+
id: i.identifier,
|
|
964
|
+
title: i.title,
|
|
965
|
+
state: state?.name ?? null,
|
|
966
|
+
priority: i.priority,
|
|
967
|
+
url: i.url
|
|
968
|
+
};
|
|
969
|
+
})
|
|
970
|
+
);
|
|
971
|
+
printResult({ data: results }, format);
|
|
972
|
+
});
|
|
973
|
+
});
|
|
974
|
+
issue.command("get").description("Get full issue details").argument("<ids...>", "Issue identifier(s) (e.g., MAIN-42 MAIN-43)").action(async (ids, _opts, cmd) => {
|
|
975
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
976
|
+
await runWithClient(globalOpts, async (client) => {
|
|
977
|
+
const format = getFormat(globalOpts.format);
|
|
978
|
+
const fetchIssue = async (id) => {
|
|
979
|
+
const issueObj = await client.issue(id);
|
|
980
|
+
const [state, assignee, delegate, labels, parent, children, comments, relations] = await Promise.all([
|
|
981
|
+
issueObj.state,
|
|
982
|
+
issueObj.assignee,
|
|
983
|
+
issueObj.delegate,
|
|
984
|
+
issueObj.labels(),
|
|
985
|
+
issueObj.parent,
|
|
986
|
+
issueObj.children(),
|
|
987
|
+
issueObj.comments(),
|
|
988
|
+
issueObj.relations()
|
|
989
|
+
]);
|
|
990
|
+
return {
|
|
991
|
+
id: issueObj.identifier,
|
|
992
|
+
title: issueObj.title,
|
|
993
|
+
description: issueObj.description ?? null,
|
|
994
|
+
state: state?.name ?? null,
|
|
995
|
+
stateType: state?.type ?? null,
|
|
996
|
+
assignee: assignee ? { id: assignee.id, name: assignee.name } : null,
|
|
997
|
+
delegate: delegate ? { id: delegate.id, name: delegate.name } : null,
|
|
998
|
+
labels: labels.nodes.map((l) => ({ id: l.id, name: l.name })),
|
|
999
|
+
priority: issueObj.priority,
|
|
1000
|
+
priorityLabel: issueObj.priorityLabel,
|
|
1001
|
+
parent: parent ? { id: parent.identifier, title: parent.title } : null,
|
|
1002
|
+
children: children.nodes.map((c) => ({
|
|
1003
|
+
id: c.identifier,
|
|
1004
|
+
title: c.title
|
|
1005
|
+
})),
|
|
1006
|
+
relations: relations.nodes.map((r) => ({
|
|
1007
|
+
type: r.type,
|
|
1008
|
+
relatedIssueId: r.relatedIssueId ?? null
|
|
1009
|
+
})),
|
|
1010
|
+
comments: comments.nodes.map((c) => ({
|
|
1011
|
+
id: c.id,
|
|
1012
|
+
body: c.body,
|
|
1013
|
+
createdAt: c.createdAt
|
|
1014
|
+
})),
|
|
1015
|
+
dueDate: issueObj.dueDate ?? null,
|
|
1016
|
+
estimate: issueObj.estimate ?? null,
|
|
1017
|
+
url: issueObj.url
|
|
1018
|
+
};
|
|
1019
|
+
};
|
|
1020
|
+
if (ids.length === 1) {
|
|
1021
|
+
const result = await fetchIssue(ids[0]);
|
|
1022
|
+
printResult({ data: result }, format);
|
|
1023
|
+
} else {
|
|
1024
|
+
const settled = await Promise.allSettled(ids.map(fetchIssue));
|
|
1025
|
+
const results = [];
|
|
1026
|
+
const warnings = [];
|
|
1027
|
+
for (let i = 0; i < settled.length; i++) {
|
|
1028
|
+
const outcome = settled[i];
|
|
1029
|
+
if (outcome.status === "fulfilled") {
|
|
1030
|
+
results.push(outcome.value);
|
|
1031
|
+
} else {
|
|
1032
|
+
const msg = outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason);
|
|
1033
|
+
warnings.push(`${ids[i]}: ${msg}`);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
printResult({ data: results, warnings: warnings.length ? warnings : void 0 }, format);
|
|
1037
|
+
}
|
|
1038
|
+
});
|
|
1039
|
+
});
|
|
1040
|
+
issue.command("create").description("Create a new issue").requiredOption("--title <text>", "Issue title").requiredOption("--team <team>", "Team name or ID").option("--description <text>", "Markdown description").option("--description-file <path>", "Read description from file").option("--assignee <user>", "Assign to user").option("--delegate <agent>", "Delegate to agent").option("--state <state>", "Initial workflow state").option("--label <label>", "Add label (repeatable)", collectArray, []).option("--priority <priority>", "Priority level (0-4)").option("--project <project>", "Add to project").option("--parent <id>", "Set parent issue").option("--blocks <id>", "This issue blocks <id> (repeatable)", collectArray, []).option("--blocked-by <id>", "This issue is blocked by <id> (repeatable)", collectArray, []).option("--related-to <id>", "Related issue (repeatable)", collectArray, []).option("--due-date <date>", "Due date (ISO format)").option("--estimate <n>", "Effort estimate").action(async (opts, cmd) => {
|
|
1041
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1042
|
+
await runWithClient(globalOpts, async (client, { credentials, agentId, credentialsDir }) => {
|
|
1043
|
+
const format = getFormat(globalOpts.format);
|
|
1044
|
+
const teamId = await resolveTeam(client, opts.team);
|
|
1045
|
+
const input = {
|
|
1046
|
+
title: opts.title,
|
|
1047
|
+
teamId
|
|
1048
|
+
};
|
|
1049
|
+
if (opts.descriptionFile) {
|
|
1050
|
+
input.description = (0, import_fs3.readFileSync)(opts.descriptionFile, "utf-8");
|
|
1051
|
+
} else if (opts.description) {
|
|
1052
|
+
input.description = opts.description;
|
|
1053
|
+
} else if (!process.stdin.isTTY) {
|
|
1054
|
+
try {
|
|
1055
|
+
const stdinContent = (0, import_fs3.readFileSync)(0, "utf-8").trim();
|
|
1056
|
+
if (stdinContent) input.description = stdinContent;
|
|
1057
|
+
} catch (err) {
|
|
1058
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1059
|
+
console.error(`Error: Failed to read from stdin: ${message}`);
|
|
1060
|
+
process.exit(4);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
if (opts.assignee) {
|
|
1064
|
+
input.assigneeId = await resolveUser(
|
|
1065
|
+
opts.assignee,
|
|
1066
|
+
credentials,
|
|
1067
|
+
client
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
if (opts.delegate) {
|
|
1071
|
+
input.delegateId = await resolveUser(
|
|
1072
|
+
opts.delegate,
|
|
1073
|
+
credentials,
|
|
1074
|
+
client
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
if (opts.state) {
|
|
1078
|
+
const team = await client.team(teamId);
|
|
1079
|
+
input.stateId = await resolveState(
|
|
1080
|
+
opts.state,
|
|
1081
|
+
team.key,
|
|
1082
|
+
client,
|
|
1083
|
+
agentId,
|
|
1084
|
+
credentialsDir
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
if (opts.label && opts.label.length > 0) {
|
|
1088
|
+
input.labelIds = await resolveLabels(client, opts.label);
|
|
1089
|
+
}
|
|
1090
|
+
if (opts.priority !== void 0) {
|
|
1091
|
+
input.priority = parseInt(opts.priority, 10);
|
|
1092
|
+
}
|
|
1093
|
+
if (opts.project) {
|
|
1094
|
+
input.projectId = await resolveProject(client, opts.project);
|
|
1095
|
+
}
|
|
1096
|
+
if (opts.parent) {
|
|
1097
|
+
input.parentId = opts.parent;
|
|
1098
|
+
}
|
|
1099
|
+
if (opts.dueDate) {
|
|
1100
|
+
input.dueDate = opts.dueDate;
|
|
1101
|
+
}
|
|
1102
|
+
if (opts.estimate !== void 0) {
|
|
1103
|
+
input.estimate = parseInt(opts.estimate, 10);
|
|
1104
|
+
}
|
|
1105
|
+
const payload = await client.createIssue(input);
|
|
1106
|
+
const created = await payload.issue;
|
|
1107
|
+
const result = {
|
|
1108
|
+
id: created?.identifier ?? null,
|
|
1109
|
+
title: created?.title ?? opts.title,
|
|
1110
|
+
url: created?.url ?? null
|
|
1111
|
+
};
|
|
1112
|
+
const blocks = opts.blocks ?? [];
|
|
1113
|
+
const blockedBy = opts.blockedBy ?? [];
|
|
1114
|
+
const relatedTo = opts.relatedTo ?? [];
|
|
1115
|
+
const hasRelations = blocks.length > 0 || blockedBy.length > 0 || relatedTo.length > 0;
|
|
1116
|
+
if (hasRelations && created) {
|
|
1117
|
+
const { succeeded, failed, warnings } = await createRelations(
|
|
1118
|
+
client,
|
|
1119
|
+
created.id,
|
|
1120
|
+
blocks,
|
|
1121
|
+
blockedBy,
|
|
1122
|
+
relatedTo
|
|
1123
|
+
);
|
|
1124
|
+
if (failed.length > 0) {
|
|
1125
|
+
result.relations = { succeeded, failed };
|
|
1126
|
+
printResult({ data: result, warnings }, format);
|
|
1127
|
+
throw new PartialSuccessError(
|
|
1128
|
+
`Issue created but some relations failed`,
|
|
1129
|
+
succeeded,
|
|
1130
|
+
failed
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1133
|
+
if (succeeded.length > 0) {
|
|
1134
|
+
result.relations = { succeeded };
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
printResult({ data: result }, format);
|
|
1138
|
+
});
|
|
1139
|
+
});
|
|
1140
|
+
issue.command("update").description("Update an existing issue").argument("<id>", "Issue identifier").option("--title <text>", "Issue title").option("--description <text>", "Markdown description").option("--description-file <path>", "Read description from file").option("--assignee <user>", 'Assign to user (pass "null" to clear)').option("--delegate <agent>", 'Delegate to agent (pass "null" to clear)').option("--state <state>", "Workflow state").option("--label <label>", "Add label (repeatable)", collectArray, []).option("--priority <priority>", "Priority level (0-4)").option("--project <project>", "Add to project").option("--parent <id>", 'Set parent issue (pass "null" to clear)').option("--blocks <id>", "This issue blocks <id> (repeatable)", collectArray, []).option("--blocked-by <id>", "This issue is blocked by <id> (repeatable)", collectArray, []).option("--related-to <id>", "Related issue (repeatable)", collectArray, []).option("--due-date <date>", "Due date (ISO format)").option("--estimate <n>", "Effort estimate").action(async (id, opts, cmd) => {
|
|
1141
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1142
|
+
await runWithClient(globalOpts, async (client, { credentials, agentId, credentialsDir }) => {
|
|
1143
|
+
const format = getFormat(globalOpts.format);
|
|
1144
|
+
const input = {};
|
|
1145
|
+
if (opts.title) {
|
|
1146
|
+
input.title = opts.title;
|
|
1147
|
+
}
|
|
1148
|
+
if (opts.descriptionFile) {
|
|
1149
|
+
input.description = (0, import_fs3.readFileSync)(opts.descriptionFile, "utf-8");
|
|
1150
|
+
} else if (opts.description) {
|
|
1151
|
+
input.description = opts.description;
|
|
1152
|
+
} else if (!process.stdin.isTTY) {
|
|
1153
|
+
try {
|
|
1154
|
+
const stdinContent = (0, import_fs3.readFileSync)(0, "utf-8").trim();
|
|
1155
|
+
if (stdinContent) input.description = stdinContent;
|
|
1156
|
+
} catch (err) {
|
|
1157
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1158
|
+
console.error(`Error: Failed to read from stdin: ${message}`);
|
|
1159
|
+
process.exit(4);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
if (opts.assignee !== void 0) {
|
|
1163
|
+
if (opts.assignee === "null") {
|
|
1164
|
+
input.assigneeId = null;
|
|
1165
|
+
} else {
|
|
1166
|
+
input.assigneeId = await resolveUser(
|
|
1167
|
+
opts.assignee,
|
|
1168
|
+
credentials,
|
|
1169
|
+
client
|
|
1170
|
+
);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
if (opts.delegate !== void 0) {
|
|
1174
|
+
if (opts.delegate === "null") {
|
|
1175
|
+
input.delegateId = null;
|
|
1176
|
+
} else {
|
|
1177
|
+
input.delegateId = await resolveUser(
|
|
1178
|
+
opts.delegate,
|
|
1179
|
+
credentials,
|
|
1180
|
+
client
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
if (opts.state) {
|
|
1185
|
+
const teamKey = parseTeamKey(id);
|
|
1186
|
+
input.stateId = await resolveState(
|
|
1187
|
+
opts.state,
|
|
1188
|
+
teamKey,
|
|
1189
|
+
client,
|
|
1190
|
+
agentId,
|
|
1191
|
+
credentialsDir
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1194
|
+
if (opts.label && opts.label.length > 0) {
|
|
1195
|
+
input.labelIds = await resolveLabels(client, opts.label);
|
|
1196
|
+
}
|
|
1197
|
+
if (opts.priority !== void 0) {
|
|
1198
|
+
input.priority = parseInt(opts.priority, 10);
|
|
1199
|
+
}
|
|
1200
|
+
if (opts.project) {
|
|
1201
|
+
input.projectId = await resolveProject(client, opts.project);
|
|
1202
|
+
}
|
|
1203
|
+
if (opts.parent !== void 0) {
|
|
1204
|
+
if (opts.parent === "null") {
|
|
1205
|
+
input.parentId = null;
|
|
1206
|
+
} else {
|
|
1207
|
+
input.parentId = opts.parent;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
if (opts.dueDate) {
|
|
1211
|
+
input.dueDate = opts.dueDate;
|
|
1212
|
+
}
|
|
1213
|
+
if (opts.estimate !== void 0) {
|
|
1214
|
+
input.estimate = parseInt(opts.estimate, 10);
|
|
1215
|
+
}
|
|
1216
|
+
const payload = await client.updateIssue(id, input);
|
|
1217
|
+
const updated = await payload.issue;
|
|
1218
|
+
const result = {
|
|
1219
|
+
id: updated?.identifier ?? id,
|
|
1220
|
+
title: updated?.title ?? null,
|
|
1221
|
+
url: updated?.url ?? null
|
|
1222
|
+
};
|
|
1223
|
+
const blocks = opts.blocks ?? [];
|
|
1224
|
+
const blockedBy = opts.blockedBy ?? [];
|
|
1225
|
+
const relatedTo = opts.relatedTo ?? [];
|
|
1226
|
+
const hasRelations = blocks.length > 0 || blockedBy.length > 0 || relatedTo.length > 0;
|
|
1227
|
+
if (hasRelations && updated) {
|
|
1228
|
+
const { succeeded, failed, warnings } = await createRelations(
|
|
1229
|
+
client,
|
|
1230
|
+
updated.id,
|
|
1231
|
+
blocks,
|
|
1232
|
+
blockedBy,
|
|
1233
|
+
relatedTo
|
|
1234
|
+
);
|
|
1235
|
+
if (failed.length > 0) {
|
|
1236
|
+
result.relations = { succeeded, failed };
|
|
1237
|
+
printResult({ data: result, warnings }, format);
|
|
1238
|
+
throw new PartialSuccessError(
|
|
1239
|
+
`Issue updated but some relations failed`,
|
|
1240
|
+
succeeded,
|
|
1241
|
+
failed
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1244
|
+
if (succeeded.length > 0) {
|
|
1245
|
+
result.relations = { succeeded };
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
printResult({ data: result }, format);
|
|
1249
|
+
});
|
|
1250
|
+
});
|
|
1251
|
+
issue.command("transition").description("Move issue to a workflow state by name").argument("<id>", "Issue identifier (e.g., MAIN-42)").argument("<state>", "Target workflow state name").action(async (id, state, _opts, cmd) => {
|
|
1252
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1253
|
+
await runWithClient(globalOpts, async (client, { agentId, credentialsDir }) => {
|
|
1254
|
+
const format = getFormat(globalOpts.format);
|
|
1255
|
+
const teamKey = parseTeamKey(id);
|
|
1256
|
+
const stateId = await resolveState(
|
|
1257
|
+
state,
|
|
1258
|
+
teamKey,
|
|
1259
|
+
client,
|
|
1260
|
+
agentId,
|
|
1261
|
+
credentialsDir
|
|
1262
|
+
);
|
|
1263
|
+
const payload = await client.updateIssue(id, { stateId });
|
|
1264
|
+
const updated = await payload.issue;
|
|
1265
|
+
printResult(
|
|
1266
|
+
{
|
|
1267
|
+
data: {
|
|
1268
|
+
id: updated?.identifier ?? id,
|
|
1269
|
+
state,
|
|
1270
|
+
url: updated?.url ?? null
|
|
1271
|
+
}
|
|
1272
|
+
},
|
|
1273
|
+
format
|
|
1274
|
+
);
|
|
1275
|
+
});
|
|
1276
|
+
});
|
|
1277
|
+
issue.command("search").description("Full-text search via searchIssues").argument("<query>", "Search query").option("--team <team>", "Boost results for a specific team").option("--include-comments", "Search within comment content").option("--include-archived", "Include archived issues in results").action(async (query, opts, cmd) => {
|
|
1278
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1279
|
+
await runWithClient(globalOpts, async (client) => {
|
|
1280
|
+
const format = getFormat(globalOpts.format);
|
|
1281
|
+
const searchOpts = {};
|
|
1282
|
+
if (opts.team) {
|
|
1283
|
+
searchOpts.teamId = await resolveTeam(client, opts.team);
|
|
1284
|
+
}
|
|
1285
|
+
if (opts.includeComments) {
|
|
1286
|
+
searchOpts.includeComments = true;
|
|
1287
|
+
}
|
|
1288
|
+
if (opts.includeArchived) {
|
|
1289
|
+
searchOpts.includeArchived = true;
|
|
1290
|
+
}
|
|
1291
|
+
const results = await client.searchIssues(query, searchOpts);
|
|
1292
|
+
const items = results.nodes.map((i) => ({
|
|
1293
|
+
id: i.identifier,
|
|
1294
|
+
title: i.title,
|
|
1295
|
+
url: i.url
|
|
1296
|
+
}));
|
|
1297
|
+
printResult({ data: items }, format);
|
|
1298
|
+
});
|
|
1299
|
+
});
|
|
1300
|
+
issue.command("archive").description("Archive an issue").argument("<id>", "Issue identifier").action(async (id, _opts, cmd) => {
|
|
1301
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1302
|
+
await runWithClient(globalOpts, async (client) => {
|
|
1303
|
+
const format = getFormat(globalOpts.format);
|
|
1304
|
+
await client.archiveIssue(id);
|
|
1305
|
+
printResult(
|
|
1306
|
+
{
|
|
1307
|
+
data: {
|
|
1308
|
+
id,
|
|
1309
|
+
status: "archived"
|
|
1310
|
+
}
|
|
1311
|
+
},
|
|
1312
|
+
format
|
|
1313
|
+
);
|
|
1314
|
+
});
|
|
1315
|
+
});
|
|
1316
|
+
issue.command("delete").description("Delete an issue").argument("<id>", "Issue identifier").action(async (id, _opts, cmd) => {
|
|
1317
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1318
|
+
await runWithClient(globalOpts, async (client) => {
|
|
1319
|
+
const format = getFormat(globalOpts.format);
|
|
1320
|
+
await client.deleteIssue(id);
|
|
1321
|
+
printResult(
|
|
1322
|
+
{
|
|
1323
|
+
data: {
|
|
1324
|
+
id,
|
|
1325
|
+
status: "deleted"
|
|
1326
|
+
}
|
|
1327
|
+
},
|
|
1328
|
+
format
|
|
1329
|
+
);
|
|
1330
|
+
});
|
|
1331
|
+
});
|
|
1332
|
+
const schedule = issue.command("schedule").description("Manage recurring issue schedules");
|
|
1333
|
+
schedule.command("create").description("Create a recurring issue schedule").requiredOption("--title <text>", "Issue title").requiredOption("--team <team>", "Team name or ID").requiredOption(
|
|
1334
|
+
"--frequency <freq>",
|
|
1335
|
+
"Recurrence frequency (daily, weekly, monthly, yearly)"
|
|
1336
|
+
).requiredOption("--start-at <date>", "Start date (ISO format, e.g. 2026-03-01)").option("--interval <n>", "Recurrence interval (default: 1)", "1").option("--description <text>", "Issue description").option("--state <state>", "Initial workflow state").option("--assignee <user>", "Assign to user").option("--priority <n>", "Priority (0-4)").action(async (opts, cmd) => {
|
|
1337
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1338
|
+
await runWithClient(globalOpts, async (client, { credentials, agentId, credentialsDir }) => {
|
|
1339
|
+
const format = getFormat(globalOpts.format);
|
|
1340
|
+
const scheduleType = FREQUENCY_MAP[opts.frequency];
|
|
1341
|
+
if (!scheduleType) {
|
|
1342
|
+
throw new ValidationError(
|
|
1343
|
+
`Invalid frequency "${opts.frequency}"`,
|
|
1344
|
+
Object.keys(FREQUENCY_MAP)
|
|
1345
|
+
);
|
|
1346
|
+
}
|
|
1347
|
+
const teamId = await resolveTeam(client, opts.team);
|
|
1348
|
+
const interval = parseInt(opts.interval, 10);
|
|
1349
|
+
if (isNaN(interval) || interval < 1) {
|
|
1350
|
+
throw new ValidationError(
|
|
1351
|
+
`Invalid interval "${opts.interval}". Expected a positive integer.`
|
|
1352
|
+
);
|
|
1353
|
+
}
|
|
1354
|
+
const templateData = {
|
|
1355
|
+
title: opts.title,
|
|
1356
|
+
teamId,
|
|
1357
|
+
schedule: {
|
|
1358
|
+
startAt: opts.startAt,
|
|
1359
|
+
interval,
|
|
1360
|
+
type: scheduleType
|
|
1361
|
+
}
|
|
1362
|
+
};
|
|
1363
|
+
if (opts.description) templateData.description = opts.description;
|
|
1364
|
+
if (opts.priority !== void 0) {
|
|
1365
|
+
const priority = parseInt(opts.priority, 10);
|
|
1366
|
+
if (isNaN(priority) || priority < 0 || priority > 4) {
|
|
1367
|
+
throw new ValidationError(
|
|
1368
|
+
`Invalid priority "${opts.priority}". Expected an integer between 0 and 4.`
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1371
|
+
templateData.priority = priority;
|
|
1372
|
+
}
|
|
1373
|
+
if (opts.state) {
|
|
1374
|
+
const team = await client.team(teamId);
|
|
1375
|
+
templateData.stateId = await resolveState(
|
|
1376
|
+
opts.state,
|
|
1377
|
+
team.key,
|
|
1378
|
+
client,
|
|
1379
|
+
agentId,
|
|
1380
|
+
credentialsDir
|
|
1381
|
+
);
|
|
1382
|
+
}
|
|
1383
|
+
if (opts.assignee) {
|
|
1384
|
+
templateData.assigneeId = await resolveUser(opts.assignee, credentials, client);
|
|
1385
|
+
}
|
|
1386
|
+
const payload = await client.createTemplate({
|
|
1387
|
+
type: "recurringIssue",
|
|
1388
|
+
name: opts.title,
|
|
1389
|
+
teamId,
|
|
1390
|
+
templateData: JSON.stringify(templateData)
|
|
1391
|
+
});
|
|
1392
|
+
const template = await payload.template;
|
|
1393
|
+
printResult({ data: { id: template.id, name: template.name } }, format);
|
|
1394
|
+
});
|
|
1395
|
+
});
|
|
1396
|
+
schedule.command("list").description("List recurring issue schedules").option("--team <team>", "Filter by team name or ID").action(async (opts, cmd) => {
|
|
1397
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1398
|
+
await runWithClient(globalOpts, async (client) => {
|
|
1399
|
+
const format = getFormat(globalOpts.format);
|
|
1400
|
+
const teamId = opts.team ? await resolveTeam(client, opts.team) : null;
|
|
1401
|
+
const allTemplates = await client.templates;
|
|
1402
|
+
const filtered = await Promise.all(
|
|
1403
|
+
allTemplates.filter((t) => t.type === "recurringIssue").map(async (t) => {
|
|
1404
|
+
const team = await t.team;
|
|
1405
|
+
return { template: t, teamId: team?.id ?? null };
|
|
1406
|
+
})
|
|
1407
|
+
);
|
|
1408
|
+
const results = filtered.filter(({ teamId: tid }) => teamId === null || tid === teamId).map(({ template: t }) => {
|
|
1409
|
+
const data = typeof t.templateData === "string" ? (() => {
|
|
1410
|
+
try {
|
|
1411
|
+
return JSON.parse(t.templateData);
|
|
1412
|
+
} catch {
|
|
1413
|
+
return {};
|
|
1414
|
+
}
|
|
1415
|
+
})() : t.templateData ?? {};
|
|
1416
|
+
return { id: t.id, name: t.name, schedule: data.schedule ?? null };
|
|
1417
|
+
});
|
|
1418
|
+
printResult({ data: results }, format);
|
|
1419
|
+
});
|
|
1420
|
+
});
|
|
1421
|
+
schedule.command("delete").description("Delete a recurring issue schedule").argument("<id>", "Template UUID").action(async (id, _opts, cmd) => {
|
|
1422
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1423
|
+
await runWithClient(globalOpts, async (client) => {
|
|
1424
|
+
const format = getFormat(globalOpts.format);
|
|
1425
|
+
await client.deleteTemplate(id);
|
|
1426
|
+
printResult({ data: { id, status: "deleted" } }, format);
|
|
1427
|
+
});
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
// src/commands/comment.ts
|
|
1432
|
+
var import_fs4 = require("fs");
|
|
1433
|
+
function registerCommentCommands(program2) {
|
|
1434
|
+
const comment = program2.command("comment").description("Add and list comments on issues");
|
|
1435
|
+
comment.command("list").description("List all comments on an issue").argument("<issue-id>", "Issue identifier (e.g., MAIN-42)").action(async (issueId, _opts, cmd) => {
|
|
1436
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1437
|
+
await runWithClient(globalOpts, async (client) => {
|
|
1438
|
+
const issue = await client.issue(issueId);
|
|
1439
|
+
const commentsConnection = await issue.comments();
|
|
1440
|
+
const comments = [];
|
|
1441
|
+
for (const c of commentsConnection.nodes) {
|
|
1442
|
+
const user = await c.user;
|
|
1443
|
+
comments.push({
|
|
1444
|
+
id: c.id,
|
|
1445
|
+
author: user?.name ?? user?.id ?? "Unknown",
|
|
1446
|
+
body: c.body,
|
|
1447
|
+
createdAt: c.createdAt,
|
|
1448
|
+
parentId: c.parentId ?? null
|
|
1449
|
+
});
|
|
1450
|
+
}
|
|
1451
|
+
const format = getFormat(globalOpts.format);
|
|
1452
|
+
printResult({ data: comments }, format);
|
|
1453
|
+
});
|
|
1454
|
+
});
|
|
1455
|
+
comment.command("add").description("Add a comment to an issue").argument("<issue-id>", "Issue identifier (e.g., MAIN-42)").option("--body <text>", "Comment body as markdown").option("--body-file <path>", "Read body from file").option("--reply-to <comment-id>", "Reply to a specific comment").action(async (issueId, opts, cmd) => {
|
|
1456
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1457
|
+
await runWithClient(globalOpts, async (client) => {
|
|
1458
|
+
let body = opts.body;
|
|
1459
|
+
if (opts.bodyFile) {
|
|
1460
|
+
body = (0, import_fs4.readFileSync)(opts.bodyFile, "utf-8");
|
|
1461
|
+
} else if (!body && !process.stdin.isTTY) {
|
|
1462
|
+
try {
|
|
1463
|
+
const stdinContent = (0, import_fs4.readFileSync)(0, "utf-8").trim();
|
|
1464
|
+
if (stdinContent) body = stdinContent;
|
|
1465
|
+
} catch (err) {
|
|
1466
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1467
|
+
console.error(`Error: Failed to read from stdin: ${message}`);
|
|
1468
|
+
process.exit(4);
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
if (!body) {
|
|
1472
|
+
console.error("Error: --body, --body-file, or stdin pipe is required");
|
|
1473
|
+
process.exit(4);
|
|
1474
|
+
}
|
|
1475
|
+
const input = { issueId, body };
|
|
1476
|
+
if (opts.replyTo) {
|
|
1477
|
+
input.parentId = opts.replyTo;
|
|
1478
|
+
}
|
|
1479
|
+
const result = await client.createComment(input);
|
|
1480
|
+
const commentNode = await result.comment;
|
|
1481
|
+
const format = getFormat(globalOpts.format);
|
|
1482
|
+
printResult(
|
|
1483
|
+
{
|
|
1484
|
+
data: {
|
|
1485
|
+
id: commentNode?.id,
|
|
1486
|
+
issueId,
|
|
1487
|
+
body,
|
|
1488
|
+
parentId: opts.replyTo ?? null,
|
|
1489
|
+
success: result.success
|
|
1490
|
+
}
|
|
1491
|
+
},
|
|
1492
|
+
format
|
|
1493
|
+
);
|
|
1494
|
+
});
|
|
1495
|
+
});
|
|
1496
|
+
comment.command("update").description("Update an existing comment").argument("<comment-id>", "Comment ID").option("--body <text>", "Updated comment body").option("--body-file <path>", "Read body from file").action(async (commentId, opts, cmd) => {
|
|
1497
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1498
|
+
await runWithClient(globalOpts, async (client) => {
|
|
1499
|
+
let body = opts.body;
|
|
1500
|
+
if (opts.bodyFile) {
|
|
1501
|
+
body = (0, import_fs4.readFileSync)(opts.bodyFile, "utf-8");
|
|
1502
|
+
} else if (!body && !process.stdin.isTTY) {
|
|
1503
|
+
try {
|
|
1504
|
+
const stdinContent = (0, import_fs4.readFileSync)(0, "utf-8").trim();
|
|
1505
|
+
if (stdinContent) body = stdinContent;
|
|
1506
|
+
} catch (err) {
|
|
1507
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1508
|
+
console.error(`Error: Failed to read from stdin: ${message}`);
|
|
1509
|
+
process.exit(4);
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
if (!body) {
|
|
1513
|
+
console.error("Error: --body, --body-file, or stdin pipe is required");
|
|
1514
|
+
process.exit(4);
|
|
1515
|
+
}
|
|
1516
|
+
const result = await client.updateComment(commentId, { body });
|
|
1517
|
+
const format = getFormat(globalOpts.format);
|
|
1518
|
+
printResult(
|
|
1519
|
+
{
|
|
1520
|
+
data: {
|
|
1521
|
+
id: commentId,
|
|
1522
|
+
body,
|
|
1523
|
+
success: result.success
|
|
1524
|
+
}
|
|
1525
|
+
},
|
|
1526
|
+
format
|
|
1527
|
+
);
|
|
1528
|
+
});
|
|
1529
|
+
});
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
// src/commands/inbox.ts
|
|
1533
|
+
var VALID_CATEGORIES = [
|
|
1534
|
+
"assignments",
|
|
1535
|
+
"mentions",
|
|
1536
|
+
"statusChanges",
|
|
1537
|
+
"commentsAndReplies",
|
|
1538
|
+
"reactions",
|
|
1539
|
+
"reviews",
|
|
1540
|
+
"appsAndIntegrations",
|
|
1541
|
+
"triage",
|
|
1542
|
+
"system"
|
|
1543
|
+
];
|
|
1544
|
+
function parseSinceDate(since) {
|
|
1545
|
+
const durationMatch = since.match(
|
|
1546
|
+
/^-?P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/i
|
|
1547
|
+
);
|
|
1548
|
+
if (durationMatch) {
|
|
1549
|
+
const days = parseInt(durationMatch[1] ?? "0", 10);
|
|
1550
|
+
const hours = parseInt(durationMatch[2] ?? "0", 10);
|
|
1551
|
+
const minutes = parseInt(durationMatch[3] ?? "0", 10);
|
|
1552
|
+
const seconds = parseInt(durationMatch[4] ?? "0", 10);
|
|
1553
|
+
const ms = (days * 86400 + hours * 3600 + minutes * 60 + seconds) * 1e3;
|
|
1554
|
+
return new Date(Date.now() - ms);
|
|
1555
|
+
}
|
|
1556
|
+
const date = new Date(since);
|
|
1557
|
+
if (isNaN(date.getTime())) {
|
|
1558
|
+
throw new Error(`Invalid date or duration: ${since}`);
|
|
1559
|
+
}
|
|
1560
|
+
return date;
|
|
1561
|
+
}
|
|
1562
|
+
function registerInboxCommands(program2) {
|
|
1563
|
+
const inbox = program2.command("inbox").description("View and manage inbox notifications");
|
|
1564
|
+
inbox.command("list", { isDefault: true }).description("List notifications").option("--include-archived", "Show all notifications (not just unprocessed)").option("--type <type>", "Filter by notification type string").option("--category <category>", "Filter by notification category").option("--since <date>", "Only notifications after this date or duration").action(async (opts, cmd) => {
|
|
1565
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1566
|
+
if (opts.category && !VALID_CATEGORIES.includes(opts.category)) {
|
|
1567
|
+
console.error(
|
|
1568
|
+
`Error: Invalid category "${opts.category}". Valid categories: ${VALID_CATEGORIES.join(", ")}`
|
|
1569
|
+
);
|
|
1570
|
+
process.exit(4);
|
|
1571
|
+
}
|
|
1572
|
+
await runWithClient(globalOpts, async (client) => {
|
|
1573
|
+
const filter = {};
|
|
1574
|
+
if (opts.type) {
|
|
1575
|
+
filter.type = { eq: opts.type };
|
|
1576
|
+
}
|
|
1577
|
+
const notificationsConnection = await client.notifications({
|
|
1578
|
+
filter: Object.keys(filter).length > 0 ? filter : void 0
|
|
1579
|
+
});
|
|
1580
|
+
let notifications = notificationsConnection.nodes;
|
|
1581
|
+
if (!opts.includeArchived) {
|
|
1582
|
+
notifications = notifications.filter(
|
|
1583
|
+
(n) => !n.archivedAt
|
|
1584
|
+
);
|
|
1585
|
+
}
|
|
1586
|
+
if (opts.category) {
|
|
1587
|
+
notifications = notifications.filter(
|
|
1588
|
+
(n) => n.category === opts.category
|
|
1589
|
+
);
|
|
1590
|
+
}
|
|
1591
|
+
if (opts.since) {
|
|
1592
|
+
const sinceDate = parseSinceDate(opts.since);
|
|
1593
|
+
notifications = notifications.filter(
|
|
1594
|
+
(n) => new Date(n.createdAt) >= sinceDate
|
|
1595
|
+
);
|
|
1596
|
+
}
|
|
1597
|
+
const results = [];
|
|
1598
|
+
for (const n of notifications) {
|
|
1599
|
+
const issue = await n.issue;
|
|
1600
|
+
results.push({
|
|
1601
|
+
id: n.id,
|
|
1602
|
+
type: n.type,
|
|
1603
|
+
createdAt: n.createdAt,
|
|
1604
|
+
archivedAt: n.archivedAt ?? null,
|
|
1605
|
+
issue: issue ? { id: issue.id, identifier: issue.identifier, title: issue.title, url: issue.url } : null
|
|
1606
|
+
});
|
|
1607
|
+
}
|
|
1608
|
+
const format = getFormat(globalOpts.format);
|
|
1609
|
+
printResult({ data: results }, format);
|
|
1610
|
+
});
|
|
1611
|
+
});
|
|
1612
|
+
inbox.command("dismiss").description("Dismiss (archive) a notification").argument("<id>", "Notification ID").action(async (id, _opts, cmd) => {
|
|
1613
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1614
|
+
await runWithClient(globalOpts, async (client) => {
|
|
1615
|
+
const result = await client.archiveNotification(id);
|
|
1616
|
+
const format = getFormat(globalOpts.format);
|
|
1617
|
+
printResult(
|
|
1618
|
+
{
|
|
1619
|
+
data: {
|
|
1620
|
+
id,
|
|
1621
|
+
status: "dismissed",
|
|
1622
|
+
success: result.success
|
|
1623
|
+
}
|
|
1624
|
+
},
|
|
1625
|
+
format
|
|
1626
|
+
);
|
|
1627
|
+
});
|
|
1628
|
+
});
|
|
1629
|
+
inbox.command("dismiss-all").description("Dismiss all unprocessed notifications").action(async (_opts, cmd) => {
|
|
1630
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1631
|
+
await runWithClient(globalOpts, async (client) => {
|
|
1632
|
+
const notificationsConnection = await client.notifications();
|
|
1633
|
+
const unarchived = notificationsConnection.nodes.filter(
|
|
1634
|
+
(n) => !n.archivedAt
|
|
1635
|
+
);
|
|
1636
|
+
let dismissed = 0;
|
|
1637
|
+
for (const n of unarchived) {
|
|
1638
|
+
await client.archiveNotification(n.id);
|
|
1639
|
+
dismissed++;
|
|
1640
|
+
}
|
|
1641
|
+
const format = getFormat(globalOpts.format);
|
|
1642
|
+
printResult(
|
|
1643
|
+
{
|
|
1644
|
+
data: {
|
|
1645
|
+
status: "dismissed-all",
|
|
1646
|
+
count: dismissed
|
|
1647
|
+
}
|
|
1648
|
+
},
|
|
1649
|
+
format
|
|
1650
|
+
);
|
|
1651
|
+
});
|
|
1652
|
+
});
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
// src/commands/delegate.ts
|
|
1656
|
+
function registerDelegateCommands(program2) {
|
|
1657
|
+
const delegate = program2.command("delegate").description("Delegation shortcuts for assigning agents to issues");
|
|
1658
|
+
delegate.command("assign").description("Delegate an issue to an agent").argument("<issue-id>", "Issue identifier (e.g., TEAM-123)").requiredOption("--to <agent>", "Agent name, email, ID, or 'me'").action(async (issueId, opts, cmd) => {
|
|
1659
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1660
|
+
await runWithClient(globalOpts, async (client, { credentials }) => {
|
|
1661
|
+
const delegateId = await resolveUser(opts.to, credentials, client);
|
|
1662
|
+
await client.updateIssue(issueId, { delegateId });
|
|
1663
|
+
const format = getFormat(globalOpts.format);
|
|
1664
|
+
printResult(
|
|
1665
|
+
{ data: { status: "delegated", issueId, delegateId } },
|
|
1666
|
+
format
|
|
1667
|
+
);
|
|
1668
|
+
});
|
|
1669
|
+
});
|
|
1670
|
+
delegate.command("list").description("List issues delegated to this agent").action(async (_opts, cmd) => {
|
|
1671
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1672
|
+
await runWithClient(globalOpts, async (client, { credentials }) => {
|
|
1673
|
+
const result = await client.issues({
|
|
1674
|
+
filter: { delegate: { id: { eq: credentials.actorId } } }
|
|
1675
|
+
});
|
|
1676
|
+
const issueData = await Promise.all(
|
|
1677
|
+
result.nodes.map(async (issue) => {
|
|
1678
|
+
const state = await issue.state;
|
|
1679
|
+
const assignee = await issue.assignee;
|
|
1680
|
+
return {
|
|
1681
|
+
id: issue.identifier,
|
|
1682
|
+
title: issue.title,
|
|
1683
|
+
state: state?.name ?? null,
|
|
1684
|
+
assignee: assignee?.name ?? null,
|
|
1685
|
+
priority: issue.priority
|
|
1686
|
+
};
|
|
1687
|
+
})
|
|
1688
|
+
);
|
|
1689
|
+
const format = getFormat(globalOpts.format);
|
|
1690
|
+
printResult({ data: issueData }, format);
|
|
1691
|
+
});
|
|
1692
|
+
});
|
|
1693
|
+
delegate.command("remove").description("Remove delegation from an issue").argument("<issue-id>", "Issue identifier (e.g., TEAM-123)").action(async (issueId, _opts, cmd) => {
|
|
1694
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1695
|
+
await runWithClient(globalOpts, async (client) => {
|
|
1696
|
+
await client.updateIssue(issueId, { delegateId: null });
|
|
1697
|
+
const format = getFormat(globalOpts.format);
|
|
1698
|
+
printResult(
|
|
1699
|
+
{ data: { status: "delegation_removed", issueId } },
|
|
1700
|
+
format
|
|
1701
|
+
);
|
|
1702
|
+
});
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
// src/commands/label.ts
|
|
1707
|
+
function registerLabelCommands(program2) {
|
|
1708
|
+
const label = program2.command("label").description("Manage labels");
|
|
1709
|
+
label.command("list").description("List all labels in the workspace").option("--team <team>", "Filter by team name or ID").action(async (opts, cmd) => {
|
|
1710
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1711
|
+
await runWithClient(globalOpts, async (client) => {
|
|
1712
|
+
const filter = {};
|
|
1713
|
+
if (opts.team) {
|
|
1714
|
+
filter.team = { name: { eqIgnoreCase: opts.team } };
|
|
1715
|
+
}
|
|
1716
|
+
const result = await client.issueLabels({
|
|
1717
|
+
filter: Object.keys(filter).length > 0 ? filter : void 0
|
|
1718
|
+
});
|
|
1719
|
+
const labels = await Promise.all(
|
|
1720
|
+
result.nodes.map(async (l) => {
|
|
1721
|
+
const team = await l.team;
|
|
1722
|
+
return {
|
|
1723
|
+
id: l.id,
|
|
1724
|
+
name: l.name,
|
|
1725
|
+
color: l.color,
|
|
1726
|
+
team: team?.name ?? null
|
|
1727
|
+
};
|
|
1728
|
+
})
|
|
1729
|
+
);
|
|
1730
|
+
const format = getFormat(globalOpts.format);
|
|
1731
|
+
printResult({ data: labels }, format);
|
|
1732
|
+
});
|
|
1733
|
+
});
|
|
1734
|
+
label.command("create").description("Create a new label").requiredOption("--name <text>", "Label name").option("--color <hex>", "Label color as hex").option("--team <team>", "Team for team-scoped label").action(async (opts, cmd) => {
|
|
1735
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1736
|
+
await runWithClient(globalOpts, async (client) => {
|
|
1737
|
+
const input = { name: opts.name };
|
|
1738
|
+
if (opts.color) input.color = opts.color;
|
|
1739
|
+
if (opts.team) {
|
|
1740
|
+
const teams = await client.teams({
|
|
1741
|
+
filter: { name: { eqIgnoreCase: opts.team } }
|
|
1742
|
+
});
|
|
1743
|
+
const team = teams.nodes[0];
|
|
1744
|
+
if (!team) {
|
|
1745
|
+
console.error(`Error: Team "${opts.team}" not found`);
|
|
1746
|
+
process.exit(4);
|
|
1747
|
+
}
|
|
1748
|
+
input.teamId = team.id;
|
|
1749
|
+
}
|
|
1750
|
+
const result = await client.createIssueLabel(input);
|
|
1751
|
+
const created = await result.issueLabel;
|
|
1752
|
+
const format = getFormat(globalOpts.format);
|
|
1753
|
+
printResult(
|
|
1754
|
+
{
|
|
1755
|
+
data: {
|
|
1756
|
+
id: created?.id,
|
|
1757
|
+
name: created?.name,
|
|
1758
|
+
color: created?.color
|
|
1759
|
+
}
|
|
1760
|
+
},
|
|
1761
|
+
format
|
|
1762
|
+
);
|
|
1763
|
+
});
|
|
1764
|
+
});
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
// src/commands/user.ts
|
|
1768
|
+
function registerUserCommands(program2) {
|
|
1769
|
+
const user = program2.command("user").description("User and agent discovery");
|
|
1770
|
+
user.command("list").description("List all users and agents in the workspace").option("--type <type>", "Filter by entity type (user, app, bot)").option("--team <team>", "Filter by team membership").action(async (opts, cmd) => {
|
|
1771
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1772
|
+
await runWithClient(globalOpts, async (client) => {
|
|
1773
|
+
let users;
|
|
1774
|
+
if (opts.team) {
|
|
1775
|
+
const teams = await client.teams({
|
|
1776
|
+
filter: { name: { eqIgnoreCase: opts.team } }
|
|
1777
|
+
});
|
|
1778
|
+
const team = teams.nodes[0];
|
|
1779
|
+
if (!team) {
|
|
1780
|
+
console.error(`Error: Team "${opts.team}" not found`);
|
|
1781
|
+
process.exit(4);
|
|
1782
|
+
}
|
|
1783
|
+
const members = await team.members();
|
|
1784
|
+
users = members.nodes;
|
|
1785
|
+
} else {
|
|
1786
|
+
const result = await client.users();
|
|
1787
|
+
users = result.nodes;
|
|
1788
|
+
}
|
|
1789
|
+
let userData = users.map((u) => ({
|
|
1790
|
+
id: u.id,
|
|
1791
|
+
name: u.name,
|
|
1792
|
+
displayName: u.displayName,
|
|
1793
|
+
email: u.email ?? null,
|
|
1794
|
+
isMe: u.isMe ?? null,
|
|
1795
|
+
active: u.active
|
|
1796
|
+
}));
|
|
1797
|
+
if (opts.type) {
|
|
1798
|
+
const type = opts.type.toLowerCase();
|
|
1799
|
+
userData = userData.filter((u) => {
|
|
1800
|
+
if (type === "app" || type === "bot") return !u.email;
|
|
1801
|
+
if (type === "user") return !!u.email;
|
|
1802
|
+
return true;
|
|
1803
|
+
});
|
|
1804
|
+
}
|
|
1805
|
+
const format = getFormat(globalOpts.format);
|
|
1806
|
+
printResult({ data: userData }, format);
|
|
1807
|
+
});
|
|
1808
|
+
});
|
|
1809
|
+
user.command("search").description("Search users/agents by name or email").argument("<query>", "Search query").action(async (query, _opts, cmd) => {
|
|
1810
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1811
|
+
await runWithClient(globalOpts, async (client) => {
|
|
1812
|
+
const result = await client.users();
|
|
1813
|
+
const queryLower = query.toLowerCase();
|
|
1814
|
+
const matches = result.nodes.filter(
|
|
1815
|
+
(u) => u.name?.toLowerCase().includes(queryLower) || u.email?.toLowerCase().includes(queryLower) || u.displayName?.toLowerCase().includes(queryLower)
|
|
1816
|
+
);
|
|
1817
|
+
const userData = matches.map((u) => ({
|
|
1818
|
+
id: u.id,
|
|
1819
|
+
name: u.name,
|
|
1820
|
+
displayName: u.displayName,
|
|
1821
|
+
email: u.email ?? null
|
|
1822
|
+
}));
|
|
1823
|
+
const format = getFormat(globalOpts.format);
|
|
1824
|
+
printResult({ data: userData }, format);
|
|
1825
|
+
});
|
|
1826
|
+
});
|
|
1827
|
+
user.command("me").description("Show this agent's identity").action(async (_opts, cmd) => {
|
|
1828
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1829
|
+
const agent = globalOpts.agent;
|
|
1830
|
+
if (!agent) {
|
|
1831
|
+
console.error("Error: --agent is required (or set LINEAR_AGENT_ID env var)");
|
|
1832
|
+
process.exit(4);
|
|
1833
|
+
}
|
|
1834
|
+
const credentialsDir = getCredentialsDir(globalOpts);
|
|
1835
|
+
const credentials = readCredentials(agent, credentialsDir);
|
|
1836
|
+
const format = getFormat(globalOpts.format);
|
|
1837
|
+
printResult(
|
|
1838
|
+
{
|
|
1839
|
+
data: {
|
|
1840
|
+
agent,
|
|
1841
|
+
actorId: credentials.actorId,
|
|
1842
|
+
workspace: credentials.workspaceSlug,
|
|
1843
|
+
authMethod: credentials.authMethod,
|
|
1844
|
+
tokenExpiresAt: credentials.tokenExpiresAt
|
|
1845
|
+
}
|
|
1846
|
+
},
|
|
1847
|
+
format
|
|
1848
|
+
);
|
|
1849
|
+
});
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
// src/commands/team.ts
|
|
1853
|
+
function registerTeamCommands(program2) {
|
|
1854
|
+
const team = program2.command("team").description("Team queries");
|
|
1855
|
+
team.command("list").description("List all teams").action(async (_opts, cmd) => {
|
|
1856
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1857
|
+
await runWithClient(globalOpts, async (client) => {
|
|
1858
|
+
const result = await client.teams();
|
|
1859
|
+
const teams = result.nodes.map((t) => ({
|
|
1860
|
+
id: t.id,
|
|
1861
|
+
key: t.key,
|
|
1862
|
+
name: t.name,
|
|
1863
|
+
description: t.description ?? null
|
|
1864
|
+
}));
|
|
1865
|
+
const format = getFormat(globalOpts.format);
|
|
1866
|
+
printResult({ data: teams }, format);
|
|
1867
|
+
});
|
|
1868
|
+
});
|
|
1869
|
+
team.command("members").description("List members of a team").argument("<team>", "Team name or key").action(async (teamName, _opts, cmd) => {
|
|
1870
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1871
|
+
await runWithClient(globalOpts, async (client) => {
|
|
1872
|
+
const teams = await client.teams({
|
|
1873
|
+
filter: { name: { eqIgnoreCase: teamName } }
|
|
1874
|
+
});
|
|
1875
|
+
let team2 = teams.nodes[0];
|
|
1876
|
+
if (!team2) {
|
|
1877
|
+
const byKey = await client.teams({
|
|
1878
|
+
filter: { key: { eq: teamName.toUpperCase() } }
|
|
1879
|
+
});
|
|
1880
|
+
team2 = byKey.nodes[0];
|
|
1881
|
+
}
|
|
1882
|
+
if (!team2) {
|
|
1883
|
+
console.error(`Error: Team "${teamName}" not found`);
|
|
1884
|
+
process.exit(4);
|
|
1885
|
+
}
|
|
1886
|
+
const members = await team2.members();
|
|
1887
|
+
const memberData = members.nodes.map((m) => ({
|
|
1888
|
+
id: m.id,
|
|
1889
|
+
name: m.name,
|
|
1890
|
+
displayName: m.displayName,
|
|
1891
|
+
email: m.email ?? null,
|
|
1892
|
+
active: m.active
|
|
1893
|
+
}));
|
|
1894
|
+
const format = getFormat(globalOpts.format);
|
|
1895
|
+
printResult({ data: memberData }, format);
|
|
1896
|
+
});
|
|
1897
|
+
});
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
// src/commands/project.ts
|
|
1901
|
+
var import_fs5 = require("fs");
|
|
1902
|
+
async function resolveTeamId(client, team) {
|
|
1903
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(team)) {
|
|
1904
|
+
return team;
|
|
1905
|
+
}
|
|
1906
|
+
const teams = await client.teams({
|
|
1907
|
+
filter: { name: { eqIgnoreCase: team } }
|
|
1908
|
+
});
|
|
1909
|
+
const match = teams.nodes[0];
|
|
1910
|
+
if (!match) {
|
|
1911
|
+
console.error(`Error: Team "${team}" not found`);
|
|
1912
|
+
process.exit(4);
|
|
1913
|
+
}
|
|
1914
|
+
return match.id;
|
|
1915
|
+
}
|
|
1916
|
+
function registerProjectCommands(program2) {
|
|
1917
|
+
const project = program2.command("project").description("Project queries");
|
|
1918
|
+
project.command("create").description("Create a new project").requiredOption("--name <text>", "Project name").requiredOption("--team <team>", "Associate project with team (name or ID)").option("--description <text>", "Project description (markdown, 255-char limit)").option("--description-file <path>", "Read description from file").option("--content <text>", "Project overview content (long-form markdown)").option("--content-file <path>", "Read project overview content from file").option("--start-date <date>", "Start date (YYYY-MM-DD)").option("--target-date <date>", "Target date (YYYY-MM-DD)").option("--lead <user>", "Project lead (name or email)").option("--priority <n>", "Priority (0=none, 1=urgent, 2=high, 3=normal, 4=low)").action(async (opts, cmd) => {
|
|
1919
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1920
|
+
await runWithClient(globalOpts, async (client, { credentials }) => {
|
|
1921
|
+
const input = {
|
|
1922
|
+
name: opts.name,
|
|
1923
|
+
teamIds: [await resolveTeamId(client, opts.team)]
|
|
1924
|
+
};
|
|
1925
|
+
if (opts.descriptionFile) {
|
|
1926
|
+
input.description = (0, import_fs5.readFileSync)(opts.descriptionFile, "utf-8");
|
|
1927
|
+
} else if (opts.description) {
|
|
1928
|
+
input.description = opts.description;
|
|
1929
|
+
}
|
|
1930
|
+
if (opts.contentFile) {
|
|
1931
|
+
input.content = (0, import_fs5.readFileSync)(opts.contentFile, "utf-8");
|
|
1932
|
+
} else if (opts.content) {
|
|
1933
|
+
input.content = opts.content;
|
|
1934
|
+
}
|
|
1935
|
+
if (opts.startDate) {
|
|
1936
|
+
input.startDate = opts.startDate;
|
|
1937
|
+
}
|
|
1938
|
+
if (opts.targetDate) {
|
|
1939
|
+
input.targetDate = opts.targetDate;
|
|
1940
|
+
}
|
|
1941
|
+
if (opts.lead) {
|
|
1942
|
+
input.leadId = await resolveUser(opts.lead, credentials, client);
|
|
1943
|
+
}
|
|
1944
|
+
if (opts.priority !== void 0) {
|
|
1945
|
+
const priority = parseInt(opts.priority, 10);
|
|
1946
|
+
if (isNaN(priority) || priority < 0 || priority > 4) {
|
|
1947
|
+
console.error(`Invalid value for --priority: "${opts.priority}". Expected an integer between 0 and 4.`);
|
|
1948
|
+
process.exit(1);
|
|
1949
|
+
}
|
|
1950
|
+
input.priority = priority;
|
|
1951
|
+
}
|
|
1952
|
+
const payload = await client.createProject(input);
|
|
1953
|
+
const created = await payload.project;
|
|
1954
|
+
const format = getFormat(globalOpts.format);
|
|
1955
|
+
printResult(
|
|
1956
|
+
{
|
|
1957
|
+
data: {
|
|
1958
|
+
id: created?.id ?? null,
|
|
1959
|
+
name: created?.name ?? opts.name,
|
|
1960
|
+
url: created?.url ?? null,
|
|
1961
|
+
success: payload.success
|
|
1962
|
+
}
|
|
1963
|
+
},
|
|
1964
|
+
format
|
|
1965
|
+
);
|
|
1966
|
+
});
|
|
1967
|
+
});
|
|
1968
|
+
project.command("list").description("List projects").option("--team <team>", "Filter by team").action(async (opts, cmd) => {
|
|
1969
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1970
|
+
await runWithClient(globalOpts, async (client) => {
|
|
1971
|
+
const filter = {};
|
|
1972
|
+
if (opts.team) {
|
|
1973
|
+
filter.accessibleTeams = { name: { eqIgnoreCase: opts.team } };
|
|
1974
|
+
}
|
|
1975
|
+
const result = await client.projects({
|
|
1976
|
+
filter: Object.keys(filter).length > 0 ? filter : void 0
|
|
1977
|
+
});
|
|
1978
|
+
const projects = result.nodes.map((p) => ({
|
|
1979
|
+
id: p.id,
|
|
1980
|
+
name: p.name,
|
|
1981
|
+
state: p.state,
|
|
1982
|
+
progress: p.progress,
|
|
1983
|
+
startDate: p.startDate ?? null,
|
|
1984
|
+
targetDate: p.targetDate ?? null
|
|
1985
|
+
}));
|
|
1986
|
+
const format = getFormat(globalOpts.format);
|
|
1987
|
+
printResult({ data: projects }, format);
|
|
1988
|
+
});
|
|
1989
|
+
});
|
|
1990
|
+
project.command("get").description("Get project details").argument("<id>", "Project ID").action(async (id, _opts, cmd) => {
|
|
1991
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
1992
|
+
await runWithClient(globalOpts, async (client) => {
|
|
1993
|
+
const p = await client.project(id);
|
|
1994
|
+
const format = getFormat(globalOpts.format);
|
|
1995
|
+
printResult(
|
|
1996
|
+
{
|
|
1997
|
+
data: {
|
|
1998
|
+
id: p.id,
|
|
1999
|
+
name: p.name,
|
|
2000
|
+
description: p.description ?? null,
|
|
2001
|
+
content: p.content ?? null,
|
|
2002
|
+
state: p.state,
|
|
2003
|
+
progress: p.progress,
|
|
2004
|
+
startDate: p.startDate ?? null,
|
|
2005
|
+
targetDate: p.targetDate ?? null
|
|
2006
|
+
}
|
|
2007
|
+
},
|
|
2008
|
+
format
|
|
2009
|
+
);
|
|
2010
|
+
});
|
|
2011
|
+
});
|
|
2012
|
+
project.command("update").description("Update project metadata").argument("<id>", "Project ID").option("--name <text>", "New project name").option("--description <text>", "Project description (markdown, 255-char limit)").option("--description-file <path>", "Read description from file").option("--content <text>", "Project overview content (long-form markdown)").option("--content-file <path>", "Read project overview content from file").option("--start-date <date>", 'Start date (YYYY-MM-DD, or "null" to clear)').option("--target-date <date>", 'Target date (YYYY-MM-DD, or "null" to clear)').option("--lead <user>", 'Project lead (name, email, or "null" to clear)').option("--priority <n>", "Priority (0=none, 1=urgent, 2=high, 3=normal, 4=low)").action(async (id, opts, cmd) => {
|
|
2013
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
2014
|
+
await runWithClient(globalOpts, async (client, { credentials }) => {
|
|
2015
|
+
const input = {};
|
|
2016
|
+
if (opts.name) {
|
|
2017
|
+
input.name = opts.name;
|
|
2018
|
+
}
|
|
2019
|
+
if (opts.descriptionFile) {
|
|
2020
|
+
input.description = (0, import_fs5.readFileSync)(opts.descriptionFile, "utf-8");
|
|
2021
|
+
} else if (opts.description) {
|
|
2022
|
+
input.description = opts.description;
|
|
2023
|
+
}
|
|
2024
|
+
if (opts.contentFile) {
|
|
2025
|
+
input.content = (0, import_fs5.readFileSync)(opts.contentFile, "utf-8");
|
|
2026
|
+
} else if (opts.content) {
|
|
2027
|
+
input.content = opts.content;
|
|
2028
|
+
}
|
|
2029
|
+
if (opts.startDate !== void 0) {
|
|
2030
|
+
input.startDate = opts.startDate === "null" ? null : opts.startDate;
|
|
2031
|
+
}
|
|
2032
|
+
if (opts.targetDate !== void 0) {
|
|
2033
|
+
input.targetDate = opts.targetDate === "null" ? null : opts.targetDate;
|
|
2034
|
+
}
|
|
2035
|
+
if (opts.lead !== void 0) {
|
|
2036
|
+
if (opts.lead === "null") {
|
|
2037
|
+
input.leadId = null;
|
|
2038
|
+
} else {
|
|
2039
|
+
input.leadId = await resolveUser(opts.lead, credentials, client);
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
if (opts.priority !== void 0) {
|
|
2043
|
+
const priority = parseInt(opts.priority, 10);
|
|
2044
|
+
if (isNaN(priority) || priority < 0 || priority > 4) {
|
|
2045
|
+
console.error(`Invalid value for --priority: "${opts.priority}". Expected an integer between 0 and 4.`);
|
|
2046
|
+
process.exit(1);
|
|
2047
|
+
}
|
|
2048
|
+
input.priority = priority;
|
|
2049
|
+
}
|
|
2050
|
+
const payload = await client.updateProject(id, input);
|
|
2051
|
+
const updated = await payload.project;
|
|
2052
|
+
const format = getFormat(globalOpts.format);
|
|
2053
|
+
printResult(
|
|
2054
|
+
{
|
|
2055
|
+
data: {
|
|
2056
|
+
id: updated?.id ?? id,
|
|
2057
|
+
name: updated?.name ?? null,
|
|
2058
|
+
url: updated?.url ?? null,
|
|
2059
|
+
success: payload.success
|
|
2060
|
+
}
|
|
2061
|
+
},
|
|
2062
|
+
format
|
|
2063
|
+
);
|
|
2064
|
+
});
|
|
2065
|
+
});
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
// src/commands/attachment.ts
|
|
2069
|
+
var import_fs6 = require("fs");
|
|
2070
|
+
var import_path3 = require("path");
|
|
2071
|
+
var CONTENT_TYPES = {
|
|
2072
|
+
".png": "image/png",
|
|
2073
|
+
".jpg": "image/jpeg",
|
|
2074
|
+
".jpeg": "image/jpeg",
|
|
2075
|
+
".gif": "image/gif",
|
|
2076
|
+
".webp": "image/webp",
|
|
2077
|
+
".svg": "image/svg+xml",
|
|
2078
|
+
".pdf": "application/pdf",
|
|
2079
|
+
".txt": "text/plain",
|
|
2080
|
+
".md": "text/markdown",
|
|
2081
|
+
".json": "application/json",
|
|
2082
|
+
".csv": "text/csv",
|
|
2083
|
+
".zip": "application/zip"
|
|
2084
|
+
};
|
|
2085
|
+
function registerAttachmentCommands(program2) {
|
|
2086
|
+
const attachment = program2.command("attachment").description("Manage issue attachments");
|
|
2087
|
+
attachment.command("add").description("Add an attachment to an issue (idempotent per URL)").argument("<issue-id>", "Issue identifier (e.g., TEAM-123)").requiredOption("--url <url>", "URL to attach").option("--title <text>", "Display title for the link").action(async (issueId, opts, cmd) => {
|
|
2088
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
2089
|
+
await runWithClient(globalOpts, async (client) => {
|
|
2090
|
+
const input = {
|
|
2091
|
+
issueId,
|
|
2092
|
+
url: opts.url
|
|
2093
|
+
};
|
|
2094
|
+
if (opts.title) input.title = opts.title;
|
|
2095
|
+
const result = await client.createAttachment(input);
|
|
2096
|
+
const created = await result.attachment;
|
|
2097
|
+
const format = getFormat(globalOpts.format);
|
|
2098
|
+
printResult(
|
|
2099
|
+
{
|
|
2100
|
+
data: {
|
|
2101
|
+
id: created?.id,
|
|
2102
|
+
url: opts.url,
|
|
2103
|
+
title: opts.title ?? null,
|
|
2104
|
+
issueId
|
|
2105
|
+
}
|
|
2106
|
+
},
|
|
2107
|
+
format
|
|
2108
|
+
);
|
|
2109
|
+
});
|
|
2110
|
+
});
|
|
2111
|
+
attachment.command("list").description("List attachments on an issue").argument("<issue-id>", "Issue identifier (e.g., TEAM-123)").action(async (issueId, _opts, cmd) => {
|
|
2112
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
2113
|
+
await runWithClient(globalOpts, async (client) => {
|
|
2114
|
+
const issue = await client.issue(issueId);
|
|
2115
|
+
const attachments = await issue.attachments();
|
|
2116
|
+
const attachmentData = attachments.nodes.map((a) => ({
|
|
2117
|
+
id: a.id,
|
|
2118
|
+
url: a.url,
|
|
2119
|
+
title: a.title ?? null
|
|
2120
|
+
}));
|
|
2121
|
+
const format = getFormat(globalOpts.format);
|
|
2122
|
+
printResult({ data: attachmentData }, format);
|
|
2123
|
+
});
|
|
2124
|
+
});
|
|
2125
|
+
attachment.command("remove").description("Remove an attachment").argument("<attachment-id>", "Attachment ID").action(async (attachmentId, _opts, cmd) => {
|
|
2126
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
2127
|
+
await runWithClient(globalOpts, async (client) => {
|
|
2128
|
+
await client.deleteAttachment(attachmentId);
|
|
2129
|
+
const format = getFormat(globalOpts.format);
|
|
2130
|
+
printResult(
|
|
2131
|
+
{ data: { status: "removed", attachmentId } },
|
|
2132
|
+
format
|
|
2133
|
+
);
|
|
2134
|
+
});
|
|
2135
|
+
});
|
|
2136
|
+
attachment.command("upload").description("Upload a local file and attach it to an issue or project").argument("<file-path>", "Path to the local file to upload").option("--issue <id>", "Issue identifier to attach the file to (e.g., TEAM-123)").option("--project <id>", "Project name or ID to upload the file for").option("--title <text>", "Display title for the attachment (defaults to filename)").action(async (filePath, opts, cmd) => {
|
|
2137
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
2138
|
+
await runWithClient(globalOpts, async (client) => {
|
|
2139
|
+
if (!opts.issue && !opts.project) {
|
|
2140
|
+
console.error("Error: --issue or --project is required");
|
|
2141
|
+
process.exit(4);
|
|
2142
|
+
}
|
|
2143
|
+
const resolvedPath = (0, import_path3.resolve)(filePath);
|
|
2144
|
+
let stat;
|
|
2145
|
+
try {
|
|
2146
|
+
stat = (0, import_fs6.statSync)(resolvedPath);
|
|
2147
|
+
} catch {
|
|
2148
|
+
console.error(`Error: file not found: ${filePath}`);
|
|
2149
|
+
process.exit(4);
|
|
2150
|
+
}
|
|
2151
|
+
if (!stat.isFile()) {
|
|
2152
|
+
console.error(`Error: ${filePath} is not a file`);
|
|
2153
|
+
process.exit(4);
|
|
2154
|
+
}
|
|
2155
|
+
const filename = (0, import_path3.basename)(resolvedPath);
|
|
2156
|
+
const ext = (0, import_path3.extname)(filename).toLowerCase();
|
|
2157
|
+
const contentType = CONTENT_TYPES[ext] ?? "application/octet-stream";
|
|
2158
|
+
const size = stat.size;
|
|
2159
|
+
const uploadPayload = await client.fileUpload(contentType, filename, size);
|
|
2160
|
+
const uploadFile = uploadPayload.uploadFile;
|
|
2161
|
+
if (!uploadFile) {
|
|
2162
|
+
throw new Error("Failed to get upload URL from Linear");
|
|
2163
|
+
}
|
|
2164
|
+
let fileContent;
|
|
2165
|
+
try {
|
|
2166
|
+
fileContent = (0, import_fs6.readFileSync)(resolvedPath);
|
|
2167
|
+
} catch {
|
|
2168
|
+
console.error(`Error: could not read file: ${filePath}`);
|
|
2169
|
+
process.exit(4);
|
|
2170
|
+
}
|
|
2171
|
+
const headers = {
|
|
2172
|
+
"Content-Type": contentType,
|
|
2173
|
+
"Cache-Control": "public, max-age=31536000"
|
|
2174
|
+
};
|
|
2175
|
+
for (const h of uploadFile.headers) {
|
|
2176
|
+
headers[h.key] = h.value;
|
|
2177
|
+
}
|
|
2178
|
+
const uploadResponse = await fetch(uploadFile.uploadUrl, {
|
|
2179
|
+
method: "PUT",
|
|
2180
|
+
headers,
|
|
2181
|
+
body: fileContent
|
|
2182
|
+
});
|
|
2183
|
+
if (!uploadResponse.ok) {
|
|
2184
|
+
throw new Error(
|
|
2185
|
+
`Upload failed: ${uploadResponse.status} ${uploadResponse.statusText}`
|
|
2186
|
+
);
|
|
2187
|
+
}
|
|
2188
|
+
const title = opts.title ?? filename;
|
|
2189
|
+
const format = getFormat(globalOpts.format);
|
|
2190
|
+
if (opts.issue) {
|
|
2191
|
+
const result = await client.createAttachment({
|
|
2192
|
+
issueId: opts.issue,
|
|
2193
|
+
url: uploadFile.assetUrl,
|
|
2194
|
+
title
|
|
2195
|
+
});
|
|
2196
|
+
const created = await result.attachment;
|
|
2197
|
+
printResult(
|
|
2198
|
+
{
|
|
2199
|
+
data: {
|
|
2200
|
+
id: created?.id,
|
|
2201
|
+
url: uploadFile.assetUrl,
|
|
2202
|
+
title,
|
|
2203
|
+
issueId: opts.issue
|
|
2204
|
+
}
|
|
2205
|
+
},
|
|
2206
|
+
format
|
|
2207
|
+
);
|
|
2208
|
+
} else {
|
|
2209
|
+
printResult(
|
|
2210
|
+
{
|
|
2211
|
+
data: {
|
|
2212
|
+
url: uploadFile.assetUrl,
|
|
2213
|
+
title,
|
|
2214
|
+
projectId: opts.project
|
|
2215
|
+
}
|
|
2216
|
+
},
|
|
2217
|
+
format
|
|
2218
|
+
);
|
|
2219
|
+
}
|
|
2220
|
+
});
|
|
2221
|
+
});
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
// src/commands/state.ts
|
|
2225
|
+
function registerStateCommands(program2) {
|
|
2226
|
+
const state = program2.command("state").description("Workflow state queries");
|
|
2227
|
+
state.command("list").description("List workflow states").option("--team <team>", "Filter by team name or key").action(async (opts, cmd) => {
|
|
2228
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
2229
|
+
await runWithClient(globalOpts, async (client, { agentId, credentialsDir }) => {
|
|
2230
|
+
let cache = readCache(agentId, credentialsDir);
|
|
2231
|
+
if (opts.team) {
|
|
2232
|
+
const teams = await client.teams({
|
|
2233
|
+
filter: { name: { eqIgnoreCase: opts.team } }
|
|
2234
|
+
});
|
|
2235
|
+
let team = teams.nodes[0];
|
|
2236
|
+
if (!team) {
|
|
2237
|
+
const byKey = await client.teams({
|
|
2238
|
+
filter: { key: { eq: opts.team.toUpperCase() } }
|
|
2239
|
+
});
|
|
2240
|
+
team = byKey.nodes[0];
|
|
2241
|
+
}
|
|
2242
|
+
if (!team) {
|
|
2243
|
+
console.error(`Error: Team "${opts.team}" not found`);
|
|
2244
|
+
process.exit(4);
|
|
2245
|
+
}
|
|
2246
|
+
const states = await team.states();
|
|
2247
|
+
const stateData = states.nodes.map((s) => ({
|
|
2248
|
+
id: s.id,
|
|
2249
|
+
name: s.name,
|
|
2250
|
+
type: s.type,
|
|
2251
|
+
color: s.color,
|
|
2252
|
+
position: s.position,
|
|
2253
|
+
team: team.name
|
|
2254
|
+
}));
|
|
2255
|
+
const stateMap = {};
|
|
2256
|
+
for (const s of states.nodes) {
|
|
2257
|
+
stateMap[s.name] = s.id;
|
|
2258
|
+
}
|
|
2259
|
+
cache = setTeamStates(cache, team.key, stateMap);
|
|
2260
|
+
writeCache(agentId, credentialsDir, cache);
|
|
2261
|
+
const format = getFormat(globalOpts.format);
|
|
2262
|
+
printResult({ data: stateData }, format);
|
|
2263
|
+
} else {
|
|
2264
|
+
const teamsResult = await client.teams();
|
|
2265
|
+
const allStates = [];
|
|
2266
|
+
for (const t of teamsResult.nodes) {
|
|
2267
|
+
const states = await t.states();
|
|
2268
|
+
const stateMap = {};
|
|
2269
|
+
for (const s of states.nodes) {
|
|
2270
|
+
stateMap[s.name] = s.id;
|
|
2271
|
+
allStates.push({
|
|
2272
|
+
id: s.id,
|
|
2273
|
+
name: s.name,
|
|
2274
|
+
type: s.type,
|
|
2275
|
+
color: s.color,
|
|
2276
|
+
position: s.position,
|
|
2277
|
+
team: t.name
|
|
2278
|
+
});
|
|
2279
|
+
}
|
|
2280
|
+
cache = setTeamStates(cache, t.key, stateMap);
|
|
2281
|
+
}
|
|
2282
|
+
writeCache(agentId, credentialsDir, cache);
|
|
2283
|
+
const format = getFormat(globalOpts.format);
|
|
2284
|
+
printResult({ data: allStates }, format);
|
|
2285
|
+
}
|
|
2286
|
+
});
|
|
2287
|
+
});
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
// src/cli.ts
|
|
2291
|
+
var pkg = JSON.parse(
|
|
2292
|
+
(0, import_fs7.readFileSync)((0, import_path4.join)(__dirname, "..", "package.json"), "utf-8")
|
|
2293
|
+
);
|
|
2294
|
+
var program = new import_commander.Command();
|
|
2295
|
+
program.name("linear").description("CLI tool for AI agents to interact with Linear").version(pkg.version).option("--agent <id>", "agent identifier (env: LINEAR_AGENT_ID)", process.env.LINEAR_AGENT_ID).option(
|
|
2296
|
+
"--credentials-dir <path>",
|
|
2297
|
+
"path to credentials directory (env: LINEAR_AGENT_CREDENTIALS_DIR)",
|
|
2298
|
+
process.env.LINEAR_AGENT_CREDENTIALS_DIR ?? "~/.linear/credentials/"
|
|
2299
|
+
).addOption(
|
|
2300
|
+
new import_commander.Option("--format <format>", "output format (default: auto-detect TTY)").choices(["json", "text"])
|
|
2301
|
+
);
|
|
2302
|
+
registerAuthCommands(program);
|
|
2303
|
+
registerIssueCommands(program);
|
|
2304
|
+
registerCommentCommands(program);
|
|
2305
|
+
registerInboxCommands(program);
|
|
2306
|
+
registerDelegateCommands(program);
|
|
2307
|
+
registerLabelCommands(program);
|
|
2308
|
+
registerUserCommands(program);
|
|
2309
|
+
registerTeamCommands(program);
|
|
2310
|
+
registerProjectCommands(program);
|
|
2311
|
+
registerAttachmentCommands(program);
|
|
2312
|
+
registerStateCommands(program);
|
|
2313
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
2314
|
+
if (err instanceof CLIError) {
|
|
2315
|
+
console.error(`Error: ${err.message}`);
|
|
2316
|
+
if (err.resolution) console.error(err.resolution);
|
|
2317
|
+
process.exit(err.exitCode);
|
|
2318
|
+
}
|
|
2319
|
+
console.error(`Error: ${err?.message ?? String(err)}`);
|
|
2320
|
+
process.exit(1);
|
|
2321
|
+
});
|