@enrichlayer/el-linear 1.9.0 → 1.15.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 (135) hide show
  1. package/README.md +139 -10
  2. package/claude-skills/linear-operations/SKILL.md +41 -1
  3. package/dist/auth/linear-credential.d.ts +27 -0
  4. package/dist/auth/linear-credential.js +1 -0
  5. package/dist/auth/oauth-app-config.d.ts +4 -3
  6. package/dist/auth/oauth-app-config.js +13 -2
  7. package/dist/auth/oauth-callback.d.ts +2 -3
  8. package/dist/auth/oauth-callback.js +2 -2
  9. package/dist/auth/oauth-client.d.ts +8 -2
  10. package/dist/auth/oauth-client.js +26 -0
  11. package/dist/auth/oauth-fs.d.ts +2 -1
  12. package/dist/auth/oauth-headless.d.ts +2 -1
  13. package/dist/auth/oauth-storage.d.ts +5 -1
  14. package/dist/auth/oauth-storage.js +1 -1
  15. package/dist/auth/oauth-token.d.ts +4 -3
  16. package/dist/auth/oauth-token.js +16 -4
  17. package/dist/auth/token-resolver.d.ts +14 -5
  18. package/dist/auth/token-resolver.js +6 -1
  19. package/dist/commands/attachments.js +2 -1
  20. package/dist/commands/batch.js +18 -21
  21. package/dist/commands/comments.js +22 -33
  22. package/dist/commands/config.js +178 -5
  23. package/dist/commands/cycles.js +2 -1
  24. package/dist/commands/documents.js +2 -1
  25. package/dist/commands/graphql.js +4 -6
  26. package/dist/commands/init/aliases.js +1 -1
  27. package/dist/commands/init/defaults.d.ts +2 -1
  28. package/dist/commands/init/index.js +45 -35
  29. package/dist/commands/init/oauth.d.ts +4 -1
  30. package/dist/commands/init/oauth.js +22 -4
  31. package/dist/commands/init/shared.d.ts +24 -2
  32. package/dist/commands/init/shared.js +35 -4
  33. package/dist/commands/init/token.d.ts +3 -3
  34. package/dist/commands/init/token.js +5 -24
  35. package/dist/commands/init/workspace.d.ts +2 -1
  36. package/dist/commands/init/workspace.js +1 -1
  37. package/dist/commands/introspect.d.ts +27 -0
  38. package/dist/commands/introspect.js +178 -0
  39. package/dist/commands/issue-id.js +1 -3
  40. package/dist/commands/issues/branch.js +9 -1
  41. package/dist/commands/issues/description.js +2 -6
  42. package/dist/commands/issues/link-references.d.ts +21 -0
  43. package/dist/commands/issues/link-references.js +171 -0
  44. package/dist/commands/issues/relations.d.ts +44 -0
  45. package/dist/commands/issues/relations.js +132 -0
  46. package/dist/commands/issues.js +269 -309
  47. package/dist/commands/labels.js +15 -24
  48. package/dist/commands/profile.js +1 -0
  49. package/dist/commands/project-milestones.js +13 -20
  50. package/dist/commands/projects.d.ts +2 -0
  51. package/dist/commands/projects.js +157 -44
  52. package/dist/commands/read-shortcut.d.ts +1 -1
  53. package/dist/commands/read-shortcut.js +28 -8
  54. package/dist/commands/refs.js +75 -8
  55. package/dist/commands/releases.js +26 -30
  56. package/dist/commands/search.js +49 -33
  57. package/dist/commands/teams.js +2 -1
  58. package/dist/commands/templates.js +9 -14
  59. package/dist/commands/users.js +5 -2
  60. package/dist/config/config.d.ts +99 -1
  61. package/dist/config/config.js +264 -52
  62. package/dist/config/error-enrichment.d.ts +62 -0
  63. package/dist/config/error-enrichment.js +417 -0
  64. package/dist/config/issue-validation.d.ts +37 -0
  65. package/dist/config/issue-validation.js +63 -1
  66. package/dist/config/paths.d.ts +2 -8
  67. package/dist/config/paths.js +4 -2
  68. package/dist/config/resolver.d.ts +8 -1
  69. package/dist/config/resolver.js +11 -5
  70. package/dist/main.js +13 -1
  71. package/dist/queries/attachments-types.d.ts +30 -0
  72. package/dist/queries/attachments-types.js +5 -0
  73. package/dist/queries/comments-types.d.ts +55 -0
  74. package/dist/queries/comments-types.js +5 -0
  75. package/dist/queries/common.d.ts +2 -2
  76. package/dist/queries/common.js +8 -0
  77. package/dist/queries/documents-types.d.ts +62 -0
  78. package/dist/queries/documents-types.js +9 -0
  79. package/dist/queries/introspect-types.d.ts +58 -0
  80. package/dist/queries/introspect-types.js +10 -0
  81. package/dist/queries/issues-types.d.ts +481 -0
  82. package/dist/queries/issues-types.js +23 -0
  83. package/dist/queries/issues.d.ts +51 -10
  84. package/dist/queries/issues.js +147 -5
  85. package/dist/queries/labels-types.d.ts +65 -0
  86. package/dist/queries/labels-types.js +5 -0
  87. package/dist/queries/project-milestones-types.d.ts +92 -0
  88. package/dist/queries/project-milestones-types.js +10 -0
  89. package/dist/queries/project-milestones.d.ts +1 -1
  90. package/dist/queries/projects-types.d.ts +76 -0
  91. package/dist/queries/projects-types.js +5 -0
  92. package/dist/queries/projects.d.ts +2 -0
  93. package/dist/queries/projects.js +22 -0
  94. package/dist/queries/releases-types.d.ts +85 -0
  95. package/dist/queries/releases-types.js +5 -0
  96. package/dist/queries/search-types.d.ts +102 -0
  97. package/dist/queries/search-types.js +6 -0
  98. package/dist/queries/templates-types.d.ts +62 -0
  99. package/dist/queries/templates-types.js +9 -0
  100. package/dist/types/linear.d.ts +21 -3
  101. package/dist/utils/auto-link-references.d.ts +3 -3
  102. package/dist/utils/auto-link-references.js +30 -34
  103. package/dist/utils/extract-field.d.ts +19 -0
  104. package/dist/utils/extract-field.js +99 -0
  105. package/dist/utils/file-service.d.ts +6 -13
  106. package/dist/utils/file-service.js +0 -2
  107. package/dist/utils/formatters/summary.js +6 -1
  108. package/dist/utils/graphql-attachments-service.js +6 -9
  109. package/dist/utils/graphql-documents-service.js +19 -25
  110. package/dist/utils/graphql-issues-service.d.ts +112 -46
  111. package/dist/utils/graphql-issues-service.js +398 -206
  112. package/dist/utils/graphql-service.d.ts +10 -12
  113. package/dist/utils/graphql-service.js +0 -3
  114. package/dist/utils/issue-reference-extractor.d.ts +7 -0
  115. package/dist/utils/issue-reference-extractor.js +5 -3
  116. package/dist/utils/issues-service-bootstrap.d.ts +28 -0
  117. package/dist/utils/issues-service-bootstrap.js +27 -0
  118. package/dist/utils/linear-service.d.ts +21 -14
  119. package/dist/utils/linear-service.js +73 -11
  120. package/dist/utils/markdown-prosemirror.js +12 -12
  121. package/dist/utils/mention-resolver.js +1 -1
  122. package/dist/utils/output.d.ts +82 -2
  123. package/dist/utils/output.js +76 -11
  124. package/dist/utils/project-slug.d.ts +21 -0
  125. package/dist/utils/project-slug.js +45 -0
  126. package/dist/utils/protected-ranges.d.ts +14 -0
  127. package/dist/utils/protected-ranges.js +88 -2
  128. package/dist/utils/sanitize-for-log.d.ts +24 -0
  129. package/dist/utils/sanitize-for-log.js +38 -0
  130. package/dist/utils/table-formatter.js +24 -0
  131. package/dist/utils/validators.d.ts +7 -2
  132. package/dist/utils/validators.js +6 -0
  133. package/dist/utils/workspace-url.d.ts +5 -1
  134. package/dist/utils/workspace-url.js +53 -7
  135. package/package.json +2 -2
