@enrichlayer/el-linear 1.5.0 → 1.7.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 (61) hide show
  1. package/README.md +32 -4
  2. package/dist/auth/oauth-app-config.d.ts +21 -0
  3. package/dist/auth/oauth-app-config.js +82 -0
  4. package/dist/commands/attachments.js +11 -12
  5. package/dist/commands/batch.js +15 -14
  6. package/dist/commands/comments.js +22 -10
  7. package/dist/commands/cycles.js +5 -4
  8. package/dist/commands/documents.js +18 -15
  9. package/dist/commands/embeds.js +4 -6
  10. package/dist/commands/graphql.js +5 -4
  11. package/dist/commands/init/aliases.js +0 -14
  12. package/dist/commands/init/defaults.d.ts +18 -1
  13. package/dist/commands/init/defaults.js +83 -8
  14. package/dist/commands/init/index.js +7 -13
  15. package/dist/commands/init/oauth.d.ts +4 -4
  16. package/dist/commands/init/oauth.js +24 -6
  17. package/dist/commands/init/shared.js +0 -1
  18. package/dist/commands/init/token.js +0 -7
  19. package/dist/commands/init/workspace.js +0 -2
  20. package/dist/commands/issue-id.js +3 -1
  21. package/dist/commands/issues.js +130 -54
  22. package/dist/commands/labels.js +23 -9
  23. package/dist/commands/profile/migrate-legacy.js +0 -1
  24. package/dist/commands/project-milestones.js +13 -12
  25. package/dist/commands/projects.js +28 -15
  26. package/dist/commands/read-shortcut.js +8 -7
  27. package/dist/commands/refs.js +4 -3
  28. package/dist/commands/releases.js +9 -8
  29. package/dist/commands/search.js +3 -2
  30. package/dist/commands/teams.js +14 -3
  31. package/dist/commands/templates.js +5 -4
  32. package/dist/commands/users.js +3 -2
  33. package/dist/config/config.d.ts +37 -0
  34. package/dist/config/issue-validation.js +1 -1
  35. package/dist/config/paths.d.ts +1 -0
  36. package/dist/config/paths.js +1 -0
  37. package/dist/config/resolver.js +1 -1
  38. package/dist/main.js +3 -2
  39. package/dist/utils/disk-cache.d.ts +51 -0
  40. package/dist/utils/disk-cache.js +178 -0
  41. package/dist/utils/download-uploads.d.ts +2 -1
  42. package/dist/utils/download-uploads.js +2 -4
  43. package/dist/utils/file-service.d.ts +26 -2
  44. package/dist/utils/file-service.js +27 -5
  45. package/dist/utils/footer.d.ts +19 -0
  46. package/dist/utils/footer.js +27 -0
  47. package/dist/utils/gdoc-parser.js +1 -1
  48. package/dist/utils/graphql-attachments-service.d.ts +1 -1
  49. package/dist/utils/graphql-attachments-service.js +2 -2
  50. package/dist/utils/graphql-documents-service.d.ts +1 -1
  51. package/dist/utils/graphql-documents-service.js +2 -2
  52. package/dist/utils/graphql-service.d.ts +2 -2
  53. package/dist/utils/graphql-service.js +7 -4
  54. package/dist/utils/linear-service.d.ts +18 -3
  55. package/dist/utils/linear-service.js +21 -6
  56. package/dist/utils/markdown-prosemirror.js +16 -9
  57. package/dist/utils/root-opts.d.ts +11 -0
  58. package/dist/utils/root-opts.js +13 -0
  59. package/dist/utils/validators.d.ts +7 -0
  60. package/dist/utils/validators.js +14 -14
  61. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Step 4 of the wizard: default labels, status defaults, term enforcement.
2
+ * Step 4 of the wizard: default labels, default assignee, default priority,
3
+ * status defaults, term enforcement, cache TTL.
3
4
  *
4
5
  * All optional. Each subsection asks "change?" with default=N so re-running
5
6
  * with no input is a no-op.
@@ -7,6 +8,19 @@
7
8
  import { type WizardConfig } from "./shared.js";
