@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
package/dist/commands/labels.js
CHANGED
|
@@ -4,9 +4,9 @@ import { CREATE_LABEL_MUTATION, FIND_PARENT_LABEL_QUERY, RESTORE_LABEL_MUTATION,
|
|
|
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";
|
|
7
|
-
import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
|
|
7
|
+
import { handleAsyncCommand, outputSuccess, warnIfTruncated, } from "../utils/output.js";
|
|
8
8
|
import { getRootOpts } from "../utils/root-opts.js";
|
|
9
|
-
import { validateHexColor } from "../utils/validators.js";
|
|
9
|
+
import { parsePositiveInt, validateHexColor } from "../utils/validators.js";
|
|
10
10
|
async function handleCreateLabel(name, options, command) {
|
|
11
11
|
const rootOpts = getRootOpts(command);
|
|
12
12
|
const teamId = resolveTeam(options.team);
|
|
@@ -23,45 +23,35 @@ async function handleCreateLabel(name, options, command) {
|
|
|
23
23
|
name: options.parent,
|
|
24
24
|
teamId,
|
|
25
25
|
});
|
|
26
|
-
const
|
|
27
|
-
const nodes = issueLabels?.nodes;
|
|
28
|
-
const parentLabel = nodes?.[0];
|
|
26
|
+
const parentLabel = parentResult.issueLabels.nodes[0];
|
|
29
27
|
if (parentLabel) {
|
|
30
28
|
input.parentId = parentLabel.id;
|
|
31
29
|
}
|
|
32
30
|
}
|
|
33
|
-
const result = await graphQLService.rawRequest(CREATE_LABEL_MUTATION, {
|
|
34
|
-
|
|
35
|
-
});
|
|
36
|
-
const createResult = result.issueLabelCreate;
|
|
37
|
-
if (!createResult?.success) {
|
|
31
|
+
const result = await graphQLService.rawRequest(CREATE_LABEL_MUTATION, { input });
|
|
32
|
+
if (!result.issueLabelCreate.success || !result.issueLabelCreate.issueLabel) {
|
|
38
33
|
throw new Error(`Failed to create label "${name}"`);
|
|
39
34
|
}
|
|
40
|
-
outputSuccess(
|
|
35
|
+
outputSuccess(result.issueLabelCreate.issueLabel);
|
|
41
36
|
}
|
|
42
37
|
async function handleRetireLabel(labelId, _options, command) {
|
|
43
38
|
const rootOpts = getRootOpts(command);
|
|
44
39
|
const graphQLService = await createGraphQLService(rootOpts);
|
|
45
|
-
const result = await graphQLService.rawRequest(RETIRE_LABEL_MUTATION, {
|
|
46
|
-
|
|
47
|
-
});
|
|
48
|
-
const retireResult = result.issueLabelRetire;
|
|
49
|
-
if (!retireResult?.success) {
|
|
40
|
+
const result = await graphQLService.rawRequest(RETIRE_LABEL_MUTATION, { id: labelId });
|
|
41
|
+
if (!result.issueLabelRetire.success || !result.issueLabelRetire.issueLabel) {
|
|
50
42
|
throw new Error(`Failed to retire label "${labelId}"`);
|
|
51
43
|
}
|
|
52
|
-
outputSuccess(
|
|
44
|
+
outputSuccess(result.issueLabelRetire.issueLabel);
|
|
53
45
|
}
|
|
54
46
|
async function handleRestoreLabel(labelId, _options, command) {
|
|
55
47
|
const rootOpts = getRootOpts(command);
|
|
56
48
|
const graphQLService = await createGraphQLService(rootOpts);
|
|
57
|
-
const result = await graphQLService.rawRequest(RESTORE_LABEL_MUTATION, {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const restoreResult = result.issueLabelRestore;
|
|
61
|
-
if (!restoreResult?.success) {
|
|
49
|
+
const result = await graphQLService.rawRequest(RESTORE_LABEL_MUTATION, { id: labelId });
|
|
50
|
+
if (!result.issueLabelRestore.success ||
|
|
51
|
+
!result.issueLabelRestore.issueLabel) {
|
|
62
52
|
throw new Error(`Failed to restore label "${labelId}"`);
|
|
63
53
|
}
|
|
64
|
-
outputSuccess(
|
|
54
|
+
outputSuccess(result.issueLabelRestore.issueLabel);
|
|
65
55
|
}
|
|
66
56
|
export function setupLabelsCommands(program) {
|
|
67
57
|
const labels = program
|
|
@@ -79,7 +69,7 @@ export function setupLabelsCommands(program) {
|
|
|
79
69
|
const rootOpts = getRootOpts(command);
|
|
80
70
|
const teamFilter = options.team ? resolveTeam(options.team) : undefined;
|
|
81
71
|
const nameFilter = options.name;
|
|
82
|
-
const limit =
|
|
72
|
+
const limit = parsePositiveInt(options.limit, "--limit");
|
|
83
73
|
const ttl = resolveCacheTTL({
|
|
84
74
|
configTTL: loadConfig().cacheTTLSeconds,
|
|
85
75
|
noCacheFlag: rootOpts.cache === false,
|
|
@@ -92,6 +82,7 @@ export function setupLabelsCommands(program) {
|
|
|
92
82
|
const service = await createLinearService(rootOpts);
|
|
93
83
|
return service.getLabels(teamFilter, limit, nameFilter);
|
|
94
84
|
});
|
|
85
|
+
warnIfTruncated(result.labels.length, limit);
|
|
95
86
|
outputSuccess({
|
|
96
87
|
data: result.labels,
|
|
97
88
|
meta: { count: result.labels.length },
|
package/dist/commands/profile.js
CHANGED
|
@@ -5,7 +5,7 @@ import { createLinearService, } from "../utils/linear-service.js";
|
|
|
5
5
|
import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
|
|
6
6
|
import { getRootOpts } from "../utils/root-opts.js";
|
|
7
7
|
import { isUuid } from "../utils/uuid.js";
|
|
8
|
-
import { validateIsoDate } from "../utils/validators.js";
|
|
8
|
+
import { parsePositiveInt, validateIsoDate } from "../utils/validators.js";
|
|
9
9
|
async function resolveMilestoneId(milestoneNameOrId, graphQLService, linearService, projectNameOrId) {
|
|
10
10
|
if (isUuid(milestoneNameOrId)) {
|
|
11
11
|
return milestoneNameOrId;
|
|
@@ -17,22 +17,17 @@ async function resolveMilestoneId(milestoneNameOrId, graphQLService, linearServi
|
|
|
17
17
|
name: milestoneNameOrId,
|
|
18
18
|
projectId,
|
|
19
19
|
});
|
|
20
|
-
|
|
21
|
-
const milestonesData = projectData?.projectMilestones;
|
|
22
|
-
nodes = milestonesData?.nodes || [];
|
|
20
|
+
nodes = findRes.project?.projectMilestones.nodes ?? [];
|
|
23
21
|
}
|
|
24
22
|
if (nodes.length === 0) {
|
|
25
|
-
const globalRes = await graphQLService.rawRequest(FIND_PROJECT_MILESTONE_BY_NAME_GLOBAL, {
|
|
26
|
-
|
|
27
|
-
});
|
|
28
|
-
const globalMilestones = globalRes.projectMilestones;
|
|
29
|
-
nodes = globalMilestones?.nodes || [];
|
|
23
|
+
const globalRes = await graphQLService.rawRequest(FIND_PROJECT_MILESTONE_BY_NAME_GLOBAL, { name: milestoneNameOrId });
|
|
24
|
+
nodes = globalRes.projectMilestones.nodes;
|
|
30
25
|
}
|
|
31
26
|
if (nodes.length === 0) {
|
|
32
27
|
throw notFoundError("Milestone", milestoneNameOrId);
|
|
33
28
|
}
|
|
34
29
|
if (nodes.length > 1) {
|
|
35
|
-
const matches = nodes.map((m) => `"${m.name}" in project "${m.project
|
|
30
|
+
const matches = nodes.map((m) => `"${m.name}" in project "${m.project.name}"`);
|
|
36
31
|
throw multipleMatchesError("milestone", milestoneNameOrId, matches, "specify --project or use the milestone ID");
|
|
37
32
|
}
|
|
38
33
|
return nodes[0].id;
|
|
@@ -44,11 +39,9 @@ async function handleListMilestones(options, command) {
|
|
|
44
39
|
const projectId = await linearService.resolveProjectId(options.project);
|
|
45
40
|
const result = await graphQLService.rawRequest(LIST_PROJECT_MILESTONES_QUERY, {
|
|
46
41
|
projectId,
|
|
47
|
-
first:
|
|
42
|
+
first: parsePositiveInt(options.limit || "50", "--limit"),
|
|
48
43
|
});
|
|
49
|
-
const
|
|
50
|
-
const milestonesData = projectData?.projectMilestones;
|
|
51
|
-
const nodes = milestonesData?.nodes || [];
|
|
44
|
+
const nodes = result.project?.projectMilestones.nodes ?? [];
|
|
52
45
|
outputSuccess({ data: nodes, meta: { count: nodes.length } });
|
|
53
46
|
}
|
|
54
47
|
async function handleReadMilestone(milestoneIdOrName, options, command) {
|
|
@@ -76,11 +69,11 @@ async function handleCreateMilestone(name, options, command) {
|
|
|
76
69
|
description: options.description,
|
|
77
70
|
targetDate: options.targetDate,
|
|
78
71
|
});
|
|
79
|
-
|
|
80
|
-
|
|
72
|
+
if (!result.projectMilestoneCreate.success ||
|
|
73
|
+
!result.projectMilestoneCreate.projectMilestone) {
|
|
81
74
|
throw new Error("Failed to create project milestone");
|
|
82
75
|
}
|
|
83
|
-
outputSuccess(
|
|
76
|
+
outputSuccess(result.projectMilestoneCreate.projectMilestone);
|
|
84
77
|
}
|
|
85
78
|
async function handleUpdateMilestone(milestoneIdOrName, options, command) {
|
|
86
79
|
const rootOpts = getRootOpts(command);
|
|
@@ -102,11 +95,11 @@ async function handleUpdateMilestone(milestoneIdOrName, options, command) {
|
|
|
102
95
|
updateVars.sortOrder = Number.parseFloat(options.sortOrder);
|
|
103
96
|
}
|
|
104
97
|
const result = await graphQLService.rawRequest(UPDATE_PROJECT_MILESTONE_MUTATION, updateVars);
|
|
105
|
-
|
|
106
|
-
|
|
98
|
+
if (!result.projectMilestoneUpdate.success ||
|
|
99
|
+
!result.projectMilestoneUpdate.projectMilestone) {
|
|
107
100
|
throw new Error("Failed to update project milestone");
|
|
108
101
|
}
|
|
109
|
-
outputSuccess(
|
|
102
|
+
outputSuccess(result.projectMilestoneUpdate.projectMilestone);
|
|
110
103
|
}
|
|
111
104
|
export function setupProjectMilestonesCommands(program) {
|
|
112
105
|
const projectMilestones = program
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { Command, OptionValues } from "commander";
|
|
2
|
+
import type { LinearProject } from "../types/linear.js";
|
|
3
|
+
export declare function sortActiveFirst(projects: LinearProject[]): LinearProject[];
|
|
2
4
|
export declare function resolveProjectStateFilter(options: OptionValues): {
|
|
3
5
|
states?: string[];
|
|
4
6
|
excludeStates?: string[];
|
|
@@ -1,15 +1,15 @@
|
|
|
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";
|
|
7
7
|
import { logger } from "../utils/logger.js";
|
|
8
|
-
import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
|
|
8
|
+
import { handleAsyncCommand, outputSuccess, outputWarning, } 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",
|
|
@@ -18,6 +18,58 @@ const VALID_PROJECT_STATES = new Set([
|
|
|
18
18
|
"completed",
|
|
19
19
|
"canceled",
|
|
20
20
|
]);
|
|
21
|
+
/**
|
|
22
|
+
* Display ordering for `projects list`: active work first, terminal states
|
|
23
|
+
* last. Without this, the SDK's `updatedAt` order surfaces recently-touched
|
|
24
|
+
* completed/canceled projects ahead of in-progress ones — so an active project
|
|
25
|
+
* can fall past the `--limit` window and an agent concludes it does not exist
|
|
26
|
+
* (the DEV-4175 silent-truncation failure). Stable sort preserves `updatedAt`
|
|
27
|
+
* within each rank.
|
|
28
|
+
*
|
|
29
|
+
* `Linear` exposes additional internal states (e.g. archived) that we don't
|
|
30
|
+
* enumerate here; anything not in this table sorts to the end via
|
|
31
|
+
* `UNRANKED_STATE`.
|
|
32
|
+
*/
|
|
33
|
+
const PROJECT_STATE_RANK = {
|
|
34
|
+
started: 0,
|
|
35
|
+
planned: 1,
|
|
36
|
+
paused: 2,
|
|
37
|
+
backlog: 3,
|
|
38
|
+
completed: 4,
|
|
39
|
+
canceled: 5,
|
|
40
|
+
};
|
|
41
|
+
// Sentinel for any state Linear surfaces that isn't in the rank table above
|
|
42
|
+
// (e.g. `archived`, future additions). Number.MAX_SAFE_INTEGER reads more
|
|
43
|
+
// deliberately than a magic `9` and stays correct if PROJECT_STATE_RANK grows.
|
|
44
|
+
const UNRANKED_STATE = Number.MAX_SAFE_INTEGER;
|
|
45
|
+
export function sortActiveFirst(projects) {
|
|
46
|
+
return [...projects].sort((a, b) => (PROJECT_STATE_RANK[a.state ?? ""] ?? UNRANKED_STATE) -
|
|
47
|
+
(PROJECT_STATE_RANK[b.state ?? ""] ?? UNRANKED_STATE));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Emit a truncation warning when `projects list` returned exactly `--limit`
|
|
51
|
+
* rows (the same `count === limit` heuristic as `warnIfTruncated` in
|
|
52
|
+
* `output.ts`). JSON buffers it into `_warnings` so the payload stays a single
|
|
53
|
+
* parseable object; every human-facing format prints to stderr so it can't be
|
|
54
|
+
* silently scrolled past. DEV-4175.
|
|
55
|
+
*
|
|
56
|
+
* The exact total is deliberately omitted from the message. Returning it would
|
|
57
|
+
* require a second `client.projects({ first: 0 })` round-trip (the SDK has no
|
|
58
|
+
* `count`-only helper) or a full pagination walk — both negate the cheap-page
|
|
59
|
+
* default. `--all` in the hint gives the caller a deterministic way to learn
|
|
60
|
+
* the real total without us guessing, so the absence is a feature, not a gap.
|
|
61
|
+
*/
|
|
62
|
+
function warnProjectsTruncated(count, limit, format) {
|
|
63
|
+
const msg = `results_truncated: returned ${count} projects matching --limit ${limit}; ` +
|
|
64
|
+
"more may exist. Re-run with --all to fetch every project " +
|
|
65
|
+
`(or --limit ${limit * 2}, or narrow with --name / --state / --team).`;
|
|
66
|
+
if (format === "json") {
|
|
67
|
+
outputWarning(msg);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
logger.error(msg);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
21
73
|
function parseStateList(value, flagName) {
|
|
22
74
|
const states = splitList(value).map((s) => s.toLowerCase());
|
|
23
75
|
for (const s of states) {
|
|
@@ -133,37 +185,29 @@ function formatProjectsOutput(projects, format, fieldNames) {
|
|
|
133
185
|
}
|
|
134
186
|
async function resolveProjectWithTeams(graphQLService, projectNameOrId) {
|
|
135
187
|
if (isUuid(projectNameOrId)) {
|
|
136
|
-
const result = await graphQLService.rawRequest(PROJECT_BY_ID_QUERY, {
|
|
137
|
-
|
|
138
|
-
});
|
|
139
|
-
const project = result.project;
|
|
140
|
-
if (!project) {
|
|
188
|
+
const result = await graphQLService.rawRequest(PROJECT_BY_ID_QUERY, { id: projectNameOrId });
|
|
189
|
+
if (!result.project) {
|
|
141
190
|
throw new Error(`Project "${projectNameOrId}" not found`);
|
|
142
191
|
}
|
|
143
|
-
const teams = project.teams;
|
|
144
192
|
return {
|
|
145
|
-
projectId: project.id,
|
|
146
|
-
projectName: project.name,
|
|
147
|
-
currentTeams:
|
|
193
|
+
projectId: result.project.id,
|
|
194
|
+
projectName: result.project.name,
|
|
195
|
+
currentTeams: result.project.teams.nodes.map((t) => ({
|
|
148
196
|
id: t.id,
|
|
149
197
|
key: t.key,
|
|
150
198
|
name: t.name,
|
|
151
199
|
})),
|
|
152
200
|
};
|
|
153
201
|
}
|
|
154
|
-
const result = await graphQLService.rawRequest(GET_PROJECT_QUERY, {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const projectNodes = result.projects?.nodes;
|
|
158
|
-
if (!projectNodes?.length) {
|
|
202
|
+
const result = await graphQLService.rawRequest(GET_PROJECT_QUERY, { name: projectNameOrId });
|
|
203
|
+
const project = result.projects.nodes[0];
|
|
204
|
+
if (!project) {
|
|
159
205
|
throw new Error(`Project "${projectNameOrId}" not found`);
|
|
160
206
|
}
|
|
161
|
-
const project = projectNodes[0];
|
|
162
|
-
const teams = project.teams;
|
|
163
207
|
return {
|
|
164
208
|
projectId: project.id,
|
|
165
209
|
projectName: project.name,
|
|
166
|
-
currentTeams:
|
|
210
|
+
currentTeams: project.teams.nodes.map((t) => ({
|
|
167
211
|
id: t.id,
|
|
168
212
|
key: t.key,
|
|
169
213
|
name: t.name,
|
|
@@ -171,12 +215,14 @@ async function resolveProjectWithTeams(graphQLService, projectNameOrId) {
|
|
|
171
215
|
};
|
|
172
216
|
}
|
|
173
217
|
function formatTeamsOutput(projectUpdate) {
|
|
218
|
+
if (!projectUpdate.project) {
|
|
219
|
+
throw new Error("Failed to update project");
|
|
220
|
+
}
|
|
174
221
|
const updatedProject = projectUpdate.project;
|
|
175
|
-
const updatedTeams = updatedProject.teams;
|
|
176
222
|
return {
|
|
177
223
|
id: updatedProject.id,
|
|
178
224
|
name: updatedProject.name,
|
|
179
|
-
teams:
|
|
225
|
+
teams: updatedProject.teams.nodes.map((t) => ({
|
|
180
226
|
id: t.id,
|
|
181
227
|
key: t.key,
|
|
182
228
|
name: t.name,
|
|
@@ -255,9 +301,7 @@ async function handleRemoveTeam(projectNameOrId, teamInput, options, command) {
|
|
|
255
301
|
projectId,
|
|
256
302
|
teamId: finalTeamId,
|
|
257
303
|
});
|
|
258
|
-
const
|
|
259
|
-
const issues = project.issues;
|
|
260
|
-
const issueNodes = issues.nodes || [];
|
|
304
|
+
const issueNodes = issueCheck.project?.issues.nodes ?? [];
|
|
261
305
|
if (issueNodes.length > 0) {
|
|
262
306
|
const examples = issueNodes
|
|
263
307
|
.slice(0, 5)
|
|
@@ -285,14 +329,10 @@ async function handleCreateProject(name, options, command) {
|
|
|
285
329
|
const graphQLService = await createGraphQLService(rootOpts);
|
|
286
330
|
// Step 1: Check for duplicate projects (case-insensitive)
|
|
287
331
|
const searchResult = await graphQLService.rawRequest(SEARCH_PROJECTS_BY_NAME_QUERY, { name });
|
|
288
|
-
const existing =
|
|
289
|
-
?.nodes ?? []).filter((p) => p.name.toLowerCase() === name.toLowerCase());
|
|
332
|
+
const existing = searchResult.projects.nodes.filter((p) => p.name.toLowerCase() === name.toLowerCase());
|
|
290
333
|
if (existing.length > 0 && !options.force) {
|
|
291
334
|
const match = existing[0];
|
|
292
|
-
const teams =
|
|
293
|
-
[])
|
|
294
|
-
.map((t) => t.key)
|
|
295
|
-
.join(", ");
|
|
335
|
+
const teams = match.teams.nodes.map((t) => t.key).join(", ");
|
|
296
336
|
throw new Error(`Project "${match.name}" already exists (state: ${match.state}, teams: ${teams}). ` +
|
|
297
337
|
`Use --force to create anyway, or use the existing project.`);
|
|
298
338
|
}
|
|
@@ -310,11 +350,11 @@ async function handleCreateProject(name, options, command) {
|
|
|
310
350
|
...(teamIds.length > 0 ? { teamIds } : {}),
|
|
311
351
|
};
|
|
312
352
|
const createResult = await graphQLService.rawRequest(CREATE_PROJECT_MUTATION, { input });
|
|
313
|
-
|
|
314
|
-
|
|
353
|
+
if (!createResult.projectCreate.success ||
|
|
354
|
+
!createResult.projectCreate.project) {
|
|
315
355
|
throw new Error("Failed to create project");
|
|
316
356
|
}
|
|
317
|
-
const project =
|
|
357
|
+
const project = createResult.projectCreate.project;
|
|
318
358
|
// Step 4: Set content if provided (separate mutation — Linear API quirk)
|
|
319
359
|
if (options.content) {
|
|
320
360
|
await graphQLService.rawRequest(UPDATE_PROJECT_MUTATION, {
|
|
@@ -322,10 +362,7 @@ async function handleCreateProject(name, options, command) {
|
|
|
322
362
|
input: { content: options.content },
|
|
323
363
|
});
|
|
324
364
|
}
|
|
325
|
-
const teamList =
|
|
326
|
-
[])
|
|
327
|
-
.map((t) => t.key)
|
|
328
|
-
.join(", ");
|
|
365
|
+
const teamList = project.teams.nodes.map((t) => t.key).join(", ");
|
|
329
366
|
outputSuccess({
|
|
330
367
|
id: project.id,
|
|
331
368
|
name: project.name,
|
|
@@ -340,6 +377,42 @@ async function handleCreateProject(name, options, command) {
|
|
|
340
377
|
: {}),
|
|
341
378
|
});
|
|
342
379
|
}
|
|
380
|
+
async function handleArchiveProject(projectNameOrId, _options, command) {
|
|
381
|
+
const rootOpts = getRootOpts(command);
|
|
382
|
+
const graphQLService = await createGraphQLService(rootOpts);
|
|
383
|
+
const linearService = await createLinearService(rootOpts);
|
|
384
|
+
const projectId = await linearService.resolveProjectId(projectNameOrId);
|
|
385
|
+
const result = await graphQLService.rawRequest(ARCHIVE_PROJECT_MUTATION, { id: projectId });
|
|
386
|
+
const payload = result.projectArchive;
|
|
387
|
+
if (!payload.success) {
|
|
388
|
+
throw new Error(`Failed to archive project "${projectNameOrId}"`);
|
|
389
|
+
}
|
|
390
|
+
outputSuccess({
|
|
391
|
+
success: true,
|
|
392
|
+
archived: true,
|
|
393
|
+
id: projectId,
|
|
394
|
+
entity: payload.entity ?? undefined,
|
|
395
|
+
lastSyncId: payload.lastSyncId,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
async function handleDeleteProject(projectNameOrId, _options, command) {
|
|
399
|
+
const rootOpts = getRootOpts(command);
|
|
400
|
+
const graphQLService = await createGraphQLService(rootOpts);
|
|
401
|
+
const linearService = await createLinearService(rootOpts);
|
|
402
|
+
const projectId = await linearService.resolveProjectId(projectNameOrId);
|
|
403
|
+
const result = await graphQLService.rawRequest(DELETE_PROJECT_MUTATION, { id: projectId });
|
|
404
|
+
const payload = result.projectDelete;
|
|
405
|
+
if (!payload.success) {
|
|
406
|
+
throw new Error(`Failed to delete project "${projectNameOrId}"`);
|
|
407
|
+
}
|
|
408
|
+
outputSuccess({
|
|
409
|
+
success: true,
|
|
410
|
+
deleted: true,
|
|
411
|
+
id: projectId,
|
|
412
|
+
entity: payload.entity ?? undefined,
|
|
413
|
+
lastSyncId: payload.lastSyncId,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
343
416
|
export function setupProjectsCommands(program) {
|
|
344
417
|
const projects = program
|
|
345
418
|
.command("projects")
|
|
@@ -354,6 +427,14 @@ export function setupProjectsCommands(program) {
|
|
|
354
427
|
.option("--content <markdown>", "full markdown body (shown in project panel)")
|
|
355
428
|
.option("--force", "create even if a project with the same name exists")
|
|
356
429
|
.action(handleAsyncCommand(handleCreateProject));
|
|
430
|
+
projects
|
|
431
|
+
.command("archive <project>")
|
|
432
|
+
.description("Archive a project (resolves names)")
|
|
433
|
+
.action(handleAsyncCommand(handleArchiveProject));
|
|
434
|
+
projects
|
|
435
|
+
.command("delete <project>")
|
|
436
|
+
.description("Delete (trash) a project (resolves names)")
|
|
437
|
+
.action(handleAsyncCommand(handleDeleteProject));
|
|
357
438
|
projects
|
|
358
439
|
.command("list")
|
|
359
440
|
.description("List projects")
|
|
@@ -361,13 +442,22 @@ export function setupProjectsCommands(program) {
|
|
|
361
442
|
.option("--format <format>", "output format (json, summary, table, md, csv)", "json")
|
|
362
443
|
.option("--fields <fields>", "columns for table/csv (comma-separated: name,state,progress,teams,lead,targetDate)")
|
|
363
444
|
.option("--name <substring>", "filter by case-insensitive substring on project name")
|
|
445
|
+
.option("--team <key|name>", "filter to projects belonging to a specific team (e.g. DEV, FE)")
|
|
364
446
|
.option("--state <names>", "include only projects in these states (comma-separated: backlog, planned, started, paused, completed, canceled)")
|
|
365
447
|
.option("--exclude-state <names>", "exclude projects in these states (comma-separated). Mutually exclusive with --state.")
|
|
366
448
|
.option("--active", "shorthand for --exclude-state completed,canceled (mutually exclusive with --state / --exclude-state)")
|
|
449
|
+
.option("--all", "fetch every project (paginates fully). Equivalent to --limit 0; overrides --limit when both are given.")
|
|
367
450
|
.action(handleAsyncCommand(async (options, command) => {
|
|
368
451
|
const rootOpts = getRootOpts(command);
|
|
369
|
-
|
|
452
|
+
// `--all` / `--limit 0` mean unlimited. Stored as `limit = 0`,
|
|
453
|
+
// which `LinearService.getProjects` interprets as "paginate
|
|
454
|
+
// fully". DEV-4175.
|
|
455
|
+
const unlimited = options.all === true || options.limit === "0";
|
|
456
|
+
const limit = unlimited
|
|
457
|
+
? 0
|
|
458
|
+
: parsePositiveInt(options.limit, "--limit");
|
|
370
459
|
const nameFilter = options.name;
|
|
460
|
+
const teamFilter = options.team;
|
|
371
461
|
const stateFilter = resolveProjectStateFilter(options);
|
|
372
462
|
const ttl = resolveCacheTTL({
|
|
373
463
|
configTTL: loadConfig().cacheTTLSeconds,
|
|
@@ -377,31 +467,54 @@ export function setupProjectsCommands(program) {
|
|
|
377
467
|
// don't collide.
|
|
378
468
|
const cacheKey = `projects-list-limit:${limit}` +
|
|
379
469
|
`-name:${nameFilter ?? "_all"}` +
|
|
470
|
+
`-team:${teamFilter ?? "_all"}` +
|
|
380
471
|
`-states:${stateFilter.states?.join(",") ?? "_any"}` +
|
|
381
472
|
`-excl:${stateFilter.excludeStates?.join(",") ?? "_none"}`;
|
|
382
473
|
const result = await cached(cacheKey, ttl, async () => {
|
|
383
474
|
const service = await createLinearService(rootOpts);
|
|
475
|
+
let teamId;
|
|
476
|
+
if (teamFilter) {
|
|
477
|
+
const resolved = resolveTeam(teamFilter);
|
|
478
|
+
teamId = await service.resolveTeamId(resolved);
|
|
479
|
+
}
|
|
384
480
|
return service.getProjects(limit, {
|
|
385
481
|
nameFilter,
|
|
482
|
+
teamId,
|
|
386
483
|
states: stateFilter.states,
|
|
387
484
|
excludeStates: stateFilter.excludeStates,
|
|
388
485
|
});
|
|
389
486
|
});
|
|
487
|
+
// Active-first sort: keeps started/planned/paused/backlog
|
|
488
|
+
// ahead of completed/canceled in every format, so the active
|
|
489
|
+
// set always fits within `--limit`. DEV-4175.
|
|
490
|
+
const sorted = sortActiveFirst(result);
|
|
390
491
|
const format = options.format;
|
|
391
|
-
|
|
492
|
+
const isTabular = format === "table" ||
|
|
392
493
|
format === "md" ||
|
|
393
494
|
format === "markdown" ||
|
|
394
|
-
format === "csv"
|
|
495
|
+
format === "csv";
|
|
496
|
+
// Emit the truncation warning BEFORE outputting: the JSON
|
|
497
|
+
// branch buffers into `_warnings`, which the next
|
|
498
|
+
// `outputSuccess()` call drains. All other formats route to
|
|
499
|
+
// stderr via `logger.error` so the visible payload (table,
|
|
500
|
+
// markdown, summary) stays clean.
|
|
501
|
+
if (!unlimited && sorted.length === limit) {
|
|
502
|
+
warnProjectsTruncated(sorted.length, limit, format);
|
|
503
|
+
}
|
|
504
|
+
if (isTabular) {
|
|
395
505
|
const fieldList = options.fields
|
|
396
506
|
? splitList(options.fields)
|
|
397
507
|
: undefined;
|
|
398
|
-
formatProjectsOutput(
|
|
508
|
+
formatProjectsOutput(sorted, format, fieldList);
|
|
399
509
|
if (format === "table") {
|
|
400
|
-
logger.info(`\n${
|
|
510
|
+
logger.info(`\n${sorted.length} projects`);
|
|
401
511
|
}
|
|
402
512
|
}
|
|
403
513
|
else {
|
|
404
|
-
outputSuccess({
|
|
514
|
+
outputSuccess({
|
|
515
|
+
data: sorted,
|
|
516
|
+
meta: { count: sorted.length },
|
|
517
|
+
});
|
|
405
518
|
}
|
|
406
519
|
}));
|
|
407
520
|
projects
|
|
@@ -9,4 +9,4 @@ export declare function setupReadShortcut(program: Command): void;
|
|
|
9
9
|
* `issues read` (in `commands/issues.ts`) both call one implementation
|
|
10
10
|
* instead of carrying byte-equivalent duplicates. ALL-938 cleanup.
|
|
11
11
|
*/
|
|
12
|
-
export declare function readIssues(issueIds: string[],
|
|
12
|
+
export declare function readIssues(issueIds: string[], options: Record<string, unknown>, command: Command): Promise<void>;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { downloadLinearUploads } from "../utils/download-uploads.js";
|
|
2
|
+
import { extractField } from "../utils/extract-field.js";
|
|
2
3
|
import { createFileService } from "../utils/file-service.js";
|
|
3
|
-
import {
|
|
4
|
-
import { createGraphQLService } from "../utils/graphql-service.js";
|
|
5
|
-
import { createLinearService } from "../utils/linear-service.js";
|
|
4
|
+
import { createIssuesService } from "../utils/issues-service-bootstrap.js";
|
|
6
5
|
import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
|
|
7
6
|
import { getRootOpts } from "../utils/root-opts.js";
|
|
8
7
|
/**
|
|
@@ -21,7 +20,10 @@ export function setupReadShortcut(program) {
|
|
|
21
20
|
.alias("view")
|
|
22
21
|
.alias("show")
|
|
23
22
|
.description("Shortcut for `issues read`. Get issue details by identifier.")
|
|
24
|
-
.
|
|
23
|
+
.option("--field <name>", 'Extract a single named section from the issue description (e.g. "Done when"). ' +
|
|
24
|
+
"Matches H2/H3 headers and bold pseudo-headers case-insensitively. " +
|
|
25
|
+
"Outputs the section text only — no JSON envelope.")
|
|
26
|
+
.addHelpText("after", '\nExamples:\n el-linear read ADM-652\n el-linear get DEV-123 DEV-456\n el-linear ADM-652 (auto-detected)\n el-linear read DEV-123 --field "Done when" (just that section)')
|
|
25
27
|
.action(handleAsyncCommand(readIssues));
|
|
26
28
|
// Catch-all: if argv looks like `el-linear ADM-652 [DEV-123 ...]`, run read
|
|
27
29
|
const originalParse = program.parse.bind(program);
|
|
@@ -54,15 +56,33 @@ export function setupReadShortcut(program) {
|
|
|
54
56
|
* `issues read` (in `commands/issues.ts`) both call one implementation
|
|
55
57
|
* instead of carrying byte-equivalent duplicates. ALL-938 cleanup.
|
|
56
58
|
*/
|
|
57
|
-
export async function readIssues(issueIds,
|
|
59
|
+
export async function readIssues(issueIds, options, command) {
|
|
58
60
|
const rootOpts = getRootOpts(command);
|
|
59
|
-
const
|
|
60
|
-
const linearService = await createLinearService(rootOpts);
|
|
61
|
-
const issuesService = new GraphQLIssuesService(graphQLService, linearService);
|
|
61
|
+
const { issuesService } = await createIssuesService(rootOpts);
|
|
62
62
|
const fileService = await createFileService(rootOpts);
|
|
63
|
+
const fieldName = typeof options.field === "string" ? options.field : null;
|
|
64
|
+
// --field is single-issue only. With multiple issues, a section
|
|
65
|
+
// extraction can't sensibly fan out to N different bodies — the
|
|
66
|
+
// caller almost always wants one section from one issue.
|
|
67
|
+
if (fieldName && issueIds.length > 1) {
|
|
68
|
+
throw new Error("--field is single-issue only; pass exactly one issueId. " +
|
|
69
|
+
"For multiple issues, drop --field and use --jq or --format summary.");
|
|
70
|
+
}
|
|
63
71
|
if (issueIds.length === 1) {
|
|
64
72
|
const issue = await issuesService.getIssueById(issueIds[0]);
|
|
65
73
|
const resolved = await downloadLinearUploads(issue, fileService);
|
|
74
|
+
if (fieldName) {
|
|
75
|
+
const section = extractField(resolved.description ?? "", fieldName);
|
|
76
|
+
if (section === null) {
|
|
77
|
+
// Print nothing to stdout, exit non-zero with a stderr hint.
|
|
78
|
+
// Mirrors `grep` semantics for "not found" — scripts can
|
|
79
|
+
// branch on the exit code.
|
|
80
|
+
process.stderr.write(`el-linear: section "${fieldName}" not found in ${resolved.identifier}'s description\n`);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
process.stdout.write(`${section}\n`);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
66
86
|
outputSuccess(resolved);
|
|
67
87
|
}
|
|
68
88
|
else {
|