@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
package/README.md
CHANGED
|
@@ -74,13 +74,41 @@ every team ends up writing themselves:
|
|
|
74
74
|
|
|
75
75
|
## Authentication
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
el-linear supports either OAuth or a personal Linear API token. OAuth is
|
|
78
|
+
configured with:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
el-linear init oauth
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
By default, that command walks you through registering your own Linear OAuth
|
|
85
|
+
app. Teams can make the flow a single browser authorization step by writing a
|
|
86
|
+
local, untracked app-defaults file at `~/.config/el-linear/team-oauth.json`
|
|
87
|
+
or pointing `EL_LINEAR_OAUTH_CONFIG` at one:
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"linearOAuth": {
|
|
92
|
+
"clientId": "your-linear-oauth-client-id",
|
|
93
|
+
"redirectPort": 8765,
|
|
94
|
+
"scopes": ["read", "write", "issues:create", "comments:create"],
|
|
95
|
+
"passwordManagerPath": "op://vault/item/client_id"
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`passwordManagerPath` is optional metadata for humans/scripts; el-linear does
|
|
101
|
+
not execute password-manager commands from it. Do not put a `client_secret` in
|
|
102
|
+
this shared file. The OAuth flow uses PKCE.
|
|
103
|
+
|
|
104
|
+
At runtime, credentials are resolved in this order:
|
|
78
105
|
|
|
79
106
|
1. `--api-token <token>` flag.
|
|
80
107
|
2. `LINEAR_API_TOKEN` environment variable.
|
|
81
|
-
3. **Active profile's**
|
|
82
|
-
4. `~/.config/el-linear/token` file (
|
|
83
|
-
5. `~/.
|
|
108
|
+
3. **Active profile's** OAuth state (`oauth.json`) from `el-linear init oauth`.
|
|
109
|
+
4. **Active profile's** `~/.config/el-linear/profiles/<name>/token` file (see *Profiles* below).
|
|
110
|
+
5. `~/.config/el-linear/token` file (legacy single-profile, recommended for human use when only one workspace is needed).
|
|
111
|
+
6. `~/.linear_api_token` file (legacy, still honored).
|
|
84
112
|
|
|
85
113
|
el-linear never logs the token.
|
|
86
114
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional local OAuth app defaults.
|
|
3
|
+
*
|
|
4
|
+
* This deliberately lives outside the packaged/default config. Teams can
|
|
5
|
+
* materialize it from a password manager, while the OSS CLI keeps requiring
|
|
6
|
+
* users to bring their own OAuth app when no local file exists.
|
|
7
|
+
*/
|
|
8
|
+
import { type OAuthScope } from "./oauth-client.js";
|
|
9
|
+
export declare const TEAM_OAUTH_CONFIG_ENV = "EL_LINEAR_OAUTH_CONFIG";
|
|
10
|
+
export interface TeamOAuthConfig {
|
|
11
|
+
clientId: string;
|
|
12
|
+
redirectPort: number;
|
|
13
|
+
scopes: OAuthScope[];
|
|
14
|
+
/**
|
|
15
|
+
* Optional human-facing pointer such as `op://vault/item/client_id`.
|
|
16
|
+
* The CLI does not execute password-manager commands from this value.
|
|
17
|
+
*/
|
|
18
|
+
passwordManagerPath?: string;
|
|
19
|
+
sourcePath: string;
|
|
20
|
+
}
|
|
21
|
+
export declare function readTeamOAuthConfig(env?: NodeJS.ProcessEnv): Promise<TeamOAuthConfig | null>;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional local OAuth app defaults.
|
|
3
|
+
*
|
|
4
|
+
* This deliberately lives outside the packaged/default config. Teams can
|
|
5
|
+
* materialize it from a password manager, while the OSS CLI keeps requiring
|
|
6
|
+
* users to bring their own OAuth app when no local file exists.
|
|
7
|
+
*/
|
|
8
|
+
import fs from "node:fs/promises";
|
|
9
|
+
import { TEAM_OAUTH_CONFIG_PATH } from "../config/paths.js";
|
|
10
|
+
import { DEFAULT_SCOPES, validateScopes, } from "./oauth-client.js";
|
|
11
|
+
export const TEAM_OAUTH_CONFIG_ENV = "EL_LINEAR_OAUTH_CONFIG";
|
|
12
|
+
export async function readTeamOAuthConfig(env = process.env) {
|
|
13
|
+
const envPath = env[TEAM_OAUTH_CONFIG_ENV]?.trim();
|
|
14
|
+
const sourcePath = envPath || TEAM_OAUTH_CONFIG_PATH;
|
|
15
|
+
let raw;
|
|
16
|
+
try {
|
|
17
|
+
raw = await fs.readFile(sourcePath, "utf8");
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
if (err.code === "ENOENT" && !envPath) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
if (err.code === "ENOENT") {
|
|
24
|
+
throw new Error(`${TEAM_OAUTH_CONFIG_ENV} points to ${sourcePath}, but that file does not exist.`);
|
|
25
|
+
}
|
|
26
|
+
throw err;
|
|
27
|
+
}
|
|
28
|
+
let parsed;
|
|
29
|
+
try {
|
|
30
|
+
parsed = JSON.parse(raw);
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
34
|
+
throw new Error(`Failed to parse ${sourcePath}: ${message}`);
|
|
35
|
+
}
|
|
36
|
+
const linearOAuth = parsed.linearOAuth;
|
|
37
|
+
if (!linearOAuth || typeof linearOAuth !== "object") {
|
|
38
|
+
throw new Error(`${sourcePath} must contain a linearOAuth object with a clientId.`);
|
|
39
|
+
}
|
|
40
|
+
const clientId = typeof linearOAuth.clientId === "string" ? linearOAuth.clientId.trim() : "";
|
|
41
|
+
if (!clientId) {
|
|
42
|
+
throw new Error(`${sourcePath} linearOAuth.clientId must be a string.`);
|
|
43
|
+
}
|
|
44
|
+
const redirectPort = parseRedirectPort(linearOAuth.redirectPort, sourcePath);
|
|
45
|
+
const scopes = parseScopes(linearOAuth.scopes, sourcePath);
|
|
46
|
+
const passwordManagerPath = parsePasswordManagerPath(linearOAuth.passwordManagerPath, sourcePath);
|
|
47
|
+
return {
|
|
48
|
+
clientId,
|
|
49
|
+
redirectPort,
|
|
50
|
+
scopes,
|
|
51
|
+
passwordManagerPath,
|
|
52
|
+
sourcePath,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function parseRedirectPort(value, sourcePath) {
|
|
56
|
+
if (value === undefined)
|
|
57
|
+
return 8765;
|
|
58
|
+
if (typeof value !== "number" ||
|
|
59
|
+
!Number.isInteger(value) ||
|
|
60
|
+
value < 1024 ||
|
|
61
|
+
value > 65535) {
|
|
62
|
+
throw new Error(`${sourcePath} linearOAuth.redirectPort must be an integer between 1024 and 65535.`);
|
|
63
|
+
}
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
function parseScopes(value, sourcePath) {
|
|
67
|
+
if (value === undefined)
|
|
68
|
+
return [...DEFAULT_SCOPES];
|
|
69
|
+
if (!Array.isArray(value) || !value.every((s) => typeof s === "string")) {
|
|
70
|
+
throw new Error(`${sourcePath} linearOAuth.scopes must be a string array.`);
|
|
71
|
+
}
|
|
72
|
+
return validateScopes(value);
|
|
73
|
+
}
|
|
74
|
+
function parsePasswordManagerPath(value, sourcePath) {
|
|
75
|
+
if (value === undefined)
|
|
76
|
+
return undefined;
|
|
77
|
+
if (typeof value !== "string") {
|
|
78
|
+
throw new Error(`${sourcePath} linearOAuth.passwordManagerPath must be a string when set.`);
|
|
79
|
+
}
|
|
80
|
+
const trimmed = value.trim();
|
|
81
|
+
return trimmed || undefined;
|
|
82
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { FileService } from "../utils/file-service.js";
|
|
1
|
+
import { createFileService } from "../utils/file-service.js";
|
|
3
2
|
import { createGraphQLAttachmentsService } from "../utils/graphql-attachments-service.js";
|
|
4
3
|
import { createLinearService } from "../utils/linear-service.js";
|
|
5
4
|
import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
|
|
5
|
+
import { getRootOpts } from "../utils/root-opts.js";
|
|
6
6
|
export function setupAttachmentsCommands(program) {
|
|
7
7
|
const attachments = program
|
|
8
8
|
.command("attachments")
|
|
@@ -14,16 +14,15 @@ export function setupAttachmentsCommands(program) {
|
|
|
14
14
|
.requiredOption("--file <path>", "path to file to upload")
|
|
15
15
|
.option("--title <title>", "attachment title (defaults to filename)")
|
|
16
16
|
.action(handleAsyncCommand(async (issueId, options, command) => {
|
|
17
|
-
const rootOpts = command
|
|
18
|
-
const
|
|
19
|
-
const linearService = createLinearService(rootOpts);
|
|
17
|
+
const rootOpts = getRootOpts(command);
|
|
18
|
+
const linearService = await createLinearService(rootOpts);
|
|
20
19
|
const resolvedIssueId = await linearService.resolveIssueId(issueId);
|
|
21
|
-
const fileService =
|
|
20
|
+
const fileService = await createFileService(rootOpts);
|
|
22
21
|
const uploadResult = await fileService.uploadFile(options.file);
|
|
23
22
|
if (!uploadResult.success) {
|
|
24
23
|
throw new Error(uploadResult.error);
|
|
25
24
|
}
|
|
26
|
-
const attachmentsService = createGraphQLAttachmentsService(rootOpts);
|
|
25
|
+
const attachmentsService = await createGraphQLAttachmentsService(rootOpts);
|
|
27
26
|
const attachment = await attachmentsService.createAttachment({
|
|
28
27
|
issueId: resolvedIssueId,
|
|
29
28
|
url: uploadResult.assetUrl,
|
|
@@ -36,10 +35,10 @@ export function setupAttachmentsCommands(program) {
|
|
|
36
35
|
.description("List attachments on an issue.")
|
|
37
36
|
.option("-l, --limit <number>", "maximum number of attachments", "50")
|
|
38
37
|
.action(handleAsyncCommand(async (issueId, options, command) => {
|
|
39
|
-
const rootOpts = command
|
|
40
|
-
const linearService = createLinearService(rootOpts);
|
|
38
|
+
const rootOpts = getRootOpts(command);
|
|
39
|
+
const linearService = await createLinearService(rootOpts);
|
|
41
40
|
const resolvedIssueId = await linearService.resolveIssueId(issueId);
|
|
42
|
-
const attachmentsService = createGraphQLAttachmentsService(rootOpts);
|
|
41
|
+
const attachmentsService = await createGraphQLAttachmentsService(rootOpts);
|
|
43
42
|
const allAttachments = await attachmentsService.listAttachments(resolvedIssueId);
|
|
44
43
|
const limit = Number.parseInt(options.limit, 10);
|
|
45
44
|
const data = allAttachments.slice(0, limit);
|
|
@@ -49,8 +48,8 @@ export function setupAttachmentsCommands(program) {
|
|
|
49
48
|
.command("delete <attachmentId>")
|
|
50
49
|
.description("Delete an attachment.")
|
|
51
50
|
.action(handleAsyncCommand(async (attachmentId, _options, command) => {
|
|
52
|
-
const rootOpts = command
|
|
53
|
-
const attachmentsService = createGraphQLAttachmentsService(rootOpts);
|
|
51
|
+
const rootOpts = getRootOpts(command);
|
|
52
|
+
const attachmentsService = await createGraphQLAttachmentsService(rootOpts);
|
|
54
53
|
await attachmentsService.deleteAttachment(attachmentId);
|
|
55
54
|
outputSuccess({ success: true, message: "Attachment deleted" });
|
|
56
55
|
}));
|
package/dist/commands/batch.js
CHANGED
|
@@ -4,6 +4,7 @@ import { createGraphQLService } from "../utils/graphql-service.js";
|
|
|
4
4
|
import { createLinearService } from "../utils/linear-service.js";
|
|
5
5
|
import { logger } from "../utils/logger.js";
|
|
6
6
|
import { handleAsyncCommand, outputSuccess, outputWarning, } from "../utils/output.js";
|
|
7
|
+
import { getRootOpts } from "../utils/root-opts.js";
|
|
7
8
|
import { splitList } from "../utils/validators.js";
|
|
8
9
|
/**
|
|
9
10
|
* Parse the --filter string into structured search arguments.
|
|
@@ -29,8 +30,8 @@ function parseFilterString(filter) {
|
|
|
29
30
|
* Returns LinearIssue[] so we can show a preview.
|
|
30
31
|
*/
|
|
31
32
|
async function resolveTargetIssues(options, rootOpts) {
|
|
32
|
-
const graphQLService = createGraphQLService(rootOpts);
|
|
33
|
-
const linearService = createLinearService(rootOpts);
|
|
33
|
+
const graphQLService = await createGraphQLService(rootOpts);
|
|
34
|
+
const linearService = await createLinearService(rootOpts);
|
|
34
35
|
const issuesService = new GraphQLIssuesService(graphQLService, linearService);
|
|
35
36
|
if (options.issues) {
|
|
36
37
|
const ids = splitList(options.issues);
|
|
@@ -95,7 +96,7 @@ async function executeBatch(issues, operation) {
|
|
|
95
96
|
return { results };
|
|
96
97
|
}
|
|
97
98
|
async function handleBatchAssign(options, command) {
|
|
98
|
-
const rootOpts = command
|
|
99
|
+
const rootOpts = getRootOpts(command);
|
|
99
100
|
if (!options.assignee) {
|
|
100
101
|
throw new Error("--assignee is required.");
|
|
101
102
|
}
|
|
@@ -116,8 +117,8 @@ async function handleBatchAssign(options, command) {
|
|
|
116
117
|
});
|
|
117
118
|
return;
|
|
118
119
|
}
|
|
119
|
-
const graphQLService = createGraphQLService(rootOpts);
|
|
120
|
-
const linearService = createLinearService(rootOpts);
|
|
120
|
+
const graphQLService = await createGraphQLService(rootOpts);
|
|
121
|
+
const linearService = await createLinearService(rootOpts);
|
|
121
122
|
const issuesService = new GraphQLIssuesService(graphQLService, linearService);
|
|
122
123
|
const { results } = await executeBatch(issues, (issue) => issuesService.updateIssue({ id: issue.identifier, assigneeId }, "adding"));
|
|
123
124
|
outputSuccess({
|
|
@@ -132,7 +133,7 @@ async function handleBatchAssign(options, command) {
|
|
|
132
133
|
});
|
|
133
134
|
}
|
|
134
135
|
async function handleBatchLabel(options, command) {
|
|
135
|
-
const rootOpts = command
|
|
136
|
+
const rootOpts = getRootOpts(command);
|
|
136
137
|
if (!(options.add || options.remove)) {
|
|
137
138
|
throw new Error("Specify --add and/or --remove for labels.");
|
|
138
139
|
}
|
|
@@ -163,8 +164,8 @@ async function handleBatchLabel(options, command) {
|
|
|
163
164
|
});
|
|
164
165
|
return;
|
|
165
166
|
}
|
|
166
|
-
const graphQLService = createGraphQLService(rootOpts);
|
|
167
|
-
const linearService = createLinearService(rootOpts);
|
|
167
|
+
const graphQLService = await createGraphQLService(rootOpts);
|
|
168
|
+
const linearService = await createLinearService(rootOpts);
|
|
168
169
|
const issuesService = new GraphQLIssuesService(graphQLService, linearService);
|
|
169
170
|
const removeLower = removeLabels.map((l) => l.toLowerCase());
|
|
170
171
|
const { results } = await executeBatch(issues, (issue) => {
|
|
@@ -197,7 +198,7 @@ async function handleBatchLabel(options, command) {
|
|
|
197
198
|
});
|
|
198
199
|
}
|
|
199
200
|
async function handleBatchMove(options, command) {
|
|
200
|
-
const rootOpts = command
|
|
201
|
+
const rootOpts = getRootOpts(command);
|
|
201
202
|
if (!options.project) {
|
|
202
203
|
throw new Error("--project is required.");
|
|
203
204
|
}
|
|
@@ -217,8 +218,8 @@ async function handleBatchMove(options, command) {
|
|
|
217
218
|
});
|
|
218
219
|
return;
|
|
219
220
|
}
|
|
220
|
-
const graphQLService = createGraphQLService(rootOpts);
|
|
221
|
-
const linearService = createLinearService(rootOpts);
|
|
221
|
+
const graphQLService = await createGraphQLService(rootOpts);
|
|
222
|
+
const linearService = await createLinearService(rootOpts);
|
|
222
223
|
const issuesService = new GraphQLIssuesService(graphQLService, linearService);
|
|
223
224
|
const { results } = await executeBatch(issues, (issue) => issuesService.updateIssue({ id: issue.identifier, projectId: options.project }, "adding"));
|
|
224
225
|
outputSuccess({
|
|
@@ -233,7 +234,7 @@ async function handleBatchMove(options, command) {
|
|
|
233
234
|
});
|
|
234
235
|
}
|
|
235
236
|
async function handleBatchStatus(options, command) {
|
|
236
|
-
const rootOpts = command
|
|
237
|
+
const rootOpts = getRootOpts(command);
|
|
237
238
|
if (!options.status) {
|
|
238
239
|
throw new Error("--status is required.");
|
|
239
240
|
}
|
|
@@ -253,8 +254,8 @@ async function handleBatchStatus(options, command) {
|
|
|
253
254
|
});
|
|
254
255
|
return;
|
|
255
256
|
}
|
|
256
|
-
const graphQLService = createGraphQLService(rootOpts);
|
|
257
|
-
const linearService = createLinearService(rootOpts);
|
|
257
|
+
const graphQLService = await createGraphQLService(rootOpts);
|
|
258
|
+
const linearService = await createLinearService(rootOpts);
|
|
258
259
|
const issuesService = new GraphQLIssuesService(graphQLService, linearService);
|
|
259
260
|
const { results } = await executeBatch(issues, (issue) => issuesService.updateIssue({ id: issue.identifier, statusId: options.status }, "adding"));
|
|
260
261
|
outputSuccess({
|
|
@@ -2,12 +2,14 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { resolveUserDisplayName } from "../config/resolver.js";
|
|
3
3
|
import { CREATE_COMMENT_MUTATION, LIST_COMMENTS_QUERY, UPDATE_COMMENT_MUTATION, } from "../queries/comments.js";
|
|
4
4
|
import { autoLinkReferences, } from "../utils/auto-link-references.js";
|
|
5
|
+
import { applyFooter } from "../utils/footer.js";
|
|
5
6
|
import { createGraphQLService, } from "../utils/graphql-service.js";
|
|
6
7
|
import { extractIssueReferences } from "../utils/issue-reference-extractor.js";
|
|
7
8
|
import { wrapIssueReferencesAsLinks } from "../utils/issue-reference-wrapper.js";
|
|
8
9
|
import { createLinearService, } from "../utils/linear-service.js";
|
|
9
10
|
import { resolveMentions } from "../utils/mention-resolver.js";
|
|
10
11
|
import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
|
|
12
|
+
import { getRootOpts } from "../utils/root-opts.js";
|
|
11
13
|
import { validateReferences } from "../utils/validate-references.js";
|
|
12
14
|
import { getWorkspaceUrlKey } from "../utils/workspace-url.js";
|
|
13
15
|
// Match Linear's bodyData validation error in multiple phrasings so a wording
|
|
@@ -98,10 +100,18 @@ async function autoLinkCommentReferences(args) {
|
|
|
98
100
|
return result;
|
|
99
101
|
}
|
|
100
102
|
async function handleCreateComment(issueId, options, command) {
|
|
101
|
-
const rootOpts = command
|
|
102
|
-
const graphQLService = createGraphQLService(rootOpts);
|
|
103
|
-
const linearService = createLinearService(rootOpts);
|
|
104
|
-
|
|
103
|
+
const rootOpts = getRootOpts(command);
|
|
104
|
+
const graphQLService = await createGraphQLService(rootOpts);
|
|
105
|
+
const linearService = await createLinearService(rootOpts);
|
|
106
|
+
// Apply messageFooter (config or --footer flag) before any further
|
|
107
|
+
// processing, so auto-link / mention-resolution see it as part of the body.
|
|
108
|
+
// Commander parses --no-footer as `options.footer === false`.
|
|
109
|
+
const noFooter = options.footer === false;
|
|
110
|
+
const explicitFooter = typeof options.footer === "string" ? options.footer : undefined;
|
|
111
|
+
const rawBody = applyFooter(readBody(options), {
|
|
112
|
+
footer: explicitFooter,
|
|
113
|
+
noFooter,
|
|
114
|
+
}) ?? "";
|
|
105
115
|
const resolvedIssueId = await linearService.resolveIssueId(issueId);
|
|
106
116
|
// Wrap valid refs as markdown links before mention resolution. Idempotent against
|
|
107
117
|
// already-wrapped refs.
|
|
@@ -171,9 +181,9 @@ async function handleCreateComment(issueId, options, command) {
|
|
|
171
181
|
outputSuccess(autoLinked ? { ...output, autoLinked } : output);
|
|
172
182
|
}
|
|
173
183
|
async function handleUpdateComment(commentId, options, command) {
|
|
174
|
-
const rootOpts = command
|
|
175
|
-
const graphQLService = createGraphQLService(rootOpts);
|
|
176
|
-
const linearService = createLinearService(rootOpts);
|
|
184
|
+
const rootOpts = getRootOpts(command);
|
|
185
|
+
const graphQLService = await createGraphQLService(rootOpts);
|
|
186
|
+
const linearService = await createLinearService(rootOpts);
|
|
177
187
|
const rawBody = readBody(options);
|
|
178
188
|
const { body, preResolved } = await prepareCommentBodyWithLinks(rawBody, options, linearService, graphQLService);
|
|
179
189
|
const autoMention = options.autoMention !== false;
|
|
@@ -218,9 +228,9 @@ async function handleUpdateComment(commentId, options, command) {
|
|
|
218
228
|
outputSuccess(autoLinked ? { ...output, autoLinked } : output);
|
|
219
229
|
}
|
|
220
230
|
async function handleListComments(issueId, options, command) {
|
|
221
|
-
const rootOpts = command
|
|
222
|
-
const graphQLService = createGraphQLService(rootOpts);
|
|
223
|
-
const linearService = createLinearService(rootOpts);
|
|
231
|
+
const rootOpts = getRootOpts(command);
|
|
232
|
+
const graphQLService = await createGraphQLService(rootOpts);
|
|
233
|
+
const linearService = await createLinearService(rootOpts);
|
|
224
234
|
const resolvedId = await linearService.resolveIssueId(issueId);
|
|
225
235
|
const result = await graphQLService.rawRequest(LIST_COMMENTS_QUERY, {
|
|
226
236
|
issueId: resolvedId,
|
|
@@ -254,6 +264,8 @@ export function setupCommentsCommands(program) {
|
|
|
254
264
|
.option("--file <path>", "read comment body from file")
|
|
255
265
|
.option("--no-auto-mention", "do not auto-convert bare team-member names to @mentions")
|
|
256
266
|
.option("--no-auto-link", "skip wrapping issue refs as markdown links and creating sidebar relations")
|
|
267
|
+
.option("--footer <text>", "text appended to the comment body (overrides config.messageFooter)")
|
|
268
|
+
.option("--no-footer", "skip the configured messageFooter for this comment")
|
|
257
269
|
.action(handleAsyncCommand(handleCreateComment));
|
|
258
270
|
comments
|
|
259
271
|
.command("update <commentId>")
|
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({
|