8
9
  export interface DefaultsStepResult {
9
10
  defaultLabels: string[] | undefined;
11
+ /**
12
+ * Default assignee identifier (alias / display name / email / UUID — same
13
+ * shapes resolveAssignee accepts) for `issues create`. The wizard does NOT
14
+ * validate this against Linear (no API call) — the runtime resolver handles
15
+ * validation at issue-create time, keeping the wizard offline.
16
+ */
17
+ defaultAssignee: string | undefined;
18
+ /**
19
+ * Default priority keyword: `none|urgent|high|medium|normal|low`. Stored
20
+ * as-is; the runtime path runs it through validatePriority() to get the
21
+ * Linear priority number.
22
+ */
23
+ defaultPriority: string | undefined;
10
24
  /**
11
25
  * Status defaults. Both fields are optional so the wizard preserves
12
26
  * partial existing configs (`{ noProject: "Backlog" }` only) byte-for-byte
@@ -21,5 +35,8 @@ export interface DefaultsStepResult {
21
35
  canonical: string;
22
36
  reject: string[];
23
37
  }> | undefined;
38
+ /** TTL (seconds) for `teams list` / `labels list` / `projects list` disk
39
+ * cache. `0` disables. */
40
+ cacheTTLSeconds: number | undefined;
24
41
  }
25
42
  export declare function runDefaultsStep(existing: WizardConfig): Promise<DefaultsStepResult>;
@@ -1,12 +1,22 @@
1
1
  /**
2
- * Step 4 of the wizard: default labels, status defaults, term enforcement.
2
+ * Step 4 of the wizard: default labels, default assignee, default priority,
3
+ * status defaults, term enforcement, cache TTL.
3
4
  *
4
5
  * All optional. Each subsection asks "change?" with default=N so re-running
5
6
  * with no input is a no-op.
6
7
  */
7
- import { confirm, input } from "@inquirer/prompts";
8
+ import { confirm, input, select } from "@inquirer/prompts";
8
9
  import { parseCsvList } from "./shared.js";
9
10
  const STATUS_FALLBACK = { noProject: "Triage", withAssigneeAndProject: "Todo" };
