linear-cli-agents 0.6.0 → 0.7.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.
Files changed (36) hide show
  1. package/README.md +68 -1
  2. package/dist/commands/documents/create.d.ts +13 -0
  3. package/dist/commands/documents/create.js +68 -0
  4. package/dist/commands/documents/delete.d.ts +9 -0
  5. package/dist/commands/documents/delete.js +32 -0
  6. package/dist/commands/documents/get.d.ts +12 -0
  7. package/dist/commands/documents/get.js +79 -0
  8. package/dist/commands/documents/list.d.ts +12 -0
  9. package/dist/commands/documents/list.js +105 -0
  10. package/dist/commands/documents/update.d.ts +16 -0
  11. package/dist/commands/documents/update.js +75 -0
  12. package/dist/commands/info.js +161 -3
  13. package/dist/commands/initiatives/archive.d.ts +12 -0
  14. package/dist/commands/initiatives/archive.js +44 -0
  15. package/dist/commands/initiatives/create.d.ts +15 -0
  16. package/dist/commands/initiatives/create.js +84 -0
  17. package/dist/commands/initiatives/delete.d.ts +9 -0
  18. package/dist/commands/initiatives/delete.js +32 -0
  19. package/dist/commands/initiatives/get.d.ts +12 -0
  20. package/dist/commands/initiatives/get.js +90 -0
  21. package/dist/commands/initiatives/list.d.ts +11 -0
  22. package/dist/commands/initiatives/list.js +135 -0
  23. package/dist/commands/initiatives/update.d.ts +18 -0
  24. package/dist/commands/initiatives/update.js +90 -0
  25. package/dist/commands/issues/bulk-update.d.ts +2 -0
  26. package/dist/commands/issues/bulk-update.js +10 -0
  27. package/dist/commands/issues/create.d.ts +2 -0
  28. package/dist/commands/issues/create.js +10 -0
  29. package/dist/commands/issues/get.d.ts +1 -0
  30. package/dist/commands/issues/get.js +19 -1
  31. package/dist/commands/issues/update.d.ts +2 -0
  32. package/dist/commands/issues/update.js +12 -0
  33. package/dist/commands/upload.d.ts +13 -0
  34. package/dist/commands/upload.js +117 -0
  35. package/oclif.manifest.json +1129 -408
  36. package/package.json +7 -1
@@ -0,0 +1,18 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class InitiativesUpdate extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ id: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
7
+ };
8
+ static flags: {
9
+ name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ status: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ 'target-date': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ 'owner-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ icon: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ color: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
+ };
17
+ run(): Promise<void>;
18
+ }
@@ -0,0 +1,90 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import { getClient } from '../../lib/client.js';
3
+ import { success, print } from '../../lib/output.js';
4
+ import { handleError, CliError, ErrorCodes } from '../../lib/errors.js';
5
+ import { LinearDocument } from '@linear/sdk';
6
+ const InitiativeStatus = LinearDocument.InitiativeStatus;
7
+ export default class InitiativesUpdate extends Command {
8
+ static description = 'Update an initiative';
9
+ static examples = [
10
+ '<%= config.bin %> initiatives update INITIATIVE_ID --name "New Name"',
11
+ '<%= config.bin %> initiatives update INITIATIVE_ID --status Completed',
12
+ '<%= config.bin %> initiatives update INITIATIVE_ID --target-date 2024-12-31',
13
+ ];
14
+ static args = {
15
+ id: Args.string({
16
+ description: 'Initiative ID',
17
+ required: true,
18
+ }),
19
+ };
20
+ static flags = {
21
+ name: Flags.string({
22
+ char: 'n',
23
+ description: 'New initiative name',
24
+ }),
25
+ description: Flags.string({
26
+ char: 'd',
27
+ description: 'New description',
28
+ }),
29
+ status: Flags.string({
30
+ char: 's',
31
+ description: 'New status',
32
+ options: ['Planned', 'Active', 'Completed'],
33
+ }),
34
+ 'target-date': Flags.string({
35
+ description: 'New target completion date (YYYY-MM-DD)',
36
+ }),
37
+ 'owner-id': Flags.string({
38
+ description: 'New owner user ID',
39
+ }),
40
+ icon: Flags.string({
41
+ description: 'New initiative icon (emoji)',
42
+ }),
43
+ color: Flags.string({
44
+ description: 'New initiative color (hex)',
45
+ }),
46
+ };
47
+ async run() {
48
+ try {
49
+ const { args, flags } = await this.parse(InitiativesUpdate);
50
+ const client = getClient();
51
+ const input = {};
52
+ if (flags.name)
53
+ input.name = flags.name;
54
+ if (flags.description)
55
+ input.description = flags.description;
56
+ if (flags.status) {
57
+ input.status = InitiativeStatus[flags.status];
58
+ }
59
+ if (flags['target-date'])
60
+ input.targetDate = flags['target-date'];
61
+ if (flags['owner-id'])
62
+ input.ownerId = flags['owner-id'];
63
+ if (flags.icon)
64
+ input.icon = flags.icon;
65
+ if (flags.color)
66
+ input.color = flags.color;
67
+ if (Object.keys(input).length === 0) {
68
+ throw new CliError(ErrorCodes.INVALID_INPUT, 'No update fields provided');
69
+ }
70
+ const payload = await client.updateInitiative(args.id, input);
71
+ if (!payload.success) {
72
+ throw new CliError(ErrorCodes.API_ERROR, 'Failed to update initiative');
73
+ }
74
+ const initiative = await payload.initiative;
75
+ if (!initiative) {
76
+ throw new CliError(ErrorCodes.API_ERROR, 'Initiative not returned');
77
+ }
78
+ print(success({
79
+ id: initiative.id,
80
+ name: initiative.name,
81
+ status: initiative.status,
82
+ updatedAt: initiative.updatedAt,
83
+ }));
84
+ }
85
+ catch (err) {
86
+ handleError(err);
87
+ this.exit(1);
88
+ }
89
+ }
90
+ }
@@ -10,6 +10,8 @@ export default class IssuesBulkUpdate extends Command {
10
10
  'project-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
11
  estimate: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
12
  'label-ids': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ 'due-date': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ 'cycle-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
15
  };
