@enrichlayer/el-linear 1.2.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 (151) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +219 -0
  3. package/claude-skills/linear-operations/SKILL.md +315 -0
  4. package/claude-skills/linear-operations/evals/evals.json +46 -0
  5. package/dist/commands/attachments.d.ts +2 -0
  6. package/dist/commands/attachments.js +57 -0
  7. package/dist/commands/batch.d.ts +2 -0
  8. package/dist/commands/batch.js +309 -0
  9. package/dist/commands/comments.d.ts +2 -0
  10. package/dist/commands/comments.js +272 -0
  11. package/dist/commands/config.d.ts +2 -0
  12. package/dist/commands/config.js +15 -0
  13. package/dist/commands/cycles.d.ts +2 -0
  14. package/dist/commands/cycles.js +63 -0
  15. package/dist/commands/documents.d.ts +2 -0
  16. package/dist/commands/documents.js +175 -0
  17. package/dist/commands/embeds.d.ts +2 -0
  18. package/dist/commands/embeds.js +65 -0
  19. package/dist/commands/gdoc.d.ts +2 -0
  20. package/dist/commands/gdoc.js +37 -0
  21. package/dist/commands/graphql.d.ts +2 -0
  22. package/dist/commands/graphql.js +70 -0
  23. package/dist/commands/init/aliases.d.ts +109 -0
  24. package/dist/commands/init/aliases.js +569 -0
  25. package/dist/commands/init/defaults.d.ts +25 -0
  26. package/dist/commands/init/defaults.js +112 -0
  27. package/dist/commands/init/index.d.ts +18 -0
  28. package/dist/commands/init/index.js +182 -0
  29. package/dist/commands/init/shared.d.ts +88 -0
  30. package/dist/commands/init/shared.js +164 -0
  31. package/dist/commands/init/token.d.ts +50 -0
  32. package/dist/commands/init/token.js +141 -0
  33. package/dist/commands/init/workspace.d.ts +20 -0
  34. package/dist/commands/init/workspace.js +80 -0
  35. package/dist/commands/issue-id.d.ts +28 -0
  36. package/dist/commands/issue-id.js +81 -0
  37. package/dist/commands/issues.d.ts +2 -0
  38. package/dist/commands/issues.js +1145 -0
  39. package/dist/commands/labels.d.ts +2 -0
  40. package/dist/commands/labels.js +100 -0
  41. package/dist/commands/project-milestones.d.ts +2 -0
  42. package/dist/commands/project-milestones.js +143 -0
  43. package/dist/commands/projects.d.ts +2 -0
  44. package/dist/commands/projects.js +336 -0
  45. package/dist/commands/read-shortcut.d.ts +6 -0
  46. package/dist/commands/read-shortcut.js +69 -0
  47. package/dist/commands/releases.d.ts +2 -0
  48. package/dist/commands/releases.js +142 -0
  49. package/dist/commands/search.d.ts +2 -0
  50. package/dist/commands/search.js +171 -0
  51. package/dist/commands/teams.d.ts +2 -0
  52. package/dist/commands/teams.js +19 -0
  53. package/dist/commands/templates.d.ts +2 -0
  54. package/dist/commands/templates.js +58 -0
  55. package/dist/commands/users.d.ts +2 -0
  56. package/dist/commands/users.js +17 -0
  57. package/dist/config/config.d.ts +43 -0
  58. package/dist/config/config.js +81 -0
  59. package/dist/config/issue-validation.d.ts +39 -0
  60. package/dist/config/issue-validation.js +264 -0
  61. package/dist/config/paths.d.ts +20 -0
  62. package/dist/config/paths.js +22 -0
  63. package/dist/config/resolver.d.ts +25 -0
  64. package/dist/config/resolver.js +183 -0
  65. package/dist/config/status-defaults.d.ts +13 -0
  66. package/dist/config/status-defaults.js +20 -0
  67. package/dist/config/term-enforcer.d.ts +31 -0
  68. package/dist/config/term-enforcer.js +69 -0
  69. package/dist/main.d.ts +2 -0
  70. package/dist/main.js +76 -0
  71. package/dist/queries/attachments.d.ts +3 -0
  72. package/dist/queries/attachments.js +35 -0
  73. package/dist/queries/comments.d.ts +3 -0
  74. package/dist/queries/comments.js +64 -0
  75. package/dist/queries/common.d.ts +2 -0
  76. package/dist/queries/common.js +109 -0
  77. package/dist/queries/cycles.d.ts +2 -0
  78. package/dist/queries/cycles.js +40 -0
  79. package/dist/queries/documents.d.ts +5 -0
  80. package/dist/queries/documents.js +67 -0
  81. package/dist/queries/introspect.d.ts +2 -0
  82. package/dist/queries/introspect.js +24 -0
  83. package/dist/queries/issues.d.ts +23 -0
  84. package/dist/queries/issues.js +380 -0
  85. package/dist/queries/labels.d.ts +4 -0
  86. package/dist/queries/labels.js +53 -0
  87. package/dist/queries/project-milestones.d.ts +6 -0
  88. package/dist/queries/project-milestones.js +125 -0
  89. package/dist/queries/projects.d.ts +7 -0
  90. package/dist/queries/projects.js +104 -0
  91. package/dist/queries/releases.d.ts +4 -0
  92. package/dist/queries/releases.js +85 -0
  93. package/dist/queries/search.d.ts +1 -0
  94. package/dist/queries/search.js +35 -0
  95. package/dist/queries/templates.d.ts +2 -0
  96. package/dist/queries/templates.js +30 -0
  97. package/dist/types/linear.d.ts +217 -0
  98. package/dist/types/linear.js +6 -0
  99. package/dist/utils/auth.d.ts +4 -0
  100. package/dist/utils/auth.js +23 -0
  101. package/dist/utils/auto-link-references.d.ts +47 -0
  102. package/dist/utils/auto-link-references.js +188 -0
  103. package/dist/utils/date-format.d.ts +4 -0
  104. package/dist/utils/date-format.js +8 -0
  105. package/dist/utils/download-uploads.d.ts +7 -0
  106. package/dist/utils/download-uploads.js +88 -0
  107. package/dist/utils/embed-parser.d.ts +8 -0
  108. package/dist/utils/embed-parser.js +54 -0
  109. package/dist/utils/error-messages.d.ts +4 -0
  110. package/dist/utils/error-messages.js +17 -0
  111. package/dist/utils/file-service.d.ts +13 -0
  112. package/dist/utils/file-service.js +239 -0
  113. package/dist/utils/gdoc-parser.d.ts +45 -0
  114. package/dist/utils/gdoc-parser.js +107 -0
  115. package/dist/utils/graphql-attachments-service.d.ts +12 -0
  116. package/dist/utils/graphql-attachments-service.js +46 -0
  117. package/dist/utils/graphql-documents-service.d.ts +18 -0
  118. package/dist/utils/graphql-documents-service.js +97 -0
  119. package/dist/utils/graphql-issues-service.d.ts +49 -0
  120. package/dist/utils/graphql-issues-service.js +925 -0
  121. package/dist/utils/graphql-service.d.ts +8 -0
  122. package/dist/utils/graphql-service.js +35 -0
  123. package/dist/utils/identifier-parser.d.ts +7 -0
  124. package/dist/utils/identifier-parser.js +20 -0
  125. package/dist/utils/issue-reference-extractor.d.ts +18 -0
  126. package/dist/utils/issue-reference-extractor.js +95 -0
  127. package/dist/utils/issue-reference-wrapper.d.ts +12 -0
  128. package/dist/utils/issue-reference-wrapper.js +91 -0
  129. package/dist/utils/linear-service.d.ts +26 -0
  130. package/dist/utils/linear-service.js +442 -0
  131. package/dist/utils/logger.d.ts +4 -0
  132. package/dist/utils/logger.js +8 -0
  133. package/dist/utils/markdown-prosemirror.d.ts +24 -0
  134. package/dist/utils/markdown-prosemirror.js +325 -0
  135. package/dist/utils/mention-resolver.d.ts +31 -0
  136. package/dist/utils/mention-resolver.js +234 -0
  137. package/dist/utils/output.d.ts +7 -0
  138. package/dist/utils/output.js +125 -0
  139. package/dist/utils/table-formatter.d.ts +4 -0
  140. package/dist/utils/table-formatter.js +249 -0
  141. package/dist/utils/usage.d.ts +2 -0
  142. package/dist/utils/usage.js +24 -0
  143. package/dist/utils/uuid.d.ts +2 -0
  144. package/dist/utils/uuid.js +8 -0
  145. package/dist/utils/validate-references.d.ts +10 -0
  146. package/dist/utils/validate-references.js +33 -0
  147. package/dist/utils/validators.d.ts +7 -0
  148. package/dist/utils/validators.js +71 -0
  149. package/dist/utils/workspace-url.d.ts +4 -0
  150. package/dist/utils/workspace-url.js +44 -0
  151. package/package.json +71 -0