11
+ const CACHE_TTL_FALLBACK = 3600;
12
+ const PRIORITY_CHOICES = [
13
+ { name: "none", value: "none" },
14
+ { name: "urgent", value: "urgent" },
15
+ { name: "high", value: "high" },
16
+ { name: "medium", value: "medium" },
17
+ { name: "normal", value: "normal" },
18
+ { name: "low", value: "low" },
19
+ ];
10
20
  export async function runDefaultsStep(existing) {
11
21
  // Idempotency rule: when the user skips a sub-section, return the existing
12
22
  // value byte-for-byte. We deliberately do NOT spread or backfill optional
@@ -14,12 +24,14 @@ export async function runDefaultsStep(existing) {
14
24
  // with no input produces a byte-identical config.
15
25
  const result = {
16
26
  defaultLabels: existing.defaultLabels,
27
+ defaultAssignee: existing.defaultAssignee,
28
+ defaultPriority: existing.defaultPriority,
17
29
  statusDefaults: existing.statusDefaults,
18
30
  terms: existing.terms,
31
+ cacheTTLSeconds: existing.cacheTTLSeconds,
19
32
  };
20
33
  // ── Default labels ────────────────────────────────────────────────
21
34
  const currentLabels = existing.defaultLabels ?? [];
22
- // biome-ignore lint/suspicious/noConsole: wizard
23
35
  console.log(` Current default labels: ${currentLabels.length > 0 ? currentLabels.join(", ") : "(none)"}`);
24
36
  const editLabels = await confirm({
25
37
  message: "Change default labels for new issues?",
@@ -33,9 +45,47 @@ export async function runDefaultsStep(existing) {
33
45
  const parsed = parseCsvList(raw);
34
46
  result.defaultLabels = parsed.length > 0 ? parsed : undefined;
35
47
  }
48
+ // ── Default assignee ──────────────────────────────────────────────
49
+ console.log(` Current default assignee: ${existing.defaultAssignee ?? "(none)"}`);
50
+ const editAssignee = await confirm({
51
+ message: "Change default assignee for new issues?",
52
+ default: false,
53
+ });
54
+ if (editAssignee) {
55
+ // We deliberately don't validate against Linear here — the wizard runs
56
+ // offline-friendly. The runtime resolver (resolveAssignee) catches
57
+ // typos at issue-create time. "none" is the explicit-clear sentinel
58
+ // so users have an unambiguous way to wipe the field.
59
+ const raw = (await input({
60
+ message: "Default assignee (alias, name, email, or 'none' to clear):",
61
+ default: existing.defaultAssignee ?? "",
62
+ })).trim();
63
+ if (!raw || raw.toLowerCase() === "none") {
64
+ result.defaultAssignee = undefined;
65
+ }
66
+ else {
67
+ result.defaultAssignee = raw;
68
+ }
69
+ }
70
+ // ── Default priority ──────────────────────────────────────────────
71
+ console.log(` Current default priority: ${existing.defaultPriority ?? "(none)"}`);
72
+ const editPriority = await confirm({
73
+ message: "Change default priority for new issues?",
74
+ default: false,
75
+ });
76
+ if (editPriority) {
77
+ // `select` always picks one option — so picking "none" stores the
78
+ // keyword string "none" (Linear's "No priority"), distinct from
79
+ // `undefined` which means "no default at all".
80
+ const choice = await select({
81
+ message: "Default priority:",
82
+ choices: PRIORITY_CHOICES,
83
+ default: existing.defaultPriority ?? "none",
84
+ });
85
+ result.defaultPriority = choice;
86
+ }
36
87
  // ── Status defaults ────────────────────────────────────────────────
37
88
  const cur = existing.statusDefaults;
38
- // biome-ignore lint/suspicious/noConsole: wizard
39
89
  console.log(` Current status defaults: noProject=${cur?.noProject ?? STATUS_FALLBACK.noProject}, ` +
40
90
  `withAssigneeAndProject=${cur?.withAssigneeAndProject ?? STATUS_FALLBACK.withAssigneeAndProject}`);
41
91
  const editStatus = await confirm({
@@ -56,12 +106,39 @@ export async function runDefaultsStep(existing) {
56
106
  withAssigneeAndProject: withAP.trim(),
57
107
  };
58
108
  }
109
+ // ── Cache TTL ─────────────────────────────────────────────────────
110
+ const currentTTL = existing.cacheTTLSeconds ?? CACHE_TTL_FALLBACK;
111
+ console.log(` Current cache TTL: ${currentTTL}s`);
112
+ const editTTL = await confirm({
113
+ message: "Change cache TTL?",
114
+ default: false,
115
+ });
116
+ if (editTTL) {
117
+ const raw = await input({
118
+ message: "Cache TTL in seconds (0 to disable):",
119
+ default: String(currentTTL),
120
+ // `validate` runs per submit; we reject anything that isn't a
121
+ // non-negative integer literal. Number("") is `0` (truthy by
122
+ // the integer test) so we explicitly require non-empty input —
123
+ // otherwise an empty submission would silently store 0.
124
+ validate: (value) => {
125
+ const trimmed = value.trim();
126
+ if (trimmed === "") {
127
+ return "Enter a non-negative integer (e.g. 3600 for 1 hour, 0 to disable).";
128
+ }
129
+ const n = Number(trimmed);
130
+ if (!Number.isFinite(n) || !Number.isInteger(n) || n < 0) {
131
+ return "Enter a non-negative integer (e.g. 3600 for 1 hour, 0 to disable).";
132
+ }
133
+ return true;
134
+ },
135
+ });
136
+ result.cacheTTLSeconds = Number(raw);
137
+ }
59
138
  // ── Term enforcement ───────────────────────────────────────────────
60
139
  const currentTerms = existing.terms ?? [];
61
- // biome-ignore lint/suspicious/noConsole: wizard
62
140
  console.log(` Current term-enforcement rules: ${currentTerms.length}`);
63
141
  for (const t of currentTerms) {
64
- // biome-ignore lint/suspicious/noConsole: wizard
65
142
  console.log(` "${t.canonical}" rejects: ${t.reject.join(", ")}`);
66
143
  }
67
144
  const editTerms = await confirm({
@@ -77,7 +154,6 @@ export async function runDefaultsStep(existing) {
77
154
  }
78
155
  async function collectTermsInteractively(initial) {
79
156
  const terms = [...initial];
80
- // biome-ignore lint/suspicious/noConsole: wizard
81
157
  console.log(" Term enforcement catches misspellings of brand or product names in issue titles " +
82
158
  "and descriptions. Define the canonical form and a list of rejected variants.");
83
159
  if (terms.length > 0) {
@@ -102,7 +178,6 @@ async function collectTermsInteractively(initial) {
102
178
  });
103
179
  const reject = parseCsvList(rejectRaw);
104
180
  if (reject.length === 0) {
105
- // biome-ignore lint/suspicious/noConsole: wizard
106
181
  console.log(" No variants given — skipping this rule.");
107
182
  continue;
108
183
  }
@@ -33,7 +33,6 @@ function withCleanExit(fn) {
33
33
  }
34
34
  catch (err) {
35
35
  if (err instanceof Error && err.name === "ExitPromptError") {
36
- // biome-ignore lint/suspicious/noConsole: wizard
37
36
  console.log("\n Cancelled.");
38
37
  process.exit(130);
39
38
  }
@@ -94,7 +93,6 @@ export function setupInitCommands(program) {
94
93
  workspaceUrlKey: existing.workspaceUrlKey ?? ws.workspaceUrlKey,
95
94
  });
96
95
  await writeConfig(merged);
97
- // biome-ignore lint/suspicious/noConsole: wizard
98
96
  console.log(" ✓ Workspace defaults saved.");
99
97
  }));
100
98
  init
@@ -111,7 +109,6 @@ export function setupInitCommands(program) {
111
109
  const result = await runAliasesImport(tokenResult.token, options.import);
112
110
  updates = result.updates;
113
111
  if (result.skipped.length > 0) {
114
- // biome-ignore lint/suspicious/noConsole: wizard
115
112
  console.log(` Skipped ${result.skipped.length} unmatched email(s): ${result.skipped.join(", ")}`);
116
113
  }
117
114
  }
@@ -121,29 +118,29 @@ export function setupInitCommands(program) {
121
118
  });
122
119
  }
123
120
  if (updates.size === 0) {
124
- // biome-ignore lint/suspicious/noConsole: wizard
125
121
  console.log(" No alias changes — config unchanged.");
126
122
  return;
127
123
  }
128
124
  const merged = mergeAliasesIntoConfig(existing, updates);
129
125
  await writeConfig(merged);
130
- // biome-ignore lint/suspicious/noConsole: wizard
131
126
  console.log(` ✓ Updated aliases for ${updates.size} user(s).`);
132
127
  }));
