@enrichlayer/el-linear 1.7.0 → 1.10.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 (108) hide show
  1. package/README.md +82 -1
  2. package/claude-skills/linear-operations/SKILL.md +55 -0
  3. package/dist/auth/oauth-callback.js +14 -7
  4. package/dist/auth/oauth-fs.d.ts +22 -0
  5. package/dist/auth/oauth-fs.js +77 -6
  6. package/dist/auth/oauth-headless.d.ts +13 -8
  7. package/dist/auth/oauth-headless.js +18 -11
  8. package/dist/auth/oauth-storage.d.ts +11 -3
  9. package/dist/auth/oauth-storage.js +15 -8
  10. package/dist/auth/token-resolver.d.ts +9 -0
  11. package/dist/auth/token-resolver.js +57 -37
  12. package/dist/commands/attachments.js +2 -1
  13. package/dist/commands/comments.js +17 -28
  14. package/dist/commands/cycles.js +2 -1
  15. package/dist/commands/documents.js +2 -1
  16. package/dist/commands/graphql.js +4 -6
  17. package/dist/commands/init/index.js +2 -0
  18. package/dist/commands/init/oauth.d.ts +6 -0
  19. package/dist/commands/init/oauth.js +8 -2
  20. package/dist/commands/init/token.d.ts +0 -8
  21. package/dist/commands/init/token.js +13 -1
  22. package/dist/commands/issue-id.js +1 -3
  23. package/dist/commands/issues/branch.d.ts +17 -0
  24. package/dist/commands/issues/branch.js +40 -0
  25. package/dist/commands/issues/description.d.ts +89 -0
  26. package/dist/commands/issues/description.js +183 -0
  27. package/dist/commands/issues/link-references.d.ts +21 -0
  28. package/dist/commands/issues/link-references.js +171 -0
  29. package/dist/commands/issues/relations.d.ts +55 -0
  30. package/dist/commands/issues/relations.js +132 -0
  31. package/dist/commands/issues.js +97 -497
  32. package/dist/commands/labels.js +20 -28
  33. package/dist/commands/profile/migrate-legacy.js +10 -33
  34. package/dist/commands/profile.js +1 -10
  35. package/dist/commands/project-milestones.js +13 -20
  36. package/dist/commands/projects.d.ts +5 -1
  37. package/dist/commands/projects.js +202 -102
  38. package/dist/commands/read-shortcut.d.ts +6 -0
  39. package/dist/commands/read-shortcut.js +6 -1
  40. package/dist/commands/refs.js +10 -1
  41. package/dist/commands/releases.js +26 -30
  42. package/dist/commands/search.js +21 -30
  43. package/dist/commands/teams.js +2 -1
  44. package/dist/commands/templates.js +128 -7
  45. package/dist/commands/users.js +3 -1
  46. package/dist/config/config.js +31 -9
  47. package/dist/config/issue-validation.js +1 -1
  48. package/dist/config/paths.d.ts +11 -0
  49. package/dist/config/paths.js +44 -6
  50. package/dist/config/resolver.js +2 -3
  51. package/dist/config/term-enforcer.js +1 -1
  52. package/dist/main.js +37 -2
  53. package/dist/queries/attachments-types.d.ts +30 -0
  54. package/dist/queries/attachments-types.js +5 -0
  55. package/dist/queries/comments-types.d.ts +49 -0
  56. package/dist/queries/comments-types.js +5 -0
  57. package/dist/queries/documents-types.d.ts +61 -0
  58. package/dist/queries/documents-types.js +9 -0
  59. package/dist/queries/introspect-types.d.ts +57 -0
  60. package/dist/queries/introspect-types.js +10 -0
  61. package/dist/queries/issues-types.d.ts +416 -0
  62. package/dist/queries/issues-types.js +23 -0
  63. package/dist/queries/issues.d.ts +2 -0
  64. package/dist/queries/issues.js +22 -0
  65. package/dist/queries/labels-types.d.ts +64 -0
  66. package/dist/queries/labels-types.js +5 -0
  67. package/dist/queries/project-milestones-types.d.ts +91 -0
  68. package/dist/queries/project-milestones-types.js +10 -0
  69. package/dist/queries/projects-types.d.ts +75 -0
  70. package/dist/queries/projects-types.js +5 -0
  71. package/dist/queries/projects.d.ts +2 -0
  72. package/dist/queries/projects.js +22 -0
  73. package/dist/queries/releases-types.d.ts +84 -0
  74. package/dist/queries/releases-types.js +5 -0
  75. package/dist/queries/search-types.d.ts +86 -0
  76. package/dist/queries/search-types.js +6 -0
  77. package/dist/queries/templates-types.d.ts +61 -0
  78. package/dist/queries/templates-types.js +9 -0
  79. package/dist/queries/templates.d.ts +3 -0
  80. package/dist/queries/templates.js +43 -0
  81. package/dist/types/linear.d.ts +8 -2
  82. package/dist/utils/auth.js +7 -9
  83. package/dist/utils/auto-link-references.js +44 -25
  84. package/dist/utils/disk-cache.js +2 -1
  85. package/dist/utils/formatters/summary.d.ts +62 -0
  86. package/dist/utils/formatters/summary.js +755 -0
  87. package/dist/utils/graphql-attachments-service.js +6 -9
  88. package/dist/utils/graphql-documents-service.js +19 -25
  89. package/dist/utils/graphql-issues-service.d.ts +118 -5
  90. package/dist/utils/graphql-issues-service.js +202 -209
  91. package/dist/utils/issue-reference-extractor.d.ts +9 -1
  92. package/dist/utils/issue-reference-extractor.js +16 -9
  93. package/dist/utils/issue-reference-wrapper.js +1 -54
  94. package/dist/utils/linear-service.d.ts +7 -3
  95. package/dist/utils/linear-service.js +27 -5
  96. package/dist/utils/markdown-prosemirror.js +17 -1
  97. package/dist/utils/mention-resolver.js +17 -5
  98. package/dist/utils/output.d.ts +7 -1
  99. package/dist/utils/output.js +38 -1
  100. package/dist/utils/protected-ranges.d.ts +33 -0
  101. package/dist/utils/protected-ranges.js +73 -0
  102. package/dist/utils/table-formatter.d.ts +36 -0
  103. package/dist/utils/table-formatter.js +46 -24
  104. package/dist/utils/validators.d.ts +9 -1
  105. package/dist/utils/validators.js +10 -0
  106. package/dist/utils/workspace-url.d.ts +5 -1
  107. package/dist/utils/workspace-url.js +35 -5
  108. package/package.json +1 -1
