@enrichlayer/el-linear 1.9.0 → 1.10.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 +13 -0
- package/dist/commands/attachments.js +2 -1
- package/dist/commands/comments.js +17 -28
- package/dist/commands/cycles.js +2 -1
- package/dist/commands/documents.js +2 -1
- package/dist/commands/graphql.js +4 -6
- package/dist/commands/issue-id.js +1 -3
- 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 +55 -0
- package/dist/commands/issues/relations.js +132 -0
- package/dist/commands/issues.js +65 -284
- package/dist/commands/labels.js +13 -23
- package/dist/commands/project-milestones.js +13 -20
- package/dist/commands/projects.js +67 -38
- package/dist/commands/refs.js +8 -0
- package/dist/commands/releases.js +26 -30
- package/dist/commands/search.js +21 -30
- package/dist/commands/teams.js +2 -1
- package/dist/commands/templates.js +9 -14
- package/dist/commands/users.js +2 -1
- package/dist/config/resolver.js +2 -3
- package/dist/queries/attachments-types.d.ts +30 -0
- package/dist/queries/attachments-types.js +5 -0
- package/dist/queries/comments-types.d.ts +49 -0
- package/dist/queries/comments-types.js +5 -0
- package/dist/queries/documents-types.d.ts +61 -0
- package/dist/queries/documents-types.js +9 -0
- package/dist/queries/introspect-types.d.ts +57 -0
- package/dist/queries/introspect-types.js +10 -0
- package/dist/queries/issues-types.d.ts +416 -0
- package/dist/queries/issues-types.js +23 -0
- package/dist/queries/issues.d.ts +2 -0
- package/dist/queries/issues.js +22 -0
- package/dist/queries/labels-types.d.ts +64 -0
- package/dist/queries/labels-types.js +5 -0
- package/dist/queries/project-milestones-types.d.ts +91 -0
- package/dist/queries/project-milestones-types.js +10 -0
- package/dist/queries/projects-types.d.ts +75 -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 +84 -0
- package/dist/queries/releases-types.js +5 -0
- package/dist/queries/search-types.d.ts +86 -0
- package/dist/queries/search-types.js +6 -0
- package/dist/queries/templates-types.d.ts +61 -0
- package/dist/queries/templates-types.js +9 -0
- package/dist/types/linear.d.ts +8 -2
- package/dist/utils/auto-link-references.js +29 -24
- 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 +12 -2
- package/dist/utils/graphql-issues-service.js +151 -172
- package/dist/utils/output.d.ts +2 -0
- package/dist/utils/output.js +16 -6
- package/dist/utils/workspace-url.d.ts +5 -1
- package/dist/utils/workspace-url.js +35 -5
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { loadConfig } from "../config/config.js";
|
|
2
2
|
import { resolveTeam } from "../config/resolver.js";
|
|
3
|
-
import { CREATE_PROJECT_MUTATION, GET_PROJECT_QUERY, GET_PROJECT_TEAM_ISSUES_QUERY, PROJECT_BY_ID_QUERY, SEARCH_PROJECTS_BY_NAME_QUERY, UPDATE_PROJECT_MUTATION, } from "../queries/projects.js";
|
|
3
|
+
import { ARCHIVE_PROJECT_MUTATION, CREATE_PROJECT_MUTATION, DELETE_PROJECT_MUTATION, GET_PROJECT_QUERY, GET_PROJECT_TEAM_ISSUES_QUERY, PROJECT_BY_ID_QUERY, SEARCH_PROJECTS_BY_NAME_QUERY, UPDATE_PROJECT_MUTATION, } from "../queries/projects.js";
|
|
4
4
|
import { cached, resolveCacheTTL } from "../utils/disk-cache.js";
|
|
5
5
|
import { createGraphQLService } from "../utils/graphql-service.js";
|
|
6
6
|
import { createLinearService } from "../utils/linear-service.js";
|
|
@@ -9,7 +9,7 @@ import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
|
|
|
9
9
|
import { getRootOpts } from "../utils/root-opts.js";
|
|
10
10
|
import { renderCsv, renderFixedWidthTable, renderMarkdownTable, } from "../utils/table-formatter.js";
|
|
11
11
|
import { isUuid } from "../utils/uuid.js";
|
|
12
|
-
import { splitList } from "../utils/validators.js";
|
|
12
|
+
import { parsePositiveInt, splitList } from "../utils/validators.js";
|
|
13
13
|
const VALID_PROJECT_STATES = new Set([
|
|
14
14
|
"backlog",
|
|
15
15
|
"planned",
|
|
@@ -133,37 +133,29 @@ function formatProjectsOutput(projects, format, fieldNames) {
|
|
|
133
133
|
}
|
|
134
134
|
async function resolveProjectWithTeams(graphQLService, projectNameOrId) {
|
|
135
135
|
if (isUuid(projectNameOrId)) {
|
|
136
|
-
const result = await graphQLService.rawRequest(PROJECT_BY_ID_QUERY, {
|
|
137
|
-
|
|
138
|
-
});
|
|
139
|
-
const project = result.project;
|
|
140
|
-
if (!project) {
|
|
136
|
+
const result = await graphQLService.rawRequest(PROJECT_BY_ID_QUERY, { id: projectNameOrId });
|
|
137
|
+
if (!result.project) {
|
|
141
138
|
throw new Error(`Project "${projectNameOrId}" not found`);
|
|
142
139
|
}
|
|
143
|
-
const teams = project.teams;
|
|
144
140
|
return {
|
|
145
|
-
projectId: project.id,
|
|
146
|
-
projectName: project.name,
|
|
147
|
-
currentTeams:
|
|
141
|
+
projectId: result.project.id,
|
|
142
|
+
projectName: result.project.name,
|
|
143
|
+
currentTeams: result.project.teams.nodes.map((t) => ({
|
|
148
144
|
id: t.id,
|
|
149
145
|
key: t.key,
|
|
150
146
|
name: t.name,
|
|
151
147
|
})),
|
|
152
148
|
};
|
|
153
149
|
}
|
|
154
|
-
const result = await graphQLService.rawRequest(GET_PROJECT_QUERY, {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const projectNodes = result.projects?.nodes;
|
|
158
|
-
if (!projectNodes?.length) {
|
|
150
|
+
const result = await graphQLService.rawRequest(GET_PROJECT_QUERY, { name: projectNameOrId });
|
|
151
|
+
const project = result.projects.nodes[0];
|
|
152
|
+
if (!project) {
|
|
159
153
|
throw new Error(`Project "${projectNameOrId}" not found`);
|
|
160
154
|
}
|
|
161
|
-
const project = projectNodes[0];
|
|
162
|
-
const teams = project.teams;
|
|
163
155
|
return {
|
|
164
156
|
projectId: project.id,
|
|
165
157
|
projectName: project.name,
|
|
166
|
-
currentTeams:
|
|
158
|
+
currentTeams: project.teams.nodes.map((t) => ({
|
|
167
159
|
id: t.id,
|
|
168
160
|
key: t.key,
|
|
169
161
|
name: t.name,
|
|
@@ -171,12 +163,14 @@ async function resolveProjectWithTeams(graphQLService, projectNameOrId) {
|
|
|
171
163
|
};
|
|
172
164
|
}
|
|
173
165
|
function formatTeamsOutput(projectUpdate) {
|
|
166
|
+
if (!projectUpdate.project) {
|
|
167
|
+
throw new Error("Failed to update project");
|
|
168
|
+
}
|
|
174
169
|
const updatedProject = projectUpdate.project;
|
|
175
|
-
const updatedTeams = updatedProject.teams;
|
|
176
170
|
return {
|
|
177
171
|
id: updatedProject.id,
|
|
178
172
|
name: updatedProject.name,
|
|
179
|
-
teams:
|
|
173
|
+
teams: updatedProject.teams.nodes.map((t) => ({
|
|
180
174
|
id: t.id,
|
|
181
175
|
key: t.key,
|
|
182
176
|
name: t.name,
|
|
@@ -255,9 +249,7 @@ async function handleRemoveTeam(projectNameOrId, teamInput, options, command) {
|
|
|
255
249
|
projectId,
|
|
256
250
|
teamId: finalTeamId,
|
|
257
251
|
});
|
|
258
|
-
const
|
|
259
|
-
const issues = project.issues;
|
|
260
|
-
const issueNodes = issues.nodes || [];
|
|
252
|
+
const issueNodes = issueCheck.project?.issues.nodes ?? [];
|
|
261
253
|
if (issueNodes.length > 0) {
|
|
262
254
|
const examples = issueNodes
|
|
263
255
|
.slice(0, 5)
|
|
@@ -285,14 +277,10 @@ async function handleCreateProject(name, options, command) {
|
|
|
285
277
|
const graphQLService = await createGraphQLService(rootOpts);
|
|
286
278
|
// Step 1: Check for duplicate projects (case-insensitive)
|
|
287
279
|
const searchResult = await graphQLService.rawRequest(SEARCH_PROJECTS_BY_NAME_QUERY, { name });
|
|
288
|
-
const existing =
|
|
289
|
-
?.nodes ?? []).filter((p) => p.name.toLowerCase() === name.toLowerCase());
|
|
280
|
+
const existing = searchResult.projects.nodes.filter((p) => p.name.toLowerCase() === name.toLowerCase());
|
|
290
281
|
if (existing.length > 0 && !options.force) {
|
|
291
282
|
const match = existing[0];
|
|
292
|
-
const teams =
|
|
293
|
-
[])
|
|
294
|
-
.map((t) => t.key)
|
|
295
|
-
.join(", ");
|
|
283
|
+
const teams = match.teams.nodes.map((t) => t.key).join(", ");
|
|
296
284
|
throw new Error(`Project "${match.name}" already exists (state: ${match.state}, teams: ${teams}). ` +
|
|
297
285
|
`Use --force to create anyway, or use the existing project.`);
|
|
298
286
|
}
|
|
@@ -310,11 +298,11 @@ async function handleCreateProject(name, options, command) {
|
|
|
310
298
|
...(teamIds.length > 0 ? { teamIds } : {}),
|
|
311
299
|
};
|
|
312
300
|
const createResult = await graphQLService.rawRequest(CREATE_PROJECT_MUTATION, { input });
|
|
313
|
-
|
|
314
|
-
|
|
301
|
+
if (!createResult.projectCreate.success ||
|
|
302
|
+
!createResult.projectCreate.project) {
|
|
315
303
|
throw new Error("Failed to create project");
|
|
316
304
|
}
|
|
317
|
-
const project =
|
|
305
|
+
const project = createResult.projectCreate.project;
|
|
318
306
|
// Step 4: Set content if provided (separate mutation — Linear API quirk)
|
|
319
307
|
if (options.content) {
|
|
320
308
|
await graphQLService.rawRequest(UPDATE_PROJECT_MUTATION, {
|
|
@@ -322,10 +310,7 @@ async function handleCreateProject(name, options, command) {
|
|
|
322
310
|
input: { content: options.content },
|
|
323
311
|
});
|
|
324
312
|
}
|
|
325
|
-
const teamList =
|
|
326
|
-
[])
|
|
327
|
-
.map((t) => t.key)
|
|
328
|
-
.join(", ");
|
|
313
|
+
const teamList = project.teams.nodes.map((t) => t.key).join(", ");
|
|
329
314
|
outputSuccess({
|
|
330
315
|
id: project.id,
|
|
331
316
|
name: project.name,
|
|
@@ -340,6 +325,42 @@ async function handleCreateProject(name, options, command) {
|
|
|
340
325
|
: {}),
|
|
341
326
|
});
|
|
342
327
|
}
|
|
328
|
+
async function handleArchiveProject(projectNameOrId, _options, command) {
|
|
329
|
+
const rootOpts = getRootOpts(command);
|
|
330
|
+
const graphQLService = await createGraphQLService(rootOpts);
|
|
331
|
+
const linearService = await createLinearService(rootOpts);
|
|
332
|
+
const projectId = await linearService.resolveProjectId(projectNameOrId);
|
|
333
|
+
const result = await graphQLService.rawRequest(ARCHIVE_PROJECT_MUTATION, { id: projectId });
|
|
334
|
+
const payload = result.projectArchive;
|
|
335
|
+
if (!payload.success) {
|
|
336
|
+
throw new Error(`Failed to archive project "${projectNameOrId}"`);
|
|
337
|
+
}
|
|
338
|
+
outputSuccess({
|
|
339
|
+
success: true,
|
|
340
|
+
archived: true,
|
|
341
|
+
id: projectId,
|
|
342
|
+
entity: payload.entity ?? undefined,
|
|
343
|
+
lastSyncId: payload.lastSyncId,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
async function handleDeleteProject(projectNameOrId, _options, command) {
|
|
347
|
+
const rootOpts = getRootOpts(command);
|
|
348
|
+
const graphQLService = await createGraphQLService(rootOpts);
|
|
349
|
+
const linearService = await createLinearService(rootOpts);
|
|
350
|
+
const projectId = await linearService.resolveProjectId(projectNameOrId);
|
|
351
|
+
const result = await graphQLService.rawRequest(DELETE_PROJECT_MUTATION, { id: projectId });
|
|
352
|
+
const payload = result.projectDelete;
|
|
353
|
+
if (!payload.success) {
|
|
354
|
+
throw new Error(`Failed to delete project "${projectNameOrId}"`);
|
|
355
|
+
}
|
|
356
|
+
outputSuccess({
|
|
357
|
+
success: true,
|
|
358
|
+
deleted: true,
|
|
359
|
+
id: projectId,
|
|
360
|
+
entity: payload.entity ?? undefined,
|
|
361
|
+
lastSyncId: payload.lastSyncId,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
343
364
|
export function setupProjectsCommands(program) {
|
|
344
365
|
const projects = program
|
|
345
366
|
.command("projects")
|
|
@@ -354,6 +375,14 @@ export function setupProjectsCommands(program) {
|
|
|
354
375
|
.option("--content <markdown>", "full markdown body (shown in project panel)")
|
|
355
376
|
.option("--force", "create even if a project with the same name exists")
|
|
356
377
|
.action(handleAsyncCommand(handleCreateProject));
|
|
378
|
+
projects
|
|
379
|
+
.command("archive <project>")
|
|
380
|
+
.description("Archive a project (resolves names)")
|
|
381
|
+
.action(handleAsyncCommand(handleArchiveProject));
|
|
382
|
+
projects
|
|
383
|
+
.command("delete <project>")
|
|
384
|
+
.description("Delete (trash) a project (resolves names)")
|
|
385
|
+
.action(handleAsyncCommand(handleDeleteProject));
|
|
357
386
|
projects
|
|
358
387
|
.command("list")
|
|
359
388
|
.description("List projects")
|
|
@@ -366,7 +395,7 @@ export function setupProjectsCommands(program) {
|
|
|
366
395
|
.option("--active", "shorthand for --exclude-state completed,canceled (mutually exclusive with --state / --exclude-state)")
|
|
367
396
|
.action(handleAsyncCommand(async (options, command) => {
|
|
368
397
|
const rootOpts = getRootOpts(command);
|
|
369
|
-
const limit =
|
|
398
|
+
const limit = parsePositiveInt(options.limit, "--limit");
|
|
370
399
|
const nameFilter = options.name;
|
|
371
400
|
const stateFilter = resolveProjectStateFilter(options);
|
|
372
401
|
const ttl = resolveCacheTTL({
|
package/dist/commands/refs.js
CHANGED
|
@@ -68,6 +68,13 @@ async function handleWrap(options, command) {
|
|
|
68
68
|
return new Set(map.keys());
|
|
69
69
|
},
|
|
70
70
|
async resolveUrlKey() {
|
|
71
|
+
// When --workspace-url-key or EL_LINEAR_WORKSPACE_URL_KEY supplies
|
|
72
|
+
// the key, the live lookup never fires — skip the GraphQL service
|
|
73
|
+
// instantiation entirely so `refs wrap --no-validate` works offline.
|
|
74
|
+
const override = options.workspaceUrlKey;
|
|
75
|
+
if (override || process.env.EL_LINEAR_WORKSPACE_URL_KEY) {
|
|
76
|
+
return getWorkspaceUrlKey(undefined, { override });
|
|
77
|
+
}
|
|
71
78
|
const graphQLService = await createGraphQLService(rootOpts);
|
|
72
79
|
return getWorkspaceUrlKey(graphQLService);
|
|
73
80
|
},
|
|
@@ -93,5 +100,6 @@ export function setupRefsCommands(program) {
|
|
|
93
100
|
.option("--file <path>", "read input from a file instead of stdin")
|
|
94
101
|
.option("--target <target>", "output format: markdown (default) or slack", "markdown")
|
|
95
102
|
.option("--no-validate", "skip workspace validation; wrap every regex match. Faster, but may produce broken links for IDs that don't exist.")
|
|
103
|
+
.option("--workspace-url-key <key>", "workspace URL key (the segment after linear.app/). Overrides config + env var. With --no-validate, makes the command fully offline.")
|
|
96
104
|
.action(handleAsyncCommand(handleWrap));
|
|
97
105
|
}
|
|
@@ -2,18 +2,24 @@ import { CREATE_RELEASE_MUTATION, GET_RELEASE_BY_ID_QUERY, GET_RELEASE_PIPELINES
|
|
|
2
2
|
import { createGraphQLService } from "../utils/graphql-service.js";
|
|
3
3
|
import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
|
|
4
4
|
import { getRootOpts } from "../utils/root-opts.js";
|
|
5
|
+
import { parsePositiveInt } from "../utils/validators.js";
|
|
5
6
|
function transformRelease(release) {
|
|
7
|
+
// Lifecycle timestamps + dates only exist on the list/detail node shapes,
|
|
8
|
+
// not on the create-mutation shape; read defensively rather than splitting
|
|
9
|
+
// the function in two.
|
|
10
|
+
const r = release;
|
|
11
|
+
const documents = release.documents?.nodes;
|
|
6
12
|
return {
|
|
7
13
|
id: release.id,
|
|
8
14
|
name: release.name,
|
|
9
|
-
description: release.description
|
|
10
|
-
version: release.version
|
|
11
|
-
url: release.url
|
|
12
|
-
startDate:
|
|
13
|
-
targetDate:
|
|
14
|
-
startedAt:
|
|
15
|
-
completedAt:
|
|
16
|
-
canceledAt:
|
|
15
|
+
description: release.description ?? undefined,
|
|
16
|
+
version: release.version ?? undefined,
|
|
17
|
+
url: release.url ?? undefined,
|
|
18
|
+
startDate: r.startDate ?? undefined,
|
|
19
|
+
targetDate: r.targetDate ?? undefined,
|
|
20
|
+
startedAt: r.startedAt ?? undefined,
|
|
21
|
+
completedAt: r.completedAt ?? undefined,
|
|
22
|
+
canceledAt: r.canceledAt ?? undefined,
|
|
17
23
|
stage: release.stage
|
|
18
24
|
? {
|
|
19
25
|
id: release.stage.id,
|
|
@@ -27,7 +33,7 @@ function transformRelease(release) {
|
|
|
27
33
|
name: release.pipeline.name,
|
|
28
34
|
}
|
|
29
35
|
: undefined,
|
|
30
|
-
documents:
|
|
36
|
+
documents: documents?.map((d) => ({
|
|
31
37
|
id: d.id,
|
|
32
38
|
title: d.title,
|
|
33
39
|
slugId: d.slugId,
|
|
@@ -40,14 +46,11 @@ async function handleCreateRelease(name, options, command) {
|
|
|
40
46
|
const rootOpts = getRootOpts(command);
|
|
41
47
|
const graphQLService = await createGraphQLService(rootOpts);
|
|
42
48
|
const pipelines = await graphQLService.rawRequest(GET_RELEASE_PIPELINES_QUERY, { first: 50 });
|
|
43
|
-
const pipelineNodes = pipelines.releasePipelines
|
|
44
|
-
|
|
45
|
-
const pipeline = pipelineNodes?.find((p) => p.id === options.pipeline ||
|
|
49
|
+
const pipelineNodes = pipelines.releasePipelines.nodes;
|
|
50
|
+
const pipeline = pipelineNodes.find((p) => p.id === options.pipeline ||
|
|
46
51
|
p.name.toLowerCase() === options.pipeline.toLowerCase());
|
|
47
52
|
if (!pipeline) {
|
|
48
|
-
const available = (
|
|
49
|
-
.map((p) => p.name)
|
|
50
|
-
.join(", ");
|
|
53
|
+
const available = pipelineNodes.map((p) => p.name).join(", ");
|
|
51
54
|
throw new Error(`Pipeline "${options.pipeline}" not found. Available: ${available || "none"}`);
|
|
52
55
|
}
|
|
53
56
|
const input = { name, pipelineId: pipeline.id };
|
|
@@ -58,21 +61,17 @@ async function handleCreateRelease(name, options, command) {
|
|
|
58
61
|
input.version = options.version;
|
|
59
62
|
}
|
|
60
63
|
if (options.stage) {
|
|
61
|
-
const
|
|
62
|
-
const stage = stageNodes?.find((s) => s.id === options.stage ||
|
|
64
|
+
const stage = pipeline.stages.nodes.find((s) => s.id === options.stage ||
|
|
63
65
|
s.name.toLowerCase() === options.stage.toLowerCase());
|
|
64
66
|
if (stage) {
|
|
65
67
|
input.stageId = stage.id;
|
|
66
68
|
}
|
|
67
69
|
}
|
|
68
|
-
const result = await graphQLService.rawRequest(CREATE_RELEASE_MUTATION, {
|
|
69
|
-
|
|
70
|
-
});
|
|
71
|
-
const releaseCreate = result.releaseCreate;
|
|
72
|
-
if (!releaseCreate?.success) {
|
|
70
|
+
const result = await graphQLService.rawRequest(CREATE_RELEASE_MUTATION, { input });
|
|
71
|
+
if (!result.releaseCreate.success || !result.releaseCreate.release) {
|
|
73
72
|
throw new Error(`Failed to create release "${name}"`);
|
|
74
73
|
}
|
|
75
|
-
outputSuccess(transformRelease(releaseCreate.release));
|
|
74
|
+
outputSuccess(transformRelease(result.releaseCreate.release));
|
|
76
75
|
}
|
|
77
76
|
export function setupReleasesCommands(program) {
|
|
78
77
|
const releases = program
|
|
@@ -92,11 +91,10 @@ export function setupReleasesCommands(program) {
|
|
|
92
91
|
filter.pipeline = { name: { eqIgnoreCase: options.pipeline } };
|
|
93
92
|
}
|
|
94
93
|
const result = await graphQLService.rawRequest(GET_RELEASES_QUERY, {
|
|
95
|
-
first:
|
|
94
|
+
first: parsePositiveInt(options.limit, "--limit"),
|
|
96
95
|
filter: Object.keys(filter).length > 0 ? filter : undefined,
|
|
97
96
|
});
|
|
98
|
-
const data =
|
|
99
|
-
?.nodes ?? []).map(transformRelease);
|
|
97
|
+
const data = result.releases.nodes.map(transformRelease);
|
|
100
98
|
outputSuccess({ data, meta: { count: data.length } });
|
|
101
99
|
}));
|
|
102
100
|
releases
|
|
@@ -126,12 +124,10 @@ export function setupReleasesCommands(program) {
|
|
|
126
124
|
const rootOpts = getRootOpts(command);
|
|
127
125
|
const graphQLService = await createGraphQLService(rootOpts);
|
|
128
126
|
const result = await graphQLService.rawRequest(GET_RELEASE_PIPELINES_QUERY, { first: 50 });
|
|
129
|
-
const data =
|
|
130
|
-
?.nodes ?? []).map((p) => ({
|
|
127
|
+
const data = result.releasePipelines.nodes.map((p) => ({
|
|
131
128
|
id: p.id,
|
|
132
129
|
name: p.name,
|
|
133
|
-
stages:
|
|
134
|
-
?.nodes ?? []).map((s) => ({
|
|
130
|
+
stages: p.stages.nodes.map((s) => ({
|
|
135
131
|
id: s.id,
|
|
136
132
|
name: s.name,
|
|
137
133
|
type: s.type,
|
package/dist/commands/search.js
CHANGED
|
@@ -3,6 +3,7 @@ import { SEMANTIC_SEARCH_QUERY } from "../queries/search.js";
|
|
|
3
3
|
import { createGraphQLService } from "../utils/graphql-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
|
const TEMPLATES_QUERY = `
|
|
7
8
|
query {
|
|
8
9
|
templates {
|
|
@@ -24,59 +25,50 @@ const VALID_TYPES = new Set([
|
|
|
24
25
|
]);
|
|
25
26
|
const WHITESPACE_RE = /\s+/;
|
|
26
27
|
function transformSearchResult(r) {
|
|
27
|
-
|
|
28
|
-
switch (rType) {
|
|
28
|
+
switch (r.type) {
|
|
29
29
|
case "issue": {
|
|
30
30
|
const issue = r.issue;
|
|
31
|
-
const state = issue?.state;
|
|
32
|
-
const team = issue?.team;
|
|
33
|
-
const assignee = issue?.assignee;
|
|
34
|
-
const project = issue?.project;
|
|
35
31
|
return {
|
|
36
32
|
type: "issue",
|
|
37
33
|
identifier: issue?.identifier,
|
|
38
34
|
title: issue?.title,
|
|
39
|
-
state: state?.name,
|
|
40
|
-
team: team?.key,
|
|
41
|
-
assignee: assignee
|
|
42
|
-
? resolveUserDisplayName(assignee.id, assignee.name)
|
|
35
|
+
state: issue?.state?.name,
|
|
36
|
+
team: issue?.team?.key,
|
|
37
|
+
assignee: issue?.assignee
|
|
38
|
+
? resolveUserDisplayName(issue.assignee.id, issue.assignee.name)
|
|
43
39
|
: undefined,
|
|
44
|
-
priority: issue?.priority,
|
|
45
|
-
project: project?.name,
|
|
40
|
+
priority: issue?.priority ?? undefined,
|
|
41
|
+
project: issue?.project?.name,
|
|
46
42
|
id: issue?.id,
|
|
47
43
|
};
|
|
48
44
|
}
|
|
49
45
|
case "project": {
|
|
50
|
-
const project = r.project;
|
|
51
46
|
return {
|
|
52
47
|
type: "project",
|
|
53
|
-
name: project?.name,
|
|
54
|
-
state: project?.state,
|
|
55
|
-
id: project?.id,
|
|
48
|
+
name: r.project?.name,
|
|
49
|
+
state: r.project?.state,
|
|
50
|
+
id: r.project?.id,
|
|
56
51
|
};
|
|
57
52
|
}
|
|
58
53
|
case "initiative": {
|
|
59
|
-
const initiative = r.initiative;
|
|
60
54
|
return {
|
|
61
55
|
type: "initiative",
|
|
62
|
-
name: initiative?.name,
|
|
63
|
-
status: initiative?.status,
|
|
64
|
-
id: initiative?.id,
|
|
56
|
+
name: r.initiative?.name,
|
|
57
|
+
status: r.initiative?.status ?? undefined,
|
|
58
|
+
id: r.initiative?.id,
|
|
65
59
|
};
|
|
66
60
|
}
|
|
67
61
|
case "document": {
|
|
68
|
-
const doc = r.document;
|
|
69
|
-
const docProject = doc?.project;
|
|
70
62
|
return {
|
|
71
63
|
type: "document",
|
|
72
|
-
title:
|
|
73
|
-
project:
|
|
74
|
-
slugId:
|
|
75
|
-
id:
|
|
64
|
+
title: r.document?.title,
|
|
65
|
+
project: r.document?.project?.name,
|
|
66
|
+
slugId: r.document?.slugId ?? undefined,
|
|
67
|
+
id: r.document?.id,
|
|
76
68
|
};
|
|
77
69
|
}
|
|
78
70
|
default:
|
|
79
|
-
return { type:
|
|
71
|
+
return { type: r.type, id: null };
|
|
80
72
|
}
|
|
81
73
|
}
|
|
82
74
|
function matchesQuery(name, query) {
|
|
@@ -122,8 +114,7 @@ function buildSemanticQuery(graphQLService, query, limit, teamOption) {
|
|
|
122
114
|
});
|
|
123
115
|
}
|
|
124
116
|
function extractSemanticResults(semanticResult, requestedTypes, onlyTemplates) {
|
|
125
|
-
|
|
126
|
-
let results = semanticSearch?.results ?? [];
|
|
117
|
+
let results = semanticResult.semanticSearch?.results ?? [];
|
|
127
118
|
if (requestedTypes && !onlyTemplates) {
|
|
128
119
|
const semanticTypes = requestedTypes.filter((t) => t !== "template");
|
|
129
120
|
results = results.filter((r) => semanticTypes.includes(r.type));
|
|
@@ -140,7 +131,7 @@ export function setupSearchCommands(program) {
|
|
|
140
131
|
.action(handleAsyncCommand(async (query, options, command) => {
|
|
141
132
|
const rootOpts = getRootOpts(command);
|
|
142
133
|
const graphQLService = await createGraphQLService(rootOpts);
|
|
143
|
-
const limit =
|
|
134
|
+
const limit = parsePositiveInt(options.limit, "--limit");
|
|
144
135
|
const requestedTypes = options.type
|
|
145
136
|
? options.type.split(",").map((t) => t.trim().toLowerCase())
|
|
146
137
|
: null;
|
package/dist/commands/teams.js
CHANGED
|
@@ -3,6 +3,7 @@ import { cached, resolveCacheTTL } from "../utils/disk-cache.js";
|
|
|
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 setupTeamsCommands(program) {
|
|
7
8
|
const teams = program
|
|
8
9
|
.command("teams")
|
|
@@ -15,7 +16,7 @@ export function setupTeamsCommands(program) {
|
|
|
15
16
|
.option("-l, --limit <number>", "limit results", "100")
|
|
16
17
|
.action(handleAsyncCommand(async (options, command) => {
|
|
17
18
|
const rootOpts = getRootOpts(command);
|
|
18
|
-
const limit =
|
|
19
|
+
const limit = parsePositiveInt(options.limit, "--limit");
|
|
19
20
|
const ttl = resolveCacheTTL({
|
|
20
21
|
configTTL: loadConfig().cacheTTLSeconds,
|
|
21
22
|
// commander's `--no-cache` produces `cache: false` on the root opts.
|
|
@@ -48,14 +48,11 @@ export function setupTemplatesCommands(program) {
|
|
|
48
48
|
.action(handleAsyncCommand(async (templateId, _options, command) => {
|
|
49
49
|
const rootOpts = getRootOpts(command);
|
|
50
50
|
const graphQLService = await createGraphQLService(rootOpts);
|
|
51
|
-
const result = await graphQLService.rawRequest(TEMPLATE_BY_ID_QUERY, {
|
|
52
|
-
|
|
53
|
-
});
|
|
54
|
-
const template = result.template;
|
|
55
|
-
if (!template) {
|
|
51
|
+
const result = await graphQLService.rawRequest(TEMPLATE_BY_ID_QUERY, { id: templateId });
|
|
52
|
+
if (!result.template) {
|
|
56
53
|
throw new Error(`Template "${templateId}" not found`);
|
|
57
54
|
}
|
|
58
|
-
outputSuccess(template);
|
|
55
|
+
outputSuccess(result.template);
|
|
59
56
|
}));
|
|
60
57
|
templates
|
|
61
58
|
.command("create")
|
|
@@ -87,11 +84,10 @@ export function setupTemplatesCommands(program) {
|
|
|
87
84
|
if (options.color)
|
|
88
85
|
input.color = options.color;
|
|
89
86
|
const result = await graphQLService.rawRequest(TEMPLATE_CREATE_MUTATION, { input });
|
|
90
|
-
|
|
91
|
-
if (!payload?.success) {
|
|
87
|
+
if (!result.templateCreate.success || !result.templateCreate.template) {
|
|
92
88
|
throw new Error("templateCreate returned success=false");
|
|
93
89
|
}
|
|
94
|
-
outputSuccess(
|
|
90
|
+
outputSuccess(result.templateCreate.template);
|
|
95
91
|
}));
|
|
96
92
|
templates
|
|
97
93
|
.command("update <templateId>")
|
|
@@ -126,11 +122,11 @@ export function setupTemplatesCommands(program) {
|
|
|
126
122
|
throw new Error("templates update: at least one of --name, --description, --data, --data-file, --icon, --color, --team-id is required");
|
|
127
123
|
}
|
|
128
124
|
const result = await graphQLService.rawRequest(TEMPLATE_UPDATE_MUTATION, { id: templateId, input });
|
|
129
|
-
|
|
130
|
-
|
|
125
|
+
if (!result.templateUpdate.success ||
|
|
126
|
+
!result.templateUpdate.template) {
|
|
131
127
|
throw new Error("templateUpdate returned success=false");
|
|
132
128
|
}
|
|
133
|
-
outputSuccess(
|
|
129
|
+
outputSuccess(result.templateUpdate.template);
|
|
134
130
|
}));
|
|
135
131
|
templates
|
|
136
132
|
.command("delete <templateId>")
|
|
@@ -139,8 +135,7 @@ export function setupTemplatesCommands(program) {
|
|
|
139
135
|
const rootOpts = getRootOpts(command);
|
|
140
136
|
const graphQLService = await createGraphQLService(rootOpts);
|
|
141
137
|
const result = await graphQLService.rawRequest(TEMPLATE_DELETE_MUTATION, { id: templateId });
|
|
142
|
-
|
|
143
|
-
if (!payload?.success) {
|
|
138
|
+
if (!result.templateDelete.success) {
|
|
144
139
|
throw new Error("templateDelete returned success=false");
|
|
145
140
|
}
|
|
146
141
|
outputSuccess({ id: templateId, deleted: true });
|
package/dist/commands/users.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createLinearService } from "../utils/linear-service.js";
|
|
2
2
|
import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
|
|
3
3
|
import { getRootOpts } from "../utils/root-opts.js";
|
|
4
|
+
import { parsePositiveInt } from "../utils/validators.js";
|
|
4
5
|
export function setupUsersCommands(program) {
|
|
5
6
|
const users = program.command("users").description("User operations");
|
|
6
7
|
users.action(() => users.help());
|
|
@@ -13,7 +14,7 @@ export function setupUsersCommands(program) {
|
|
|
13
14
|
.action(handleAsyncCommand(async (options, command) => {
|
|
14
15
|
const rootOpts = getRootOpts(command);
|
|
15
16
|
const service = await createLinearService(rootOpts);
|
|
16
|
-
const result = await service.getUsers(options.active,
|
|
17
|
+
const result = await service.getUsers(options.active, parsePositiveInt(options.limit, "--limit"), options.name);
|
|
17
18
|
outputSuccess({ data: result, meta: { count: result.length } });
|
|
18
19
|
}));
|
|
19
20
|
}
|
package/dist/config/resolver.js
CHANGED
|
@@ -93,11 +93,10 @@ export async function resolveAssignee(input, rootOpts) {
|
|
|
93
93
|
if (input.toLowerCase() === "me") {
|
|
94
94
|
const graphQLService = await createGraphQLService(rootOpts);
|
|
95
95
|
const result = await graphQLService.rawRequest("{ viewer { id } }");
|
|
96
|
-
|
|
97
|
-
if (!viewer?.id) {
|
|
96
|
+
if (!result.viewer?.id) {
|
|
98
97
|
throw new Error('Could not resolve "me" — viewer query returned no user. Check your API token.');
|
|
99
98
|
}
|
|
100
|
-
return viewer.id;
|
|
99
|
+
return result.viewer.id;
|
|
101
100
|
}
|
|
102
101
|
return resolveMember(input);
|
|
103
102
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed response shapes for the queries in `./attachments.ts`.
|
|
3
|
+
* See `./issues-types.ts` for the rationale (ALL-937).
|
|
4
|
+
*/
|
|
5
|
+
/** Mirrors `ATTACHMENT_FRAGMENT` from `src/queries/attachments.ts`. */
|
|
6
|
+
export interface AttachmentNode {
|
|
7
|
+
id: string;
|
|
8
|
+
title: string | null;
|
|
9
|
+
url: string;
|
|
10
|
+
createdAt: string | null;
|
|
11
|
+
updatedAt: string | null;
|
|
12
|
+
}
|
|
13
|
+
export interface CreateAttachmentResponse {
|
|
14
|
+
attachmentCreate: {
|
|
15
|
+
success: boolean;
|
|
16
|
+
attachment: AttachmentNode | null;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export interface DeleteAttachmentResponse {
|
|
20
|
+
attachmentDelete: {
|
|
21
|
+
success: boolean;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export interface ListAttachmentsResponse {
|
|
25
|
+
issue: {
|
|
26
|
+
attachments: {
|
|
27
|
+
nodes: AttachmentNode[];
|
|
28
|
+
};
|
|
29
|
+
} | null;
|
|
30
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed response shapes for the queries in `./comments.ts`.
|
|
3
|
+
* See `./issues-types.ts` for the rationale (ALL-937).
|
|
4
|
+
*/
|
|
5
|
+
export interface CommentUserRef {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
displayName: string | null;
|
|
9
|
+
url: string | null;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Mirrors the comment selection set used by `LIST_COMMENTS_QUERY`,
|
|
13
|
+
* `CREATE_COMMENT_MUTATION`, and (with `issue` overlay)
|
|
14
|
+
* `UPDATE_COMMENT_MUTATION`.
|
|
15
|
+
*/
|
|
16
|
+
export interface CommentNode {
|
|
17
|
+
id: string;
|
|
18
|
+
body: string;
|
|
19
|
+
createdAt: string;
|
|
20
|
+
updatedAt: string;
|
|
21
|
+
user: CommentUserRef;
|
|
22
|
+
}
|
|
23
|
+
export interface UpdatedCommentNode extends CommentNode {
|
|
24
|
+
issue: {
|
|
25
|
+
id: string;
|
|
26
|
+
identifier: string;
|
|
27
|
+
} | null;
|
|
28
|
+
}
|
|
29
|
+
export interface ListCommentsResponse {
|
|
30
|
+
issue: {
|
|
31
|
+
id: string;
|
|
32
|
+
identifier: string;
|
|
33
|
+
comments: {
|
|
34
|
+
nodes: CommentNode[];
|
|
35
|
+
};
|
|
36
|
+
} | null;
|
|
37
|
+
}
|
|
38
|
+
export interface CreateCommentResponse {
|
|
39
|
+
commentCreate: {
|
|
40
|
+
success: boolean;
|
|
41
|
+
comment: CommentNode | null;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export interface UpdateCommentResponse {
|
|
45
|
+
commentUpdate: {
|
|
46
|
+
success: boolean;
|
|
47
|
+
comment: UpdatedCommentNode | null;
|
|
48
|
+
};
|
|
49
|
+
}
|