133
128
  init
134
129
  .command("defaults")
135
- .description("Default labels, status defaults, term enforcement rules")
130
+ .description("Default labels, default assignee, default priority, status defaults, cache TTL, term enforcement")
136
131
  .action(withCleanExit(async () => {
137
132
  const existing = await readConfig();
138
133
  printStep("defaults", "Defaults");
139
134
  const result = await runDefaultsStep(existing);
140
135
  const merged = assignDefined(existing, {
141
136
  defaultLabels: result.defaultLabels,
137
+ defaultAssignee: result.defaultAssignee,
138
+ defaultPriority: result.defaultPriority,
142
139
  statusDefaults: result.statusDefaults,
143
140
  terms: result.terms,
141
+ cacheTTLSeconds: result.cacheTTLSeconds,
144
142
  });
145
143
  await writeConfig(merged);
146
- // biome-ignore lint/suspicious/noConsole: wizard
147
144
  console.log(" ✓ Defaults saved.");
148
145
  }));
149
146
  }
@@ -156,9 +153,7 @@ export async function runFullWizard(options = {}) {
156
153
  await runFullWizardImpl(opts);
157
154
  }
158
155
  async function runFullWizardImpl(options) {
159
- // biome-ignore lint/suspicious/noConsole: wizard
160
156
  console.log("Welcome to el-linear. This wizard will set up your config.");
161
- // biome-ignore lint/suspicious/noConsole: wizard
162
157
  console.log("Skip is the default at every prompt; only the API token is required.\n");
163
158
  const existing = await readConfig();
164
159
  // Step 1: token (required)
@@ -183,8 +178,11 @@ async function runFullWizardImpl(options) {
183
178
  // defaults step: result is `existing.X` itself when the user skipped
184
179
  // the edit branch, so direct assignment is safe.
185
180
  defaultLabels: defaults.defaultLabels,
181
+ defaultAssignee: defaults.defaultAssignee,
182
+ defaultPriority: defaults.defaultPriority,
186
183
  statusDefaults: defaults.statusDefaults,
187
184
  terms: defaults.terms,
185
+ cacheTTLSeconds: defaults.cacheTTLSeconds,
188
186
  // workspace step: `ws.defaultTeam` may be the existing value (user
189
187
  // skipped) or a new pick.
190
188
  defaultTeam: ws.defaultTeam,
@@ -197,12 +195,8 @@ async function runFullWizardImpl(options) {
197
195
  merged = mergeAliasesIntoConfig(merged, aliasUpdates);
198
196
  }
199
197
  await writeConfig(merged);
200
- // biome-ignore lint/suspicious/noConsole: wizard
201
198
  console.log("\n✓ Setup complete.");
202
- // biome-ignore lint/suspicious/noConsole: wizard
203
199
  console.log(" Token: ~/.config/el-linear/token (mode 0600)");
204
- // biome-ignore lint/suspicious/noConsole: wizard
205
200
  console.log(" Config: ~/.config/el-linear/config.json");
206
- // biome-ignore lint/suspicious/noConsole: wizard
207
201
  console.log("\nTry: el-linear teams list");
208
202
  }
@@ -2,10 +2,10 @@
2
2
  * Wizard step for OAuth 2.0 (PKCE flow) authorization.
3
3
  *
4
4
  * Flow:
5
- * 1. Present a "what is this?" intro pointing the user at Linear's OAuth
6
- * app registration page. (Until we ship a shared OAuth client_id,
7
- * every user registers their own app.)
8
- * 2. Prompt for `client_id`, optional `client_secret`, port, scopes.
5
+ * 1. Read optional local/team OAuth app defaults, if present.
6
+ * 2. Otherwise present a "what is this?" intro pointing the user at
7
+ * Linear's OAuth app registration page, then prompt for `client_id`,
8
+ * optional `client_secret`, port, scopes.
9
9
  * 3. Generate PKCE verifier + state, build the authorize URL.
10
10
  * 4. Try to open the system browser; fall back to printing the URL.
11
11
  * 5. Spin a localhost listener (or fall back to pasted-code prompt) to
@@ -2,10 +2,10 @@
2
2
  * Wizard step for OAuth 2.0 (PKCE flow) authorization.
3
3
  *
4
4
  * Flow:
5
- * 1. Present a "what is this?" intro pointing the user at Linear's OAuth
6
- * app registration page. (Until we ship a shared OAuth client_id,
7
- * every user registers their own app.)
8
- * 2. Prompt for `client_id`, optional `client_secret`, port, scopes.
5
+ * 1. Read optional local/team OAuth app defaults, if present.
6
+ * 2. Otherwise present a "what is this?" intro pointing the user at
7
+ * Linear's OAuth app registration page, then prompt for `client_id`,
8
+ * optional `client_secret`, port, scopes.
9
9
  * 3. Generate PKCE verifier + state, build the authorize URL.
10
10
  * 4. Try to open the system browser; fall back to printing the URL.
11
11
  * 5. Spin a localhost listener (or fall back to pasted-code prompt) to
@@ -19,6 +19,7 @@
19
19
  */
20
20
  import { spawn } from "node:child_process";
21
21
  import { checkbox, input, password, select } from "@inquirer/prompts";
22
+ import { readTeamOAuthConfig } from "../../auth/oauth-app-config.js";
22
23
  import { DEFAULT_CALLBACK_PATH, runLocalhostCallback, } from "../../auth/oauth-callback.js";
23
24
  import { ALL_SCOPES, buildAuthorizeUrl, DEFAULT_SCOPES, generatePkce, generateState, SCOPE_DESCRIPTIONS, validateScopes, } from "../../auth/oauth-client.js";
24
25
  import { promptForPastedCode } from "../../auth/oauth-headless.js";
@@ -165,6 +166,22 @@ async function promptRegistration(defaults) {
165
166
  scopes: validateScopes(scopes),
166
167
  };
167
168
  }