@@ -129,39 +129,54 @@ function pad(str, width) {
129
129
  const truncated = truncate(str, width);
130
130
  return truncated + " ".repeat(Math.max(0, width - truncated.length));
131
131
  }
132
- export function formatTable(issues, fieldNames) {
133
- const columnKeys = fieldNames ?? DEFAULT_COLUMNS;
134
- const columns = columnKeys
135
- .map((key) => ALL_COLUMNS[key])
136
- .filter((col) => col !== undefined);
132
+ /**
133
+ * Render `rows` as a fixed-width text table using `columns`. Generic
134
+ * over the row type `T` so issues, projects, and other resources
135
+ * share the same width-padding + colorize machinery.
136
+ */
137
+ export function renderFixedWidthTable(rows, columns) {
137
138
  if (columns.length === 0) {
138
139
  return "No valid columns specified.";
139
140
  }
140
141
  const header = columns.map((col) => pad(col.header, col.width)).join(" ");
141
142
  const separator = columns.map((col) => "─".repeat(col.width)).join("──");
142
- const rows = issues.map((issue) => columns
143
+ const body = rows.map((row) => columns
143
144
  .map((col) => {
144
- const padded = pad(col.extract(issue), col.width);
145
- return col.colorize ? col.colorize(padded, issue) : padded;
145
+ const padded = pad(col.extract(row), col.width);
146
+ return col.colorize ? col.colorize(padded, row) : padded;
146
147
  })
147
148
  .join(" "));
148
- return [header, separator, ...rows].join("\n");
149
+ return [header, separator, ...body].join("\n");
149
150
  }
150
- export function formatCsv(issues, fieldNames) {
151
- const columnKeys = fieldNames ?? DEFAULT_COLUMNS;
152
- const columns = columnKeys
153
- .map((key) => ALL_COLUMNS[key])
154
- .filter((col) => col !== undefined);
151
+ /**
152
+ * Render `rows` as CSV using `columns`. Quotes any value containing
153
+ * `,` or `"` (RFC 4180-style escaping). Generic over the row type.
154
+ */
155
+ export function renderCsv(rows, columns) {
155
156
  const header = columns.map((col) => col.header).join(",");
156
- const rows = issues.map((issue) => columns
157
+ const body = rows.map((row) => columns
157
158
  .map((col) => {
158
- const value = col.extract(issue);
159
+ const value = col.extract(row);
159
160
  return value.includes(",") || value.includes('"')
160
161
  ? `"${value.replace(/"/g, '""')}"`
161
162
  : value;
162
163
  })
163
164
  .join(","));
164
- return [header, ...rows].join("\n");
165
+ return [header, ...body].join("\n");
166
+ }
167
+ export function formatTable(issues, fieldNames) {
168
+ const columnKeys = fieldNames ?? DEFAULT_COLUMNS;
169
+ const columns = columnKeys
170
+ .map((key) => ALL_COLUMNS[key])
171
+ .filter((col) => col !== undefined);
172
+ return renderFixedWidthTable(issues, columns);
173
+ }
174
+ export function formatCsv(issues, fieldNames) {
175
+ const columnKeys = fieldNames ?? DEFAULT_COLUMNS;
176
+ const columns = columnKeys
177
+ .map((key) => ALL_COLUMNS[key])
178
+ .filter((col) => col !== undefined);
179
+ return renderCsv(issues, columns);
165
180
  }
166
181
  // ── Markdown formatting ────────────────────────────────────────
167
182
  const STATUS_MD = {
@@ -234,16 +249,23 @@ const MD_COLUMNS = {
234
249
  function escapeMarkdownCell(str) {
235
250
  return str.replace(/\|/g, "\\|").replace(/\n/g, " ");
236
251
  }
237
- export function formatMarkdown(issues, fieldNames) {
238
- const columnKeys = fieldNames ?? DEFAULT_COLUMNS;
239
- const columns = columnKeys
240
- .map((key) => MD_COLUMNS[key])
241
- .filter((col) => col !== undefined);
252
+ /**
253
+ * Render `rows` as a markdown table using `columns`. Generic over
254
+ * the row type. Escapes `|` and newlines in cell values.
255
+ */
256
+ export function renderMarkdownTable(rows, columns) {
242
257
  if (columns.length === 0) {
243
258
  return "No valid columns specified.";
244
259
  }
245
260
  const header = `| ${columns.map((col) => col.header).join(" | ")} |`;
246
261
  const divider = `| ${columns.map((col) => (col.align === "right" ? "---:" : "---")).join(" | ")} |`;
247
- const rows = issues.map((issue) => `| ${columns.map((col) => escapeMarkdownCell(col.extract(issue))).join(" | ")} |`);
248
- return [header, divider, ...rows].join("\n");
262
+ const body = rows.map((row) => `| ${columns.map((col) => escapeMarkdownCell(col.extract(row))).join(" | ")} |`);
263
+ return [header, divider, ...body].join("\n");
264
+ }
265
+ export function formatMarkdown(issues, fieldNames) {
266
+ const columnKeys = fieldNames ?? DEFAULT_COLUMNS;
267
+ const columns = columnKeys
268
+ .map((key) => MD_COLUMNS[key])
269
+ .filter((col) => col !== undefined);
270
+ return renderMarkdownTable(issues, columns);
249
271
  }
@@ -9,6 +9,14 @@ export declare function parsePositiveInt(value: string, flagName: string): numbe
9
9
  export declare function validatePriority(value: string): number;
10
10
  export declare function validateHexColor(value: string): string;
11
11
  export declare function validateIsoDate(value: string): string;
12
- export declare function splitList(value: string): string[];
12
+ /**
13
+ * Split a comma-separated list into trimmed, non-empty items.
14
+ *
15
+ * Accepts `undefined` so call sites can pipe through commander's
16
+ * optional flag values without an explicit truthy guard. `false`
17
+ * (commander's representation of `--no-foo`) and empty string both
18
+ * resolve to `[]`.
19
+ */
20
+ export declare function splitList(value: string | undefined | null | false): string[];
13
21
  export declare function parsePriorityFilter(value: string): number[];
14
22
  export declare const PRIORITY_LABELS: Record<number, string>;
@@ -42,7 +42,17 @@ export function validateIsoDate(value) {
42
42
  }
43
43
  return value;
44
44
  }
45
+ /**
46
+ * Split a comma-separated list into trimmed, non-empty items.
47
+ *
48
+ * Accepts `undefined` so call sites can pipe through commander's
49
+ * optional flag values without an explicit truthy guard. `false`
50
+ * (commander's representation of `--no-foo`) and empty string both
51
+ * resolve to `[]`.
52
+ */
45
53
  export function splitList(value) {
54
+ if (!value)
55
+ return [];
46
56
  return value
47
57
  .split(",")
48
58
  .map((s) => s.trim())
@@ -1,4 +1,8 @@
1
1
  import type { GraphQLService } from "./graphql-service.js";
2
- export declare function getWorkspaceUrlKey(graphQLService: GraphQLService): Promise<string>;
2
+ export interface GetWorkspaceUrlKeyOptions {
3
+ /** Highest-priority source. Typically the `--workspace-url-key` flag. */
4
+ override?: string;
5
+ }
6
+ export declare function getWorkspaceUrlKey(graphQLService?: GraphQLService, options?: GetWorkspaceUrlKeyOptions): Promise<string>;
3
7
  /** Test helper: clear the cached URL key. Not called from production code. */
4
8
  export declare function _resetWorkspaceUrlKeyCache(): void;
@@ -4,11 +4,20 @@ import { loadConfig } from "../config/config.js";
4
4
  * issue URLs). Used to build canonical markdown link URLs like
5
5
  * `https://linear.app/<urlKey>/issue/<identifier>/`.
6
6
  *
7
- * Resolution order:
8
- * 1. `config.workspaceUrlKey` if explicitly set in el-linear config
9
- * 2. `viewer.organization.urlKey` from the Linear API (fetched once, cached)
7
+ * Resolution order (highest priority first):
10
8
  *
11
- * Cached in-process for the lifetime of the CLI invocation.
9
+ * 1. `options.override` — typically wired from a `--workspace-url-key` CLI flag
10
+ * (per-invocation, wins over everything).
11
+ * 2. `EL_LINEAR_WORKSPACE_URL_KEY` env var.
12
+ * 3. `config.workspaceUrlKey` from `~/.config/el-linear/config.json`.
13
+ * 4. Live `viewer.organization.urlKey` GraphQL query (cached in-process).
14
+ *
15
+ * Layers 1–3 short-circuit without touching the network — this makes
16
+ * `el-linear refs wrap --no-validate` work offline once any one of them
17
+ * is set. Layer 4 is the original behavior preserved as a fallback.
18
+ *
19
+ * The graphQLService is only needed for layer 4 — pass `undefined` (or omit it)
20
+ * when the caller knows the network will not be used.
12
21
  */
13
22
  const VIEWER_ORG_URL_KEY_QUERY = /* GraphQL */ `
14
23
  query ViewerOrgUrlKey {
@@ -19,16 +28,37 @@ const VIEWER_ORG_URL_KEY_QUERY = /* GraphQL */ `
19
28
  }
20
29
  }
21
30
  `;
31
+ const WORKSPACE_URL_KEY_ENV = "EL_LINEAR_WORKSPACE_URL_KEY";
22
32
  let cachedUrlKey;
23
- export async function getWorkspaceUrlKey(graphQLService) {
33
+ export async function getWorkspaceUrlKey(graphQLService, options = {}) {
34
+ // 1. Per-invocation override — highest priority.
35
+ if (options.override) {
36
+ return options.override;
37
+ }
38
+ // 2. Env var. Read on every call (cheap; lets tests/CI flip the value
39
+ // mid-process without restarting). Not cached, so a test that sets
40
+ // EL_LINEAR_WORKSPACE_URL_KEY then unsets it sees the unset state.
41
+ const envKey = process.env[WORKSPACE_URL_KEY_ENV];
42
+ if (envKey) {
43
+ return envKey;
44
+ }
45
+ // Layers 3-4 are cached in-process — config and the live lookup are
46
+ // both stable for the CLI invocation's lifetime.
24
47
  if (cachedUrlKey) {
25
48
  return cachedUrlKey;
26
49
  }
50
+ // 3. Config override.
27
51
  const { workspaceUrlKey } = loadConfig();
28
52
  if (workspaceUrlKey) {
29
53
  cachedUrlKey = workspaceUrlKey;
30
54
  return workspaceUrlKey;
31
55
  }
56
+ // 4. Live API lookup — requires a GraphQL service.
57
+ if (!graphQLService) {
58
+ throw new Error("Could not resolve Linear workspace URL key — no override, env, or config set, " +
59
+ "and no GraphQL service was provided for the live lookup. " +
60
+ `Set \`workspaceUrlKey\` in your el-linear config, or \`${WORKSPACE_URL_KEY_ENV}\` in the environment.`);
61
+ }
32
62
  const data = await graphQLService.rawRequest(VIEWER_ORG_URL_KEY_QUERY);
33
63
  const fetched = data?.viewer?.organization?.urlKey;
34
64
  if (!fetched) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enrichlayer/el-linear",
3
- "version": "1.7.0",
3
+ "version": "1.10.0",
4
4
  "description": "A pragmatic CLI for Linear.app — deterministic team/label/member resolution, structured issue validation, configurable term enforcement, and a GraphQL escape hatch.",
5
5
  "main": "dist/main.js",
6
6
  "types": "dist/main.d.ts",