linear-sdk-cli 0.1.0 → 0.1.2

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 (71) hide show
  1. package/CHANGELOG.md +46 -46
  2. package/COVERAGE.md +5 -5
  3. package/MIGRATING.md +69 -71
  4. package/README.md +88 -96
  5. package/package.json +8 -9
  6. package/skills/linear-sdk-cli/SKILL.md +3 -13
  7. package/skills/linear-sdk-cli/references/api.md +9 -9
  8. package/skills/linear-sdk-cli/references/auth.md +0 -18
  9. package/skills/linear-sdk-cli/references/commands.md +2 -2
  10. package/skills/linear-sdk-cli/references/config.md +11 -9
  11. package/skills/linear-sdk-cli/references/issue.md +12 -63
  12. package/skills/linear-sdk-cli/references/team.md +0 -16
  13. package/skills/linear-sdk-cli/scripts/generate-docs.ts +2 -6
  14. package/src/bin/linear.ts +6 -16
  15. package/src/cli.ts +1 -3
  16. package/src/commands/agent-session.ts +5 -13
  17. package/src/commands/api.ts +9 -15
  18. package/src/commands/attach.ts +4 -15
  19. package/src/commands/comment.ts +2 -6
  20. package/src/commands/completion.ts +6 -2
  21. package/src/commands/cycle.ts +1 -6
  22. package/src/commands/discover.ts +2 -6
  23. package/src/commands/document.ts +1 -4
  24. package/src/commands/initiative.ts +1 -5
  25. package/src/commands/issue.ts +88 -376
  26. package/src/commands/meta.ts +31 -139
  27. package/src/commands/milestone.ts +1 -4
  28. package/src/commands/notification.ts +6 -15
  29. package/src/commands/project.ts +4 -17
  30. package/src/commands/team.ts +0 -13
  31. package/src/commands/webhook.ts +2 -12
  32. package/src/config.ts +183 -500
  33. package/src/context.ts +4 -13
  34. package/src/git.ts +3 -28
  35. package/src/lib/body.ts +2 -26
  36. package/src/lib/errors.ts +7 -32
  37. package/src/lib/introspect.ts +7 -76
  38. package/src/lib/keyring.ts +9 -51
  39. package/src/lib/options.ts +12 -108
  40. package/src/lib/output-shapes.ts +9 -81
  41. package/src/lib/pagination.ts +17 -50
  42. package/src/lib/resolve.ts +7 -40
  43. package/src/lib/shape.ts +12 -16
  44. package/src/lib/upload.ts +1 -3
  45. package/src/output/format.ts +18 -96
  46. package/src/output/table.ts +7 -7
  47. package/src/services/agent-session.ts +3 -11
  48. package/src/services/attachment.ts +5 -14
  49. package/src/services/comment.ts +4 -12
  50. package/src/services/cycle.ts +4 -11
  51. package/src/services/document.ts +10 -29
  52. package/src/services/favorite.ts +9 -15
  53. package/src/services/initiative-update.ts +15 -16
  54. package/src/services/initiative.ts +10 -15
  55. package/src/services/issue.ts +18 -262
  56. package/src/services/label.ts +2 -12
  57. package/src/services/milestone.ts +5 -8
  58. package/src/services/notification.ts +2 -16
  59. package/src/services/organization.ts +18 -23
  60. package/src/services/project.ts +34 -55
  61. package/src/services/roadmap.ts +6 -7
  62. package/src/services/state.ts +3 -9
  63. package/src/services/team.ts +24 -67
  64. package/src/services/user.ts +2 -2
  65. package/src/services/webhook.ts +17 -28
  66. package/skills/linear-sdk-cli/references/open.md +0 -29
  67. package/src/commands/open.ts +0 -84
  68. package/src/lib/open.ts +0 -32
  69. package/src/output/color.ts +0 -52
  70. package/src/output/markdown.ts +0 -133
  71. package/src/output/pager.ts +0 -131
package/src/context.ts CHANGED
@@ -8,7 +8,6 @@ import type { LinearClient } from "@linear/sdk";
8
8
  import { resolveConfig, type ResolvedConfig } from "./config.js";
