@enrichlayer/el-linear 1.4.0 → 1.6.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 (75) hide show
  1. package/README.md +54 -0
  2. package/dist/auth/oauth-callback.d.ts +40 -0
  3. package/dist/auth/oauth-callback.js +142 -0
  4. package/dist/auth/oauth-client.d.ts +55 -0
  5. package/dist/auth/oauth-client.js +134 -0
  6. package/dist/auth/oauth-fs.d.ts +1 -0
  7. package/dist/auth/oauth-fs.js +29 -0
  8. package/dist/auth/oauth-headless.d.ts +38 -0
  9. package/dist/auth/oauth-headless.js +50 -0
  10. package/dist/auth/oauth-storage.d.ts +51 -0
  11. package/dist/auth/oauth-storage.js +87 -0
  12. package/dist/auth/oauth-token.d.ts +70 -0
  13. package/dist/auth/oauth-token.js +141 -0
  14. package/dist/auth/token-resolver.d.ts +48 -0
  15. package/dist/auth/token-resolver.js +95 -0
  16. package/dist/commands/attachments.js +11 -12
  17. package/dist/commands/batch.js +15 -14
  18. package/dist/commands/comments.js +22 -10
  19. package/dist/commands/cycles.js +5 -4
  20. package/dist/commands/documents.js +18 -15
  21. package/dist/commands/embeds.js +4 -6
  22. package/dist/commands/graphql.js +5 -4
  23. package/dist/commands/init/aliases.js +0 -14
  24. package/dist/commands/init/defaults.js +0 -6
  25. package/dist/commands/init/index.js +22 -12
  26. package/dist/commands/init/oauth.d.ts +85 -0
  27. package/dist/commands/init/oauth.js +308 -0
  28. package/dist/commands/init/shared.js +0 -1
  29. package/dist/commands/init/token.js +0 -7
  30. package/dist/commands/init/workspace.js +0 -2
  31. package/dist/commands/issue-id.js +3 -1
  32. package/dist/commands/issues.js +87 -46
  33. package/dist/commands/labels.js +9 -8
  34. package/dist/commands/profile/migrate-legacy.d.ts +96 -0
  35. package/dist/commands/profile/migrate-legacy.js +271 -0
  36. package/dist/commands/profile.js +6 -0
  37. package/dist/commands/project-milestones.js +13 -12
  38. package/dist/commands/projects.js +18 -14
  39. package/dist/commands/read-shortcut.js +8 -7
  40. package/dist/commands/refs.js +4 -3
  41. package/dist/commands/releases.js +9 -8
  42. package/dist/commands/search.js +3 -2
  43. package/dist/commands/teams.js +3 -2
  44. package/dist/commands/templates.js +5 -4
  45. package/dist/commands/users.js +3 -2
  46. package/dist/config/config.d.ts +18 -0
  47. package/dist/config/issue-validation.js +1 -1
  48. package/dist/config/resolver.js +1 -1
  49. package/dist/main.js +1 -1
  50. package/dist/utils/auth.js +8 -0
  51. package/dist/utils/download-uploads.d.ts +2 -1
  52. package/dist/utils/download-uploads.js +2 -4
  53. package/dist/utils/file-service.d.ts +26 -2
  54. package/dist/utils/file-service.js +27 -5
  55. package/dist/utils/footer.d.ts +19 -0
  56. package/dist/utils/footer.js +27 -0
  57. package/dist/utils/gdoc-parser.js +1 -1
  58. package/dist/utils/graphql-attachments-service.d.ts +1 -1
  59. package/dist/utils/graphql-attachments-service.js +2 -2
  60. package/dist/utils/graphql-documents-service.d.ts +1 -1
  61. package/dist/utils/graphql-documents-service.js +2 -2
  62. package/dist/utils/graphql-service.d.ts +18 -3
  63. package/dist/utils/graphql-service.js +26 -11
  64. package/dist/utils/legacy-config-detection.d.ts +47 -0
  65. package/dist/utils/legacy-config-detection.js +90 -0
  66. package/dist/utils/linear-service.d.ts +18 -3
  67. package/dist/utils/linear-service.js +21 -6
  68. package/dist/utils/markdown-prosemirror.js +16 -9
  69. package/dist/utils/migration-hint.d.ts +46 -0
  70. package/dist/utils/migration-hint.js +90 -0
  71. package/dist/utils/root-opts.d.ts +11 -0
  72. package/dist/utils/root-opts.js +13 -0
  73. package/dist/utils/validators.d.ts +7 -0
  74. package/dist/utils/validators.js +14 -14
  75. package/package.json +1 -1
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Detects "legacy drift" — the on-disk state where a user upgraded el-linear
3
+ * to >=1.4.0 (named profiles) while their old `~/.config/el-linear/config.json`
4
+ * is still present but the legacy single-file `token` (and any per-profile
5
+ * token) is missing or unreadable. In that state every command fails with
6
+ * "No API token found" and the user has no clear migration path.
7
+ *
8
+ * This module is **pure detection** — it returns a discriminated state and
9
+ * nothing else. The hint emission lives in `migration-hint.ts` and is wired
10
+ * into the auth-failure path (`auth.ts`) so the user gets a single clear
11
+ * stderr line *before* the regular auth error fires.
12
+ *
13
+ * The state shape is intentionally a discriminated union so callers can match
14
+ * exhaustively without re-checking individual booleans:
15
+ *
16
+ * { kind: 'no-drift' }
17
+ * — healthy: legacy single-file layout *or* a working active profile.
18
+ *
19
+ * { kind: 'legacy-no-token' }
20
+ * — `config.json` exists but no token (legacy or per-profile) does. This
21
+ * is the post-upgrade case: 1.4.0 expects per-profile tokens; legacy
22
+ * config was never migrated.
23
+ *
24
+ * { kind: 'broken-active-profile' }
25
+ * — `active-profile` points at a name whose directory doesn't exist.
26
+ * Typically caused by an interrupted `profile remove` or a hand-edit.
27
+ */
28
+ import fs from "node:fs";
29
+ import path from "node:path";
30
+ import { ACTIVE_PROFILE_FILE, CONFIG_PATH, PROFILES_DIR, TOKEN_PATH, } from "../config/paths.js";
31
+ const DEFAULT_FS_OPS = {
32
+ existsSync: (p) => fs.existsSync(p),
33
+ readFileSync: (p) => fs.readFileSync(p, "utf8"),
34
+ readdirSync: (p) => fs.readdirSync(p),
35
+ };
36
+ /**
37
+ * Detect drift between the legacy single-file layout and the >=1.4 named-
38
+ * profiles layout. Pure — `fsImpl` is overridable so tests can drive every
39
+ * branch without touching the filesystem.
40
+ */
41
+ export function detectLegacyDrift(fsImpl = DEFAULT_FS_OPS) {
42
+ // Branch 1: broken active-profile pointer.
43
+ // The `active-profile` marker names a profile that doesn't exist on disk —
44
+ // classic post-`profile remove` orphan, or a hand-edit typo. We classify
45
+ // this *before* the legacy-no-token branch because the user explicitly
46
+ // asked for that profile; the right fix is `profile use <good-name>`, not
47
+ // migration.
48
+ if (fsImpl.existsSync(ACTIVE_PROFILE_FILE)) {
49
+ let pointedAt = "";
50
+ try {
51
+ pointedAt = fsImpl.readFileSync(ACTIVE_PROFILE_FILE).trim();
52
+ }
53
+ catch {
54
+ pointedAt = "";
55
+ }
56
+ if (pointedAt.length > 0) {
57
+ const profileDir = path.join(PROFILES_DIR, pointedAt);
58
+ if (!fsImpl.existsSync(profileDir)) {
59
+ return { kind: "broken-active-profile", pointedAt };
60
+ }
61
+ }
62
+ }
63
+ // Branch 2: legacy drift.
64
+ // Legacy `config.json` exists; legacy `token` doesn't; no profiles configured.
65
+ // "No profiles configured" = profiles dir missing OR exists-but-empty.
66
+ if (!fsImpl.existsSync(CONFIG_PATH)) {
67
+ return { kind: "no-drift" };
68
+ }
69
+ if (fsImpl.existsSync(TOKEN_PATH)) {
70
+ return { kind: "no-drift" };
71
+ }
72
+ const profilesEmpty = isProfilesDirEmpty(fsImpl);
73
+ if (!profilesEmpty) {
74
+ return { kind: "no-drift" };
75
+ }
76
+ return { kind: "legacy-no-token", legacyConfigPath: CONFIG_PATH };
77
+ }
78
+ function isProfilesDirEmpty(fsImpl) {
79
+ if (!fsImpl.existsSync(PROFILES_DIR))
80
+ return true;
81
+ try {
82
+ const entries = fsImpl.readdirSync(PROFILES_DIR);
83
+ return entries.length === 0;
84
+ }
85
+ catch {
86
+ // Unreadable dir — treat as empty for the purposes of drift detection
87
+ // so we don't suppress the hint on a permissions edge case.
88
+ return true;
89
+ }
90
+ }
@@ -1,8 +1,23 @@
1
1
  import type { LinearComment, LinearCycleDetail, LinearCycleSummary, LinearLabel, LinearProject, LinearTeam, LinearUser } from "../types/linear.js";