14
16
  run(): Promise<void>;
15
17
  }
@@ -35,6 +35,12 @@ export default class IssuesBulkUpdate extends Command {
35
35
  'label-ids': Flags.string({
36
36
  description: 'Comma-separated label IDs (replaces existing labels)',
37
37
  }),
38
+ 'due-date': Flags.string({
39
+ description: 'Due date (YYYY-MM-DD)',
40
+ }),
41
+ 'cycle-id': Flags.string({
42
+ description: 'Cycle (sprint) ID',
43
+ }),
38
44
  };
39
45
  async run() {
40
46
  try {
@@ -55,6 +61,10 @@ export default class IssuesBulkUpdate extends Command {
55
61
  input.estimate = flags.estimate;
56
62
  if (flags['label-ids'])
57
63
  input.labelIds = flags['label-ids'].split(',').map((id) => id.trim());
64
+ if (flags['due-date'])
65
+ input.dueDate = flags['due-date'];
66
+ if (flags['cycle-id'])
67
+ input.cycleId = flags['cycle-id'];
58
68
  if (Object.keys(input).length === 0) {
59
69
  throw new CliError(ErrorCodes.INVALID_INPUT, 'No update fields provided. Use at least one of: --state-id, --priority, --assignee-id, --project-id, --estimate, --label-ids');
60
70
  }
@@ -13,6 +13,8 @@ export default class IssuesCreate extends Command {
13
13
  'project-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
14
  estimate: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
15
  'label-ids': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
+ 'due-date': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
17
+ 'cycle-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
18
  };
17
19
  run(): Promise<void>;
18
20
  }
@@ -46,6 +46,12 @@ export default class IssuesCreate extends Command {
46
46
  'label-ids': Flags.string({
47
47
  description: 'Comma-separated label IDs',
48
48
  }),
49
+ 'due-date': Flags.string({
50
+ description: 'Due date (YYYY-MM-DD)',
51
+ }),
52
+ 'cycle-id': Flags.string({
53
+ description: 'Cycle (sprint) ID',
54
+ }),
49
55
  };
50
56
  async run() {
51
57
  try {
@@ -89,6 +95,10 @@ export default class IssuesCreate extends Command {
89
95
  input.estimate = flags.estimate;
90
96
  if (flags['label-ids'])
91
97
  input.labelIds = flags['label-ids'].split(',');
98
+ if (flags['due-date'])
99
+ input.dueDate = flags['due-date'];
100
+ if (flags['cycle-id'])
101
+ input.cycleId = flags['cycle-id'];
92
102
  }
93
103
  // Create the issue
94
104
  const payload = await client.createIssue(input);
@@ -7,6 +7,7 @@ export default class IssuesGet extends Command {
7
7
  };
8
8
  static flags: {
9
9
  format: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
+ 'with-attachments': import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
11
  };
11
12
  run(): Promise<void>;
12
13
  }
@@ -8,6 +8,7 @@ export default class IssuesGet extends Command {
8
8
  static examples = [
9
9
  '<%= config.bin %> issues get ENG-123',
10
10
  '<%= config.bin %> issues get ENG-123 --format table',
11
+ '<%= config.bin %> issues get ENG-123 --with-attachments',
11
12
  '<%= config.bin %> issues get abc123',
12
13
  ];
13
14
  static args = {
@@ -23,6 +24,10 @@ export default class IssuesGet extends Command {
23
24
  options: ['json', 'table', 'plain'],
24
25
  default: 'json',
25
26
  }),
27
+ 'with-attachments': Flags.boolean({
28
+ description: 'Include attachments (linked PRs, commits, etc.)',
29
+ default: false,
30
+ }),
26
31
  };
27
32
  async run() {
28
33
  try {
@@ -35,12 +40,13 @@ export default class IssuesGet extends Command {
35
40
  throw new CliError(ErrorCodes.NOT_FOUND, `Issue ${args.id} not found`);
36
41
  }
37
42
  // Fetch related data
38
- const [state, assignee, team, labels, comments] = await Promise.all([
43
+ const [state, assignee, team, labels, comments, attachments] = await Promise.all([
39
44
  issue.state,
40
45
  issue.assignee,
41
46
  issue.team,
42
47
  issue.labels(),
43
48
  issue.comments(),
49
+ flags['with-attachments'] ? issue.attachments() : Promise.resolve(null),
44
50
  ]);
45
51
  const data = {
46
52
  id: issue.id,
@@ -81,6 +87,17 @@ export default class IssuesGet extends Command {
81
87
  color: label.color,
82
88
  })),
83
89
  commentsCount: comments.nodes.length,
90
+ ...(attachments && {
91
+ attachments: attachments.nodes.map((attachment) => ({
92
+ id: attachment.id,
93
+ title: attachment.title,
94
+ subtitle: attachment.subtitle ?? null,
95
+ url: attachment.url,
96
+ sourceType: attachment.sourceType ?? null,
97
+ metadata: attachment.metadata,
98
+ createdAt: attachment.createdAt,
99
+ })),
100
+ }),
84
101
  };
85
102
  if (format === 'json') {
86
103
  print(success(data));
@@ -96,6 +113,7 @@ export default class IssuesGet extends Command {
96
113
  labels: data.labels.map((l) => l.name).join(', ') || 'None',
97
114
  estimate: data.estimate ?? 'None',
98
115
  comments: data.commentsCount,
116
+ ...('attachments' in data && { attachments: data.attachments?.length ?? 0 }),
99
117
  url: data.url,
100
118
  createdAt: data.createdAt,
101
119
  updatedAt: data.updatedAt,
@@ -15,6 +15,8 @@ export default class IssuesUpdate extends Command {
15
15
  'project-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
16
  estimate: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
17
17
  'label-ids': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
18
+ 'due-date': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
19
+ 'cycle-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
18
20
  };
19
21
  run(): Promise<void>;
20
22
  }
@@ -48,6 +48,12 @@ export default class IssuesUpdate extends Command {
48
48
  'label-ids': Flags.string({
49
49
  description: 'Comma-separated label IDs (replaces existing labels)',
50
50
  }),
51
+ 'due-date': Flags.string({
52
+ description: 'Due date (YYYY-MM-DD, use empty string to clear)',
53
+ }),
54
+ 'cycle-id': Flags.string({
55
+ description: 'Cycle (sprint) ID (use empty string to remove from cycle)',
56
+ }),
51
57
  };
52
58
  async run() {
53
59
  try {
@@ -84,6 +90,12 @@ export default class IssuesUpdate extends Command {
84
90
  input.estimate = flags.estimate;
85
91
  if (flags['label-ids'])
86
92
  input.labelIds = flags['label-ids'].split(',');
93
+ if (flags['due-date'] !== undefined) {
94
+ input.dueDate = flags['due-date'] || null;
95
+ }
96
+ if (flags['cycle-id'] !== undefined) {
97
+ input.cycleId = flags['cycle-id'] || null;
98
+ }
87
99
  if (Object.keys(input).length === 0) {
88
100
  throw new CliError(ErrorCodes.INVALID_INPUT, 'No update fields provided. Use --input or individual flags.');
89
101
  }
@@ -0,0 +1,13 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Upload extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ file: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
7
+ };
8
+ static flags: {
9
+ 'content-type': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ markdown: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ };
12
+ run(): Promise<void>;
13
+ }
@@ -0,0 +1,117 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import { readFileSync, statSync, existsSync } from 'node:fs';
3
+ import { basename, extname } from 'node:path';
4
+ import { getClient } from '../lib/client.js';
5
+ import { success, print } from '../lib/output.js';
6
+ import { handleError, CliError, ErrorCodes } from '../lib/errors.js';
7
+ const MIME_TYPES = {
8
+ '.png': 'image/png',
9
+ '.jpg': 'image/jpeg',
10
+ '.jpeg': 'image/jpeg',
11
+ '.gif': 'image/gif',
12
+ '.webp': 'image/webp',
13
+ '.svg': 'image/svg+xml',
14
+ '.pdf': 'application/pdf',
15
+ '.doc': 'application/msword',
16
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
17
+ '.xls': 'application/vnd.ms-excel',
18
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
19
+ '.ppt': 'application/vnd.ms-powerpoint',
20
+ '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
21
+ '.txt': 'text/plain',
22
+ '.md': 'text/markdown',
23
+ '.json': 'application/json',
24
+ '.csv': 'text/csv',
25
+ '.zip': 'application/zip',
26
+ '.mp4': 'video/mp4',
27
+ '.webm': 'video/webm',
28
+ '.mp3': 'audio/mpeg',
29
+ '.wav': 'audio/wav',
30
+ };
31
+ function getMimeType(filePath) {
32
+ const ext = extname(filePath).toLowerCase();
33
+ return MIME_TYPES[ext] ?? 'application/octet-stream';
34
+ }
35
+ export default class Upload extends Command {
36
+ static description = 'Upload a file to Linear and get the asset URL';
37
+ static examples = [
38
+ '<%= config.bin %> upload ./screenshot.png',
39
+ '<%= config.bin %> upload ./document.pdf --content-type application/pdf',
40
+ '<%= config.bin %> upload ./image.png --markdown',
41
+ ];
42
+ static args = {
43
+ file: Args.string({
44
+ description: 'Path to the file to upload',
45
+ required: true,
46
+ }),
47
+ };
48
+ static flags = {
49
+ 'content-type': Flags.string({
50
+ description: 'Override the content type (MIME type)',
51
+ }),
52
+ markdown: Flags.boolean({
53
+ char: 'm',
54
+ description: 'Output as markdown link/image',
55
+ default: false,
56
+ }),
57
+ };
58
+ async run() {
59
+ try {
60
+ const { args, flags } = await this.parse(Upload);
61
+ const client = getClient();
62
+ const filePath = args.file;
63
+ if (!existsSync(filePath)) {
64
+ throw new CliError(ErrorCodes.NOT_FOUND, `File not found: ${filePath}`);
65
+ }
66
+ const stats = statSync(filePath);
67
+ if (!stats.isFile()) {
68
+ throw new CliError(ErrorCodes.INVALID_INPUT, `Not a file: ${filePath}`);
69
+ }
70
+ const fileName = basename(filePath);
71
+ const contentType = flags['content-type'] ?? getMimeType(filePath);
72
+ const fileSize = stats.size;
73
+ const uploadPayload = await client.fileUpload(contentType, fileName, fileSize);
74
+ if (!uploadPayload.success || !uploadPayload.uploadFile) {
75
+ throw new CliError(ErrorCodes.API_ERROR, 'Failed to request upload URL');
76
+ }
77
+ const { uploadUrl, assetUrl } = uploadPayload.uploadFile;
78
+ const uploadHeaders = uploadPayload.uploadFile.headers;
79
+ const fileContent = readFileSync(filePath);
80
+ const headers = {
81
+ 'Content-Type': contentType,
82
+ 'Cache-Control': 'public, max-age=31536000',
83
+ };
84
+ for (const { key, value } of uploadHeaders) {
85
+ headers[key] = value;
86
+ }
87
+ const response = await fetch(uploadUrl, {
88
+ method: 'PUT',
89
+ headers,
90
+ body: fileContent,
91
+ });
92
+ if (!response.ok) {
93
+ throw new CliError(ErrorCodes.API_ERROR, `Upload failed: ${response.status} ${response.statusText}`);
94
+ }
95
+ if (flags.markdown) {
96
+ const isImage = contentType.startsWith('image/');
97
+ const markdown = isImage ? `![${fileName}](${assetUrl})` : `[${fileName}](${assetUrl})`;
98
+ print(success({
99
+ assetUrl,
100
+ markdown,
101
+ }));
102
+ }
103
+ else {
104
+ print(success({
105
+ fileName,
106
+ contentType,
107
+ size: fileSize,
108
+ assetUrl,
109
+ }));
110
+ }
111
+ }
112
+ catch (err) {
113
+ handleError(err);
114
+ this.exit(1);
115
+ }
116
+ }
117
+ }