@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.
- package/README.md +54 -0
- package/dist/auth/oauth-callback.d.ts +40 -0
- package/dist/auth/oauth-callback.js +142 -0
- package/dist/auth/oauth-client.d.ts +55 -0
- package/dist/auth/oauth-client.js +134 -0
- package/dist/auth/oauth-fs.d.ts +1 -0
- package/dist/auth/oauth-fs.js +29 -0
- package/dist/auth/oauth-headless.d.ts +38 -0
- package/dist/auth/oauth-headless.js +50 -0
- package/dist/auth/oauth-storage.d.ts +51 -0
- package/dist/auth/oauth-storage.js +87 -0
- package/dist/auth/oauth-token.d.ts +70 -0
- package/dist/auth/oauth-token.js +141 -0
- package/dist/auth/token-resolver.d.ts +48 -0
- package/dist/auth/token-resolver.js +95 -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.js +0 -6
- package/dist/commands/init/index.js +22 -12
- package/dist/commands/init/oauth.d.ts +85 -0
- package/dist/commands/init/oauth.js +308 -0
- 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 +87 -46
- package/dist/commands/labels.js +9 -8
- package/dist/commands/profile/migrate-legacy.d.ts +96 -0
- package/dist/commands/profile/migrate-legacy.js +271 -0
- package/dist/commands/profile.js +6 -0
- package/dist/commands/project-milestones.js +13 -12
- package/dist/commands/projects.js +18 -14
- 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 +3 -2
- package/dist/commands/templates.js +5 -4
- package/dist/commands/users.js +3 -2
- package/dist/config/config.d.ts +18 -0
- package/dist/config/issue-validation.js +1 -1
- package/dist/config/resolver.js +1 -1
- package/dist/main.js +1 -1
- package/dist/utils/auth.js +8 -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 +18 -3
- package/dist/utils/graphql-service.js +26 -11
- package/dist/utils/legacy-config-detection.d.ts +47 -0
- package/dist/utils/legacy-config-detection.js +90 -0
- 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/migration-hint.d.ts +46 -0
- package/dist/utils/migration-hint.js +90 -0
- 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
package/dist/commands/cycles.js
CHANGED
|
@@ -2,6 +2,7 @@ import { resolveTeam } from "../config/resolver.js";
|
|
|
2
2
|
import { invalidParameterError, notFoundError, requiresParameterError, } from "../utils/error-messages.js";
|
|
3
3
|
import { createLinearService } from "../utils/linear-service.js";
|
|
4
4
|
import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
|
|
5
|
+
import { getRootOpts } from "../utils/root-opts.js";
|
|
5
6
|
export function setupCyclesCommands(program) {
|
|
6
7
|
const cycles = program
|
|
7
8
|
.command("cycles")
|
|
@@ -19,9 +20,9 @@ export function setupCyclesCommands(program) {
|
|
|
19
20
|
if (options.aroundActive && !options.team) {
|
|
20
21
|
throw requiresParameterError("--around-active", "--team");
|
|
21
22
|
}
|
|
22
|
-
const rootOpts = command
|
|
23
|
+
const rootOpts = getRootOpts(command);
|
|
23
24
|
const teamFilter = options.team ? resolveTeam(options.team) : undefined;
|
|
24
|
-
const linearService = createLinearService(rootOpts);
|
|
25
|
+
const linearService = await createLinearService(rootOpts);
|
|
25
26
|
const allCycles = await linearService.getCycles(teamFilter, options.active || undefined, Number.parseInt(options.limit, 10));
|
|
26
27
|
if (options.aroundActive) {
|
|
27
28
|
const n = Number.parseInt(options.aroundActive, 10);
|
|
@@ -51,11 +52,11 @@ export function setupCyclesCommands(program) {
|
|
|
51
52
|
.option("--team <team>", "team key, name, or ID to scope name lookup")
|
|
52
53
|
.option("--issues-first <n>", "how many issues to fetch (default 50)", "50")
|
|
53
54
|
.action(handleAsyncCommand(async (cycleIdOrName, options, command) => {
|
|
54
|
-
const rootOpts = command
|
|
55
|
+
const rootOpts = getRootOpts(command);
|
|
55
56
|
const teamFilter = options.team
|
|
56
57
|
? resolveTeam(options.team)
|
|
57
58
|
: undefined;
|
|
58
|
-
const linearService = createLinearService(rootOpts);
|
|
59
|
+
const linearService = await createLinearService(rootOpts);
|
|
59
60
|
const cycleId = await linearService.resolveCycleId(cycleIdOrName, teamFilter);
|
|
60
61
|
const cycle = await linearService.getCycleById(cycleId, Number.parseInt(options.issuesFirst || "50", 10));
|
|
61
62
|
outputSuccess(cycle);
|
|
@@ -2,6 +2,7 @@ import { createGraphQLAttachmentsService } from "../utils/graphql-attachments-se
|
|
|
2
2
|
import { createGraphQLDocumentsService } from "../utils/graphql-documents-service.js";
|
|
3
3
|
import { createLinearService } from "../utils/linear-service.js";
|
|
4
4
|
import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
|
|
5
|
+
import { getRootOpts } from "../utils/root-opts.js";
|
|
5
6
|
function extractDocumentIdFromUrl(url) {
|
|
6
7
|
try {
|
|
7
8
|
const parsed = new URL(url);
|
|
@@ -25,9 +26,9 @@ function extractDocumentIdFromUrl(url) {
|
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
28
|
async function handleCreateDocument(options, command) {
|
|
28
|
-
const rootOpts = command
|
|
29
|
-
const documentsService = createGraphQLDocumentsService(rootOpts);
|
|
30
|
-
const linearService = createLinearService(rootOpts);
|
|
29
|
+
const rootOpts = getRootOpts(command);
|
|
30
|
+
const documentsService = await createGraphQLDocumentsService(rootOpts);
|
|
31
|
+
const linearService = await createLinearService(rootOpts);
|
|
31
32
|
const document = await documentsService.createDocument({
|
|
32
33
|
title: options.title,
|
|
33
34
|
content: options.content,
|
|
@@ -44,7 +45,7 @@ async function handleCreateDocument(options, command) {
|
|
|
44
45
|
color: options.color,
|
|
45
46
|
});
|
|
46
47
|
if (options.attachTo) {
|
|
47
|
-
const attachmentsService = createGraphQLAttachmentsService(rootOpts);
|
|
48
|
+
const attachmentsService = await createGraphQLAttachmentsService(rootOpts);
|
|
48
49
|
const issueId = await linearService.resolveIssueId(options.attachTo);
|
|
49
50
|
try {
|
|
50
51
|
await attachmentsService.createAttachment({
|
|
@@ -66,15 +67,15 @@ async function handleListDocuments(options, command) {
|
|
|
66
67
|
if (options.project && options.issue) {
|
|
67
68
|
throw new Error("Cannot use --project and --issue together. Choose one filter.");
|
|
68
69
|
}
|
|
69
|
-
const rootOpts = command
|
|
70
|
-
const documentsService = createGraphQLDocumentsService(rootOpts);
|
|
71
|
-
const linearService = createLinearService(rootOpts);
|
|
70
|
+
const rootOpts = getRootOpts(command);
|
|
71
|
+
const documentsService = await createGraphQLDocumentsService(rootOpts);
|
|
72
|
+
const linearService = await createLinearService(rootOpts);
|
|
72
73
|
const limit = Number.parseInt(options.limit || "50", 10);
|
|
73
74
|
if (Number.isNaN(limit) || limit < 1) {
|
|
74
75
|
throw new Error(`Invalid limit "${options.limit}": must be a positive number`);
|
|
75
76
|
}
|
|
76
77
|
if (options.issue) {
|
|
77
|
-
const attachmentsService = createGraphQLAttachmentsService(rootOpts);
|
|
78
|
+
const attachmentsService = await createGraphQLAttachmentsService(rootOpts);
|
|
78
79
|
const issueId = await linearService.resolveIssueId(options.issue);
|
|
79
80
|
const attachments = await attachmentsService.listAttachments(issueId);
|
|
80
81
|
const documentSlugIds = [
|
|
@@ -129,9 +130,9 @@ export function setupDocumentsCommands(program) {
|
|
|
129
130
|
.option("--icon <icon>", "document icon")
|
|
130
131
|
.option("--color <color>", "icon color")
|
|
131
132
|
.action(handleAsyncCommand(async (documentId, options, command) => {
|
|
132
|
-
const rootOpts = command
|
|
133
|
-
const documentsService = createGraphQLDocumentsService(rootOpts);
|
|
134
|
-
const linearService = createLinearService(rootOpts);
|
|
133
|
+
const rootOpts = getRootOpts(command);
|
|
134
|
+
const documentsService = await createGraphQLDocumentsService(rootOpts);
|
|
135
|
+
const linearService = await createLinearService(rootOpts);
|
|
135
136
|
const input = {};
|
|
136
137
|
if (options.title) {
|
|
137
138
|
input.title = options.title;
|
|
@@ -154,8 +155,9 @@ export function setupDocumentsCommands(program) {
|
|
|
154
155
|
.command("read <documentId>")
|
|
155
156
|
.description("Read a document")
|
|
156
157
|
.action(handleAsyncCommand(async (documentId, _options, command) => {
|
|
157
|
-
const rootOpts = command
|
|
158
|
-
|
|
158
|
+
const rootOpts = getRootOpts(command);
|
|
159
|
+
const documentsService = await createGraphQLDocumentsService(rootOpts);
|
|
160
|
+
outputSuccess(await documentsService.getDocument(documentId));
|
|
159
161
|
}));
|
|
160
162
|
documents
|
|
161
163
|
.command("list")
|
|
@@ -168,8 +170,9 @@ export function setupDocumentsCommands(program) {
|
|
|
168
170
|
.command("delete <documentId>")
|
|
169
171
|
.description("Delete (trash) a document")
|
|
170
172
|
.action(handleAsyncCommand(async (documentId, _options, command) => {
|
|
171
|
-
const rootOpts = command
|
|
172
|
-
await createGraphQLDocumentsService(rootOpts)
|
|
173
|
+
const rootOpts = getRootOpts(command);
|
|
174
|
+
const documentsService = await createGraphQLDocumentsService(rootOpts);
|
|
175
|
+
await documentsService.deleteDocument(documentId);
|
|
173
176
|
outputSuccess({ success: true, message: "Document moved to trash" });
|
|
174
177
|
}));
|
|
175
178
|
}
|
package/dist/commands/embeds.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { FileService } from "../utils/file-service.js";
|
|
1
|
+
import { createFileService } from "../utils/file-service.js";
|
|
3
2
|
import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
|
|
3
|
+
import { getRootOpts } from "../utils/root-opts.js";
|
|
4
4
|
export function setupEmbedsCommands(program) {
|
|
5
5
|
const embeds = program
|
|
6
6
|
.command("embeds")
|
|
@@ -12,8 +12,7 @@ export function setupEmbedsCommands(program) {
|
|
|
12
12
|
.option("--output <path>", "output file path")
|
|
13
13
|
.option("--overwrite", "overwrite existing file", false)
|
|
14
14
|
.action(handleAsyncCommand(async (url, options, command) => {
|
|
15
|
-
const
|
|
16
|
-
const fileService = new FileService(apiToken);
|
|
15
|
+
const fileService = await createFileService(getRootOpts(command));
|
|
17
16
|
const result = await fileService.downloadFile(url, {
|
|
18
17
|
output: options.output,
|
|
19
18
|
overwrite: options.overwrite,
|
|
@@ -40,8 +39,7 @@ export function setupEmbedsCommands(program) {
|
|
|
40
39
|
.command("upload <file>")
|
|
41
40
|
.description("Upload a file to Linear storage.")
|
|
42
41
|
.action(handleAsyncCommand(async (filePath, _options, command) => {
|
|
43
|
-
const
|
|
44
|
-
const fileService = new FileService(apiToken);
|
|
42
|
+
const fileService = await createFileService(getRootOpts(command));
|
|
45
43
|
const result = await fileService.uploadFile(filePath);
|
|
46
44
|
if (result.success) {
|
|
47
45
|
outputSuccess({
|
package/dist/commands/graphql.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import { INTROSPECT_ROOT_QUERY, INTROSPECT_TYPE_QUERY, } from "../queries/introspect.js";
|
|
3
3
|
import { createGraphQLService } from "../utils/graphql-service.js";
|
|
4
4
|
import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
|
|
5
|
+
import { getRootOpts } from "../utils/root-opts.js";
|
|
5
6
|
async function executeQuery(query, options, command) {
|
|
6
7
|
let finalQuery = query;
|
|
7
8
|
if (options.file) {
|
|
@@ -22,8 +23,8 @@ async function executeQuery(query, options, command) {
|
|
|
22
23
|
throw new Error(`Invalid JSON in --variables: ${options.variables}`);
|
|
23
24
|
}
|
|
24
25
|
}
|
|
25
|
-
const rootOpts = command
|
|
26
|
-
const graphQLService = createGraphQLService(rootOpts);
|
|
26
|
+
const rootOpts = getRootOpts(command);
|
|
27
|
+
const graphQLService = await createGraphQLService(rootOpts);
|
|
27
28
|
const result = await graphQLService.rawRequest(finalQuery, variables);
|
|
28
29
|
outputSuccess(result);
|
|
29
30
|
}
|
|
@@ -41,8 +42,8 @@ export function setupGraphQLCommands(program) {
|
|
|
41
42
|
.description("Introspect the Linear GraphQL schema. Without args lists root query fields; with a type name shows its fields and args.")
|
|
42
43
|
.option("--filter <pattern>", "filter fields by name (case-insensitive substring match)")
|
|
43
44
|
.action(handleAsyncCommand(async (typeName, options, command) => {
|
|
44
|
-
const rootOpts = command
|
|
45
|
-
const graphQLService = createGraphQLService(rootOpts);
|
|
45
|
+
const rootOpts = getRootOpts(command);
|
|
46
|
+
const graphQLService = await createGraphQLService(rootOpts);
|
|
46
47
|
if (typeName) {
|
|
47
48
|
const result = await graphQLService.rawRequest(INTROSPECT_TYPE_QUERY, { typeName });
|
|
48
49
|
const typeInfo = result.__type;
|
|
@@ -159,13 +159,11 @@ export async function runAliasesStep(token, existing, options = {}) {
|
|
|
159
159
|
default: false,
|
|
160
160
|
});
|
|
161
161
|
if (!proceed) {
|
|
162
|
-
// biome-ignore lint/suspicious/noConsole: wizard
|
|
163
162
|
console.log(" Skipped — run `el-linear init aliases` to add or edit aliases later.");
|
|
164
163
|
return new Map();
|
|
165
164
|
}
|
|
166
165
|
const allUsers = await fetchAllUsers(token);
|
|
167
166
|
if (allUsers.length === 0) {
|
|
168
|
-
// biome-ignore lint/suspicious/noConsole: wizard
|
|
169
167
|
console.log(" No active users visible to this token.");
|
|
170
168
|
return new Map();
|
|
171
169
|
}
|
|
@@ -176,25 +174,20 @@ export async function runAliasesStep(token, existing, options = {}) {
|
|
|
176
174
|
const collisions = findDisplayNameCollisions(allUsers);
|
|
177
175
|
const collidingIds = new Set();
|
|
178
176
|
if (collisions.size > 0) {
|
|
179
|
-
// biome-ignore lint/suspicious/noConsole: wizard
|
|
180
177
|
console.log(` ⚠ ${collisions.size} display name(s) are shared by multiple active users — skipping those to avoid alias corruption:`);
|
|
181
178
|
for (const [name, group] of collisions) {
|
|
182
|
-
// biome-ignore lint/suspicious/noConsole: wizard
|
|
183
179
|
console.log(` "${name}" — ${group.map((u) => u.email ?? u.id).join(", ")}`);
|
|
184
180
|
for (const u of group)
|
|
185
181
|
collidingIds.add(u.id);
|
|
186
182
|
}
|
|
187
|
-
// biome-ignore lint/suspicious/noConsole: wizard
|
|
188
183
|
console.log(" (Add aliases manually in ~/.config/el-linear/config.json for now; schema migration in 1.3.0.)");
|
|
189
184
|
}
|
|
190
185
|
const users = allUsers.filter((u) => !collidingIds.has(u.id));
|
|
191
186
|
if (users.length === 0) {
|
|
192
|
-
// biome-ignore lint/suspicious/noConsole: wizard
|
|
193
187
|
console.log(" No safe-to-alias users remain after collision filter.");
|
|
194
188
|
return new Map();
|
|
195
189
|
}
|
|
196
190
|
const startIdx = await resolveResumePoint(users);
|
|
197
|
-
// biome-ignore lint/suspicious/noConsole: wizard
|
|
198
191
|
console.log(` Walking ${users.length - startIdx}/${users.length} users. Type 'q' at any prompt to stop and save progress.`);
|
|
199
192
|
const updates = new Map();
|
|
200
193
|
let lastCompletedIdx = startIdx - 1;
|
|
@@ -213,7 +206,6 @@ export async function runAliasesStep(token, existing, options = {}) {
|
|
|
213
206
|
const update = await promptForUser(i + 1, users.length, u, existing);
|
|
214
207
|
if (update === "quit") {
|
|
215
208
|
await saveProgress();
|
|
216
|
-
// biome-ignore lint/suspicious/noConsole: wizard
|
|
217
209
|
console.log(` Saved progress at ${lastCompletedIdx + 1}/${users.length}. Resume with \`el-linear init aliases\`.`);
|
|
218
210
|
return updates;
|
|
219
211
|
}
|
|
@@ -225,7 +217,6 @@ export async function runAliasesStep(token, existing, options = {}) {
|
|
|
225
217
|
lastCompletedIdx = i;
|
|
226
218
|
}
|
|
227
219
|
await clearAliasesProgress();
|
|
228
|
-
// biome-ignore lint/suspicious/noConsole: wizard
|
|
229
220
|
console.log(` ✓ Walked all ${users.length} users.`);
|
|
230
221
|
return updates;
|
|
231
222
|
}
|
|
@@ -253,7 +244,6 @@ async function resolveResumePoint(users) {
|
|
|
253
244
|
return 0;
|
|
254
245
|
const idx = users.findIndex((u) => u.id === progress.lastCompletedUserId);
|
|
255
246
|
if (idx < 0) {
|
|
256
|
-
// biome-ignore lint/suspicious/noConsole: wizard
|
|
257
247
|
console.log(" Saved progress points at a user no longer in the workspace. Starting over.");
|
|
258
248
|
await clearAliasesProgress();
|
|
259
249
|
return 0;
|
|
@@ -272,18 +262,14 @@ async function promptForUser(index, total, user, existing) {
|
|
|
272
262
|
const currentAliases = currentAliasesFor(existing, fullName);
|
|
273
263
|
const currentGithub = currentHandleFor(existing, "github", fullName);
|
|
274
264
|
const currentGitlab = currentHandleFor(existing, "gitlab", fullName);
|
|
275
|
-
// biome-ignore lint/suspicious/noConsole: wizard
|
|
276
265
|
console.log(`\n [${index}/${total}] ${fullName}${user.email ? ` <${user.email}>` : ""}`);
|
|
277
266
|
if (currentAliases.length > 0) {
|
|
278
|
-
// biome-ignore lint/suspicious/noConsole: wizard
|
|
279
267
|
console.log(` Current aliases: ${currentAliases.join(", ")}`);
|
|
280
268
|
}
|
|
281
269
|
if (currentGithub) {
|
|
282
|
-
// biome-ignore lint/suspicious/noConsole: wizard
|
|
283
270
|
console.log(` Current GitHub: ${currentGithub}`);
|
|
284
271
|
}
|
|
285
272
|
if (currentGitlab) {
|
|
286
|
-
// biome-ignore lint/suspicious/noConsole: wizard
|
|
287
273
|
console.log(` Current GitLab: ${currentGitlab}`);
|
|
288
274
|
}
|
|
289
275
|
const action = await select({
|
|
@@ -19,7 +19,6 @@ export async function runDefaultsStep(existing) {
|
|
|
19
19
|
};
|
|
20
20
|
// ── Default labels ────────────────────────────────────────────────
|
|
21
21
|
const currentLabels = existing.defaultLabels ?? [];
|
|
22
|
-
// biome-ignore lint/suspicious/noConsole: wizard
|
|
23
22
|
console.log(` Current default labels: ${currentLabels.length > 0 ? currentLabels.join(", ") : "(none)"}`);
|
|
24
23
|
const editLabels = await confirm({
|
|
25
24
|
message: "Change default labels for new issues?",
|
|
@@ -35,7 +34,6 @@ export async function runDefaultsStep(existing) {
|
|
|
35
34
|
}
|
|
36
35
|
// ── Status defaults ────────────────────────────────────────────────
|
|
37
36
|
const cur = existing.statusDefaults;
|
|
38
|
-
// biome-ignore lint/suspicious/noConsole: wizard
|
|
39
37
|
console.log(` Current status defaults: noProject=${cur?.noProject ?? STATUS_FALLBACK.noProject}, ` +
|
|
40
38
|
`withAssigneeAndProject=${cur?.withAssigneeAndProject ?? STATUS_FALLBACK.withAssigneeAndProject}`);
|
|
41
39
|
const editStatus = await confirm({
|
|
@@ -58,10 +56,8 @@ export async function runDefaultsStep(existing) {
|
|
|
58
56
|
}
|
|
59
57
|
// ── Term enforcement ───────────────────────────────────────────────
|
|
60
58
|
const currentTerms = existing.terms ?? [];
|
|
61
|
-
// biome-ignore lint/suspicious/noConsole: wizard
|
|
62
59
|
console.log(` Current term-enforcement rules: ${currentTerms.length}`);
|
|
63
60
|
for (const t of currentTerms) {
|
|
64
|
-
// biome-ignore lint/suspicious/noConsole: wizard
|
|
65
61
|
console.log(` "${t.canonical}" rejects: ${t.reject.join(", ")}`);
|
|
66
62
|
}
|
|
67
63
|
const editTerms = await confirm({
|
|
@@ -77,7 +73,6 @@ export async function runDefaultsStep(existing) {
|
|
|
77
73
|
}
|
|
78
74
|
async function collectTermsInteractively(initial) {
|
|
79
75
|
const terms = [...initial];
|
|
80
|
-
// biome-ignore lint/suspicious/noConsole: wizard
|
|
81
76
|
console.log(" Term enforcement catches misspellings of brand or product names in issue titles " +
|
|
82
77
|
"and descriptions. Define the canonical form and a list of rejected variants.");
|
|
83
78
|
if (terms.length > 0) {
|
|
@@ -102,7 +97,6 @@ async function collectTermsInteractively(initial) {
|
|
|
102
97
|
});
|
|
103
98
|
const reject = parseCsvList(rejectRaw);
|
|
104
99
|
if (reject.length === 0) {
|
|
105
|
-
// biome-ignore lint/suspicious/noConsole: wizard
|
|
106
100
|
console.log(" No variants given — skipping this rule.");
|
|
107
101
|
continue;
|
|
108
102
|
}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { mergeAliasesIntoConfig, runAliasesImport, runAliasesStep, } from "./aliases.js";
|
|
18
18
|
import { runDefaultsStep } from "./defaults.js";
|
|
19
|
+
import { runOAuthRevoke, runOAuthStep } from "./oauth.js";
|
|
19
20
|
import { assignDefined, printStep, readConfig, writeConfig, } from "./shared.js";
|
|
20
21
|
import { runTokenStep } from "./token.js";
|
|
21
22
|
import { runWorkspaceStep } from "./workspace.js";
|
|
@@ -32,7 +33,6 @@ function withCleanExit(fn) {
|
|
|
32
33
|
}
|
|
33
34
|
catch (err) {
|
|
34
35
|
if (err instanceof Error && err.name === "ExitPromptError") {
|
|
35
|
-
// biome-ignore lint/suspicious/noConsole: wizard
|
|
36
36
|
console.log("\n Cancelled.");
|
|
37
37
|
process.exit(130);
|
|
38
38
|
}
|
|
@@ -56,6 +56,27 @@ export function setupInitCommands(program) {
|
|
|
56
56
|
printStep("token", "Linear API token");
|
|
57
57
|
await runTokenStep({ force: options.force ?? false });
|
|
58
58
|
}));
|
|
59
|
+
init
|
|
60
|
+
.command("oauth")
|
|
61
|
+
.description("Authorize via OAuth 2.0 (PKCE) — alternative to a personal API token")
|
|
62
|
+
.option("--force", "ignore existing tokens; re-authorize unconditionally")
|
|
63
|
+
.option("--revoke", "revoke and remove the stored OAuth tokens")
|
|
64
|
+
.option("--no-browser", "skip the browser-open + localhost listener; paste the code manually")
|
|
65
|
+
.option("--port <port>", "localhost callback port (default 8765)", (value) => Number.parseInt(value, 10))
|
|
66
|
+
.action(withCleanExit(async (options) => {
|
|
67
|
+
printStep("oauth", "Linear OAuth (PKCE)");
|
|
68
|
+
if (options.revoke) {
|
|
69
|
+
const result = await runOAuthRevoke();
|
|
70
|
+
console.log(` ${result.message}`);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
await runOAuthStep({
|
|
74
|
+
force: options.force ?? false,
|
|
75
|
+
// commander's `--no-browser` produces `browser: false`.
|
|
76
|
+
noBrowser: options.browser === false,
|
|
77
|
+
port: options.port,
|
|
78
|
+
});
|
|
79
|
+
}));
|
|
59
80
|
init
|
|
60
81
|
.command("workspace")
|
|
61
82
|
.description("Set the default team and refresh team UUID cache")
|
|
@@ -72,7 +93,6 @@ export function setupInitCommands(program) {
|
|
|
72
93
|
workspaceUrlKey: existing.workspaceUrlKey ?? ws.workspaceUrlKey,
|
|
73
94
|
});
|
|
74
95
|
await writeConfig(merged);
|
|
75
|
-
// biome-ignore lint/suspicious/noConsole: wizard
|
|
76
96
|
console.log(" ✓ Workspace defaults saved.");
|
|
77
97
|
}));
|
|
78
98
|
init
|
|
@@ -89,7 +109,6 @@ export function setupInitCommands(program) {
|
|
|
89
109
|
const result = await runAliasesImport(tokenResult.token, options.import);
|
|
90
110
|
updates = result.updates;
|
|
91
111
|
if (result.skipped.length > 0) {
|
|
92
|
-
// biome-ignore lint/suspicious/noConsole: wizard
|
|
93
112
|
console.log(` Skipped ${result.skipped.length} unmatched email(s): ${result.skipped.join(", ")}`);
|
|
94
113
|
}
|
|
95
114
|
}
|
|
@@ -99,13 +118,11 @@ export function setupInitCommands(program) {
|
|
|
99
118
|
});
|
|
100
119
|
}
|
|
101
120
|
if (updates.size === 0) {
|
|
102
|
-
// biome-ignore lint/suspicious/noConsole: wizard
|
|
103
121
|
console.log(" No alias changes — config unchanged.");
|
|
104
122
|
return;
|
|
105
123
|
}
|
|
106
124
|
const merged = mergeAliasesIntoConfig(existing, updates);
|
|
107
125
|
await writeConfig(merged);
|
|
108
|
-
// biome-ignore lint/suspicious/noConsole: wizard
|
|
109
126
|
console.log(` ✓ Updated aliases for ${updates.size} user(s).`);
|
|
110
127
|
}));
|
|
111
128
|
init
|
|
@@ -121,7 +138,6 @@ export function setupInitCommands(program) {
|
|
|
121
138
|
terms: result.terms,
|
|
122
139
|
});
|
|
123
140
|
await writeConfig(merged);
|
|
124
|
-
// biome-ignore lint/suspicious/noConsole: wizard
|
|
125
141
|
console.log(" ✓ Defaults saved.");
|
|
126
142
|
}));
|
|
127
143
|
}
|
|
@@ -134,9 +150,7 @@ export async function runFullWizard(options = {}) {
|
|
|
134
150
|
await runFullWizardImpl(opts);
|
|
135
151
|
}
|
|
136
152
|
async function runFullWizardImpl(options) {
|
|
137
|
-
// biome-ignore lint/suspicious/noConsole: wizard
|
|
138
153
|
console.log("Welcome to el-linear. This wizard will set up your config.");
|
|
139
|
-
// biome-ignore lint/suspicious/noConsole: wizard
|
|
140
154
|
console.log("Skip is the default at every prompt; only the API token is required.\n");
|
|
141
155
|
const existing = await readConfig();
|
|
142
156
|
// Step 1: token (required)
|
|
@@ -175,12 +189,8 @@ async function runFullWizardImpl(options) {
|
|
|
175
189
|
merged = mergeAliasesIntoConfig(merged, aliasUpdates);
|
|
176
190
|
}
|
|
177
191
|
await writeConfig(merged);
|
|
178
|
-
// biome-ignore lint/suspicious/noConsole: wizard
|
|
179
192
|
console.log("\n✓ Setup complete.");
|
|
180
|
-
// biome-ignore lint/suspicious/noConsole: wizard
|
|
181
193
|
console.log(" Token: ~/.config/el-linear/token (mode 0600)");
|
|
182
|
-
// biome-ignore lint/suspicious/noConsole: wizard
|
|
183
194
|
console.log(" Config: ~/.config/el-linear/config.json");
|
|
184
|
-
// biome-ignore lint/suspicious/noConsole: wizard
|
|
185
195
|
console.log("\nTry: el-linear teams list");
|
|
186
196
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wizard step for OAuth 2.0 (PKCE flow) authorization.
|
|
3
|
+
*
|
|
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.
|
|
9
|
+
* 3. Generate PKCE verifier + state, build the authorize URL.
|
|
10
|
+
* 4. Try to open the system browser; fall back to printing the URL.
|
|
11
|
+
* 5. Spin a localhost listener (or fall back to pasted-code prompt) to
|
|
12
|
+
* receive the redirect.
|
|
13
|
+
* 6. Exchange code for tokens.
|
|
14
|
+
* 7. Validate by calling `viewer { ... }` with the new bearer token.
|
|
15
|
+
* 8. Persist `oauth.json` to the active profile.
|
|
16
|
+
*
|
|
17
|
+
* Idempotent: if a fresh `oauth.json` already exists, offer keep / re-auth /
|
|
18
|
+
* revoke before doing anything else.
|
|
19
|
+
*/
|
|
20
|
+
import { runLocalhostCallback } from "../../auth/oauth-callback.js";
|
|
21
|
+
import { type OAuthState } from "../../auth/oauth-storage.js";
|
|
22
|
+
import { type FetchLike } from "../../auth/oauth-token.js";
|
|
23
|
+
interface ViewerResponse {
|
|
24
|
+
viewer: {
|
|
25
|
+
id: string;
|
|
26
|
+
name: string;
|
|
27
|
+
email: string;
|
|
28
|
+
displayName: string;
|
|
29
|
+
organization: {
|
|
30
|
+
urlKey: string;
|
|
31
|
+
name: string;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export interface OAuthStepOptions {
|
|
36
|
+
/** Force re-authorization even if existing state is valid. */
|
|
37
|
+
force?: boolean;
|
|
38
|
+
/** Skip the localhost listener; use the headless code-paste prompt. */
|
|
39
|
+
noBrowser?: boolean;
|
|
40
|
+
/** Override the localhost port. Default 8765. */
|
|
41
|
+
port?: number;
|
|
42
|
+
/** Test seam for the OAuth token endpoint. */
|
|
43
|
+
fetchImpl?: FetchLike;
|
|
44
|
+
/**
|
|
45
|
+
* Test seam for the localhost listener. Default uses the real one.
|
|
46
|
+
*/
|
|
47
|
+
runLocalhostCallbackImpl?: typeof runLocalhostCallback;
|
|
48
|
+
/** Test seam for the system-browser opener. Default uses spawn. */
|
|
49
|
+
openBrowser?: (url: string) => Promise<void>;
|
|
50
|
+
/** Test seam for `viewer` validation against the new bearer token. */
|
|
51
|
+
validateViewer?: (oauthToken: string) => Promise<ViewerResponse["viewer"]>;
|
|
52
|
+
}
|
|
53
|
+
export interface OAuthStepResult {
|
|
54
|
+
state: OAuthState;
|
|
55
|
+
viewer: ViewerResponse["viewer"];
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Open `url` in the system browser. Forks per-platform:
|
|
59
|
+
* - darwin → `open <url>`
|
|
60
|
+
* - win32 → `start "" "<url>"` (cmd builtin)
|
|
61
|
+
* - other → `xdg-open <url>` (Linux / BSD with desktop env)
|
|
62
|
+
*
|
|
63
|
+
* Returns once the spawn call succeeds — does NOT wait for the browser to
|
|
64
|
+
* actually load. Throws on spawn failure (e.g. xdg-open not installed in
|
|
65
|
+
* a barebones container).
|
|
66
|
+
*/
|
|
67
|
+
export declare function openSystemBrowser(url: string): Promise<void>;
|
|
68
|
+
/**
|
|
69
|
+
* Run the OAuth step. Returns the final stored state + viewer info.
|
|
70
|
+
*
|
|
71
|
+
* If the user opts to keep their existing tokens, returns the existing
|
|
72
|
+
* state unchanged (no network calls).
|
|
73
|
+
*/
|
|
74
|
+
export declare function runOAuthStep(options?: OAuthStepOptions): Promise<OAuthStepResult>;
|
|
75
|
+
/**
|
|
76
|
+
* `init oauth --revoke`: revoke the active profile's tokens and remove
|
|
77
|
+
* `oauth.json`. Best-effort on the network call.
|
|
78
|
+
*/
|
|
79
|
+
export declare function runOAuthRevoke(options?: {
|
|
80
|
+
fetchImpl?: FetchLike;
|
|
81
|
+
}): Promise<{
|
|
82
|
+
revoked: boolean;
|
|
83
|
+
message: string;
|
|
84
|
+
}>;
|
|
85
|
+
export {};
|