@enrichlayer/el-linear 1.7.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 +82 -1
- package/claude-skills/linear-operations/SKILL.md +55 -0
- package/dist/auth/oauth-callback.js +14 -7
- package/dist/auth/oauth-fs.d.ts +22 -0
- package/dist/auth/oauth-fs.js +77 -6
- package/dist/auth/oauth-headless.d.ts +13 -8
- package/dist/auth/oauth-headless.js +18 -11
- package/dist/auth/oauth-storage.d.ts +11 -3
- package/dist/auth/oauth-storage.js +15 -8
- package/dist/auth/token-resolver.d.ts +9 -0
- package/dist/auth/token-resolver.js +57 -37
- 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/init/index.js +2 -0
- package/dist/commands/init/oauth.d.ts +6 -0
- package/dist/commands/init/oauth.js +8 -2
- package/dist/commands/init/token.d.ts +0 -8
- package/dist/commands/init/token.js +13 -1
- package/dist/commands/issue-id.js +1 -3
- package/dist/commands/issues/branch.d.ts +17 -0
- package/dist/commands/issues/branch.js +40 -0
- package/dist/commands/issues/description.d.ts +89 -0
- package/dist/commands/issues/description.js +183 -0
- 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 +97 -497
- package/dist/commands/labels.js +20 -28
- package/dist/commands/profile/migrate-legacy.js +10 -33
- package/dist/commands/profile.js +1 -10
- package/dist/commands/project-milestones.js +13 -20
- package/dist/commands/projects.d.ts +5 -1
- package/dist/commands/projects.js +202 -102
- package/dist/commands/read-shortcut.d.ts +6 -0
- package/dist/commands/read-shortcut.js +6 -1
- package/dist/commands/refs.js +10 -1
- 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 +128 -7
- package/dist/commands/users.js +3 -1
- package/dist/config/config.js +31 -9
- package/dist/config/issue-validation.js +1 -1
- package/dist/config/paths.d.ts +11 -0
- package/dist/config/paths.js +44 -6
- package/dist/config/resolver.js +2 -3
- package/dist/config/term-enforcer.js +1 -1
- package/dist/main.js +37 -2
- 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/queries/templates.d.ts +3 -0
- package/dist/queries/templates.js +43 -0
- package/dist/types/linear.d.ts +8 -2
- package/dist/utils/auth.js +7 -9
- package/dist/utils/auto-link-references.js +44 -25
- package/dist/utils/disk-cache.js +2 -1
- package/dist/utils/formatters/summary.d.ts +62 -0
- package/dist/utils/formatters/summary.js +755 -0
- 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 +118 -5
- package/dist/utils/graphql-issues-service.js +202 -209
- package/dist/utils/issue-reference-extractor.d.ts +9 -1
- package/dist/utils/issue-reference-extractor.js +16 -9
- package/dist/utils/issue-reference-wrapper.js +1 -54
- package/dist/utils/linear-service.d.ts +7 -3
- package/dist/utils/linear-service.js +27 -5
- package/dist/utils/markdown-prosemirror.js +17 -1
- package/dist/utils/mention-resolver.js +17 -5
- package/dist/utils/output.d.ts +7 -1
- package/dist/utils/output.js +38 -1
- package/dist/utils/protected-ranges.d.ts +33 -0
- package/dist/utils/protected-ranges.js +73 -0
- package/dist/utils/table-formatter.d.ts +36 -0
- package/dist/utils/table-formatter.js +46 -24
- package/dist/utils/validators.d.ts +9 -1
- package/dist/utils/validators.js +10 -0
- package/dist/utils/workspace-url.d.ts +5 -1
- package/dist/utils/workspace-url.js +35 -5
- package/package.json +1 -1
|
@@ -1,114 +1,161 @@
|
|
|
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
8
|
import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
|
|
9
9
|
import { getRootOpts } from "../utils/root-opts.js";
|
|
10
|
+
import { renderCsv, renderFixedWidthTable, renderMarkdownTable, } from "../utils/table-formatter.js";
|
|
10
11
|
import { isUuid } from "../utils/uuid.js";
|
|
11
|
-
import { splitList } from "../utils/validators.js";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
case "name":
|
|
26
|
-
row[f] = p.name;
|
|
27
|
-
break;
|
|
28
|
-
case "state":
|
|
29
|
-
row[f] = p.state ?? "";
|
|
30
|
-
break;
|
|
31
|
-
case "progress":
|
|
32
|
-
row[f] =
|
|
33
|
-
p.progress !== undefined ? `${Math.round(p.progress * 100)}%` : "";
|
|
34
|
-
break;
|
|
35
|
-
case "teams":
|
|
36
|
-
row[f] = p.teams?.map((t) => t.key).join(", ") ?? "";
|
|
37
|
-
break;
|
|
38
|
-
case "lead":
|
|
39
|
-
row[f] = p.lead?.name ?? "";
|
|
40
|
-
break;
|
|
41
|
-
case "targetDate":
|
|
42
|
-
row[f] = p.targetDate ?? "";
|
|
43
|
-
break;
|
|
44
|
-
case "id":
|
|
45
|
-
row[f] = p.id;
|
|
46
|
-
break;
|
|
47
|
-
default:
|
|
48
|
-
row[f] = String(p[f] ?? "");
|
|
49
|
-
}
|
|
12
|
+
import { parsePositiveInt, splitList } from "../utils/validators.js";
|
|
13
|
+
const VALID_PROJECT_STATES = new Set([
|
|
14
|
+
"backlog",
|
|
15
|
+
"planned",
|
|
16
|
+
"started",
|
|
17
|
+
"paused",
|
|
18
|
+
"completed",
|
|
19
|
+
"canceled",
|
|
20
|
+
]);
|
|
21
|
+
function parseStateList(value, flagName) {
|
|
22
|
+
const states = splitList(value).map((s) => s.toLowerCase());
|
|
23
|
+
for (const s of states) {
|
|
24
|
+
if (!VALID_PROJECT_STATES.has(s)) {
|
|
25
|
+
throw new Error(`Invalid state "${s}" for ${flagName}. Valid: ${[...VALID_PROJECT_STATES].join(", ")}.`);
|
|
50
26
|
}
|
|
51
|
-
|
|
52
|
-
|
|
27
|
+
}
|
|
28
|
+
return states;
|
|
29
|
+
}
|
|
30
|
+
export function resolveProjectStateFilter(options) {
|
|
31
|
+
const flags = [options.state, options.excludeState, options.active].filter(Boolean).length;
|
|
32
|
+
if (flags > 1) {
|
|
33
|
+
throw new Error("--state, --exclude-state, and --active are mutually exclusive.");
|
|
34
|
+
}
|
|
35
|
+
if (options.active) {
|
|
36
|
+
return { excludeStates: ["completed", "canceled"] };
|
|
37
|
+
}
|
|
38
|
+
if (options.state) {
|
|
39
|
+
return { states: parseStateList(options.state, "--state") };
|
|
40
|
+
}
|
|
41
|
+
if (options.excludeState) {
|
|
42
|
+
return {
|
|
43
|
+
excludeStates: parseStateList(options.excludeState, "--exclude-state"),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
48
|
+
// ── Project column definitions ────────────────────────────────
|
|
49
|
+
//
|
|
50
|
+
// Pre-fix this file inlined an 80-line `formatProjectsOutput`
|
|
51
|
+
// function that hand-rolled column width math, table padding, CSV
|
|
52
|
+
// quoting, and markdown pipe syntax — all duplicating
|
|
53
|
+
// `utils/table-formatter.ts`. Now we declare `ColumnDef<LinearProject>`
|
|
54
|
+
// arrays and let the shared renderers do the work. ALL-938 cleanup.
|
|
55
|
+
const PROJECT_COLUMNS = {
|
|
56
|
+
name: { key: "name", header: "Name", width: 32, extract: (p) => p.name },
|
|
57
|
+
state: {
|
|
58
|
+
key: "state",
|
|
59
|
+
header: "State",
|
|
60
|
+
width: 10,
|
|
61
|
+
extract: (p) => p.state ?? "",
|
|
62
|
+
},
|
|
63
|
+
progress: {
|
|
64
|
+
key: "progress",
|
|
65
|
+
header: "Progress",
|
|
66
|
+
width: 10,
|
|
67
|
+
extract: (p) => p.progress !== undefined ? `${Math.round(p.progress * 100)}%` : "",
|
|
68
|
+
},
|
|
69
|
+
teams: {
|
|
70
|
+
key: "teams",
|
|
71
|
+
header: "Teams",
|
|
72
|
+
width: 18,
|
|
73
|
+
extract: (p) => p.teams?.map((t) => t.key).join(", ") ?? "",
|
|
74
|
+
},
|
|
75
|
+
lead: {
|
|
76
|
+
key: "lead",
|
|
77
|
+
header: "Lead",
|
|
78
|
+
width: 18,
|
|
79
|
+
extract: (p) => p.lead?.name ?? "",
|
|
80
|
+
},
|
|
81
|
+
targetDate: {
|
|
82
|
+
key: "targetDate",
|
|
83
|
+
header: "Target",
|
|
84
|
+
width: 12,
|
|
85
|
+
extract: (p) => p.targetDate ?? "",
|
|
86
|
+
},
|
|
87
|
+
id: { key: "id", header: "ID", width: 38, extract: (p) => p.id },
|
|
88
|
+
};
|
|
89
|
+
const PROJECT_MD_COLUMNS = {
|
|
90
|
+
name: { header: "Name", extract: (p) => p.name },
|
|
91
|
+
state: { header: "State", extract: (p) => p.state ?? "" },
|
|
92
|
+
progress: {
|
|
93
|
+
header: "Progress",
|
|
94
|
+
extract: (p) => p.progress !== undefined ? `${Math.round(p.progress * 100)}%` : "",
|
|
95
|
+
},
|
|
96
|
+
teams: {
|
|
97
|
+
header: "Teams",
|
|
98
|
+
extract: (p) => p.teams?.map((t) => t.key).join(", ") ?? "",
|
|
99
|
+
},
|
|
100
|
+
lead: { header: "Lead", extract: (p) => p.lead?.name ?? "" },
|
|
101
|
+
targetDate: { header: "Target", extract: (p) => p.targetDate ?? "" },
|
|
102
|
+
id: { header: "ID", extract: (p) => p.id },
|
|
103
|
+
};
|
|
104
|
+
const DEFAULT_PROJECT_COLUMNS = [
|
|
105
|
+
"name",
|
|
106
|
+
"state",
|
|
107
|
+
"progress",
|
|
108
|
+
"teams",
|
|
109
|
+
"lead",
|
|
110
|
+
"targetDate",
|
|
111
|
+
];
|
|
112
|
+
function formatProjectsOutput(projects, format, fieldNames) {
|
|
113
|
+
const keys = fieldNames ?? DEFAULT_PROJECT_COLUMNS;
|
|
53
114
|
if (format === "csv") {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
115
|
+
const columns = keys
|
|
116
|
+
.map((k) => PROJECT_COLUMNS[k])
|
|
117
|
+
.filter((c) => c !== undefined);
|
|
118
|
+
logger.info(renderCsv(projects, columns));
|
|
58
119
|
return;
|
|
59
120
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
? `| ${fields.map((f, i) => f.padEnd(widths[i])).join(" | ")} |`
|
|
67
|
-
: fields.map((f, i) => ` ${f.padEnd(widths[i])} `).join("|");
|
|
68
|
-
logger.info(header);
|
|
69
|
-
logger.info(separator);
|
|
70
|
-
for (const row of rows) {
|
|
71
|
-
const line = format === "md"
|
|
72
|
-
? `| ${fields.map((f, i) => (row[f] ?? "").padEnd(widths[i])).join(" | ")} |`
|
|
73
|
-
: fields
|
|
74
|
-
.map((f, i) => ` ${(row[f] ?? "").padEnd(widths[i])} `)
|
|
75
|
-
.join("|");
|
|
76
|
-
logger.info(line);
|
|
121
|
+
if (format === "md") {
|
|
122
|
+
const columns = keys
|
|
123
|
+
.map((k) => PROJECT_MD_COLUMNS[k])
|
|
124
|
+
.filter((c) => c !== undefined);
|
|
125
|
+
logger.info(renderMarkdownTable(projects, columns));
|
|
126
|
+
return;
|
|
77
127
|
}
|
|
128
|
+
// Default: fixed-width text table.
|
|
129
|
+
const columns = keys
|
|
130
|
+
.map((k) => PROJECT_COLUMNS[k])
|
|
131
|
+
.filter((c) => c !== undefined);
|
|
132
|
+
logger.info(renderFixedWidthTable(projects, columns));
|
|
78
133
|
}
|
|
79
134
|
async function resolveProjectWithTeams(graphQLService, projectNameOrId) {
|
|
80
135
|
if (isUuid(projectNameOrId)) {
|
|
81
|
-
const result = await graphQLService.rawRequest(PROJECT_BY_ID_QUERY, {
|
|
82
|
-
|
|
83
|
-
});
|
|
84
|
-
const project = result.project;
|
|
85
|
-
if (!project) {
|
|
136
|
+
const result = await graphQLService.rawRequest(PROJECT_BY_ID_QUERY, { id: projectNameOrId });
|
|
137
|
+
if (!result.project) {
|
|
86
138
|
throw new Error(`Project "${projectNameOrId}" not found`);
|
|
87
139
|
}
|
|
88
|
-
const teams = project.teams;
|
|
89
140
|
return {
|
|
90
|
-
projectId: project.id,
|
|
91
|
-
projectName: project.name,
|
|
92
|
-
currentTeams:
|
|
141
|
+
projectId: result.project.id,
|
|
142
|
+
projectName: result.project.name,
|
|
143
|
+
currentTeams: result.project.teams.nodes.map((t) => ({
|
|
93
144
|
id: t.id,
|
|
94
145
|
key: t.key,
|
|
95
146
|
name: t.name,
|
|
96
147
|
})),
|
|
97
148
|
};
|
|
98
149
|
}
|
|
99
|
-
const result = await graphQLService.rawRequest(GET_PROJECT_QUERY, {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const projectNodes = result.projects?.nodes;
|
|
103
|
-
if (!projectNodes?.length) {
|
|
150
|
+
const result = await graphQLService.rawRequest(GET_PROJECT_QUERY, { name: projectNameOrId });
|
|
151
|
+
const project = result.projects.nodes[0];
|
|
152
|
+
if (!project) {
|
|
104
153
|
throw new Error(`Project "${projectNameOrId}" not found`);
|
|
105
154
|
}
|
|
106
|
-
const project = projectNodes[0];
|
|
107
|
-
const teams = project.teams;
|
|
108
155
|
return {
|
|
109
156
|
projectId: project.id,
|
|
110
157
|
projectName: project.name,
|
|
111
|
-
currentTeams:
|
|
158
|
+
currentTeams: project.teams.nodes.map((t) => ({
|
|
112
159
|
id: t.id,
|
|
113
160
|
key: t.key,
|
|
114
161
|
name: t.name,
|
|
@@ -116,12 +163,14 @@ async function resolveProjectWithTeams(graphQLService, projectNameOrId) {
|
|
|
116
163
|
};
|
|
117
164
|
}
|
|
118
165
|
function formatTeamsOutput(projectUpdate) {
|
|
166
|
+
if (!projectUpdate.project) {
|
|
167
|
+
throw new Error("Failed to update project");
|
|
168
|
+
}
|
|
119
169
|
const updatedProject = projectUpdate.project;
|
|
120
|
-
const updatedTeams = updatedProject.teams;
|
|
121
170
|
return {
|
|
122
171
|
id: updatedProject.id,
|
|
123
172
|
name: updatedProject.name,
|
|
124
|
-
teams:
|
|
173
|
+
teams: updatedProject.teams.nodes.map((t) => ({
|
|
125
174
|
id: t.id,
|
|
126
175
|
key: t.key,
|
|
127
176
|
name: t.name,
|
|
@@ -200,9 +249,7 @@ async function handleRemoveTeam(projectNameOrId, teamInput, options, command) {
|
|
|
200
249
|
projectId,
|
|
201
250
|
teamId: finalTeamId,
|
|
202
251
|
});
|
|
203
|
-
const
|
|
204
|
-
const issues = project.issues;
|
|
205
|
-
const issueNodes = issues.nodes || [];
|
|
252
|
+
const issueNodes = issueCheck.project?.issues.nodes ?? [];
|
|
206
253
|
if (issueNodes.length > 0) {
|
|
207
254
|
const examples = issueNodes
|
|
208
255
|
.slice(0, 5)
|
|
@@ -230,14 +277,10 @@ async function handleCreateProject(name, options, command) {
|
|
|
230
277
|
const graphQLService = await createGraphQLService(rootOpts);
|
|
231
278
|
// Step 1: Check for duplicate projects (case-insensitive)
|
|
232
279
|
const searchResult = await graphQLService.rawRequest(SEARCH_PROJECTS_BY_NAME_QUERY, { name });
|
|
233
|
-
const existing =
|
|
234
|
-
?.nodes ?? []).filter((p) => p.name.toLowerCase() === name.toLowerCase());
|
|
280
|
+
const existing = searchResult.projects.nodes.filter((p) => p.name.toLowerCase() === name.toLowerCase());
|
|
235
281
|
if (existing.length > 0 && !options.force) {
|
|
236
282
|
const match = existing[0];
|
|
237
|
-
const teams =
|
|
238
|
-
[])
|
|
239
|
-
.map((t) => t.key)
|
|
240
|
-
.join(", ");
|
|
283
|
+
const teams = match.teams.nodes.map((t) => t.key).join(", ");
|
|
241
284
|
throw new Error(`Project "${match.name}" already exists (state: ${match.state}, teams: ${teams}). ` +
|
|
242
285
|
`Use --force to create anyway, or use the existing project.`);
|
|
243
286
|
}
|
|
@@ -255,11 +298,11 @@ async function handleCreateProject(name, options, command) {
|
|
|
255
298
|
...(teamIds.length > 0 ? { teamIds } : {}),
|
|
256
299
|
};
|
|
257
300
|
const createResult = await graphQLService.rawRequest(CREATE_PROJECT_MUTATION, { input });
|
|
258
|
-
|
|
259
|
-
|
|
301
|
+
if (!createResult.projectCreate.success ||
|
|
302
|
+
!createResult.projectCreate.project) {
|
|
260
303
|
throw new Error("Failed to create project");
|
|
261
304
|
}
|
|
262
|
-
const project =
|
|
305
|
+
const project = createResult.projectCreate.project;
|
|
263
306
|
// Step 4: Set content if provided (separate mutation — Linear API quirk)
|
|
264
307
|
if (options.content) {
|
|
265
308
|
await graphQLService.rawRequest(UPDATE_PROJECT_MUTATION, {
|
|
@@ -267,10 +310,7 @@ async function handleCreateProject(name, options, command) {
|
|
|
267
310
|
input: { content: options.content },
|
|
268
311
|
});
|
|
269
312
|
}
|
|
270
|
-
const teamList =
|
|
271
|
-
[])
|
|
272
|
-
.map((t) => t.key)
|
|
273
|
-
.join(", ");
|
|
313
|
+
const teamList = project.teams.nodes.map((t) => t.key).join(", ");
|
|
274
314
|
outputSuccess({
|
|
275
315
|
id: project.id,
|
|
276
316
|
name: project.name,
|
|
@@ -285,6 +325,42 @@ async function handleCreateProject(name, options, command) {
|
|
|
285
325
|
: {}),
|
|
286
326
|
});
|
|
287
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
|
+
}
|
|
288
364
|
export function setupProjectsCommands(program) {
|
|
289
365
|
const projects = program
|
|
290
366
|
.command("projects")
|
|
@@ -299,22 +375,46 @@ export function setupProjectsCommands(program) {
|
|
|
299
375
|
.option("--content <markdown>", "full markdown body (shown in project panel)")
|
|
300
376
|
.option("--force", "create even if a project with the same name exists")
|
|
301
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));
|
|
302
386
|
projects
|
|
303
387
|
.command("list")
|
|
304
388
|
.description("List projects")
|
|
305
389
|
.option("-l, --limit <number>", "limit results", "100")
|
|
306
|
-
.option("--format <format>", "output format (json, table, md, csv)", "json")
|
|
390
|
+
.option("--format <format>", "output format (json, summary, table, md, csv)", "json")
|
|
307
391
|
.option("--fields <fields>", "columns for table/csv (comma-separated: name,state,progress,teams,lead,targetDate)")
|
|
392
|
+
.option("--name <substring>", "filter by case-insensitive substring on project name")
|
|
393
|
+
.option("--state <names>", "include only projects in these states (comma-separated: backlog, planned, started, paused, completed, canceled)")
|
|
394
|
+
.option("--exclude-state <names>", "exclude projects in these states (comma-separated). Mutually exclusive with --state.")
|
|
395
|
+
.option("--active", "shorthand for --exclude-state completed,canceled (mutually exclusive with --state / --exclude-state)")
|
|
308
396
|
.action(handleAsyncCommand(async (options, command) => {
|
|
309
397
|
const rootOpts = getRootOpts(command);
|
|
310
|
-
const limit =
|
|
398
|
+
const limit = parsePositiveInt(options.limit, "--limit");
|
|
399
|
+
const nameFilter = options.name;
|
|
400
|
+
const stateFilter = resolveProjectStateFilter(options);
|
|
311
401
|
const ttl = resolveCacheTTL({
|
|
312
402
|
configTTL: loadConfig().cacheTTLSeconds,
|
|
313
403
|
noCacheFlag: rootOpts.cache === false,
|
|
314
404
|
});
|
|
315
|
-
|
|
405
|
+
// Cache key includes filter inputs so different filter combinations
|
|
406
|
+
// don't collide.
|
|
407
|
+
const cacheKey = `projects-list-limit:${limit}` +
|
|
408
|
+
`-name:${nameFilter ?? "_all"}` +
|
|
409
|
+
`-states:${stateFilter.states?.join(",") ?? "_any"}` +
|
|
410
|
+
`-excl:${stateFilter.excludeStates?.join(",") ?? "_none"}`;
|
|
411
|
+
const result = await cached(cacheKey, ttl, async () => {
|
|
316
412
|
const service = await createLinearService(rootOpts);
|
|
317
|
-
return service.getProjects(limit
|
|
413
|
+
return service.getProjects(limit, {
|
|
414
|
+
nameFilter,
|
|
415
|
+
states: stateFilter.states,
|
|
416
|
+
excludeStates: stateFilter.excludeStates,
|
|
417
|
+
});
|
|
318
418
|
});
|
|
319
419
|
const format = options.format;
|
|
320
420
|
if (format === "table" ||
|
|
@@ -4,3 +4,9 @@ import type { Command } from "commander";
|
|
|
4
4
|
* issue identifiers so `el-linear ADM-652` works without the `issues read` prefix.
|
|
5
5
|
*/
|
|
6
6
|
export declare function setupReadShortcut(program: Command): void;
|
|
7
|
+
/**
|
|
8
|
+
* Shared `issues read` body — exported so the top-level shortcut and
|
|
9
|
+
* `issues read` (in `commands/issues.ts`) both call one implementation
|
|
10
|
+
* instead of carrying byte-equivalent duplicates. ALL-938 cleanup.
|
|
11
|
+
*/
|
|
12
|
+
export declare function readIssues(issueIds: string[], _options: Record<string, unknown>, command: Command): Promise<void>;
|
|
@@ -49,7 +49,12 @@ export function setupReadShortcut(program) {
|
|
|
49
49
|
return originalParse(argv, parseOptions);
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Shared `issues read` body — exported so the top-level shortcut and
|
|
54
|
+
* `issues read` (in `commands/issues.ts`) both call one implementation
|
|
55
|
+
* instead of carrying byte-equivalent duplicates. ALL-938 cleanup.
|
|
56
|
+
*/
|
|
57
|
+
export async function readIssues(issueIds, _options, command) {
|
|
53
58
|
const rootOpts = getRootOpts(command);
|
|
54
59
|
const graphQLService = await createGraphQLService(rootOpts);
|
|
55
60
|
const linearService = await createLinearService(rootOpts);
|
package/dist/commands/refs.js
CHANGED
|
@@ -3,6 +3,7 @@ import { createGraphQLService } from "../utils/graphql-service.js";
|
|
|
3
3
|
import { extractIssueReferences } from "../utils/issue-reference-extractor.js";
|
|
4
4
|
import { wrapIssueReferencesAsLinks, } from "../utils/issue-reference-wrapper.js";
|
|
5
5
|
import { createLinearService } from "../utils/linear-service.js";
|
|
6
|
+
import { logger } from "../utils/logger.js";
|
|
6
7
|
import { handleAsyncCommand } from "../utils/output.js";
|
|
7
8
|
import { getRootOpts } from "../utils/root-opts.js";
|
|
8
9
|
import { validateReferences } from "../utils/validate-references.js";
|
|
@@ -67,13 +68,20 @@ async function handleWrap(options, command) {
|
|
|
67
68
|
return new Set(map.keys());
|
|
68
69
|
},
|
|
69
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
|
+
}
|
|
70
78
|
const graphQLService = await createGraphQLService(rootOpts);
|
|
71
79
|
return getWorkspaceUrlKey(graphQLService);
|
|
72
80
|
},
|
|
73
81
|
};
|
|
74
82
|
if (!validate) {
|
|
75
83
|
// stderr advisory only — keeps stdout a clean text stream for piping.
|
|
76
|
-
|
|
84
|
+
logger.error("el-linear refs wrap: --no-validate set; emitting links for every regex match without checking the workspace.");
|
|
77
85
|
}
|
|
78
86
|
const wrapped = await wrapRefsCore({ text, target, validate }, deps);
|
|
79
87
|
process.stdout.write(wrapped);
|
|
@@ -92,5 +100,6 @@ export function setupRefsCommands(program) {
|
|
|
92
100
|
.option("--file <path>", "read input from a file instead of stdin")
|
|
93
101
|
.option("--target <target>", "output format: markdown (default) or slack", "markdown")
|
|
94
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.")
|
|
95
104
|
.action(handleAsyncCommand(handleWrap));
|
|
96
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,
|