@@ -0,0 +1,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function setupLabelsCommands(program: Command): void;
@@ -0,0 +1,100 @@
1
+ import { resolveTeam } from "../config/resolver.js";
2
+ import { CREATE_LABEL_MUTATION, FIND_PARENT_LABEL_QUERY, RESTORE_LABEL_MUTATION, RETIRE_LABEL_MUTATION, } from "../queries/labels.js";
3
+ import { createGraphQLService } from "../utils/graphql-service.js";
4
+ import { createLinearService } from "../utils/linear-service.js";
5
+ import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
6
+ import { validateHexColor } from "../utils/validators.js";
7
+ async function handleCreateLabel(name, options, command) {
8
+ const rootOpts = command.parent.parent.opts();
9
+ const teamId = resolveTeam(options.team);
10
+ const graphQLService = createGraphQLService(rootOpts);
11
+ const input = { name, teamId };
12
+ if (options.color) {
13
+ input.color = validateHexColor(options.color);
14
+ }
15
+ if (options.description) {
16
+ input.description = options.description;
17
+ }
18
+ if (options.parent) {
19
+ const parentResult = await graphQLService.rawRequest(FIND_PARENT_LABEL_QUERY, {
20
+ name: options.parent,
21
+ teamId,
22
+ });
23
+ const issueLabels = parentResult.issueLabels;
24
+ const nodes = issueLabels?.nodes;
25
+ const parentLabel = nodes?.[0];
26
+ if (parentLabel) {
27
+ input.parentId = parentLabel.id;
28
+ }
29
+ }
30
+ const result = await graphQLService.rawRequest(CREATE_LABEL_MUTATION, {
31
+ input,
32
+ });
33
+ const createResult = result.issueLabelCreate;
34
+ if (!createResult?.success) {
35
+ throw new Error(`Failed to create label "${name}"`);
36
+ }
37
+ outputSuccess(createResult.issueLabel);
38
+ }
39
+ async function handleRetireLabel(labelId, _options, command) {
40
+ const rootOpts = command.parent.parent.opts();
41
+ const graphQLService = createGraphQLService(rootOpts);
42
+ const result = await graphQLService.rawRequest(RETIRE_LABEL_MUTATION, {
43
+ id: labelId,
44
+ });
45
+ const retireResult = result.issueLabelRetire;
46
+ if (!retireResult?.success) {
47
+ throw new Error(`Failed to retire label "${labelId}"`);
48
+ }
49
+ outputSuccess(retireResult.issueLabel);
50
+ }
51
+ async function handleRestoreLabel(labelId, _options, command) {
52
+ const rootOpts = command.parent.parent.opts();
53
+ const graphQLService = createGraphQLService(rootOpts);
54
+ const result = await graphQLService.rawRequest(RESTORE_LABEL_MUTATION, {
55
+ id: labelId,
56
+ });
57
+ const restoreResult = result.issueLabelRestore;
58
+ if (!restoreResult?.success) {
59
+ throw new Error(`Failed to restore label "${labelId}"`);
60
+ }
61
+ outputSuccess(restoreResult.issueLabel);
62
+ }
63
+ export function setupLabelsCommands(program) {
64
+ const labels = program
65
+ .command("labels")
66
+ .alias("label")
67
+ .description("Label operations");
68
+ labels.action(() => labels.help());
69
+ labels
70
+ .command("list")
71
+ .description("List all available labels")
72
+ .option("--team <team>", "filter by team key, name, or ID")
73
+ .option("-l, --limit <number>", "limit results", "100")
74
+ .action(handleAsyncCommand(async (options, command) => {
75
+ const rootOpts = command.parent.parent.opts();
76
+ const teamFilter = options.team ? resolveTeam(options.team) : undefined;
77
+ const service = createLinearService(rootOpts);
78
+ const result = await service.getLabels(teamFilter, Number.parseInt(options.limit, 10));
79
+ outputSuccess({
80
+ data: result.labels,
81
+ meta: { count: result.labels.length },
82
+ });
83
+ }));
84
+ labels
85
+ .command("create <name>")
86
+ .description("Create a new label on a team")
87
+ .requiredOption("--team <team>", "team key, name, or ID")
88
+ .option("--color <color>", "label color (hex, e.g. #e06666)")
89
+ .option("--description <desc>", "label description")
90
+ .option("--parent <parent>", "parent label group name or ID")
91
+ .action(handleAsyncCommand(handleCreateLabel));
92
+ labels
93
+ .command("retire <labelId>")
94
+ .description("Retire a label (soft-delete — can be restored later)")
95
+ .action(handleAsyncCommand(handleRetireLabel));
96
+ labels
97
+ .command("restore <labelId>")
98
+ .description("Restore a previously retired label")
99
+ .action(handleAsyncCommand(handleRestoreLabel));
100
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function setupProjectMilestonesCommands(program: Command): void;
@@ -0,0 +1,143 @@
1
+ import { CREATE_PROJECT_MILESTONE_MUTATION, FIND_PROJECT_MILESTONE_BY_NAME_GLOBAL, FIND_PROJECT_MILESTONE_BY_NAME_SCOPED, GET_PROJECT_MILESTONE_BY_ID_QUERY, LIST_PROJECT_MILESTONES_QUERY, UPDATE_PROJECT_MILESTONE_MUTATION, } from "../queries/project-milestones.js";
2
+ import { multipleMatchesError, notFoundError, } from "../utils/error-messages.js";
3
+ import { createGraphQLService, } from "../utils/graphql-service.js";
4
+ import { createLinearService, } from "../utils/linear-service.js";
5
+ import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
6
+ import { isUuid } from "../utils/uuid.js";
7
+ import { validateIsoDate } from "../utils/validators.js";
8
+ async function resolveMilestoneId(milestoneNameOrId, graphQLService, linearService, projectNameOrId) {
9
+ if (isUuid(milestoneNameOrId)) {
10
+ return milestoneNameOrId;
11
+ }
12
+ let nodes = [];
13
+ if (projectNameOrId) {
14
+ const projectId = await linearService.resolveProjectId(projectNameOrId);
15
+ const findRes = await graphQLService.rawRequest(FIND_PROJECT_MILESTONE_BY_NAME_SCOPED, {
16
+ name: milestoneNameOrId,
17
+ projectId,
18
+ });
19
+ const projectData = findRes.project;
20
+ const milestonesData = projectData?.projectMilestones;
21
+ nodes = milestonesData?.nodes || [];
22
+ }
23
+ if (nodes.length === 0) {
24
+ const globalRes = await graphQLService.rawRequest(FIND_PROJECT_MILESTONE_BY_NAME_GLOBAL, {
25
+ name: milestoneNameOrId,
26
+ });
27
+ const globalMilestones = globalRes.projectMilestones;
28
+ nodes = globalMilestones?.nodes || [];
29
+ }
30
+ if (nodes.length === 0) {
31
+ throw notFoundError("Milestone", milestoneNameOrId);
32
+ }
33
+ if (nodes.length > 1) {
34
+ const matches = nodes.map((m) => `"${m.name}" in project "${m.project?.name}"`);
35
+ throw multipleMatchesError("milestone", milestoneNameOrId, matches, "specify --project or use the milestone ID");
36
+ }
37
+ return nodes[0].id;
38
+ }
39
+ async function handleListMilestones(options, command) {
40
+ const rootOpts = command.parent.parent.opts();
41
+ const graphQLService = createGraphQLService(rootOpts);
42
+ const linearService = createLinearService(rootOpts);
43
+ const projectId = await linearService.resolveProjectId(options.project);
44
+ const result = await graphQLService.rawRequest(LIST_PROJECT_MILESTONES_QUERY, {
45
+ projectId,
46
+ first: Number.parseInt(options.limit || "50", 10),
47
+ });
48
+ const projectData = result.project;
49
+ const milestonesData = projectData?.projectMilestones;
50
+ const nodes = milestonesData?.nodes || [];
51
+ outputSuccess({ data: nodes, meta: { count: nodes.length } });
52
+ }
53
+ async function handleReadMilestone(milestoneIdOrName, options, command) {
54
+ const rootOpts = command.parent.parent.opts();
55
+ const graphQLService = createGraphQLService(rootOpts);
56
+ const linearService = createLinearService(rootOpts);
57
+ const milestoneId = await resolveMilestoneId(milestoneIdOrName, graphQLService, linearService, options.project);
58
+ const result = await graphQLService.rawRequest(GET_PROJECT_MILESTONE_BY_ID_QUERY, {
59
+ id: milestoneId,
60
+ issuesFirst: Number.parseInt(options.issuesFirst || "50", 10),
61
+ });
62
+ outputSuccess(result.projectMilestone);
63
+ }
64
+ async function handleCreateMilestone(name, options, command) {
65
+ const rootOpts = command.parent.parent.opts();
66
+ const graphQLService = createGraphQLService(rootOpts);
67
+ const linearService = createLinearService(rootOpts);
68
+ const projectId = await linearService.resolveProjectId(options.project);
69
+ if (options.targetDate) {
70
+ validateIsoDate(options.targetDate);
71
+ }
72
+ const result = await graphQLService.rawRequest(CREATE_PROJECT_MILESTONE_MUTATION, {
73
+ projectId,
74
+ name,
75
+ description: options.description,
76
+ targetDate: options.targetDate,
77
+ });
78
+ const createData = result.projectMilestoneCreate;
79
+ if (!createData?.success) {
80
+ throw new Error("Failed to create project milestone");
81
+ }
82
+ outputSuccess(createData.projectMilestone);
83
+ }
84
+ async function handleUpdateMilestone(milestoneIdOrName, options, command) {
85
+ const rootOpts = command.parent.parent.opts();
86
+ const graphQLService = createGraphQLService(rootOpts);
87
+ const linearService = createLinearService(rootOpts);
88
+ const milestoneId = await resolveMilestoneId(milestoneIdOrName, graphQLService, linearService, options.project);
89
+ const updateVars = { id: milestoneId };
90
+ if (options.name !== undefined) {
91
+ updateVars.name = options.name;
92
+ }
93
+ if (options.description !== undefined) {
94
+ updateVars.description = options.description;
95
+ }
96
+ if (options.targetDate !== undefined) {
97
+ validateIsoDate(options.targetDate);
98
+ updateVars.targetDate = options.targetDate;
99
+ }
100
+ if (options.sortOrder !== undefined) {
101
+ updateVars.sortOrder = Number.parseFloat(options.sortOrder);
102
+ }
103
+ const result = await graphQLService.rawRequest(UPDATE_PROJECT_MILESTONE_MUTATION, updateVars);
104
+ const updateData = result.projectMilestoneUpdate;
105
+ if (!updateData?.success) {
106
+ throw new Error("Failed to update project milestone");
107
+ }
108
+ outputSuccess(updateData.projectMilestone);
109
+ }
110
+ export function setupProjectMilestonesCommands(program) {
111
+ const projectMilestones = program
112
+ .command("project-milestones")
113
+ .description("Project milestone operations");
114
+ projectMilestones.action(() => projectMilestones.help());
115
+ projectMilestones
116
+ .command("list")
117
+ .description("List milestones in a project")
118
+ .requiredOption("--project <project>", "project name or ID")
119
+ .option("-l, --limit <number>", "limit results", "50")
120
+ .action(handleAsyncCommand(handleListMilestones));
121
+ projectMilestones
122
+ .command("read <milestoneIdOrName>")
123
+ .description("Get milestone details including issues.")
124
+ .option("--project <project>", "project name or ID to scope name lookup")
125
+ .option("--issues-first <n>", "how many issues to fetch (default 50)", "50")
126
+ .action(handleAsyncCommand(handleReadMilestone));
127
+ projectMilestones
128
+ .command("create <name>")
129
+ .description("Create a new project milestone")
130
+ .requiredOption("--project <project>", "project name or ID")
131
+ .option("-d, --description <description>", "milestone description")
132
+ .option("--target-date <date>", "target date in ISO format (YYYY-MM-DD)")
133
+ .action(handleAsyncCommand(handleCreateMilestone));
134
+ projectMilestones
135
+ .command("update <milestoneIdOrName>")
136
+ .description("Update an existing project milestone.")
137
+ .option("--project <project>", "project name or ID to scope name lookup")
138
+ .option("-n, --name <name>", "new milestone name")
139
+ .option("-d, --description <description>", "new milestone description")
140
+ .option("--target-date <date>", "new target date in ISO format (YYYY-MM-DD)")
141
+ .option("--sort-order <number>", "new sort order")
142
+ .action(handleAsyncCommand(handleUpdateMilestone));
143
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function setupProjectsCommands(program: Command): void;
@@ -0,0 +1,336 @@
1
+ import { resolveTeam } from "../config/resolver.js";
2
+ import { CREATE_PROJECT_MUTATION, GET_PROJECT_QUERY, GET_PROJECT_TEAM_ISSUES_QUERY, PROJECT_BY_ID_QUERY, SEARCH_PROJECTS_BY_NAME_QUERY, UPDATE_PROJECT_MUTATION, } from "../queries/projects.js";
3
+ import { createGraphQLService } from "../utils/graphql-service.js";
4
+ import { createLinearService } from "../utils/linear-service.js";
5
+ import { logger } from "../utils/logger.js";
6
+ import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
7
+ import { isUuid } from "../utils/uuid.js";
8
+ import { splitList } from "../utils/validators.js";
9
+ function formatProjectsOutput(projects, format, fieldNames) {
10
+ const fields = fieldNames ?? [
11
+ "name",
12
+ "state",
13
+ "progress",
14
+ "teams",
15
+ "lead",
16
+ "targetDate",
17
+ ];
18
+ const rows = projects.map((p) => {
19
+ const row = {};
20
+ for (const f of fields) {
21
+ switch (f) {
22
+ case "name":
23
+ row[f] = p.name;
24
+ break;
25
+ case "state":
26
+ row[f] = p.state ?? "";
27
+ break;
28
+ case "progress":
29
+ row[f] =
30
+ p.progress !== undefined ? `${Math.round(p.progress * 100)}%` : "";
31
+ break;
32
+ case "teams":
33
+ row[f] = p.teams?.map((t) => t.key).join(", ") ?? "";
34
+ break;
35
+ case "lead":
36
+ row[f] = p.lead?.name ?? "";
37
+ break;
38
+ case "targetDate":
39
+ row[f] = p.targetDate ?? "";
40
+ break;
41
+ case "id":
42
+ row[f] = p.id;
43
+ break;
44
+ default:
45
+ row[f] = String(p[f] ?? "");
46
+ }
47
+ }
48
+ return row;
49
+ });
50
+ if (format === "csv") {
51
+ logger.info(fields.join(","));
52
+ for (const row of rows) {
53
+ logger.info(fields.map((f) => `"${(row[f] ?? "").replace(/"/g, '""')}"`).join(","));
54
+ }
55
+ return;
56
+ }
57
+ // table and md formats
58
+ const widths = fields.map((f) => Math.max(f.length, ...rows.map((r) => (r[f] ?? "").length)));
59
+ const separator = format === "md"
60
+ ? `| ${widths.map((w) => "-".repeat(w)).join(" | ")} |`
61
+ : widths.map((w) => "-".repeat(w + 2)).join("+");
62
+ const header = format === "md"
63
+ ? `| ${fields.map((f, i) => f.padEnd(widths[i])).join(" | ")} |`
64
+ : fields.map((f, i) => ` ${f.padEnd(widths[i])} `).join("|");
65
+ logger.info(header);
66
+ logger.info(separator);
67
+ for (const row of rows) {
68
+ const line = format === "md"
69
+ ? `| ${fields.map((f, i) => (row[f] ?? "").padEnd(widths[i])).join(" | ")} |`
70
+ : fields
71
+ .map((f, i) => ` ${(row[f] ?? "").padEnd(widths[i])} `)
72
+ .join("|");
73
+ logger.info(line);
74
+ }
75
+ }
76
+ async function resolveProjectWithTeams(graphQLService, projectNameOrId) {
77
+ if (isUuid(projectNameOrId)) {
78
+ const result = await graphQLService.rawRequest(PROJECT_BY_ID_QUERY, {
79
+ id: projectNameOrId,
80
+ });
81
+ const project = result.project;
82
+ if (!project) {
83
+ throw new Error(`Project "${projectNameOrId}" not found`);
84
+ }
85
+ const teams = project.teams;
86
+ return {
87
+ projectId: project.id,
88
+ projectName: project.name,
89
+ currentTeams: (teams?.nodes || []).map((t) => ({
90
+ id: t.id,
91
+ key: t.key,
92
+ name: t.name,
93
+ })),
94
+ };
95
+ }
96
+ const result = await graphQLService.rawRequest(GET_PROJECT_QUERY, {
97
+ name: projectNameOrId,
98
+ });
99
+ const projectNodes = result.projects?.nodes;
100
+ if (!projectNodes?.length) {
101
+ throw new Error(`Project "${projectNameOrId}" not found`);
102
+ }
103
+ const project = projectNodes[0];
104
+ const teams = project.teams;
105
+ return {
106
+ projectId: project.id,
107
+ projectName: project.name,
108
+ currentTeams: (teams?.nodes || []).map((t) => ({
109
+ id: t.id,
110
+ key: t.key,
111
+ name: t.name,
112
+ })),
113
+ };
114
+ }
115
+ function formatTeamsOutput(projectUpdate) {
116
+ const updatedProject = projectUpdate.project;
117
+ const updatedTeams = updatedProject.teams;
118
+ return {
119
+ id: updatedProject.id,
120
+ name: updatedProject.name,
121
+ teams: (updatedTeams.nodes || []).map((t) => ({
122
+ id: t.id,
123
+ key: t.key,
124
+ name: t.name,
125
+ })),
126
+ };
127
+ }
128
+ async function handleAddTeam(projectNameOrId, teamInput, _options, command) {
129
+ const rootOpts = command.parent.parent.opts();
130
+ const graphQLService = createGraphQLService(rootOpts);
131
+ const finalTeamId = await createLinearService(rootOpts).resolveTeamId(resolveTeam(teamInput));
132
+ const { projectId, currentTeams } = await resolveProjectWithTeams(graphQLService, projectNameOrId);
133
+ const currentTeamIds = currentTeams.map((t) => t.id);
134
+ if (currentTeamIds.includes(finalTeamId)) {
135
+ outputSuccess({
136
+ message: `Team "${teamInput}" is already associated with the project`,
137
+ projectId,
138
+ });
139
+ return;
140
+ }
141
+ const updateResult = await graphQLService.rawRequest(UPDATE_PROJECT_MUTATION, {
142
+ id: projectId,
143
+ input: { teamIds: [...currentTeamIds, finalTeamId] },
144
+ });
145
+ const projectUpdate = updateResult.projectUpdate;
146
+ if (!projectUpdate.success) {
147
+ throw new Error("Failed to update project");
148
+ }
149
+ outputSuccess(formatTeamsOutput(projectUpdate));
150
+ }
151
+ async function handleAddTeams(projectNameOrId, teamInputs, _options, command) {
152
+ const rootOpts = command.parent.parent.opts();
153
+ const graphQLService = createGraphQLService(rootOpts);
154
+ const resolvedTeamIds = await Promise.all(teamInputs.map((t) => createLinearService(rootOpts).resolveTeamId(resolveTeam(t))));
155
+ const { projectId, currentTeams } = await resolveProjectWithTeams(graphQLService, projectNameOrId);
156
+ const currentTeamIds = currentTeams.map((t) => t.id);
157
+ const newTeamIds = resolvedTeamIds.filter((id) => !currentTeamIds.includes(id));
158
+ const skippedInputs = teamInputs.filter((_, i) => currentTeamIds.includes(resolvedTeamIds[i]));
159
+ if (newTeamIds.length === 0) {
160
+ outputSuccess({
161
+ message: "All teams are already associated with the project",
162
+ projectId,
163
+ skipped: skippedInputs,
164
+ });
165
+ return;
166
+ }
167
+ const mergedTeamIds = [...currentTeamIds, ...newTeamIds];
168
+ const updateResult = await graphQLService.rawRequest(UPDATE_PROJECT_MUTATION, {
169
+ id: projectId,
170
+ input: { teamIds: mergedTeamIds },
171
+ });
172
+ const projectUpdate = updateResult.projectUpdate;
173
+ if (!projectUpdate.success) {
174
+ throw new Error("Failed to update project");
175
+ }
176
+ const result = formatTeamsOutput(projectUpdate);
177
+ outputSuccess({
178
+ ...result,
179
+ added: newTeamIds.length,
180
+ skipped: skippedInputs.length > 0 ? skippedInputs : undefined,
181
+ });
182
+ }
183
+ async function handleRemoveTeam(projectNameOrId, teamInput, options, command) {
184
+ const rootOpts = command.parent.parent.opts();
185
+ const graphQLService = createGraphQLService(rootOpts);
186
+ const finalTeamId = await createLinearService(rootOpts).resolveTeamId(resolveTeam(teamInput));
187
+ const { projectId, projectName, currentTeams } = await resolveProjectWithTeams(graphQLService, projectNameOrId);
188
+ const currentTeamIds = currentTeams.map((t) => t.id);
189
+ if (!currentTeamIds.includes(finalTeamId)) {
190
+ throw new Error(`Team "${teamInput}" is not associated with project "${projectName}"`);
191
+ }
192
+ if (!options.force) {
193
+ const issueCheck = await graphQLService.rawRequest(GET_PROJECT_TEAM_ISSUES_QUERY, {
194
+ projectId,
195
+ teamId: finalTeamId,
196
+ });
197
+ const project = issueCheck.project;
198
+ const issues = project.issues;
199
+ const issueNodes = issues.nodes || [];
200
+ if (issueNodes.length > 0) {
201
+ const examples = issueNodes
202
+ .slice(0, 5)
203
+ .map((i) => `${i.identifier}: ${i.title}`)
204
+ .join("\n ");
205
+ throw new Error(`Cannot remove team "${teamInput}" — it has ${issueNodes.length}${issueNodes.length >= 50 ? "+" : ""} issues in project "${projectName}". Reassign or remove those issues first.\n ${examples}${issueNodes.length > 5 ? "\n ..." : ""}\n\nUse --force to bypass this check.`);
206
+ }
207
+ }
208
+ const updatedTeamIds = currentTeamIds.filter((id) => id !== finalTeamId);
209
+ const updateResult = await graphQLService.rawRequest(UPDATE_PROJECT_MUTATION, {
210
+ id: projectId,
211
+ input: { teamIds: updatedTeamIds },
212
+ });
213
+ const projectUpdate = updateResult.projectUpdate;
214
+ if (!projectUpdate.success) {
215
+ throw new Error("Failed to update project");
216
+ }
217
+ outputSuccess({
218
+ ...formatTeamsOutput(projectUpdate),
219
+ removed: teamInput,
220
+ });
221
+ }
222
+ async function handleCreateProject(name, options, command) {
223
+ const rootOpts = command.parent.parent.opts();
224
+ const graphQLService = createGraphQLService(rootOpts);
225
+ // Step 1: Check for duplicate projects (case-insensitive)
226
+ const searchResult = await graphQLService.rawRequest(SEARCH_PROJECTS_BY_NAME_QUERY, { name });
227
+ const existing = (searchResult.projects
228
+ ?.nodes ?? []).filter((p) => p.name.toLowerCase() === name.toLowerCase());
229
+ if (existing.length > 0 && !options.force) {
230
+ const match = existing[0];
231
+ const teams = (match.teams?.nodes ??
232
+ [])
233
+ .map((t) => t.key)
234
+ .join(", ");
235
+ throw new Error(`Project "${match.name}" already exists (state: ${match.state}, teams: ${teams}). ` +
236
+ `Use --force to create anyway, or use the existing project.`);
237
+ }
238
+ // Step 2: Resolve team IDs
239
+ const service = createLinearService(rootOpts);
240
+ const teamKeys = options.team ? splitList(options.team) : [];
241
+ const teamIds = [];
242
+ for (const key of teamKeys) {
243
+ teamIds.push(await service.resolveTeamId(resolveTeam(key)));
244
+ }
245
+ // Step 3: Create the project
246
+ const input = {
247
+ name,
248
+ ...(options.description ? { description: options.description } : {}),
249
+ ...(teamIds.length > 0 ? { teamIds } : {}),
250
+ };
251
+ const createResult = await graphQLService.rawRequest(CREATE_PROJECT_MUTATION, { input });
252
+ const created = createResult.projectCreate;
253
+ if (!created?.success) {
254
+ throw new Error("Failed to create project");
255
+ }
256
+ const project = created.project;
257
+ // Step 4: Set content if provided (separate mutation — Linear API quirk)
258
+ if (options.content) {
259
+ await graphQLService.rawRequest(UPDATE_PROJECT_MUTATION, {
260
+ id: project.id,
261
+ input: { content: options.content },
262
+ });
263
+ }
264
+ const teamList = (project.teams?.nodes ??
265
+ [])
266
+ .map((t) => t.key)
267
+ .join(", ");
268
+ outputSuccess({
269
+ id: project.id,
270
+ name: project.name,
271
+ state: project.state ?? "planned",
272
+ teams: teamList,
273
+ ...(existing.length > 0
274
+ ? {
275
+ _warnings: [
276
+ `Created despite existing project "${existing[0].name}" (--force used)`,
277
+ ],
278
+ }
279
+ : {}),
280
+ });
281
+ }
282
+ export function setupProjectsCommands(program) {
283
+ const projects = program
284
+ .command("projects")
285
+ .alias("project")
286
+ .description("Project operations");
287
+ projects.action(() => projects.help());
288
+ projects
289
+ .command("create <name>")
290
+ .description("Create a project (checks for duplicates first)")
291
+ .option("--team <teams>", "comma-separated team keys (e.g., FE,DEV)")
292
+ .option("-d, --description <text>", "short summary (max 255 chars, shown in lists)")
293
+ .option("--content <markdown>", "full markdown body (shown in project panel)")
294
+ .option("--force", "create even if a project with the same name exists")
295
+ .action(handleAsyncCommand(handleCreateProject));
296
+ projects
297
+ .command("list")
298
+ .description("List projects")
299
+ .option("-l, --limit <number>", "limit results", "100")
300
+ .option("--format <format>", "output format (json, table, md, csv)", "json")
301
+ .option("--fields <fields>", "columns for table/csv (comma-separated: name,state,progress,teams,lead,targetDate)")
302
+ .action(handleAsyncCommand(async (options, command) => {
303
+ const rootOpts = command.parent.parent.opts();
304
+ const service = createLinearService(rootOpts);
305
+ const result = await service.getProjects(Number.parseInt(options.limit, 10));
306
+ const format = options.format;
307
+ if (format === "table" ||
308
+ format === "md" ||
309
+ format === "markdown" ||
310
+ format === "csv") {
311
+ const fieldList = options.fields
312
+ ? splitList(options.fields)
313
+ : undefined;
314
+ formatProjectsOutput(result, format, fieldList);
315
+ if (format === "table") {
316
+ logger.info(`\n${result.length} projects`);
317
+ }
318
+ }
319
+ else {
320
+ outputSuccess({ data: result, meta: { count: result.length } });
321
+ }
322
+ }));
323
+ projects
324
+ .command("add-team <project> <team>")
325
+ .description("Associate a project with an additional team (resolves names)")
326
+ .action(handleAsyncCommand(handleAddTeam));
327
+ projects
328
+ .command("add-teams <project> <teams...>")
329
+ .description("Associate a project with multiple teams in one operation (resolves names)")
330
+ .action(handleAsyncCommand(handleAddTeams));
331
+ projects
332
+ .command("remove-team <project> <team>")
333
+ .description("Remove a team from a project (checks for issues first)")
334
+ .option("--force", "remove even if the team has issues in the project")
335
+ .action(handleAsyncCommand(handleRemoveTeam));
336
+ }
@@ -0,0 +1,6 @@
1
+ import type { Command } from "commander";
2
+ /**
3
+ * Registers a top-level `read` command and a catch-all that auto-detects
4
+ * issue identifiers so `el-linear ADM-652` works without the `issues read` prefix.
5
+ */
6
+ export declare function setupReadShortcut(program: Command): void;
@@ -0,0 +1,69 @@
1
+ import { getApiToken } from "../utils/auth.js";
2
+ import { downloadLinearUploads } from "../utils/download-uploads.js";
3
+ import { GraphQLIssuesService } from "../utils/graphql-issues-service.js";
4
+ import { createGraphQLService } from "../utils/graphql-service.js";
5
+ import { createLinearService } from "../utils/linear-service.js";
6
+ import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
7
+ /**
8
+ * Issue ID pattern: 1-5 uppercase letters, dash, 1+ digits (e.g. ADM-652, DEV-12).
9
+ */
10
+ const ISSUE_ID_PATTERN = /^[A-Z]{1,5}-\d+$/;
11
+ /**
12
+ * Registers a top-level `read` command and a catch-all that auto-detects
13
+ * issue identifiers so `el-linear ADM-652` works without the `issues read` prefix.
14
+ */
15
+ export function setupReadShortcut(program) {
16
+ // Top-level `read` / `get` / `view` / `show` command
17
+ program
18
+ .command("read <issueId...>")
19
+ .alias("get")
20
+ .alias("view")
21
+ .alias("show")
22
+ .description("Shortcut for `issues read`. Get issue details by identifier.")
23
+ .addHelpText("after", "\nExamples:\n el-linear read ADM-652\n el-linear get DEV-123 DEV-456\n el-linear ADM-652 (auto-detected)")
24
+ .action(handleAsyncCommand(readIssues));
25
+ // Catch-all: if argv looks like `el-linear ADM-652 [DEV-123 ...]`, run read
26
+ const originalParse = program.parse.bind(program);
27
+ program.parse = (argv, parseOptions) => {
28
+ const args = argv ?? process.argv;
29
+ const userArgs = args.slice(2); // skip node + script
30
+ // Only intercept when every positional arg looks like an issue ID
31
+ // and there are no flags (to avoid hijacking other commands)
32
+ const positionals = userArgs.filter((a) => !a.startsWith("-"));
33
+ const flags = userArgs.filter((a) => a.startsWith("-"));
34
+ const onlyCompatFlags = flags.every((f) => f === "--json" || f.startsWith("--api-token"));
35
+ if (positionals.length > 0 &&
36
+ positionals.every((a) => ISSUE_ID_PATTERN.test(a)) &&
37
+ onlyCompatFlags) {
38
+ // Check the first positional isn't a known command
39
+ const knownCommands = new Set(program.commands
40
+ .map((c) => c.name())
41
+ .concat(program.commands.flatMap((c) => c.aliases())));
42
+ if (!knownCommands.has(positionals[0])) {
43
+ // Rewrite to `read <ids...>`
44
+ const rewritten = [...args.slice(0, 2), "read", ...userArgs];
45
+ return originalParse(rewritten, parseOptions);
46
+ }
47
+ }
48
+ return originalParse(argv, parseOptions);
49
+ };
50
+ }
51
+ async function readIssues(issueIds, _options, command) {
52
+ const rootOpts = command.parent.opts();
53
+ const graphQLService = createGraphQLService(rootOpts);
54
+ const linearService = createLinearService(rootOpts);
55
+ const issuesService = new GraphQLIssuesService(graphQLService, linearService);
56
+ const apiToken = getApiToken(rootOpts);
57
+ if (issueIds.length === 1) {
58
+ const issue = await issuesService.getIssueById(issueIds[0]);
59
+ const resolved = await downloadLinearUploads(issue, apiToken);
60
+ outputSuccess(resolved);
61
+ }
62
+ else {
63
+ const results = await Promise.all(issueIds.map(async (id) => {
64
+ const issue = await issuesService.getIssueById(id);
65
+ return downloadLinearUploads(issue, apiToken);
66
+ }));
67
+ outputSuccess(results);
68
+ }
69
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function setupReleasesCommands(program: Command): void;