@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.
- package/README.md +32 -4
- package/dist/auth/oauth-app-config.d.ts +21 -0
- package/dist/auth/oauth-app-config.js +82 -0
- package/dist/commands/attachments.js +11 -12
- package/dist/commands/batch.js +15 -14
- package/dist/commands/comments.js +22 -10
- package/dist/commands/cycles.js +5 -4
- package/dist/commands/documents.js +18 -15
- package/dist/commands/embeds.js +4 -6
- package/dist/commands/graphql.js +5 -4
- package/dist/commands/init/aliases.js +0 -14
- package/dist/commands/init/defaults.d.ts +18 -1
- package/dist/commands/init/defaults.js +83 -8
- package/dist/commands/init/index.js +7 -13
- package/dist/commands/init/oauth.d.ts +4 -4
- package/dist/commands/init/oauth.js +24 -6
- package/dist/commands/init/shared.js +0 -1
- package/dist/commands/init/token.js +0 -7
- package/dist/commands/init/workspace.js +0 -2
- package/dist/commands/issue-id.js +3 -1
- package/dist/commands/issues.js +130 -54
- package/dist/commands/labels.js +23 -9
- package/dist/commands/profile/migrate-legacy.js +0 -1
- package/dist/commands/project-milestones.js +13 -12
- package/dist/commands/projects.js +28 -15
- package/dist/commands/read-shortcut.js +8 -7
- package/dist/commands/refs.js +4 -3
- package/dist/commands/releases.js +9 -8
- package/dist/commands/search.js +3 -2
- package/dist/commands/teams.js +14 -3
- package/dist/commands/templates.js +5 -4
- package/dist/commands/users.js +3 -2
- package/dist/config/config.d.ts +37 -0
- package/dist/config/issue-validation.js +1 -1
- package/dist/config/paths.d.ts +1 -0
- package/dist/config/paths.js +1 -0
- package/dist/config/resolver.js +1 -1
- package/dist/main.js +3 -2
- package/dist/utils/disk-cache.d.ts +51 -0
- package/dist/utils/disk-cache.js +178 -0
- package/dist/utils/download-uploads.d.ts +2 -1
- package/dist/utils/download-uploads.js +2 -4
- package/dist/utils/file-service.d.ts +26 -2
- package/dist/utils/file-service.js +27 -5
- package/dist/utils/footer.d.ts +19 -0
- package/dist/utils/footer.js +27 -0
- package/dist/utils/gdoc-parser.js +1 -1
- package/dist/utils/graphql-attachments-service.d.ts +1 -1
- package/dist/utils/graphql-attachments-service.js +2 -2
- package/dist/utils/graphql-documents-service.d.ts +1 -1
- package/dist/utils/graphql-documents-service.js +2 -2
- package/dist/utils/graphql-service.d.ts +2 -2
- package/dist/utils/graphql-service.js +7 -4
- package/dist/utils/linear-service.d.ts +18 -3
- package/dist/utils/linear-service.js +21 -6
- package/dist/utils/markdown-prosemirror.js +16 -9
- package/dist/utils/root-opts.d.ts +11 -0
- package/dist/utils/root-opts.js +13 -0
- package/dist/utils/validators.d.ts +7 -0
- package/dist/utils/validators.js +14 -14
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Step 4 of the wizard: default labels,
|
|
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,
|
|
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
|
|
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.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
|
217
|
-
|
|
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({
|
|
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
|
}));
|