9
9
  import { createClient, setRetryReporter } from "./client.js";
10
10
  import { Output } from "./output/format.js";
11
- import { isDebugEnabled, shouldUseColor } from "./output/color.js";
12
11
 
13
12
  /**
14
13
  * The globals as the parser actually stores them.
@@ -58,17 +57,10 @@ export class Context {
58
57
  // of the config (the default team for creates, cycle lookups, …) is
59
58
  // single-valued, so those commands read the full list off their own opts
60
59
  // and the config sees the first key.
61
- flags: {
62
- apiKey: options.apiKey,
63
- team: firstTeam(options.team),
64
- workspace: options.workspace,
65
- },
66
- });
67
- const color = shouldUseColor({
68
- disabled: options.noAnsi,
69
- json: options.json,
70
- isTTY: process.stdout.isTTY === true,
60
+ flags: { apiKey: options.apiKey, team: firstTeam(options.team), workspace: options.workspace },
71
61
  });
62
+ const color =
63
+ options.noAnsi !== true && options.json !== true && process.stdout.isTTY === true;
72
64
  // Four independent reasons not to prompt, and any one of them is enough.
73
65
  // The first two are the user saying so; the last two are the situation
74
66
  // saying so. `--json` counts because JSON is what a script or an agent
@@ -85,9 +77,8 @@ export class Context {
85
77
  json: options.json === true,
86
78
  color,
87
79
  quiet: options.quiet === true,
88
- debug: isDebugEnabled(options.debug),
80
+ debug: options.debug === true,
89
81
  fields: options.fields,
90
- isTTY: process.stdout.isTTY === true,
91
82
  });
92
83
  // Rate-limit waits are status, so they go through the same sink as every
93
84
  // other status line: stderr, silenced by --quiet, never on JSON stdout.
package/src/git.ts CHANGED
@@ -5,7 +5,6 @@
5
5
  */
6
6
 
7
7
  import { execFileSync } from "node:child_process";
8
- import { CliError } from "./lib/errors.js";
9
8
 
10
9
  /**
11
10
  * Extract a Linear issue identifier (e.g. `TES-123`) from a git branch name.
@@ -75,11 +74,7 @@ export function buildTrailer(identifier: string, opts: { references?: boolean }
75
74
  * word directly before the id and links — and closes — the issue. The URL line is
76
75
  * for the humans reading the log.
77
76
  */