169
+ async function resolveRegistration(defaults) {
170
+ const teamConfig = await readTeamOAuthConfig();
171
+ if (!teamConfig) {
172
+ return promptRegistration({ port: defaults.manualPort });
173
+ }
174
+ const port = defaults.requestedPort ?? teamConfig.redirectPort;
175
+ logLine("");
176
+ logLine(TS(`Using Linear OAuth app defaults from ${teamConfig.sourcePath}.`));
177
+ logLine(TS(`Callback URL: http://localhost:${port}${DEFAULT_CALLBACK_PATH}`));
178
+ logLine("");
179
+ return {
180
+ clientId: teamConfig.clientId,
181
+ port,
182
+ scopes: teamConfig.scopes,
183
+ };
184
+ }
168
185
  /**
169
186
  * Default viewer-validation routine. Calls `viewer { ... }` with the new
170
187
  * bearer token to confirm Linear accepted it. Reused for both the wizard
@@ -213,8 +230,9 @@ export async function runOAuthStep(options = {}) {
213
230
  }
214
231
  // Both `reauth` and `revoked` fall through to the re-auth flow.
215
232
  }
216
- const reg = await promptRegistration({
217
- port: options.port ?? extractPortFromRedirect(existing) ?? DEFAULT_PORT,
233
+ const reg = await resolveRegistration({
234
+ manualPort: options.port ?? extractPortFromRedirect(existing) ?? DEFAULT_PORT,
235
+ requestedPort: options.port,
218
236
  });
219
237
  const redirectUri = `http://localhost:${reg.port}${DEFAULT_CALLBACK_PATH}`;
220
238
  const pkce = generatePkce();
@@ -174,7 +174,6 @@ export async function clearAliasesProgress() {
174
174
  * Print a step header in the wizard. Step strings like "1/4" or "2/4 (skipped)".
175
175
  */