@@ -0,0 +1,178 @@
1
+ /**
2
+ * `el-linear introspect` and `el-linear validate-flag` — expose the
3
+ * commander command tree so external linters (e.g. CI steps that scan
4
+ * SKILL.md files for `el-linear <cmd> --<flag>` references) can verify
5
+ * that hardcoded flag names actually exist on the current binary.
6
+ *
7
+ * Without this, a flag rename inside `el-linear` (say `--parent-ticket`
8
+ * → `--parent`) silently breaks every skill that prose-references the
9
+ * old name. Skills then fail at runtime, far from the rename commit.
10
+ *
11
+ * `introspect` dumps the full tree — name, description, version,
12
+ * options, aliases, and recursively nested subcommands.
13
+ *
14
+ * `validate-flag` is a thin ergonomic wrapper for the CI-lint use case:
15
+ *
16
+ * $ el-linear validate-flag issues create --parent-ticket
17
+ * { "ok": true, "command": ["issues", "create"], "flag": "--parent-ticket" }
18
+ * $ el-linear validate-flag issues create --does-not-exist
19
+ * { "ok": false, ... "error": "..." } # exit code 1
20
+ *
21
+ * Both commands ignore root-level flags (`--api-token`, `--json`, etc.)
22
+ * when walking subcommand options; those are dumped at the root, not
23
+ * under every command. `validate-flag` walks ancestors so a global flag
24
+ * (e.g. `--format`) validates on any subcommand.
25
+ */
26
+ import { outputSuccess } from "../utils/output.js";
27
+ function describeOption(opt) {
28
+ return {
29
+ flags: opt.flags,
30
+ long: opt.long ?? null,
31
+ short: opt.short ?? null,
32
+ description: opt.description,
33
+ required: opt.required,
34
+ optional: opt.optional,
35
+ variadic: opt.variadic,
36
+ defaultValue: opt.defaultValue,
37
+ negated: opt.negate,
38
+ };
39
+ }
40
+ function describeCommand(cmd) {
41
+ return {
42
+ name: cmd.name(),
43
+ aliases: cmd.aliases(),
44
+ description: cmd.description(),
45
+ usage: cmd.usage(),
46
+ options: cmd.options.map(describeOption),
47
+ commands: cmd.commands.map(describeCommand),
48
+ };
49
+ }
50
+ /**
51
+ * Walk the command tree to find the command at `path`. Returns null if
52
+ * any segment doesn't resolve. Matches both canonical name and aliases
53
+ * so `el-linear validate-flag issue get --foo` works the same as
54
+ * `el-linear validate-flag issues read --foo`.
55
+ */
56
+ function findCommand(root, path) {
57
+ let current = root;
58
+ for (const segment of path) {
59
+ // Match by canonical name or alias. Commander throws on
60
+ // duplicate aliases at registration time, so first-match-wins is
61
+ // only meaningful for the name/alias choice within a single
62
+ // command, not for ambiguity across siblings.
63
+ const next = current.commands.find((c) => c.name() === segment || c.aliases().includes(segment));
64
+ if (!next)
65
+ return null;
66
+ current = next;
67
+ }
68
+ return current;
69
+ }
70
+ export function setupIntrospectCommand(program) {
71
+ program
72
+ .command("cli-introspect [path...]")
73
+ .description("Dump the el-linear CLI command tree as JSON so external linters can verify flag references in prose (e.g. SKILL.md). Pass a path (`cli-introspect issues create`) to dump a specific subtree. Distinct from `introspect` (which queries the Linear GraphQL schema).")
74
+ .action((path) => {
75
+ const target = path.length === 0 ? program : findCommand(program, path);
76
+ if (!target) {
77
+ outputSuccess({
78
+ error: `Command not found: ${path.join(" ")}`,
79
+ path,
80
+ });
81
+ process.exit(1);
82
+ return; // Defense: explicit so test correctness doesn't
83
+ // depend on the vi.spyOn(process.exit) mock throwing.
84
+ }
85
+ if (path.length === 0) {
86
+ outputSuccess({
87
+ name: program.name(),
88
+ description: program.description(),
89
+ version: program.version() ?? null,
90
+ options: program.options.map(describeOption),
91
+ commands: program.commands.map(describeCommand),
92
+ });
93
+ }
94
+ else {
95
+ outputSuccess(describeCommand(target));
96
+ }
97
+ });
98
+ program
99
+ .command("validate-flag [args...]")
100
+ .description("Verify a flag exists on a command. Two equivalent forms:\n" +
101
+ " $ el-linear validate-flag --check-flag --parent-ticket issues create\n" +
102
+ " $ el-linear validate-flag issues create --parent-ticket\n" +
103
+ "The first form is unambiguous regardless of whether the flag-to-check " +
104
+ "takes an argument; the positional form needs `--` before flags that " +
105
+ "take args (e.g. `validate-flag -- issues create --jq`) so commander " +
106
+ "doesn't try to consume the next positional as the flag's value. " +
107
+ "Exits 0 if the flag is defined on the target command (or any " +
108
+ "ancestor for inherited globals), 1 otherwise.")
109
+ // Without these, commander tries to parse `--parent-ticket` (the
110
+ // flag we want to *check*) as if it were an option OF validate-flag
111
+ // itself. We need it to pass through as a positional arg.
112
+ .allowUnknownOption(true)
113
+ .option("--check-flag <flag>", "the flag to look up (unambiguous alternative to passing it positionally — use this when the flag-being-checked is one that takes an argument)")
114
+ .action((args, options) => {
115
+ // Resolve the flag-to-check from either `--check-flag <flag>` or
116
+ // the first positional arg that starts with `-`. The explicit
117
+ // option form sidesteps commander's positional ambiguity for
118
+ // flags that take arguments (e.g. `--jq <filter>`).
119
+ let flag;
120
+ let path;
121
+ if (options.checkFlag) {
122
+ flag = options.checkFlag;
123
+ path = args; // all positionals are the command path
124
+ }
125
+ else {
126
+ const flagIdx = args.findIndex((a) => a.startsWith("-"));
127
+ if (flagIdx === -1) {
128
+ outputSuccess({
129
+ ok: false,
130
+ error: "No flag argument found. Pass `--check-flag <flag>` or include `--<flag>` in the positional args.",
131
+ args,
132
+ });
133
+ process.exit(1);
134
+ return;
135
+ }
136
+ path = args.slice(0, flagIdx);
137
+ flag = args[flagIdx];
138
+ }
139
+ const target = path.length === 0 ? program : findCommand(program, path);
140
+ if (!target) {
141
+ outputSuccess({
142
+ ok: false,
143
+ error: `Command not found: ${path.join(" ") || "(root)"}`,
144
+ command: path,
145
+ flag,
146
+ });
147
+ process.exit(1);
148
+ return;
149
+ }
150
+ // Walk the target + ancestors so root-level globals
151
+ // (--api-token, --format, etc.) validate when a skill writes
152
+ // `el-linear issues create --format summary`.
153
+ const ancestors = [];
154
+ for (let c = target; c; c = c.parent) {
155
+ ancestors.push(c);
156
+ }
157
+ const allOptions = ancestors.flatMap((c) => c.options);
158
+ const found = allOptions.find((opt) => opt.long === flag || opt.short === flag);
159
+ if (!found) {
160
+ outputSuccess({
161
+ ok: false,
162
+ error: `Flag not found: ${flag}`,
163
+ command: path,
164
+ flag,
165
+ availableOptions: target.options.map((o) => o.long ?? o.short),
166
+ });
167
+ process.exit(1);
168
+ return;
169
+ }
170
+ outputSuccess({
171
+ ok: true,
172
+ command: path,
173
+ flag,
174
+ flags: found.flags,
175
+ description: found.description,
176
+ });
177
+ });
178
+ }
@@ -75,9 +75,7 @@ export function setupIssueIdCommand(program) {
75
75
  const service = await createGraphQLService({
76
76
  apiToken: rootOpts.apiToken,
77
77
  });
78
- const result = (await service.rawRequest(ISSUE_QUERY, {
79
- id: parsed.issueId,
80
- }));
78
+ const result = await service.rawRequest(ISSUE_QUERY, { id: parsed.issueId });
81
79
  outputSuccess({ ...parsed, issue: result.issue ?? null });
82
80
  }));