2
- import { type AuthOptions } from "./auth.js";
2
+ import type { AuthOptions } from "./auth.js";
3
+ /**
4
+ * Constructor arg shapes for `LinearService`. Three variants:
5
+ * - `string` → personal API token (legacy; sent without `Bearer` prefix).
6
+ * - `{apiKey: string}` → personal API token (explicit).
7
+ * - `{oauthToken: string}` → OAuth access token (sent as
8
+ * `Authorization: Bearer <token>` via the SDK's accessToken option).
9
+ *
10
+ * The string variant exists because hundreds of call sites and tests pass
11
+ * a plain string. We continue to support it indefinitely.
12
+ */
13
+ export type LinearServiceAuth = string | {
14
+ apiKey: string;
15
+ } | {
16
+ oauthToken: string;
17
+ };
3
18
  export declare class LinearService {
4
19
  private readonly client;
5
- constructor(apiToken: string);
20
+ constructor(auth: LinearServiceAuth);
6
21
  resolveIssueId(issueId: string): Promise<string>;
7
22
  getTeams(limit?: number): Promise<LinearTeam[]>;
8
23
  resolveUserId(nameOrEmailOrId: string): Promise<string>;
@@ -23,4 +38,4 @@ export declare class LinearService {
23
38
  resolveCycleId(cycleNameOrId: string, teamFilter?: string): Promise<string>;
24
39
  resolveProjectId(projectNameOrId: string): Promise<string>;
25
40
  }
26
- export declare function createLinearService(options: AuthOptions): LinearService;
41
+ export declare function createLinearService(options: AuthOptions): Promise<LinearService>;
@@ -1,6 +1,6 @@
1
1
  import { LinearClient } from "@linear/sdk";
2
+ import { getActiveAuth } from "../auth/token-resolver.js";
2
3
  import { resolveUserDisplayName } from "../config/resolver.js";
3
- import { getApiToken } from "./auth.js";
4
4
  import { toISOStringOrNow, toISOStringOrUndefined } from "./date-format.js";
5
5
  import { multipleMatchesError, notFoundError } from "./error-messages.js";
6
6
  import { parseIssueIdentifier } from "./identifier-parser.js";
@@ -21,10 +21,22 @@ function teamIdFilter(teamId) {
21
21
  function nonEmptyFilter(filter) {
22
22
  return Object.keys(filter).length > 0 ? filter : undefined;
23
23
  }
24
+ function buildLinearClient(auth) {
25
+ if (typeof auth === "string") {
26
+ return new LinearClient({ apiKey: auth });
27
+ }
28
+ if ("oauthToken" in auth) {
29
+ // Linear's SDK natively supports OAuth via the `accessToken` option,
30
+ // which causes the underlying transport to send
31
+ // `Authorization: Bearer <token>` instead of the personal-token shape.
32
+ return new LinearClient({ accessToken: auth.oauthToken });
33
+ }
34
+ return new LinearClient({ apiKey: auth.apiKey });
35
+ }
24
36
  export class LinearService {
25
37
  client;
26
- constructor(apiToken) {
27
- this.client = new LinearClient({ apiKey: apiToken });
38
+ constructor(auth) {
39
+ this.client = buildLinearClient(auth);
28
40
  }
29
41
  async resolveIssueId(issueId) {
30
42
  if (isUuid(issueId)) {
@@ -436,7 +448,10 @@ export class LinearService {
436
448
  return projectsConnection.nodes[0].id;
437
449
  }
438
450
  }
439
- export function createLinearService(options) {
440
- const apiToken = getApiToken(options);
441
- return new LinearService(apiToken);
451
+ export async function createLinearService(options) {
452
+ const auth = await getActiveAuth(options);
453
+ if (auth.kind === "oauth") {
454
+ return new LinearService({ oauthToken: auth.token });
455
+ }
456
+ return new LinearService({ apiKey: auth.token });
442
457
  }
@@ -106,8 +106,11 @@ function parseBulletList(state, line) {
106
106
  return false;
107
107
  }
108
108
  const items = [];
109
- while (state.i < state.lines.length && BULLET_RE.test(state.lines[state.i])) {
109
+ while (state.i < state.lines.length) {
110
110
  const m = state.lines[state.i].match(BULLET_RE);
111
+ if (!m) {
112
+ break;
113
+ }
111
114
  items.push({
112
115
  type: "listItem",
113
116
  content: [{ type: "paragraph", content: parseInline(m[1]) }],
@@ -122,9 +125,11 @@ function parseOrderedList(state, line) {
122
125
  return false;
123
126
  }
124
127
  const items = [];
125
- while (state.i < state.lines.length &&
126
- ORDERED_RE.test(state.lines[state.i])) {
128
+ while (state.i < state.lines.length) {
127
129
  const m = state.lines[state.i].match(ORDERED_RE);
130
+ if (!m) {
131
+ break;
132
+ }
128
133
  items.push({
129
134
  type: "listItem",
130
135
  content: [{ type: "paragraph", content: parseInline(m[1]) }],
@@ -139,9 +144,11 @@ function parseBlockquote(state) {
139
144
  return false;
140
145
  }
141
146
  const quoteLines = [];
142
- while (state.i < state.lines.length &&
143
- BLOCKQUOTE_RE.test(state.lines[state.i])) {
147
+ while (state.i < state.lines.length) {
144
148
  const m = state.lines[state.i].match(BLOCKQUOTE_RE);
149
+ if (!m) {
150
+ break;
151
+ }
145
152
  quoteLines.push(m[1]);
146
153
  state.i++;
147
154
  }
@@ -283,7 +290,7 @@ function findEarliestInlineMatch(text) {
283
290
  const codeMatch = text.match(INLINE_CODE_RE);
284
291
  if (codeMatch) {
285
292
  candidates.push({
286
- index: codeMatch.index,
293
+ index: codeMatch.index ?? 0,
287
294
  length: codeMatch[0].length,
288
295
  innerText: codeMatch[1],
289
296
  marks: [{ type: "code" }],
@@ -292,7 +299,7 @@ function findEarliestInlineMatch(text) {
292
299
  const linkMatch = text.match(INLINE_LINK_RE);
293
300
  if (linkMatch) {
294
301
  candidates.push({
295
- index: linkMatch.index,
302
+ index: linkMatch.index ?? 0,
296
303
  length: linkMatch[0].length,
297
304
  innerText: linkMatch[1],
298
305
  marks: [{ type: "link", attrs: { href: linkMatch[2] } }],
@@ -301,7 +308,7 @@ function findEarliestInlineMatch(text) {
301
308
  const boldMatch = text.match(INLINE_BOLD_RE);
302
309
  if (boldMatch) {
303
310
  candidates.push({
304
- index: boldMatch.index,
311
+ index: boldMatch.index ?? 0,
305
312
  length: boldMatch[0].length,
306
313
  innerText: boldMatch[1] ?? boldMatch[2],
307
314
  marks: [{ type: "bold" }],
@@ -310,7 +317,7 @@ function findEarliestInlineMatch(text) {
310
317
  const italicMatch = text.match(INLINE_ITALIC_RE);
311
318
  if (italicMatch) {
312
319
  candidates.push({
313
- index: italicMatch.index,
320
+ index: italicMatch.index ?? 0,
314
321
  length: italicMatch[0].length,
315
322
  innerText: italicMatch[1] ?? italicMatch[2],
316
323
  marks: [{ type: "italic" }],
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Once-per-process stderr hint for legacy-config drift.
3
+ *
4
+ * Wired into the auth-failure path in `auth.ts`. When the legacy single-file
5
+ * config exists but no token can be found anywhere (the >=1.4 post-upgrade
6
+ * scenario), we emit one stderr line pointing the user at
7
+ * `el-linear profile migrate-legacy` *before* the regular "No API token
8
+ * found" error fires.
9
+ *
10
+ * Constraints:
11
+ *
12
+ * - **stderr only** — stdout is reserved for the JSON error payload that
13
+ * machine callers parse, and the hint must not corrupt that stream.
14
+ * - **Once per process** — even if a script invokes el-linear in a tight
15
+ * loop within a single Node process (uncommon but possible), only the
16
+ * first failure prints. Anything more is noise.
17
+ * - **Suppressible** — `EL_LINEAR_SKIP_MIGRATION_HINT=1` silences the hint
18
+ * for users who've decided to stay on the legacy layout intentionally.
19
+ * The env var is read at *emission time*, not module load, so toggling
20
+ * it in tests works without re-importing.
21
+ * - **Non-blocking** — never throws, never delays the underlying auth
22
+ * error. The hint is purely informational.
23
+ */
24
+ import { type DetectionFsOps, type LegacyDriftState } from "./legacy-config-detection.js";
25
+ /**
26
+ * Test seam — resets the once-per-process latch so each test starts fresh.
27
+ * Production code never calls this.
28
+ */
29
+ export declare function _resetMigrationHintForTests(): void;
30
+ /**
31
+ * Emit the migration hint to stderr if (and only if) legacy drift is
32
+ * detected, the latch hasn't fired this process, and the suppress env var
33
+ * isn't set. Returns the detection state so callers can branch (or log).
34
+ *
35
+ * Optional `fsImpl` lets tests drive detection without touching disk.
36
+ * Optional `stderr` lets tests capture the output without spying on the
37
+ * global stream.
38
+ */
39
+ export declare function maybeEmitMigrationHint(fsImpl?: DetectionFsOps, stderr?: {
40
+ write: (chunk: string) => void;
41
+ }): LegacyDriftState;
42
+ /**
43
+ * Render the hint string for a given state. Returns null when no hint
44
+ * should be emitted (no-drift). Exported for unit testing.
45
+ */
46
+ export declare function formatHint(state: LegacyDriftState): string | null;
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Once-per-process stderr hint for legacy-config drift.
3
+ *
4
+ * Wired into the auth-failure path in `auth.ts`. When the legacy single-file
5
+ * config exists but no token can be found anywhere (the >=1.4 post-upgrade
6
+ * scenario), we emit one stderr line pointing the user at
7
+ * `el-linear profile migrate-legacy` *before* the regular "No API token
8
+ * found" error fires.
9
+ *
10
+ * Constraints:
11
+ *
12
+ * - **stderr only** — stdout is reserved for the JSON error payload that
13
+ * machine callers parse, and the hint must not corrupt that stream.
14
+ * - **Once per process** — even if a script invokes el-linear in a tight
15
+ * loop within a single Node process (uncommon but possible), only the
16
+ * first failure prints. Anything more is noise.
17
+ * - **Suppressible** — `EL_LINEAR_SKIP_MIGRATION_HINT=1` silences the hint
18
+ * for users who've decided to stay on the legacy layout intentionally.
19
+ * The env var is read at *emission time*, not module load, so toggling
20
+ * it in tests works without re-importing.
21
+ * - **Non-blocking** — never throws, never delays the underlying auth
22
+ * error. The hint is purely informational.
23
+ */
24
+ import { detectLegacyDrift, } from "./legacy-config-detection.js";
25
+ let hintAlreadyEmitted = false;
26
+ /**
27
+ * Test seam — resets the once-per-process latch so each test starts fresh.
28
+ * Production code never calls this.
29
+ */
30
+ export function _resetMigrationHintForTests() {
31
+ hintAlreadyEmitted = false;
32
+ }
33
+ /**
34
+ * Emit the migration hint to stderr if (and only if) legacy drift is
35
+ * detected, the latch hasn't fired this process, and the suppress env var
36
+ * isn't set. Returns the detection state so callers can branch (or log).
37
+ *
38
+ * Optional `fsImpl` lets tests drive detection without touching disk.
39
+ * Optional `stderr` lets tests capture the output without spying on the
40
+ * global stream.
41
+ */
42
+ export function maybeEmitMigrationHint(fsImpl, stderr = process.stderr) {
43
+ const state = detectLegacyDrift(fsImpl);
44
+ // Read the env var at call time — the suppress flag may be flipped between
45
+ // commands in long-lived test processes.
46
+ if (process.env.EL_LINEAR_SKIP_MIGRATION_HINT === "1") {
47
+ return state;
48
+ }
49
+ if (hintAlreadyEmitted) {
50
+ return state;
51
+ }
52
+ const message = formatHint(state);
53
+ if (message === null) {
54
+ return state;
55
+ }
56
+ hintAlreadyEmitted = true;
57
+ stderr.write(`${message}\n`);
58
+ return state;
59
+ }
60
+ /**
61
+ * Render the hint string for a given state. Returns null when no hint
62
+ * should be emitted (no-drift). Exported for unit testing.
63
+ */
64
+ export function formatHint(state) {
65
+ if (state.kind === "no-drift")
66
+ return null;
67
+ if (state.kind === "broken-active-profile") {
68
+ return [
69
+ `el-linear: active profile "${state.pointedAt}" doesn't exist.`,
70
+ "Switch with:",
71
+ "",
72
+ " el-linear profile use <name>",
73
+ "",
74
+ "Or list available profiles:",
75
+ "",
76
+ " el-linear profile list",
77
+ "",
78
+ "Or suppress this hint with EL_LINEAR_SKIP_MIGRATION_HINT=1.",
79
+ ].join("\n");
80
+ }
81
+ // legacy-no-token
82
+ return [
83
+ `el-linear: legacy config detected at ${state.legacyConfigPath}`,
84
+ "but no token. Migrate with:",
85
+ "",
86
+ " el-linear profile migrate-legacy [--name <profile>]",
87
+ "",
88
+ "Or suppress this hint with EL_LINEAR_SKIP_MIGRATION_HINT=1.",
89
+ ].join("\n");
90
+ }
@@ -0,0 +1,11 @@
1
+ import type { Command, OptionValues } from "commander";
2
+ /**
3
+ * Extract root command options from inside a subcommand action handler.
4
+ *
5
+ * Commander guarantees `command.parent` exists inside any subcommand action,
6
+ * and `command.parent.parent` exists inside a nested subcommand action
7
+ * (e.g. `el-linear comments create` — the `create` command is nested under
8
+ * `comments`, which is nested under the root program). The non-null
9
+ * assertions live here so call sites can stay clean and typed.
10
+ */
11
+ export declare function getRootOpts(command: Command): OptionValues;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Extract root command options from inside a subcommand action handler.
3
+ *
4
+ * Commander guarantees `command.parent` exists inside any subcommand action,
5
+ * and `command.parent.parent` exists inside a nested subcommand action
6
+ * (e.g. `el-linear comments create` — the `create` command is nested under
7
+ * `comments`, which is nested under the root program). The non-null
8
+ * assertions live here so call sites can stay clean and typed.
9
+ */
10
+ export function getRootOpts(command) {
11
+ // biome-ignore lint/style/noNonNullAssertion: command.parent / .parent.parent are guaranteed by commander inside subcommand actions; see docstring
12
+ return (command.parent?.parent ?? command.parent).opts();
13
+ }
@@ -1,4 +1,11 @@
1
1
  export declare function parsePositiveInt(value: string, flagName: string): number;
2
+ /**
3
+ * Parse a single priority value (for `issues create --priority`, `issues update --priority`).
4
+ *
5
+ * Accepts:
6
+ * - keywords: none | urgent | high | medium | normal | low
7
+ * - numbers: 0 (no priority), 1 (urgent), 2 (high), 3 (medium), 4 (low)
8
+ */
2
9
  export declare function validatePriority(value: string): number;
3
10
  export declare function validateHexColor(value: string): string;
4
11
  export declare function validateIsoDate(value: string): string;
@@ -8,14 +8,21 @@ export function parsePositiveInt(value, flagName) {
8
8
  }
9
9
  return n;
10
10
  }
11
+ /**
12
+ * Parse a single priority value (for `issues create --priority`, `issues update --priority`).
13
+ *
14
+ * Accepts:
15
+ * - keywords: none | urgent | high | medium | normal | low
16
+ * - numbers: 0 (no priority), 1 (urgent), 2 (high), 3 (medium), 4 (low)
17
+ */
11
18
  export function validatePriority(value) {
12
19
  const asName = PRIORITY_NAMES[value.toLowerCase()];
13
- if (asName !== undefined && asName >= 1) {
20
+ if (asName !== undefined) {
14
21
  return asName;
15
22
  }
16
23
  const n = Number.parseInt(value, 10);
17
- if (Number.isNaN(n) || n < 1 || n > 4) {
18
- throw invalidParameterError("--priority", `"${value}" is not valid. Use names (urgent, high, medium/normal, low) or numbers (1-4).`);
24
+ if (Number.isNaN(n) || n < 0 || n > 4) {
25
+ throw invalidParameterError("--priority", `"${value}" is not valid. Use names (none, urgent, high, medium/normal, low) or numbers (0-4).`);
19
26
  }
20
27
  return n;
21
28
  }
@@ -41,6 +48,9 @@ export function splitList(value) {
41
48
  .map((s) => s.trim())
42
49
  .filter((s) => s.length > 0);
43
50
  }
51
+ // Shared keyword → Linear priority number map. `none` and `0` mean "No priority"
52
+ // (Linear stores it as a real state, not absence). `1..4` are the rated priorities,
53
+ // `urgent` (1) being the highest.
44
54
  const PRIORITY_NAMES = {
45
55
  none: 0,
46
56
  urgent: 1,
@@ -50,17 +60,7 @@ const PRIORITY_NAMES = {
50
60
  low: 4,
51
61
  };
52
62
  export function parsePriorityFilter(value) {
53
- return splitList(value).map((item) => {
54
- const asName = PRIORITY_NAMES[item.toLowerCase()];
55
- if (asName !== undefined) {
56
- return asName;
57
- }
58
- const n = Number.parseInt(item, 10);
59
- if (Number.isNaN(n) || n < 0 || n > 4) {
60
- throw invalidParameterError("--priority", `"${item}" is not valid. Use names (urgent, high, medium, low, none) or numbers (0-4).`);
61
- }
62
- return n;
63
- });
63
+ return splitList(value).map((item) => validatePriority(item));
64
64
  }
65
65
  export const PRIORITY_LABELS = {
66
66
  0: "No priority",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enrichlayer/el-linear",
3
- "version": "1.4.0",
3
+ "version": "1.6.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",