176
176
  export function printStep(label, title) {
177
- // biome-ignore lint/suspicious/noConsole: wizard output is meant for stdout
178
177
  console.log(`\n[${label}] ${title}`);
179
178
  }
180
179
  /**
@@ -91,7 +91,6 @@ export async function runTokenStep(options = {}) {
91
91
  if (existing && !options.force) {
92
92
  try {
93
93
  const viewer = await validateToken(existing);
94
- // biome-ignore lint/suspicious/noConsole: wizard
95
94
  console.log(` Existing token works — authenticated as ${viewer.displayName} <${viewer.email}>.`);
96
95
  const replace = await confirm({
97
96
  message: "Replace this token?",
@@ -104,13 +103,10 @@ export async function runTokenStep(options = {}) {
104
103
  catch (err) {
105
104
  const raw = err instanceof Error ? err.message : String(err);
106
105
  const message = sanitizeForLog(raw);
107
- // biome-ignore lint/suspicious/noConsole: wizard
108
106
  console.log(` Existing token failed validation (${message}). You'll need to provide a new one.`);
109
107
  }
110
108
  }
111
- // biome-ignore lint/suspicious/noConsole: wizard
112
109
  console.log(` Generate a personal API token: ${TOKEN_GENERATION_URL}`);
113
- // biome-ignore lint/suspicious/noConsole: wizard
114
110
  console.log(" The token stays on your machine. el-linear only sends it to Linear's API. " +
115
111
  "It's stored at ~/.config/el-linear/token (mode 0600).");
116
112
  // Up to three attempts before giving up.
@@ -123,17 +119,14 @@ export async function runTokenStep(options = {}) {
123
119
  validate: (input) => input.trim().length > 0 || "Token cannot be empty",
124
120
  })).trim();
125
121
  try {
126
- // biome-ignore lint/suspicious/noConsole: wizard
127
122
  console.log(" Validating…");
128
123
  const viewer = await validateToken(token);
129
124
  await writeToken(token);
130
- // biome-ignore lint/suspicious/noConsole: wizard
131
125
  console.log(` ✓ Authenticated as ${viewer.displayName} <${viewer.email}> (${viewer.organization.name}).`);
132
126
  return { token, viewer };
133
127
  }
134
128
  catch (err) {
135
129
  const raw = err instanceof Error ? err.message : String(err);
136
- // biome-ignore lint/suspicious/noConsole: wizard
137
130
  console.log(` ✗ ${sanitizeForLog(raw)}`);
138
131
  }
139
132
  }
@@ -33,7 +33,6 @@ export async function runWorkspaceStep(token, workspaceUrlKey, existing) {
33
33
  for (const t of teams) {
34
34
  teamMap[t.key] = t.id;
35
35
  }
36
- // biome-ignore lint/suspicious/noConsole: wizard
37
36
  console.log(` Workspace: ${workspaceUrlKey} (${teams.length} teams)`);
38
37
  const currentDefault = existing.defaultTeam || "";
39
38
  // `change` reflects what the user agreed to: `true` means "open the picker
@@ -53,7 +52,6 @@ export async function runWorkspaceStep(token, workspaceUrlKey, existing) {
53
52
  };
54
53
  }
55
54
  if (teams.length === 0) {
56
- // biome-ignore lint/suspicious/noConsole: wizard
57
55
  console.log(" No teams visible to this token. Skipping default-team picker.");
58
56
  return {
59
57
  workspaceUrlKey,
@@ -72,7 +72,9 @@ export function setupIssueIdCommand(program) {
72
72
  return;
73
73
  }
74
74
  const rootOpts = command.parent?.opts() ?? {};
75
- const service = createGraphQLService({ apiToken: rootOpts.apiToken });
75
+ const service = await createGraphQLService({
76
+ apiToken: rootOpts.apiToken,
77
+ });
76
78
  const result = (await service.rawRequest(ISSUE_QUERY, {
77
79
  id: parsed.issueId,
78
80
  }));