@enrichlayer/el-linear 1.9.0 → 1.15.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 +139 -10
- package/claude-skills/linear-operations/SKILL.md +41 -1
- package/dist/auth/linear-credential.d.ts +27 -0
- package/dist/auth/linear-credential.js +1 -0
- package/dist/auth/oauth-app-config.d.ts +4 -3
- package/dist/auth/oauth-app-config.js +13 -2
- package/dist/auth/oauth-callback.d.ts +2 -3
- package/dist/auth/oauth-callback.js +2 -2
- package/dist/auth/oauth-client.d.ts +8 -2
- package/dist/auth/oauth-client.js +26 -0
- package/dist/auth/oauth-fs.d.ts +2 -1
- package/dist/auth/oauth-headless.d.ts +2 -1
- package/dist/auth/oauth-storage.d.ts +5 -1
- package/dist/auth/oauth-storage.js +1 -1
- package/dist/auth/oauth-token.d.ts +4 -3
- package/dist/auth/oauth-token.js +16 -4
- package/dist/auth/token-resolver.d.ts +14 -5
- package/dist/auth/token-resolver.js +6 -1
- package/dist/commands/attachments.js +2 -1
- package/dist/commands/batch.js +18 -21
- package/dist/commands/comments.js +22 -33
- package/dist/commands/config.js +178 -5
- package/dist/commands/cycles.js +2 -1
- package/dist/commands/documents.js +2 -1
- package/dist/commands/graphql.js +4 -6
- package/dist/commands/init/aliases.js +1 -1
- package/dist/commands/init/defaults.d.ts +2 -1
- package/dist/commands/init/index.js +45 -35
- package/dist/commands/init/oauth.d.ts +4 -1
- package/dist/commands/init/oauth.js +22 -4
- package/dist/commands/init/shared.d.ts +24 -2
- package/dist/commands/init/shared.js +35 -4
- package/dist/commands/init/token.d.ts +3 -3
- package/dist/commands/init/token.js +5 -24
- package/dist/commands/init/workspace.d.ts +2 -1
- package/dist/commands/init/workspace.js +1 -1
- package/dist/commands/introspect.d.ts +27 -0
- package/dist/commands/introspect.js +178 -0
- package/dist/commands/issue-id.js +1 -3
- package/dist/commands/issues/branch.js +9 -1
- package/dist/commands/issues/description.js +2 -6
- package/dist/commands/issues/link-references.d.ts +21 -0
- package/dist/commands/issues/link-references.js +171 -0
- package/dist/commands/issues/relations.d.ts +44 -0
- package/dist/commands/issues/relations.js +132 -0
- package/dist/commands/issues.js +269 -309
- package/dist/commands/labels.js +15 -24
- package/dist/commands/profile.js +1 -0
- package/dist/commands/project-milestones.js +13 -20
- package/dist/commands/projects.d.ts +2 -0
- package/dist/commands/projects.js +157 -44
- package/dist/commands/read-shortcut.d.ts +1 -1
- package/dist/commands/read-shortcut.js +28 -8
- package/dist/commands/refs.js +75 -8
- package/dist/commands/releases.js +26 -30
- package/dist/commands/search.js +49 -33
- package/dist/commands/teams.js +2 -1
- package/dist/commands/templates.js +9 -14
- package/dist/commands/users.js +5 -2
- package/dist/config/config.d.ts +99 -1
- package/dist/config/config.js +264 -52
- package/dist/config/error-enrichment.d.ts +62 -0
- package/dist/config/error-enrichment.js +417 -0
- package/dist/config/issue-validation.d.ts +37 -0
- package/dist/config/issue-validation.js +63 -1
- package/dist/config/paths.d.ts +2 -8
- package/dist/config/paths.js +4 -2
- package/dist/config/resolver.d.ts +8 -1
- package/dist/config/resolver.js +11 -5
- package/dist/main.js +13 -1
- package/dist/queries/attachments-types.d.ts +30 -0
- package/dist/queries/attachments-types.js +5 -0
- package/dist/queries/comments-types.d.ts +55 -0
- package/dist/queries/comments-types.js +5 -0
- package/dist/queries/common.d.ts +2 -2
- package/dist/queries/common.js +8 -0
- package/dist/queries/documents-types.d.ts +62 -0
- package/dist/queries/documents-types.js +9 -0
- package/dist/queries/introspect-types.d.ts +58 -0
- package/dist/queries/introspect-types.js +10 -0
- package/dist/queries/issues-types.d.ts +481 -0
- package/dist/queries/issues-types.js +23 -0
- package/dist/queries/issues.d.ts +51 -10
- package/dist/queries/issues.js +147 -5
- package/dist/queries/labels-types.d.ts +65 -0
- package/dist/queries/labels-types.js +5 -0
- package/dist/queries/project-milestones-types.d.ts +92 -0
- package/dist/queries/project-milestones-types.js +10 -0
- package/dist/queries/project-milestones.d.ts +1 -1
- package/dist/queries/projects-types.d.ts +76 -0
- package/dist/queries/projects-types.js +5 -0
- package/dist/queries/projects.d.ts +2 -0
- package/dist/queries/projects.js +22 -0
- package/dist/queries/releases-types.d.ts +85 -0
- package/dist/queries/releases-types.js +5 -0
- package/dist/queries/search-types.d.ts +102 -0
- package/dist/queries/search-types.js +6 -0
- package/dist/queries/templates-types.d.ts +62 -0
- package/dist/queries/templates-types.js +9 -0
- package/dist/types/linear.d.ts +21 -3
- package/dist/utils/auto-link-references.d.ts +3 -3
- package/dist/utils/auto-link-references.js +30 -34
- package/dist/utils/extract-field.d.ts +19 -0
- package/dist/utils/extract-field.js +99 -0
- package/dist/utils/file-service.d.ts +6 -13
- package/dist/utils/file-service.js +0 -2
- package/dist/utils/formatters/summary.js +6 -1
- package/dist/utils/graphql-attachments-service.js +6 -9
- package/dist/utils/graphql-documents-service.js +19 -25
- package/dist/utils/graphql-issues-service.d.ts +112 -46
- package/dist/utils/graphql-issues-service.js +398 -206
- package/dist/utils/graphql-service.d.ts +10 -12
- package/dist/utils/graphql-service.js +0 -3
- package/dist/utils/issue-reference-extractor.d.ts +7 -0
- package/dist/utils/issue-reference-extractor.js +5 -3
- package/dist/utils/issues-service-bootstrap.d.ts +28 -0
- package/dist/utils/issues-service-bootstrap.js +27 -0
- package/dist/utils/linear-service.d.ts +21 -14
- package/dist/utils/linear-service.js +73 -11
- package/dist/utils/markdown-prosemirror.js +12 -12
- package/dist/utils/mention-resolver.js +1 -1
- package/dist/utils/output.d.ts +82 -2
- package/dist/utils/output.js +76 -11
- package/dist/utils/project-slug.d.ts +21 -0
- package/dist/utils/project-slug.js +45 -0
- package/dist/utils/protected-ranges.d.ts +14 -0
- package/dist/utils/protected-ranges.js +88 -2
- package/dist/utils/sanitize-for-log.d.ts +24 -0
- package/dist/utils/sanitize-for-log.js +38 -0
- package/dist/utils/table-formatter.js +24 -0
- package/dist/utils/validators.d.ts +7 -2
- package/dist/utils/validators.js +6 -0
- package/dist/utils/workspace-url.d.ts +5 -1
- package/dist/utils/workspace-url.js +53 -7
- package/package.json +2 -2
|
@@ -3,6 +3,7 @@ import { createGraphQLAttachmentsService } from "../utils/graphql-attachments-se
|
|
|
3
3
|
import { createLinearService } from "../utils/linear-service.js";
|
|
4
4
|
import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
|
|
5
5
|
import { getRootOpts } from "../utils/root-opts.js";
|
|
6
|
+
import { parsePositiveInt } from "../utils/validators.js";
|
|
6
7
|
export function setupAttachmentsCommands(program) {
|
|
7
8
|
const attachments = program
|
|
8
9
|
.command("attachments")
|
|
@@ -40,7 +41,7 @@ export function setupAttachmentsCommands(program) {
|
|
|
40
41
|
const resolvedIssueId = await linearService.resolveIssueId(issueId);
|
|
41
42
|
const attachmentsService = await createGraphQLAttachmentsService(rootOpts);
|
|
42
43
|
const allAttachments = await attachmentsService.listAttachments(resolvedIssueId);
|
|
43
|
-
const limit =
|
|
44
|
+
const limit = parsePositiveInt(options.limit, "--limit");
|
|
44
45
|
const data = allAttachments.slice(0, limit);
|
|
45
46
|
outputSuccess({ data, meta: { count: data.length } });
|
|
46
47
|
}));
|
package/dist/commands/batch.js
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
import { resolveAssignee, resolveLabels, resolveTeam, } from "../config/resolver.js";
|
|
2
|
-
import {
|
|
3
|
-
import { createGraphQLService } from "../utils/graphql-service.js";
|
|
4
|
-
import { createLinearService } from "../utils/linear-service.js";
|
|
1
|
+
import { resolveAssignee, resolveLabels, resolveMember, resolveTeam, } from "../config/resolver.js";
|
|
2
|
+
import { createIssuesService } from "../utils/issues-service-bootstrap.js";
|
|
5
3
|
import { logger } from "../utils/logger.js";
|
|
6
4
|
import { handleAsyncCommand, outputSuccess, outputWarning, } from "../utils/output.js";
|
|
7
5
|
import { getRootOpts } from "../utils/root-opts.js";
|
|
8
6
|
import { splitList } from "../utils/validators.js";
|
|
9
7
|
/**
|
|
10
8
|
* Parse the --filter string into structured search arguments.
|
|
11
|
-
* Format: "status:Backlog team:DEV label:Bug assignee:Alice project:Sprint12"
|
|
9
|
+
* Format: "status:Backlog team:DEV label:Bug assignee:Alice delegate:Claude project:Sprint12"
|
|
12
10
|
*/
|
|
13
11
|
function parseFilterString(filter) {
|
|
14
12
|
const result = {};
|
|
@@ -30,21 +28,28 @@ function parseFilterString(filter) {
|
|
|
30
28
|
* Returns LinearIssue[] so we can show a preview.
|
|
31
29
|
*/
|
|
32
30
|
async function resolveTargetIssues(options, rootOpts) {
|
|
33
|
-
const
|
|
34
|
-
const linearService = await createLinearService(rootOpts);
|
|
35
|
-
const issuesService = new GraphQLIssuesService(graphQLService, linearService);
|
|
31
|
+
const { issuesService } = await createIssuesService(rootOpts);
|
|
36
32
|
if (options.issues) {
|
|
37
33
|
const ids = splitList(options.issues);
|
|
38
34
|
return Promise.all(ids.map((id) => issuesService.getIssueById(id)));
|
|
39
35
|
}
|
|
40
36
|
if (options.filter) {
|
|
41
37
|
const filters = parseFilterString(options.filter);
|
|
38
|
+
// Typed as SearchIssueArgs (not Record<string, unknown>) so a future
|
|
39
|
+
// rename of any field — like the DEV-4068 T4 `projectId` → `project`
|
|
40
|
+
// migration — surfaces here at compile time instead of silently
|
|
41
|
+
// dropping the filter.
|
|
42
42
|
const searchArgs = {
|
|
43
43
|
teamId: filters.team ? resolveTeam(filters.team) : undefined,
|
|
44
44
|
assigneeId: filters.assignee
|
|
45
45
|
? await resolveAssignee(filters.assignee, rootOpts)
|
|
46
46
|
: undefined,
|
|
47
|
-
|
|
47
|
+
delegateId: filters.delegate
|
|
48
|
+
? resolveMember(filters.delegate)
|
|
49
|
+
: undefined,
|
|
50
|
+
project: filters.project
|
|
51
|
+
? { kind: "id", id: filters.project }
|
|
52
|
+
: undefined,
|
|
48
53
|
labelNames: filters.label ? splitList(filters.label) : undefined,
|
|
49
54
|
status: filters.status ? splitList(filters.status) : undefined,
|
|
50
55
|
limit: 50,
|
|
@@ -117,9 +122,7 @@ async function handleBatchAssign(options, command) {
|
|
|
117
122
|
});
|
|
118
123
|
return;
|
|
119
124
|
}
|
|
120
|
-
const
|
|
121
|
-
const linearService = await createLinearService(rootOpts);
|
|
122
|
-
const issuesService = new GraphQLIssuesService(graphQLService, linearService);
|
|
125
|
+
const { issuesService } = await createIssuesService(rootOpts);
|
|
123
126
|
const { results } = await executeBatch(issues, (issue) => issuesService.updateIssue({ id: issue.identifier, assigneeId }, "adding"));
|
|
124
127
|
outputSuccess({
|
|
125
128
|
action: "assign",
|
|
@@ -164,9 +167,7 @@ async function handleBatchLabel(options, command) {
|
|
|
164
167
|
});
|
|
165
168
|
return;
|
|
166
169
|
}
|
|
167
|
-
const
|
|
168
|
-
const linearService = await createLinearService(rootOpts);
|
|
169
|
-
const issuesService = new GraphQLIssuesService(graphQLService, linearService);
|
|
170
|
+
const { issuesService } = await createIssuesService(rootOpts);
|
|
170
171
|
const removeLower = removeLabels.map((l) => l.toLowerCase());
|
|
171
172
|
const { results } = await executeBatch(issues, (issue) => {
|
|
172
173
|
// Filter out labels to remove (by name, case-insensitive)
|
|
@@ -218,9 +219,7 @@ async function handleBatchMove(options, command) {
|
|
|
218
219
|
});
|
|
219
220
|
return;
|
|
220
221
|
}
|
|
221
|
-
const
|
|
222
|
-
const linearService = await createLinearService(rootOpts);
|
|
223
|
-
const issuesService = new GraphQLIssuesService(graphQLService, linearService);
|
|
222
|
+
const { issuesService } = await createIssuesService(rootOpts);
|
|
224
223
|
const { results } = await executeBatch(issues, (issue) => issuesService.updateIssue({ id: issue.identifier, projectId: options.project }, "adding"));
|
|
225
224
|
outputSuccess({
|
|
226
225
|
action: "move",
|
|
@@ -254,9 +253,7 @@ async function handleBatchStatus(options, command) {
|
|
|
254
253
|
});
|
|
255
254
|
return;
|
|
256
255
|
}
|
|
257
|
-
const
|
|
258
|
-
const linearService = await createLinearService(rootOpts);
|
|
259
|
-
const issuesService = new GraphQLIssuesService(graphQLService, linearService);
|
|
256
|
+
const { issuesService } = await createIssuesService(rootOpts);
|
|
260
257
|
const { results } = await executeBatch(issues, (issue) => issuesService.updateIssue({ id: issue.identifier, statusId: options.status }, "adding"));
|
|
261
258
|
outputSuccess({
|
|
262
259
|
action: "status",
|
|
@@ -11,39 +11,38 @@ import { resolveMentions } from "../utils/mention-resolver.js";
|
|
|
11
11
|
import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
|
|
12
12
|
import { getRootOpts } from "../utils/root-opts.js";
|
|
13
13
|
import { validateReferences } from "../utils/validate-references.js";
|
|
14
|
+
import { parsePositiveInt } from "../utils/validators.js";
|
|
14
15
|
import { getWorkspaceUrlKey } from "../utils/workspace-url.js";
|
|
15
16
|
// Match Linear's bodyData validation error in multiple phrasings so a wording
|
|
16
17
|
// tweak on their side doesn't silently regress the fallback path.
|
|
17
18
|
const BODY_DATA_ERROR_RE = /prosemirror|bodydata|invalid.*body/i;
|
|
18
19
|
const ISSUE_IDENTIFIER_REGEX = /^[A-Z][A-Z0-9]*-\d+$/;
|
|
19
20
|
function readBody(options) {
|
|
20
|
-
if (options.
|
|
21
|
-
return readFileSync(options.
|
|
21
|
+
if (options.bodyFile) {
|
|
22
|
+
return readFileSync(options.bodyFile, "utf-8");
|
|
22
23
|
}
|
|
23
24
|
if (options.body) {
|
|
24
25
|
return options.body;
|
|
25
26
|
}
|
|
26
|
-
throw new Error("Either --body or --file is required");
|
|
27
|
+
throw new Error("Either --body or --body-file is required");
|
|
27
28
|
}
|
|
28
29
|
async function fetchSelfUserId(graphQLService) {
|
|
29
30
|
try {
|
|
30
31
|
const result = await graphQLService.rawRequest("{ viewer { id } }");
|
|
31
|
-
|
|
32
|
-
return viewer?.id;
|
|
32
|
+
return result.viewer?.id;
|
|
33
33
|
}
|
|
34
34
|
catch {
|
|
35
35
|
return;
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
function transformComment(comment) {
|
|
39
|
-
const user = comment.user;
|
|
40
39
|
return {
|
|
41
40
|
id: comment.id,
|
|
42
41
|
body: comment.body,
|
|
43
42
|
user: {
|
|
44
|
-
id: user.id,
|
|
45
|
-
name: resolveUserDisplayName(user.id, user.name),
|
|
46
|
-
url: user.url
|
|
43
|
+
id: comment.user.id,
|
|
44
|
+
name: resolveUserDisplayName(comment.user.id, comment.user.name),
|
|
45
|
+
url: comment.user.url ?? undefined,
|
|
47
46
|
},
|
|
48
47
|
createdAt: comment.createdAt,
|
|
49
48
|
updatedAt: comment.updatedAt,
|
|
@@ -133,9 +132,7 @@ async function handleCreateComment(issueId, options, command) {
|
|
|
133
132
|
}
|
|
134
133
|
let result;
|
|
135
134
|
try {
|
|
136
|
-
result = await graphQLService.rawRequest(CREATE_COMMENT_MUTATION, {
|
|
137
|
-
input,
|
|
138
|
-
});
|
|
135
|
+
result = await graphQLService.rawRequest(CREATE_COMMENT_MUTATION, { input });
|
|
139
136
|
}
|
|
140
137
|
catch (err) {
|
|
141
138
|
// If the bodyData ProseMirror document is rejected (invalid shape,
|
|
@@ -147,16 +144,14 @@ async function handleCreateComment(issueId, options, command) {
|
|
|
147
144
|
issueId: resolvedIssueId,
|
|
148
145
|
body,
|
|
149
146
|
};
|
|
150
|
-
result = await graphQLService.rawRequest(CREATE_COMMENT_MUTATION, {
|
|
151
|
-
input: fallbackInput,
|
|
152
|
-
});
|
|
147
|
+
result = await graphQLService.rawRequest(CREATE_COMMENT_MUTATION, { input: fallbackInput });
|
|
153
148
|
}
|
|
154
149
|
else {
|
|
155
150
|
throw err;
|
|
156
151
|
}
|
|
157
152
|
}
|
|
158
153
|
const mutation = result.commentCreate;
|
|
159
|
-
if (!mutation.success) {
|
|
154
|
+
if (!mutation.success || !mutation.comment) {
|
|
160
155
|
throw new Error("Failed to create comment");
|
|
161
156
|
}
|
|
162
157
|
// We don't have the parent issue's identifier from the create mutation response,
|
|
@@ -201,22 +196,18 @@ async function handleUpdateComment(commentId, options, command) {
|
|
|
201
196
|
else {
|
|
202
197
|
input.body = body;
|
|
203
198
|
}
|
|
204
|
-
const result = await graphQLService.rawRequest(UPDATE_COMMENT_MUTATION, {
|
|
205
|
-
id: commentId,
|
|
206
|
-
input,
|
|
207
|
-
});
|
|
199
|
+
const result = await graphQLService.rawRequest(UPDATE_COMMENT_MUTATION, { id: commentId, input });
|
|
208
200
|
const mutation = result.commentUpdate;
|
|
209
|
-
if (!mutation.success) {
|
|
201
|
+
if (!mutation.success || !mutation.comment) {
|
|
210
202
|
throw new Error("Failed to update comment");
|
|
211
203
|
}
|
|
212
204
|
const comment = mutation.comment;
|
|
213
|
-
const issue = comment.issue;
|
|
214
205
|
let autoLinked;
|
|
215
|
-
if (issue) {
|
|
206
|
+
if (comment.issue) {
|
|
216
207
|
// rawBody (pre-wrap) — see note in handleCreateComment about keyword inference
|
|
217
208
|
autoLinked = await autoLinkCommentReferences({
|
|
218
|
-
parentIssueUuid: issue.id,
|
|
219
|
-
parentIssueIdentifier: issue.identifier
|
|
209
|
+
parentIssueUuid: comment.issue.id,
|
|
210
|
+
parentIssueIdentifier: comment.issue.identifier,
|
|
220
211
|
body: rawBody,
|
|
221
212
|
preResolved,
|
|
222
213
|
options,
|
|
@@ -234,19 +225,17 @@ async function handleListComments(issueId, options, command) {
|
|
|
234
225
|
const resolvedId = await linearService.resolveIssueId(issueId);
|
|
235
226
|
const result = await graphQLService.rawRequest(LIST_COMMENTS_QUERY, {
|
|
236
227
|
issueId: resolvedId,
|
|
237
|
-
first:
|
|
228
|
+
first: parsePositiveInt(options.limit, "--limit"),
|
|
238
229
|
});
|
|
239
|
-
|
|
240
|
-
if (!issue) {
|
|
230
|
+
if (!result.issue) {
|
|
241
231
|
throw new Error(`Issue "${issueId}" not found`);
|
|
242
232
|
}
|
|
243
|
-
const
|
|
244
|
-
const nodes = commentsData.nodes.map(transformComment);
|
|
233
|
+
const nodes = result.issue.comments.nodes.map(transformComment);
|
|
245
234
|
outputSuccess({
|
|
246
235
|
data: nodes,
|
|
247
236
|
meta: {
|
|
248
237
|
count: nodes.length,
|
|
249
|
-
issue: issue.identifier,
|
|
238
|
+
issue: result.issue.identifier,
|
|
250
239
|
},
|
|
251
240
|
});
|
|
252
241
|
}
|
|
@@ -261,7 +250,7 @@ export function setupCommentsCommands(program) {
|
|
|
261
250
|
.description("Create new comment on issue.")
|
|
262
251
|
.addHelpText("after", "\nBoth UUID and identifiers like ABC-123 are supported.\nBare references to known team members (e.g. 'Dima') are auto-converted to @mentions — pass --no-auto-mention to disable.\nIssue identifiers (e.g. DEV-123) are wrapped as markdown links and added as 'related' relations on the parent issue — pass --no-auto-link to disable.")
|
|
263
252
|
.option("--body <body>", "comment body (inline)")
|
|
264
|
-
.option("--file <path>", "read comment body from file")
|
|
253
|
+
.option("--body-file <path>", "read comment body from file")
|
|
265
254
|
.option("--no-auto-mention", "do not auto-convert bare team-member names to @mentions")
|
|
266
255
|
.option("--no-auto-link", "skip wrapping issue refs as markdown links and creating sidebar relations")
|
|
267
256
|
.option("--footer <text>", "text appended to the comment body (overrides config.messageFooter)")
|
|
@@ -271,7 +260,7 @@ export function setupCommentsCommands(program) {
|
|
|
271
260
|
.command("update <commentId>")
|
|
272
261
|
.description("Update an existing comment.")
|
|
273
262
|
.option("--body <body>", "new comment body (inline)")
|
|
274
|
-
.option("--file <path>", "read new comment body from file")
|
|
263
|
+
.option("--body-file <path>", "read new comment body from file")
|
|
275
264
|
.option("--no-auto-mention", "do not auto-convert bare team-member names to @mentions")
|
|
276
265
|
.option("--no-auto-link", "skip wrapping issue refs as markdown links and creating sidebar relations")
|
|
277
266
|
.action(handleAsyncCommand(handleUpdateComment));
|
package/dist/commands/config.js
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { getActiveTeamConfigInfo, getActiveTeamConfigPath, loadConfig, loadLocalConfig, } from "../config/config.js";
|
|
5
|
+
import { resolveActiveProfile } from "../config/paths.js";
|
|
6
|
+
import { handleAsyncCommand, outputSuccess, outputWarning, } from "../utils/output.js";
|
|
7
|
+
import { updateConfig } from "./init/shared.js";
|
|
8
|
+
/**
|
|
9
|
+
* Expand a leading `~` to the user's home directory. Shells expand `~` before
|
|
10
|
+
* the command sees it, but a literal `~/...` pasted from documentation reaches
|
|
11
|
+
* Node verbatim. We handle it so `el-linear config team set-path ~/git/...`
|
|
12
|
+
* works regardless of whether the shell did the expansion first.
|
|
13
|
+
*/
|
|
14
|
+
function expandHome(p) {
|
|
15
|
+
if (p === "~")
|
|
16
|
+
return os.homedir();
|
|
17
|
+
if (p.startsWith("~/"))
|
|
18
|
+
return path.join(os.homedir(), p.slice(2));
|
|
19
|
+
return p;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Render the team-config source token returned by `getActiveTeamConfigInfo()`
|
|
23
|
+
* as a human-readable string for `config team show`. Kept next to the command
|
|
24
|
+
* (not in `config/config.ts`) so the wire format is owned by the renderer,
|
|
25
|
+
* not the resolver. DEV-4258.
|
|
26
|
+
*/
|
|
27
|
+
function sourceLabel(source) {
|
|
28
|
+
switch (source) {
|
|
29
|
+
case "env":
|
|
30
|
+
return "EL_LINEAR_TEAM_CONFIG env var";
|
|
31
|
+
case "personal":
|
|
32
|
+
return "teamConfigPath in personal config";
|
|
33
|
+
case "marker":
|
|
34
|
+
return "auto-discovered via ~/.config/el-tools-root";
|
|
35
|
+
case null:
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const LOCAL_SETTABLE_KEYS = [
|
|
40
|
+
"assigneeEmail",
|
|
41
|
+
"defaultAssignee",
|
|
42
|
+
"defaultPriority",
|
|
43
|
+
"cacheTTLSeconds",
|
|
44
|
+
];
|
|
3
45
|
export function setupConfigCommands(program) {
|
|
4
46
|
const config = program
|
|
5
47
|
.command("config")
|
|
@@ -7,9 +49,140 @@ export function setupConfigCommands(program) {
|
|
|
7
49
|
config.action(() => config.help());
|
|
8
50
|
config
|
|
9
51
|
.command("show")
|
|
10
|
-
.description("Show resolved configuration")
|
|
11
|
-
.addHelpText("after", "\nDumps the merged config from
|
|
52
|
+
.description("Show resolved configuration (team file → personal config → local overrides)")
|
|
53
|
+
.addHelpText("after", "\nDumps the merged config from the active team file + config.json + local.json.\n\nExamples:\n el-linear config show")
|
|
54
|
+
.action(() => {
|
|
55
|
+
const data = loadConfig();
|
|
56
|
+
const teamConfig = getActiveTeamConfigPath();
|
|
57
|
+
const local = loadLocalConfig();
|
|
58
|
+
const out = { data };
|
|
59
|
+
if (teamConfig)
|
|
60
|
+
out.teamConfig = teamConfig;
|
|
61
|
+
if (Object.keys(local).length > 0)
|
|
62
|
+
out.local = local;
|
|
63
|
+
outputSuccess(out);
|
|
64
|
+
});
|
|
65
|
+
const local = config
|
|
66
|
+
.command("local")
|
|
67
|
+
.description("Manage user-local config overrides (local.json)");
|
|
68
|
+
local.action(() => local.help());
|
|
69
|
+
local
|
|
70
|
+
.command("show")
|
|
71
|
+
.description("Show the raw contents of local.json")
|
|
72
|
+
.addHelpText("after", `\nShows ~/.config/el-linear/local.json (or the per-profile equivalent).\n\nExamples:\n el-linear config local show`)
|
|
73
|
+
.action(() => {
|
|
74
|
+
outputSuccess({ data: loadLocalConfig() });
|
|
75
|
+
});
|
|
76
|
+
const team = config
|
|
77
|
+
.command("team")
|
|
78
|
+
.description("Manage the shared team config layer (teamConfigPath in personal config)");
|
|
79
|
+
team.action(() => team.help());
|
|
80
|
+
team
|
|
81
|
+
.command("show")
|
|
82
|
+
.description("Show the active team config layer: path, source (env var, personal config, or auto-discovered marker), file status, top-level keys it contributes")
|
|
83
|
+
.addHelpText("after", "\nResolves the team config in the same order loadConfig() does:\n EL_LINEAR_TEAM_CONFIG (env) > teamConfigPath (personal config) > ~/.config/el-tools-root marker\n\nExamples:\n el-linear config team show")
|
|
12
84
|
.action(() => {
|
|
13
|
-
|
|
85
|
+
const info = getActiveTeamConfigInfo();
|
|
86
|
+
const teamPath = info.path;
|
|
87
|
+
const source = sourceLabel(info.source);
|
|
88
|
+
const out = {
|
|
89
|
+
teamConfigPath: teamPath ?? null,
|
|
90
|
+
source,
|
|
91
|
+
};
|
|
92
|
+
if (teamPath) {
|
|
93
|
+
try {
|
|
94
|
+
const raw = fs.readFileSync(teamPath, "utf8");
|
|
95
|
+
const parsed = JSON.parse(raw);
|
|
96
|
+
out.exists = true;
|
|
97
|
+
out.valid = true;
|
|
98
|
+
out.providedKeys = Object.keys(parsed).sort();
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
out.exists = fs.existsSync(teamPath);
|
|
102
|
+
out.valid = false;
|
|
103
|
+
out.error = err.message;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
outputSuccess({ data: out });
|
|
14
107
|
});
|
|
108
|
+
team
|
|
109
|
+
.command("set-path <path>")
|
|
110
|
+
.description("Write teamConfigPath into personal config.json. Path is resolved to an absolute path. The target file must exist and be valid JSON — a pointer to a missing or broken file is footgun-shaped and refused.")
|
|
111
|
+
.addHelpText("after", "\nUse this when adopting a shared team config (e.g. one checked into a tools repo):\n el-linear config team set-path ~/git/enrichlayer/tools/config/el-linear.shared.json\n\nRelative paths resolve against the current working directory; absolute paths are stored as-is. EL_LINEAR_TEAM_CONFIG in the environment still overrides the personal field when both are set — a warning surfaces in that case.")
|
|
112
|
+
.action(handleAsyncCommand(async (rawPath) => {
|
|
113
|
+
const resolved = path.resolve(expandHome(rawPath));
|
|
114
|
+
if (!fs.existsSync(resolved)) {
|
|
115
|
+
throw new Error(`Team config file not found at ${resolved}. Create the file (or fix the path) before pointing personal config at it.`);
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
JSON.parse(fs.readFileSync(resolved, "utf8"));
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
throw new Error(`Team config at ${resolved} is not valid JSON: ${err.message}`);
|
|
122
|
+
}
|
|
123
|
+
await updateConfig((current) => ({
|
|
124
|
+
...current,
|
|
125
|
+
teamConfigPath: resolved,
|
|
126
|
+
}));
|
|
127
|
+
const envOverride = process.env.EL_LINEAR_TEAM_CONFIG?.trim() || undefined;
|
|
128
|
+
if (envOverride !== undefined && envOverride !== resolved) {
|
|
129
|
+
outputWarning(`EL_LINEAR_TEAM_CONFIG=${envOverride} is set in this shell and overrides the personal config field. Unset it (or align it) for the persistent setting to take effect.`);
|
|
130
|
+
}
|
|
131
|
+
outputSuccess({
|
|
132
|
+
data: {
|
|
133
|
+
teamConfigPath: resolved,
|
|
134
|
+
written: resolveActiveProfile().configPath,
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
}));
|
|
138
|
+
team
|
|
139
|
+
.command("clear")
|
|
140
|
+
.description("Remove teamConfigPath from personal config.json. EL_LINEAR_TEAM_CONFIG (env) is unaffected and continues to override if set.")
|
|
141
|
+
.action(handleAsyncCommand(async () => {
|
|
142
|
+
let removed;
|
|
143
|
+
await updateConfig((current) => {
|
|
144
|
+
removed = current.teamConfigPath;
|
|
145
|
+
const { teamConfigPath: _drop, ...rest } = current;
|
|
146
|
+
return rest;
|
|
147
|
+
});
|
|
148
|
+
const envOverride = process.env.EL_LINEAR_TEAM_CONFIG?.trim() || undefined;
|
|
149
|
+
if (envOverride !== undefined) {
|
|
150
|
+
outputWarning(`EL_LINEAR_TEAM_CONFIG=${envOverride} is still set in this shell; it overrides the personal field and remains active until unset.`);
|
|
151
|
+
}
|
|
152
|
+
outputSuccess({
|
|
153
|
+
data: {
|
|
154
|
+
cleared: true,
|
|
155
|
+
previousTeamConfigPath: removed ?? null,
|
|
156
|
+
written: resolveActiveProfile().configPath,
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
}));
|
|
160
|
+
local
|
|
161
|
+
.command("set <key> <value>")
|
|
162
|
+
.description(`Set a key in local.json. Keys: ${LOCAL_SETTABLE_KEYS.join(", ")}`)
|
|
163
|
+
.addHelpText("after", `\nAllowed keys:\n assigneeEmail — your Linear account email (used as default --assignee)\n defaultAssignee — alias / display name / email / UUID\n defaultPriority — none|urgent|high|medium|normal|low\n cacheTTLSeconds — integer seconds (0 = disable cache)\n\nExamples:\n el-linear config local set assigneeEmail you@example.com\n el-linear config local set defaultPriority medium\n el-linear config local set cacheTTLSeconds 0`)
|
|
164
|
+
.action(handleAsyncCommand(async (key, value) => {
|
|
165
|
+
if (!LOCAL_SETTABLE_KEYS.includes(key)) {
|
|
166
|
+
throw new Error(`Unknown local config key "${key}". Allowed: ${LOCAL_SETTABLE_KEYS.join(", ")}`);
|
|
167
|
+
}
|
|
168
|
+
const active = resolveActiveProfile();
|
|
169
|
+
const localPath = active.localConfigPath;
|
|
170
|
+
let existing = {};
|
|
171
|
+
if (fs.existsSync(localPath)) {
|
|
172
|
+
try {
|
|
173
|
+
existing = JSON.parse(fs.readFileSync(localPath, "utf8"));
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
// overwrite a corrupt file
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const typedValue = key === "cacheTTLSeconds" ? Number(value) : value;
|
|
180
|
+
if (key === "cacheTTLSeconds" && Number.isNaN(typedValue)) {
|
|
181
|
+
throw new Error(`cacheTTLSeconds must be an integer, got "${value}"`);
|
|
182
|
+
}
|
|
183
|
+
const updated = { ...existing, [key]: typedValue };
|
|
184
|
+
fs.mkdirSync(path.dirname(localPath), { recursive: true });
|
|
185
|
+
fs.writeFileSync(localPath, `${JSON.stringify(updated, null, 2)}\n`, "utf8");
|
|
186
|
+
outputSuccess({ data: updated });
|
|
187
|
+
}));
|
|
15
188
|
}
|
package/dist/commands/cycles.js
CHANGED
|
@@ -3,6 +3,7 @@ import { invalidParameterError, notFoundError, requiresParameterError, } from ".
|
|
|
3
3
|
import { createLinearService } from "../utils/linear-service.js";
|
|
4
4
|
import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
|
|
5
5
|
import { getRootOpts } from "../utils/root-opts.js";
|
|
6
|
+
import { parsePositiveInt } from "../utils/validators.js";
|
|
6
7
|
export function setupCyclesCommands(program) {
|
|
7
8
|
const cycles = program
|
|
8
9
|
.command("cycles")
|
|
@@ -23,7 +24,7 @@ export function setupCyclesCommands(program) {
|
|
|
23
24
|
const rootOpts = getRootOpts(command);
|
|
24
25
|
const teamFilter = options.team ? resolveTeam(options.team) : undefined;
|
|
25
26
|
const linearService = await createLinearService(rootOpts);
|
|
26
|
-
const allCycles = await linearService.getCycles(teamFilter, options.active || undefined,
|
|
27
|
+
const allCycles = await linearService.getCycles(teamFilter, options.active || undefined, parsePositiveInt(options.limit, "--limit"));
|
|
27
28
|
if (options.aroundActive) {
|
|
28
29
|
const n = Number.parseInt(options.aroundActive, 10);
|
|
29
30
|
if (Number.isNaN(n) || n < 0) {
|
|
@@ -3,6 +3,7 @@ import { createGraphQLDocumentsService } from "../utils/graphql-documents-servic
|
|
|
3
3
|
import { createLinearService } from "../utils/linear-service.js";
|
|
4
4
|
import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
|
|
5
5
|
import { getRootOpts } from "../utils/root-opts.js";
|
|
6
|
+
import { parsePositiveInt } from "../utils/validators.js";
|
|
6
7
|
function extractDocumentIdFromUrl(url) {
|
|
7
8
|
try {
|
|
8
9
|
const parsed = new URL(url);
|
|
@@ -70,7 +71,7 @@ async function handleListDocuments(options, command) {
|
|
|
70
71
|
const rootOpts = getRootOpts(command);
|
|
71
72
|
const documentsService = await createGraphQLDocumentsService(rootOpts);
|
|
72
73
|
const linearService = await createLinearService(rootOpts);
|
|
73
|
-
const limit =
|
|
74
|
+
const limit = parsePositiveInt(options.limit || "50", "--limit");
|
|
74
75
|
if (Number.isNaN(limit) || limit < 1) {
|
|
75
76
|
throw new Error(`Invalid limit "${options.limit}": must be a positive number`);
|
|
76
77
|
}
|
package/dist/commands/graphql.js
CHANGED
|
@@ -46,16 +46,14 @@ export function setupGraphQLCommands(program) {
|
|
|
46
46
|
const graphQLService = await createGraphQLService(rootOpts);
|
|
47
47
|
if (typeName) {
|
|
48
48
|
const result = await graphQLService.rawRequest(INTROSPECT_TYPE_QUERY, { typeName });
|
|
49
|
-
|
|
50
|
-
if (!typeInfo) {
|
|
49
|
+
if (!result.__type) {
|
|
51
50
|
throw new Error(`Type "${typeName}" not found in schema`);
|
|
52
51
|
}
|
|
53
|
-
outputSuccess(
|
|
52
|
+
outputSuccess(result.__type);
|
|
54
53
|
}
|
|
55
54
|
else {
|
|
56
55
|
const result = await graphQLService.rawRequest(INTROSPECT_ROOT_QUERY);
|
|
57
|
-
|
|
58
|
-
let fields = queryType?.fields ?? [];
|
|
56
|
+
let fields = result.__type?.fields ?? [];
|
|
59
57
|
if (options.filter) {
|
|
60
58
|
const pattern = options.filter.toLowerCase();
|
|
61
59
|
fields = fields.filter((f) => f.name.toLowerCase().includes(pattern));
|
|
@@ -63,7 +61,7 @@ export function setupGraphQLCommands(program) {
|
|
|
63
61
|
const summary = fields.map((f) => ({
|
|
64
62
|
name: f.name,
|
|
65
63
|
description: f.description ?? null,
|
|
66
|
-
args:
|
|
64
|
+
args: f.args.map((a) => a.name),
|
|
67
65
|
}));
|
|
68
66
|
outputSuccess({ fields: summary, count: summary.length });
|
|
69
67
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* with no input is a no-op.
|
|
7
7
|
*/
|
|
8
8
|
import { type WizardConfig } from "./shared.js";
|
|
9
|
-
|
|
9
|
+
interface DefaultsStepResult {
|
|
10
10
|
defaultLabels: string[] | undefined;
|
|
11
11
|
/**
|
|
12
12
|
* Default assignee identifier (alias / display name / email / UUID — same
|
|
@@ -40,3 +40,4 @@ export interface DefaultsStepResult {
|
|
|
40
40
|
cacheTTLSeconds: number | undefined;
|
|
41
41
|
}
|
|
42
42
|
export declare function runDefaultsStep(existing: WizardConfig): Promise<DefaultsStepResult>;
|
|
43
|
+
export {};
|