83
81
  }
@@ -36,5 +36,13 @@ export function gitCheckoutBranch(branchName) {
36
36
  outputWarning("Not inside a git repository — skipping branch checkout.");
37
37
  return;
38
38
  }
39
- execFileSync("git", ["checkout", "-b", branchName], { stdio: "pipe" });
39
+ // `--` separates the new-branch name from any ref. Without it, a
40
+ // branch name starting with `-` (server bug, malicious team member
41
+ // crafting an issue title that survives slugification, or a forked
42
+ // Linear-compatible API) would be parsed as a git flag rather than
43
+ // a ref. The default `feature/` prefix from `toBranchName` already
44
+ // blocks the common case, but the prefix is configurable — empty-
45
+ // prefix callers would lose the defense without this terminator.
46
+ // Defense-in-depth (DEV-4064).
47
+ execFileSync("git", ["checkout", "-b", branchName, "--"], { stdio: "pipe" });
40
48
  }
@@ -147,12 +147,8 @@ export async function prepareDescriptionRewrite(description, selfIdentifier, lin
147
147
  };
148
148
  }
149
149
  export async function pushDescriptionUpdate(issueUuid, description, graphQLService) {
150
- const updateResult = await graphQLService.rawRequest(UPDATE_ISSUE_MUTATION, {
151
- id: issueUuid,
152
- input: { description },
153
- });
154
- const mutation = updateResult.issueUpdate;
155
- if (!mutation?.success) {
150
+ const updateResult = await graphQLService.rawRequest(UPDATE_ISSUE_MUTATION, { id: issueUuid, input: { description } });
151
+ if (!updateResult.issueUpdate.success) {
156
152
  throw new Error("Failed to rewrite description");
157
153
  }
158
154
  }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * `el-linear issues link-references` handler.
3
+ *
4
+ * Two modes:
5
+ * - Single-issue (`<issueId>`): scan one issue's description (and
6
+ * optionally its comments) for issue refs, create sidebar
7
+ * relations for any that aren't already linked, optionally
8
+ * rewrite the description to wrap the bare refs as markdown
9
+ * links.
10
+ * - Batch (`--team <key>`): walk the team's recent issues and run
11
+ * the same auto-link pass on each. Description rewrite is
12
+ * intentionally disabled in batch mode — bulk rewrites
13
+ * bot-author every issue's revision history, which makes
14
+ * "who edited this?" attribution noisy.
15
+ *
16
+ * Extracted from `commands/issues.ts` (ALL-938) so that file can
17
+ * focus on commander wiring + the create / update / read / search
18
+ * handlers.
19
+ */
20
+ import type { Command, OptionValues } from "commander";
21
+ export declare function handleLinkReferencesIssue(issueIdOrTeamFlag: string | undefined, options: OptionValues, command: Command): Promise<void>;
@@ -0,0 +1,171 @@
1
+ /**
2
+ * `el-linear issues link-references` handler.
3
+ *
4
+ * Two modes:
5
+ * - Single-issue (`<issueId>`): scan one issue's description (and
6
+ * optionally its comments) for issue refs, create sidebar
7
+ * relations for any that aren't already linked, optionally
8
+ * rewrite the description to wrap the bare refs as markdown
9
+ * links.
10
+ * - Batch (`--team <key>`): walk the team's recent issues and run
11
+ * the same auto-link pass on each. Description rewrite is
12
+ * intentionally disabled in batch mode — bulk rewrites
13
+ * bot-author every issue's revision history, which makes
14
+ * "who edited this?" attribution noisy.
15
+ *
16
+ * Extracted from `commands/issues.ts` (ALL-938) so that file can
17
+ * focus on commander wiring + the create / update / read / search
18
+ * handlers.
19
+ */
20
+ import { resolveTeam } from "../../config/resolver.js";
21
+ import { LIST_COMMENTS_QUERY } from "../../queries/comments.js";
22
+ import { GET_ISSUE_RELATIONS_QUERY, SCAN_ISSUES_QUERY, } from "../../queries/issues.js";
23
+ import { autoLinkReferences, } from "../../utils/auto-link-references.js";
24
+ import { createGraphQLService, } from "../../utils/graphql-service.js";
25
+ import { createLinearService, } from "../../utils/linear-service.js";
26
+ import { outputSuccess } from "../../utils/output.js";
27
+ import { getRootOpts } from "../../utils/root-opts.js";
28
+ import { parsePositiveInt } from "../../utils/validators.js";
29
+ import { prepareDescriptionRewrite, pushDescriptionUpdate, } from "./description.js";
30
+ async function fetchCommentBodies(issueUuid, graphQLService) {
31
+ const result = await graphQLService.rawRequest(LIST_COMMENTS_QUERY, {
32
+ issueId: issueUuid,
33
+ first: 250,
34
+ });
35
+ const nodes = result.issue?.comments.nodes ?? [];
36
+ return nodes.map((c) => c.body).filter((b) => b.length > 0);
37
+ }
38
+ async function linkReferencesForIssue(args) {
39
+ const comments = args.includeComments
40
+ ? await fetchCommentBodies(args.issueUuid, args.graphQLService)
41
+ : undefined;
42
+ return autoLinkReferences({
43
+ issueId: args.issueUuid,
44
+ identifier: args.identifier,
45
+ description: args.description,
46
+ comments,
47
+ dryRun: args.dryRun,
48
+ graphQLService: args.graphQLService,
49
+ linearService: args.linearService,
50
+ preResolved: args.preResolved,
51
+ });
52
+ }
53
+ async function handleLinkReferencesSingle(issueId, options, graphQLService, linearService) {
54
+ const resolvedId = await linearService.resolveIssueId(issueId);
55
+ // GET_ISSUE_RELATIONS_QUERY also returns description, so this single call gives us everything
56
+ // we need for the description-only path. Comments are fetched separately on demand.
57
+ const issueResult = await graphQLService.rawRequest(GET_ISSUE_RELATIONS_QUERY, { id: resolvedId });
58
+ if (!issueResult.issue) {
59
+ throw new Error(`Issue "${issueId}" not found`);
60
+ }
61
+ const issue = issueResult.issue;
62
+ const identifier = issue.identifier;
63
+ const description = issue.description ?? "";
64
+ const dryRun = Boolean(options.dryRun);
65
+ const includeComments = Boolean(options.includeComments);
66
+ const rewriteDescription = Boolean(options.rewriteDescription);
67
+ // If rewriting, validate refs once up front so we can both wrap the description and
68
+ // pass the resolved map down to autoLink — avoids a second resolve round trip.
69
+ const rewrite = rewriteDescription
70
+ ? await prepareDescriptionRewrite(description, identifier, linearService, graphQLService)
71
+ : { preResolved: undefined, wrapped: undefined };
72
+ const autoLinked = await linkReferencesForIssue({
73
+ issueUuid: resolvedId,
74
+ identifier,
75
+ description,
76
+ includeComments,
77
+ dryRun,
78
+ graphQLService,
79
+ linearService,
80
+ preResolved: rewrite.preResolved,
81
+ });
82
+ let descriptionRewritten = false;
83
+ if (rewriteDescription && rewrite.wrapped && !dryRun) {
84
+ await pushDescriptionUpdate(resolvedId, rewrite.wrapped, graphQLService);
85
+ descriptionRewritten = true;
86
+ }
87
+ outputSuccess({
88
+ id: resolvedId,
89
+ identifier,
90
+ title: issue.title,
91
+ autoLinked,
92
+ ...(rewriteDescription
93
+ ? {
94
+ descriptionRewritten,
95
+ ...(dryRun && rewrite.wrapped
96
+ ? { descriptionPreview: rewrite.wrapped }
97
+ : {}),
98
+ }
99
+ : {}),
100
+ meta: {
101
+ dryRun,
102
+ includeComments,
103
+ ...(rewriteDescription ? { rewriteDescription: true } : {}),
104
+ },
105
+ });
106
+ }
107
+ async function handleLinkReferencesBatch(teamInput, options, graphQLService, linearService) {
108
+ const teamId = resolveTeam(teamInput);
109
+ const limit = options.limit
110
+ ? parsePositiveInt(options.limit, "--limit")
111
+ : 100;
112
+ const dryRun = Boolean(options.dryRun);
113
+ const includeComments = Boolean(options.includeComments);
114
+ const scan = await graphQLService.rawRequest(SCAN_ISSUES_QUERY, {
115
+ filter: { team: { id: { eq: teamId } } },
116
+ first: limit,
117
+ });
118
+ const nodes = scan.issues.nodes;
119
+ const entries = [];
120
+ for (const node of nodes) {
121
+ const issueUuid = node.id;
122
+ const identifier = node.identifier;
123
+ const description = node.description ?? "";
124
+ const autoLinked = await linkReferencesForIssue({
125
+ issueUuid,
126
+ identifier,
127
+ description,
128
+ includeComments,
129
+ dryRun,
130
+ graphQLService,
131
+ linearService,
132
+ });
133
+ entries.push({ identifier, autoLinked });
134
+ }
135
+ // Summary counts across the batch
136
+ const totalLinked = entries.reduce((n, e) => n + e.autoLinked.linked.length, 0);
137
+ const totalSkipped = entries.reduce((n, e) => n + e.autoLinked.skipped.length, 0);
138
+ const totalFailed = entries.reduce((n, e) => n + e.autoLinked.failed.length, 0);
139
+ outputSuccess({
140
+ data: entries,
141
+ meta: {
142
+ count: entries.length,
143
+ team: teamInput,
144
+ dryRun,
145
+ includeComments,
146
+ totals: {
147
+ linked: totalLinked,
148
+ skipped: totalSkipped,
149
+ failed: totalFailed,
150
+ },
151
+ },
152
+ });
153
+ }
154
+ export async function handleLinkReferencesIssue(issueIdOrTeamFlag, options, command) {
155
+ const rootOpts = getRootOpts(command);
156
+ const graphQLService = await createGraphQLService(rootOpts);
157
+ const linearService = await createLinearService(rootOpts);
158
+ if (options.team) {
159
+ if (issueIdOrTeamFlag) {
160
+ throw new Error("Provide either an issueId positional argument OR --team, not both. With --team, omit the issueId.");
161
+ }
162
+ if (options.rewriteDescription) {
163
+ throw new Error("--rewrite-description is single-issue only. Use it case by case (one issueId per invocation) — bulk description rewrites bot-author the whole team's history.");
164
+ }
165
+ return handleLinkReferencesBatch(options.team, options, graphQLService, linearService);
166
+ }
167
+ if (!issueIdOrTeamFlag) {
168
+ throw new Error("issueId is required (or use --team <key> to scan a whole team)");
169
+ }
170
+ return handleLinkReferencesSingle(issueIdOrTeamFlag, options, graphQLService, linearService);
171
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Issue-relation helpers — `transformIssueRelation` for the create
3
+ * path, plus the `RelatedIssueEntry` builders used by the
4
+ * `issues related` query path.
5
+ *
6
+ * Extracted from `commands/issues.ts` (ALL-938) so that file can
7
+ * focus on commander wiring + handlers.
8
+ */
9
+ import type { RelationIncomingNode, RelationOutgoingNode } from "../../queries/issues-types.js";
10
+ import type { LinearIssueRelation, LinearPriority } from "../../types/linear.js";
11
+ import type { GraphQLService } from "../../utils/graphql-service.js";
12
+ import type { LinearService } from "../../utils/linear-service.js";
13
+ /**
14
+ * Walk the `--related-to` / `--blocks` / `--blocked-by` /
15
+ * `--duplicate-of` flags and create the corresponding
16
+ * IssueRelation rows. Used by `issues create`.
17
+ */
18
+ export declare function createRelations(sourceId: string, options: Record<string, unknown>, graphQLService: GraphQLService, linearService: LinearService): Promise<LinearIssueRelation[]>;
19
+ export interface RelatedIssueEntry {
20
+ direction: "outgoing" | "incoming";
21
+ id: string;
22
+ issue: {
23
+ id: string;
24
+ identifier: string;
25
+ title: string;
26
+ state?: {
27
+ id: string;
28
+ name: string;
29
+ };
30
+ priority?: LinearPriority;
31
+ assignee?: {
32
+ id: string;
33
+ name: string;
34
+ };
35
+ team?: {
36
+ id: string;
37
+ key: string;
38
+ name: string;
39
+ };
40
+ };
41
+ type: string;
42
+ }
43
+ export declare function buildOutgoingRelationEntries(nodes: RelationOutgoingNode[] | undefined): RelatedIssueEntry[];
44
+ export declare function buildIncomingRelationEntries(nodes: RelationIncomingNode[] | undefined): RelatedIssueEntry[];
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Issue-relation helpers — `transformIssueRelation` for the create
3
+ * path, plus the `RelatedIssueEntry` builders used by the
4
+ * `issues related` query path.
5
+ *
6
+ * Extracted from `commands/issues.ts` (ALL-938) so that file can
7
+ * focus on commander wiring + handlers.
8
+ */
9
+ import { ISSUE_RELATION_CREATE_MUTATION } from "../../queries/issues.js";
10
+ import { splitList } from "../../utils/validators.js";
11
+ function transformIssueRelation(rel) {
12
+ return {
13
+ id: rel.id,
14
+ type: rel.type,
15
+ issue: {
16
+ id: rel.issue.id,
17
+ identifier: rel.issue.identifier,
18
+ title: rel.issue.title,
19
+ },
20
+ relatedIssue: {
21
+ id: rel.relatedIssue.id,
22
+ identifier: rel.relatedIssue.identifier,
23
+ title: rel.relatedIssue.title,
24
+ },
25
+ };
26
+ }
27
+ /**
28
+ * Walk the `--related-to` / `--blocks` / `--blocked-by` /
29
+ * `--duplicate-of` flags and create the corresponding
30
+ * IssueRelation rows. Used by `issues create`.
31
+ */
32
+ export async function createRelations(sourceId, options, graphQLService, linearService) {
33
+ const relations = [];
34
+ const relationSpecs = [
35
+ { option: "relatedTo", type: "related" },
36
+ { option: "blocks", type: "blocks" },
37
+ { option: "blockedBy", type: "blocks", reverse: true },
38
+ { option: "duplicateOf", type: "duplicate" },
39
+ ];
40
+ for (const spec of relationSpecs) {
41
+ const value = options[spec.option];
42
+ if (!value) {
43
+ continue;
44
+ }
45
+ for (const id of splitList(value)) {
46
+ const targetId = await linearService.resolveIssueId(id);
47
+ const result = await graphQLService.rawRequest(ISSUE_RELATION_CREATE_MUTATION, {
48
+ input: {
49
+ issueId: spec.reverse ? targetId : sourceId,
50
+ relatedIssueId: spec.reverse ? sourceId : targetId,
51
+ type: spec.type,
52
+ },
53
+ });
54
+ const issueRelation = result.issueRelationCreate.issueRelation;
55
+ if (!issueRelation) {
56
+ throw new Error(`Failed to create ${spec.type} relation from ${sourceId} to ${id}`);
57
+ }
58
+ relations.push(transformIssueRelation(issueRelation));
59
+ }
60
+ }
61
+ return relations;
62
+ }
63
+ /**
64
+ * Invert relation type for incoming (inverse) relations so the output
65
+ * reads naturally from the perspective of the queried issue. E.g. if
66
+ * DEV-100 "blocks" DEV-200, and we query DEV-200, the inverse relation
67
+ * type is "blocks" but direction is incoming → "blockedBy".
68
+ */
69
+ function normalizeInverseType(type) {
70
+ if (type === "blocks") {
71
+ return "blockedBy";
72
+ }
73
+ return type;
74
+ }
75
+ function buildRelatedIssueSummary(peer) {
76
+ return {
77
+ id: peer.id,
78
+ identifier: peer.identifier,
79
+ title: peer.title,
80
+ ...(peer.state
81
+ ? { state: { id: peer.state.id, name: peer.state.name } }
82
+ : {}),
83
+ ...(peer.priority == null ? {} : { priority: peer.priority }),
84
+ ...(peer.assignee
85
+ ? {
86
+ assignee: {
87
+ id: peer.assignee.id,
88
+ name: peer.assignee.name,
89
+ },
90
+ }
91
+ : {}),
92
+ ...(peer.team
93
+ ? {
94
+ team: {
95
+ id: peer.team.id,
96
+ key: peer.team.key,
97
+ name: peer.team.name,
98
+ },
99
+ }
100
+ : {}),
101
+ };
102
+ }
103
+ export function buildOutgoingRelationEntries(nodes) {
104
+ const entries = [];
105
+ for (const rel of nodes ?? []) {
106
+ if (!rel.relatedIssue) {
107
+ continue;
108
+ }
109
+ entries.push({
110
+ id: rel.id,
111
+ type: rel.type,
112
+ direction: "outgoing",
113
+ issue: buildRelatedIssueSummary(rel.relatedIssue),
114
+ });
115
+ }
116
+ return entries;
117
+ }
118
+ export function buildIncomingRelationEntries(nodes) {
119
+ const entries = [];
120
+ for (const rel of nodes ?? []) {
121
+ if (!rel.issue) {
122
+ continue;
123
+ }
124
+ entries.push({
125
+ id: rel.id,
126
+ type: normalizeInverseType(rel.type),
127
+ direction: "incoming",
128
+ issue: buildRelatedIssueSummary(rel.issue),
129
+ });
130
+ }
131
+ return entries;
132
+ }