78
- export function buildTrailers(
79
- identifier: string,
80
- url: string,
81
- opts: { references?: boolean } = {},
82
- ): string {
77
+ export function buildTrailers(identifier: string, url: string, opts: { references?: boolean } = {}): string {
83
78
  return `Linear-issue: ${buildTrailer(identifier, opts)}\nLinear-issue-url: ${url}`;
84
79
  }
85
80
 
@@ -141,34 +136,14 @@ export interface CheckoutResult {
141
136
  created: boolean;
142
137
  }
143
138
 
144
- /** Run the mutating checkout command and retain git's explanation on failure. */
145
- function gitCheckout(args: string[], cwd?: string): void {
146
- try {
147
- execFileSync("git", args, {
148
- cwd,
149
- stdio: ["ignore", "pipe", "pipe"],
150
- encoding: "utf8",
151
- });
152
- } catch (err) {
153
- const e = err as NodeJS.ErrnoException & { status?: number; stderr?: Buffer | string };
154
- if (e.code === "ENOENT")
155
- throw new CliError("Git is required to check out an issue branch.", "runtime");
156
- const stderr = e.stderr ? e.stderr.toString().trim() : "";
157
- throw new CliError(
158
- stderr || `git ${args.join(" ")} exited with code ${e.status ?? 1}.`,
159
- "runtime",
160
- );
161
- }
162
- }
163
-
164
139
  /** Checkout `branch`, creating it if it does not exist. */
165
140
  export function checkoutBranch(branch: string, cwd?: string): CheckoutResult {
166
141
  const exists =
167
142
  git(["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`], cwd) !== undefined;
168
143
  if (exists) {
169
- gitCheckout(["checkout", branch], cwd);
144
+ execFileSync("git", ["checkout", branch], { cwd, stdio: "ignore" });
170
145
  return { branch, created: false };
171
146
  }
172
- gitCheckout(["checkout", "-b", branch], cwd);
147
+ execFileSync("git", ["checkout", "-b", branch], { cwd, stdio: "ignore" });
173
148
  return { branch, created: true };
174
149
  }
package/src/lib/body.ts CHANGED
@@ -30,7 +30,7 @@ export interface BodyInputs {
30
30
  }
31
31
 
32
32
  export function resolveBody(inputs: BodyInputs): string | undefined {
33
- if (inputs.arg !== undefined) return validateInlineBody(inputs.arg);
33
+ if (inputs.arg !== undefined) return inputs.arg;
34
34
  if (inputs.file !== undefined) return readBodyFile(inputs.file);
35
35
  if (inputs.interactive) return openEditor(inputs.template ?? "");
36
36
  if (inputs.editorRequested) {
@@ -42,27 +42,6 @@ export function resolveBody(inputs: BodyInputs): string | undefined {
42
42
  return undefined;
43
43
  }
44
44
 
45
- /**
46
- * A quoted shell argument does not interpret `\n`. This is an especially easy
47
- * mistake for an agent composing Markdown in one command: Linear then stores
48
- * and renders the two literal characters, often across an entire issue body.
49
- *
50
- * Do not silently unescape them. A literal backslash-n can be intentional in
51
- * source code, JSON, or a regular expression, and changing it would corrupt
52
- * user data. File/stdin input is the unambiguous route for both real newlines
53
- * and intentional escape syntax.
54
- */
55
- export function validateInlineBody(body: string): string {
56
- if (body.includes("\\n")) {
57
- throw usageError(
58
- "Inline text contains a literal \\n sequence; shell quotes do not turn it into a line break. " +
59
- "Use this command's *-file option with '-' and provide the Markdown on stdin. " +
60
- "If the literal \\n is intentional, file input preserves it unchanged.",
61
- );
62
- }
63
- return body;
64
- }
65
-
66
45
  function readBodyFile(file: string): string {
67
46
  if (file === "-") return readStdinSync();
68
47
  try {
@@ -87,10 +66,7 @@ export function readStdinSync(): string {
87
66
  * so it is split shell-style (quotes and backslashes honoured) into argv, and
88
67
  * the file is appended. Exported for tests.
89
68
  */
90
- export function editorCommand(env: NodeJS.ProcessEnv = process.env): {
91
- raw: string;
92
- argv: string[];
93
- } {
69
+ export function editorCommand(env: NodeJS.ProcessEnv = process.env): { raw: string; argv: string[] } {
94
70
  const raw = env.VISUAL || env.EDITOR || "vi";
95
71
  return { raw, argv: shellSplit(raw) };
96
72
  }
package/src/lib/errors.ts CHANGED
@@ -50,28 +50,21 @@ export class CliError extends Error {
50
50
  readonly exitCode: ExitCodeValue;
51
51
  /** Extra detail surfaced only with --debug (e.g. raw GraphQL errors). */
52
52
  readonly detail?: unknown;
53
- /** A separately actionable hint; machine callers never have to parse prose. */
54
- readonly suggestion?: string;
55
53
 
56
- constructor(message: string, code: ErrorCode = "runtime", detail?: unknown, suggestion?: string) {
54
+ constructor(message: string, code: ErrorCode = "runtime", detail?: unknown) {
57
55
  super(message);
58
56
  this.name = "CliError";
59
57
  this.code = code;
60
58
  this.exitCode = EXIT_BY_CODE[code];
61
59
  this.detail = detail;
62
- this.suggestion = suggestion;
63
60
  }
64
61
  }
65
62
 
66
63
  /** Convenience constructors for the common cases. */
67
- export const usageError = (m: string, suggestion?: string) =>
68
- new CliError(m, "usage", undefined, suggestion);
69
- export const notFound = (m: string, suggestion?: string) =>
70
- new CliError(m, "not_found", undefined, suggestion);
71
- export const ambiguous = (m: string, suggestion?: string) =>
72
- new CliError(m, "ambiguous", undefined, suggestion);
73
- export const authError = (m: string, suggestion?: string) =>
74
- new CliError(m, "auth", undefined, suggestion);
64
+ export const usageError = (m: string) => new CliError(m, "usage");
65
+ export const notFound = (m: string) => new CliError(m, "not_found");
66
+ export const ambiguous = (m: string) => new CliError(m, "ambiguous");
67
+ export const authError = (m: string) => new CliError(m, "auth");
75
68
 
76
69
  /**
77
70
  * Map an unknown thrown value (typically a Linear SDK error) into a CliError
@@ -84,10 +77,7 @@ export function normalizeError(err: unknown): CliError {
84
77
  if (err instanceof Error) {
85
78
  const name = err.constructor?.name ?? err.name ?? "";
86
79
  const anyErr = err as Record<string, any>;
87
- const gqlErrors: any[] =
88
- [anyErr.errors, anyErr.response?.errors, anyErr.raw?.response?.errors].find(
89
- (errors) => Array.isArray(errors) && errors.length > 0,
90
- ) ?? [];
80
+ const gqlErrors: any[] = anyErr.errors ?? anyErr.raw?.response?.errors ?? [];
91
81
  const type: string | undefined =
92
82
  anyErr.type ?? gqlErrors[0]?.extensions?.type ?? gqlErrors[0]?.extensions?.code;
93
83
  const message = pickMessage(err, gqlErrors);
@@ -101,9 +91,7 @@ export function normalizeError(err: unknown): CliError {
101
91
  // semantically it is a not-found — reclassify so `view <bad-id>` exits 3.
102
92
  if (
103
93
  (code === "validation" || code === "api") &&
104
- /could ?n[o']t find|not found|does not exist|no such|referenced \w+\.?$/i.test(
105
- messageCandidates(err, gqlErrors).join("; "),
106
- )
94
+ /could ?n[o']t find|not found|does not exist|no such|referenced \w+\.?$/i.test(message)
107
95
  ) {
108
96
  code = "not_found";
109
97
  }
@@ -174,22 +162,9 @@ function classify(name: string, type: string | undefined): ErrorCode {
174
162
  }
175
163
 
176
164
  function pickMessage(err: Error, gqlErrors: any[]): string {
177
- const presentable = gqlErrors
178
- .map((e) => e?.extensions?.userPresentableMessage)
179
- .find((message) => typeof message === "string" && message.trim().length > 0);
180
- if (presentable) return presentable;
181
-
182
165
  const fromGql = gqlErrors
183
166
  .map((e) => e?.message)
184
167
  .filter(Boolean)
185
168
  .join("; ");
186
169
  return fromGql || err.message || "Unknown error";
187
170
  }
188
-
189
- /** All message forms are retained for classification even when a nicer one is displayed. */
190
- function messageCandidates(err: Error, gqlErrors: any[]): string[] {
191
- return [
192
- err.message,
193
- ...gqlErrors.flatMap((error) => [error?.message, error?.extensions?.userPresentableMessage]),
194
- ].filter((message): message is string => typeof message === "string" && message.length > 0);
195
- }
@@ -6,13 +6,6 @@
6
6
  import type { Command } from "commander";
7
7
  import type { OutputShape } from "./shape.js";
8
8
  import { outputShapeOf } from "./output-shapes.js";
9
- import {
10
- FIELDS_COMMANDS,
11
- LIMIT_COMMANDS,
12
- TEAM_COMMANDS,
13
- YES_COMMANDS,
14
- globalOptionKeys,
15
- } from "./options.js";
16
9
 
17
10
  export interface CommandArgument {
18
11
  name: string;
@@ -23,18 +16,6 @@ export interface CommandArgument {
23
16
  export interface CommandOption {
24
17
  flags: string;
25
18
  description: string;
26
- /** Commander's destination key (e.g. `includeArchived`). */
27
- attribute: string;
28
- /** Whether the option's value is required/optional after the flag. */
29
- valueRequired: boolean;
30
- valueOptional: boolean;
31
- variadic: boolean;
32
- choices?: string[];
33
- defaultValue?: unknown;
34
- /** True for the flags inherited from the root CLI contract. */
35
- global: boolean;
36
- /** Whether this global has an effect on this command. Local options are always applicable. */
37
- applicable: boolean;
38
19
  }
39
20
 
40
21
  export interface CommandNode {
@@ -77,37 +58,17 @@ export function walkCommands(root: Command): CommandNode[] {
77
58
  }
78
59
 
79
60
  function describe(cmd: Command, path: string[]): CommandNode {
80
- const args: CommandArgument[] =
81
- (cmd as any).registeredArguments?.map((a: any) => ({
82
- name: a.name(),
83
- required: a.required === true,
84
- variadic: a.variadic === true,
85
- })) ?? [];
61
+ const args: CommandArgument[] = (cmd as any).registeredArguments?.map((a: any) => ({
62
+ name: a.name(),
63
+ required: a.required === true,
64
+ variadic: a.variadic === true,
65
+ })) ?? [];
86
66
 
87
- const joined = path.join(" ");
88
- const defaultName = (cmd as any)._defaultCommandName as string | undefined;
89
- const effectivePath = defaultName ? `${joined} ${defaultName}` : joined;
90
- const runnable = Boolean((cmd as any)._actionHandler) || defaultName !== undefined;
91
- const globalKeys = new Set(globalOptionKeys());
92
67
  const options: CommandOption[] = cmd.options
93
68
  .filter((o: any) => !o.hidden)
94
- .map((o: any) => {
95
- const attribute = o.attributeName() as string;
96
- const global = globalKeys.has(attribute) && isGlobalFlag(o.long);
97
- return {
98
- flags: o.flags as string,
99
- description: (o.description as string) ?? "",
100
- attribute,
101
- valueRequired: o.required === true,
102
- valueOptional: o.optional === true,
103
- variadic: o.variadic === true,
104
- ...(Array.isArray(o.argChoices) ? { choices: [...o.argChoices] as string[] } : {}),
105
- ...(o.defaultValue !== undefined ? { defaultValue: o.defaultValue as unknown } : {}),
106
- global,
107
- applicable: global ? globalApplies(effectivePath, o.long, runnable) : true,
108
- };
109
- });
69
+ .map((o: any) => ({ flags: o.flags as string, description: (o.description as string) ?? "" }));
110
70
 
71
+ const joined = path.join(" ");
111
72
  const output = outputShapeOf(joined);
112
73
  return {
113
74
  path: joined,
@@ -118,33 +79,3 @@ function describe(cmd: Command, path: string[]): CommandNode {
118
79
  ...(output ? { output } : {}),
119
80
  };
120
81
  }
121
-
122
- const GLOBAL_LONGS = new Set([
123
- "--json",
124
- "--no-ansi",
125
- "--api-key",
126
- "--workspace",
127
- "--team",
128
- "--limit",
129
- "--all",
130
- "--fields",
131
- "--yes",
132
- "--quiet",
133
- "--no-input",
134
- "--debug",
135
- ]);
136
-
137
- function isGlobalFlag(long: string | undefined): boolean {
138
- return long !== undefined && GLOBAL_LONGS.has(long);
139
- }
140
-
141
- function globalApplies(path: string, long: string | undefined, hasAction: boolean): boolean {
142
- if (!hasAction) return false;
143
- if (long === "--fields") return FIELDS_COMMANDS.has(path);
144
- if (long === "--limit" || long === "--all") return LIMIT_COMMANDS.has(path);
145
- if (long === "--team") return TEAM_COMMANDS.has(path);
146
- if (long === "--yes") return YES_COMMANDS.has(path);
147
- // The remaining globals control the shared parser/context/error/output
148
- // boundary and are intentionally safe on every runnable command.
149
- return true;
150
- }
@@ -71,9 +71,7 @@ function runSecurity(args: string[], input?: string) {
71
71
  */
72
72
  function quoteForSecurity(token: string, what: string): string {
73
73
  if (/[\r\n]/.test(token)) {
74
- throw new KeyringError(
75
- `The ${what} contains a line break and cannot be stored in the keychain.`,
76
- );
74
+ throw new KeyringError(`The ${what} contains a line break and cannot be stored in the keychain.`);
77
75
  }
78
76
  return `"${token.replace(/[\\"]/g, (c) => `\\${c}`)}"`;
79
77
  }
@@ -100,16 +98,7 @@ export const macosKeychain: KeyringBackend = {
100
98
  // never an argv element. (`add-generic-password -w` as the LAST option
101
99
  // prompts instead — but on /dev/tty, so it hangs in a real terminal.)
102
100
  const line =
103
- [
104
- "add-generic-password",
105
- "-a",
106
- quoteForSecurity(account, "workspace slug"),
107
- "-s",
108
- KEYRING_SERVICE,
109
- "-U",
110
- "-w",
111
- quoteForSecurity(secret, "API key"),
112
- ].join(" ") + "\n";
101
+ ["add-generic-password", "-a", quoteForSecurity(account, "workspace slug"), "-s", KEYRING_SERVICE, "-U", "-w", quoteForSecurity(secret, "API key")].join(" ") + "\n";
113
102
  const r = runSecurity(["-i"], line);
114
103
  if (r.status !== 0) {
115
104
  throw new KeyringError(
@@ -161,15 +150,7 @@ export const linuxSecretService: KeyringBackend = {
161
150
 
162
151
  set(account, secret) {
163
152
  const r = runSecretTool(
164
- [
165
- "store",
166
- "--label",
167
- `${KEYRING_SERVICE}: ${account}`,
168
- "service",
169
- KEYRING_SERVICE,
170
- "account",
171
- account,
172
- ],
153
+ ["store", "--label", `${KEYRING_SERVICE}: ${account}`, "service", KEYRING_SERVICE, "account", account],
173
154
  secret,
174
155
  );
175
156
  if (r.status !== 0) {
@@ -197,24 +178,12 @@ export const linuxSecretService: KeyringBackend = {
197
178
  // Selection
198
179
  // ---------------------------------------------------------------------------
199
180
 
200
- /** Explicit test/application override; `undefined` means automatic detection. */
201
- let overrideBackend: KeyringBackend | null | undefined;
202
- /** Automatic detection is cached only while HOME is unchanged. */
203
- let detectedBackend: KeyringBackend | null | undefined;
204
- let detectedHome: string | undefined;
181
+ /** `undefined` = not decided yet; `null` = decided: no keyring here. */
182
+ let selected: KeyringBackend | null | undefined;
205
183
 
206
184
  function detect(): KeyringBackend | null {
207
185
  if (process.platform === "darwin") {
208
- if (!existsSync(SECURITY)) return null;
209
- // `security add-generic-password` opens a system dialog when the process
210
- // has no default user keychain (common with an isolated HOME in tests,
211
- // containers, and launch agents). Probe first so that environment is
212
- // treated like every other machine without a usable keyring and callers
213
- // can fall back to the 0600 credential file without surprising UI.
214
- const probe = spawnSync(SECURITY, ["default-keychain", "-d", "user"], {
215
- encoding: "utf8",
216
- });
217
- return !probe.error && probe.status === 0 ? macosKeychain : null;
186
+ return existsSync(SECURITY) ? macosKeychain : null;
218
187
  }
219
188
  if (process.platform === "linux") {
220
189
  // With no arguments secret-tool prints usage and exits 2; a completed run
@@ -231,13 +200,8 @@ function detect(): KeyringBackend | null {
231
200
  * error condition, just a platform.
232
201
  */
233
202
  export function keyring(): KeyringBackend | null {
234
- if (overrideBackend !== undefined) return overrideBackend;
235
- const home = process.env.HOME;
236
- if (detectedBackend === undefined || detectedHome !== home) {
237
- detectedBackend = detect();
238
- detectedHome = home;
239
- }
240
- return detectedBackend;
203
+ if (selected === undefined) selected = detect();
204
+ return selected;
241
205
  }
242
206
 
243
207
  /**
@@ -245,13 +209,7 @@ export function keyring(): KeyringBackend | null {
245
209
  * never touch the developer's real Keychain. `undefined` restores detection.
246
210
  */
247
211
  export function setKeyringBackend(backend: KeyringBackend | null | undefined): void {
248
- overrideBackend = backend;
249
- if (backend === undefined) {
250
- // Tests routinely swap HOME to an isolated fixture. Do not let a backend
251
- // detected against the real login keychain escape into that environment.
252
- detectedBackend = undefined;
253
- detectedHome = undefined;
254
- }
212
+ selected = backend;
255
213
  }
256
214
 
257
215
  /** An in-memory backend for tests. */
@@ -6,24 +6,12 @@
6
6
  import { Command, Option } from "commander";
7
7
  import { usageError, type CliError } from "./errors.js";
8
8
 
9
- /**
10
- * Repeatable `--var k=v` collector. JSON literals become their real types
11
- * (`2`, `true`, `null`, arrays/objects); ordinary text stays a string.
12
- */
13
- export function collectKeyVal(
14
- value: string,
15
- previous: Record<string, unknown> = {},
16
- ): Record<string, unknown> {
9
+ /** Repeatable `--var k=v` collector → { k: v }. */
10
+ export function collectKeyVal(value: string, previous: Record<string, string> = {}): Record<string, string> {
17
11
  const eq = value.indexOf("=");
18
12
  if (eq === -1) throw usageError(`Expected key=value, got '${value}'`);
19
13
  const key = value.slice(0, eq);
20
- if (!key) throw usageError(`Expected a non-empty key in key=value, got '${value}'`);
21
- const raw = value.slice(eq + 1);
22
- try {
23
- previous[key] = JSON.parse(raw);
24
- } catch {
25
- previous[key] = raw;
26
- }
14
+ previous[key] = value.slice(eq + 1);
27
15
  return previous;
28
16
  }
29
17
 
@@ -388,7 +376,6 @@ export const FIELDS_COMMANDS: ReadonlySet<string> = new Set([
388
376
  "state view",
389
377
  "team list",
390
378
  "team view",
391
- "team id",
392
379
  "team members",
393
380
  "team states",
394
381
  "team labels",
@@ -436,65 +423,6 @@ export const LIMIT_COMMANDS: ReadonlySet<string> = new Set([
436
423
  "webhook list",
437
424
  ]);
438
425
 
439
- /** Commands where the global team changes resolution, filtering, or a mutation. */
440
- export const TEAM_COMMANDS: ReadonlySet<string> = new Set([
441
- "config show",
442
- "config init",
443
- "issue view",
444
- "issue list",
445
- "issue mine",
446
- "issue search",
447
- "issue create",
448
- "issue update",
449
- "team view",
450
- "team members",
451
- "team states",
452
- "team labels",
453
- "team cycles",
454
- "team update",
455
- "project list",
456
- "project create",
457
- // Registered locally by project update so it can reject the dangerous
458
- // singular spelling with a tailored `--teams` explanation.
459
- "project update",
460
- "cycle list",
461
- "cycle view",
462
- "cycle current",
463
- "cycle create",
464
- "cycle update",
465
- "state list",
466
- "state view",
467
- "label list",
468
- "label create",
469
- "document list",
470
- "document create",
471
- "document update",
472
- "webhook create",
473
- ]);
474
-
475
- /** Commands whose confirmation prompt is bypassed by `--yes`. */
476
- export const YES_COMMANDS: ReadonlySet<string> = new Set([
477
- "attachment delete",
478
- "auth logout",
479
- "comment delete",
480
- "issue comment delete",
481
- "document delete",
482
- "favorite remove",
483
- "initiative archive",
484
- "initiative remove-project",
485
- "initiative delete",
486
- "issue archive",
487
- "issue delete",
488
- "label delete",
489
- "milestone delete",
490
- "notification archive",
491
- "project archive",
492
- "project delete",
493
- "roadmap delete",
494
- "team delete",
495
- "webhook delete",
496
- ]);
497
-
498
426
  /** `project list` for the `list` command under `project` — the path without the program name. */
499
427
  export function subcommandPath(cmd: Command): string {
500
428
  const names: string[] = [];
@@ -533,9 +461,7 @@ export function assertGlobalsApply(cmd: Command): void {
533
461
  const files = fileOptionsOf(cmd);
534
462
  throw usageError(
535
463
  `--fields does not apply to \`linear ${path}\`: it prints a receipt, not a table or detail block to select fields from.` +
536
- (files.length
537
- ? ` To read from a file, use ${files.join(" or ")} (-f is --fields here, not a file).`
538
- : ""),
464
+ (files.length ? ` To read from a file, use ${files.join(" or ")} (-f is --fields here, not a file).` : ""),
539
465
  );
540
466
  }
541
467
  if (!LIMIT_COMMANDS.has(path)) {
@@ -551,12 +477,6 @@ export function assertGlobalsApply(cmd: Command): void {
551
477
  throw usageError(`--all does not apply to \`linear ${path}\`: it is not a paged query.`);
552
478
  }
553
479
  }
554
- if (merged.team !== undefined && !TEAM_COMMANDS.has(path)) {
555
- throw usageError(`--team does not apply to \`linear ${path}\`.`);
556
- }
557
- if (merged.yes === true && !YES_COMMANDS.has(path)) {
558
- throw usageError(`--yes does not apply to \`linear ${path}\`: it has no confirmation prompt.`);
559
- }
560
480
  }
561
481
 
562
482
  /** Opt-outs for the shared filter sets; `issue mine` is fixed to the viewer. */
@@ -582,10 +502,9 @@ export interface FilterOptionSet {
582
502
  */
583
503
  export function addCoreFilterOptions(cmd: Command, set: FilterOptionSet = {}): Command {
584
504
  cmd.addOption(
585
- new Option(
586
- "-t, --team <key>",
587
- "filter by team key (repeatable; default: configured team)",
588
- ).argParser(collectArray),
505
+ new Option("-t, --team <key>", "filter by team key (repeatable; default: configured team)").argParser(
506
+ collectArray,
507
+ ),
589
508
  );
590
509
  cmd.addOption(
591
510
  new Option("-s, --state <name>", "filter by workflow state name/type (repeatable)").argParser(
@@ -602,19 +521,11 @@ export function addCoreFilterOptions(cmd: Command, set: FilterOptionSet = {}): C
602
521
  new Option("--project-label <name>", "filter by the project's label (excludes --project)"),
603
522
  )
604
523
  .addOption(new Option("--milestone <name>", "filter by project milestone"))
605
- .addOption(
606
- new Option("-l, --label <name>", "filter by label (repeat to narrow)").argParser(parseList),
607
- )
608
- .addOption(
609
- new Option("-P, --priority <0-4>", "filter by priority").argParser(parsePriorityFilter),
610
- )
524
+ .addOption(new Option("-l, --label <name>", "filter by label (repeat to narrow)").argParser(parseList))
525
+ .addOption(new Option("-P, --priority <0-4>", "filter by priority").argParser(parsePriorityFilter))
611
526
  .addOption(new Option(CYCLE_FLAG, CYCLE_DESC))
612
- .addOption(
613
- new Option("--created-after <date>", "only issues created at/after a date (YYYY-MM-DD)"),
614
- )
615
- .addOption(
616
- new Option("--updated-after <date>", "only issues updated at/after a date (YYYY-MM-DD)"),
617
- )
527
+ .addOption(new Option("--created-after <date>", "only issues created at/after a date (YYYY-MM-DD)"))
528
+ .addOption(new Option("--updated-after <date>", "only issues updated at/after a date (YYYY-MM-DD)"))
618
529
  .addOption(new Option("--all-teams", "search every team, ignoring the default team"))
619
530
  .addOption(new Option("--include-archived", "include archived issues"));
620
531
  }
@@ -627,14 +538,7 @@ export function addCoreFilterOptions(cmd: Command, set: FilterOptionSet = {}): C
627
538
  export function addFilterOptions(cmd: Command, set: FilterOptionSet = {}): Command {
628
539
  addCoreFilterOptions(cmd, set)
629
540
  .addOption(new Option("--query <text>", "full-text search"))
630
- .addOption(
631
- new Option("--sort <field>", "sort order").choices([
632
- "priority",
633
- "manual",
634
- "updated",
635
- "created",
636
- ]),
637
- );
541
+ .addOption(new Option("--sort <field>", "sort order").choices(["priority", "updated", "created"]));
638
542
  // `--search` is the reference CLI's spelling of the same filter.
639
543
  return addAliasOption(cmd, "--search <text>", "--query");
640
544
  }