linear-sdk-cli 0.3.3 → 0.5.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/CHANGELOG.md +15 -0
- package/COVERAGE.md +13 -9
- package/MIGRATING.md +6 -0
- package/README.md +280 -86
- package/package.json +5 -5
- package/skills/linear-sdk-cli/SKILL.md +10 -0
- package/skills/linear-sdk-cli/references/auth.md +1 -0
- package/skills/linear-sdk-cli/references/config.md +10 -7
- package/skills/linear-sdk-cli/references/custom-view.md +205 -0
- package/skills/linear-sdk-cli/references/initiative.md +10 -5
- package/skills/linear-sdk-cli/references/issue.md +222 -77
- package/skills/linear-sdk-cli/references/notification.md +3 -3
- package/skills/linear-sdk-cli/references/project.md +9 -4
- package/src/cli.ts +2 -0
- package/src/commands/custom-view.ts +279 -0
- package/src/commands/initiative.ts +23 -6
- package/src/commands/issue.ts +313 -12
- package/src/commands/meta.ts +55 -9
- package/src/commands/notification.ts +5 -3
- package/src/commands/project.ts +21 -3
- package/src/config.ts +111 -10
- package/src/lib/delegation.ts +240 -0
- package/src/lib/json-input.ts +46 -0
- package/src/lib/options.ts +23 -3
- package/src/lib/output-shapes.ts +102 -3
- package/src/output/lifecycle.ts +10 -0
- package/src/services/custom-view.ts +376 -0
- package/src/services/initiative.ts +12 -3
- package/src/services/issue.ts +498 -21
- package/src/services/project.ts +14 -3
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/** `linear custom-view` (alias `cv`) — manage saved custom views. */
|
|
2
|
+
|
|
3
|
+
import { Command, Option } from "commander";
|
|
4
|
+
import type { Context } from "../context.js";
|
|
5
|
+
import { action } from "../lib/action.js";
|
|
6
|
+
import { resolveBody } from "../lib/body.js";
|
|
7
|
+
import { usageError } from "../lib/errors.js";
|
|
8
|
+
import { parseJsonObjectInput } from "../lib/json-input.js";
|
|
9
|
+
import { confirmDestructive, promptInput, promptSelect } from "../lib/prompt.js";
|
|
10
|
+
import type { Column } from "../output/table.js";
|
|
11
|
+
import * as svc from "../services/custom-view.js";
|
|
12
|
+
|
|
13
|
+
const ROW_COLUMNS: Column<svc.CustomViewRow>[] = [
|
|
14
|
+
{ key: "name", header: "Name", value: (row) => row.name, max: 40 },
|
|
15
|
+
{ key: "type", header: "Type", value: (row) => row.type, max: 12 },
|
|
16
|
+
{ key: "shared", header: "Shared", value: (row) => (row.shared ? "yes" : "no") },
|
|
17
|
+
{ key: "owner", header: "Owner", value: (row) => row.owner?.displayName ?? "—", max: 24 },
|
|
18
|
+
{
|
|
19
|
+
key: "team",
|
|
20
|
+
header: "Team",
|
|
21
|
+
value: (row) => (row.team ? `${row.team.key} ${row.team.name}` : "—"),
|
|
22
|
+
max: 28,
|
|
23
|
+
},
|
|
24
|
+
{ key: "id", header: "ID", value: (row) => row.id },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const RESULT_COLUMNS: Column<svc.CustomViewResultRow>[] = [
|
|
28
|
+
{ key: "type", header: "Type", value: (row) => row.type, max: 12 },
|
|
29
|
+
{ key: "identifier", header: "Key", value: (row) => row.identifier ?? "—", max: 16 },
|
|
30
|
+
{ key: "name", header: "Name", value: (row) => row.name, max: 60 },
|
|
31
|
+
{ key: "id", header: "ID", value: (row) => row.id },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
function filterFrom(opts: Record<string, any>): Record<string, unknown> | undefined {
|
|
35
|
+
return parseJsonObjectInput({ inline: opts.filter, file: opts.filterFile, label: "filter" });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function visibility(opts: Record<string, any>): boolean | undefined {
|
|
39
|
+
if (opts.shared && opts.personal) {
|
|
40
|
+
throw usageError("Pass either --shared or --personal, not both.");
|
|
41
|
+
}
|
|
42
|
+
if (opts.shared) return true;
|
|
43
|
+
if (opts.personal) return false;
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function addFilterOptions(command: Command): Command {
|
|
48
|
+
return command
|
|
49
|
+
.addOption(
|
|
50
|
+
new Option("--filter <json>", "typed Linear filter as a JSON object").conflicts("filterFile"),
|
|
51
|
+
)
|
|
52
|
+
.addOption(
|
|
53
|
+
new Option(
|
|
54
|
+
"--filter-file <path>",
|
|
55
|
+
"read the filter JSON from a file ('-' = stdin)",
|
|
56
|
+
).conflicts("filter"),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function addVisibilityOptions(command: Command): Command {
|
|
61
|
+
return command
|
|
62
|
+
.addOption(new Option("--shared", "share the view with the workspace").conflicts("personal"))
|
|
63
|
+
.addOption(new Option("--personal", "make the view owner-only").conflicts("shared"));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function addDescriptionOptions(command: Command, update = false): Command {
|
|
67
|
+
command
|
|
68
|
+
.addOption(
|
|
69
|
+
new Option("-d, --description <text>", update ? "new description" : "view description")
|
|
70
|
+
.conflicts("descriptionFile")
|
|
71
|
+
.conflicts("clearDescription"),
|
|
72
|
+
)
|
|
73
|
+
.addOption(
|
|
74
|
+
new Option("--description-file <path>", "read description from a file ('-' = stdin)")
|
|
75
|
+
.conflicts("description")
|
|
76
|
+
.conflicts("clearDescription"),
|
|
77
|
+
);
|
|
78
|
+
return command;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function receipt(view: any) {
|
|
82
|
+
return {
|
|
83
|
+
id: view.id,
|
|
84
|
+
name: view.name,
|
|
85
|
+
type: svc.customViewType(view.modelName),
|
|
86
|
+
shared: view.shared,
|
|
87
|
+
slugId: view.slugId,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function registerCustomView(program: Command): void {
|
|
92
|
+
const customView = program
|
|
93
|
+
.command("custom-view")
|
|
94
|
+
.alias("cv")
|
|
95
|
+
.description("Create and manage saved custom views");
|
|
96
|
+
|
|
97
|
+
customView
|
|
98
|
+
.command("list")
|
|
99
|
+
.alias("ls")
|
|
100
|
+
.description("List accessible workspace and team custom views")
|
|
101
|
+
.action(
|
|
102
|
+
action(async (ctx: Context) => {
|
|
103
|
+
const rows = await svc.listCustomViews(ctx.client, ctx.limit);
|
|
104
|
+
ctx.output.list(rows, ROW_COLUMNS, rows);
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
customView
|
|
109
|
+
.command("view <id>", { isDefault: true })
|
|
110
|
+
.alias("show")
|
|
111
|
+
.description("Show a custom view by UUID")
|
|
112
|
+
.action(
|
|
113
|
+
action(async (ctx: Context, _opts, id: string) => {
|
|
114
|
+
const detail = await svc.getCustomViewDetail(ctx.client, id);
|
|
115
|
+
ctx.output.detail(detail, [
|
|
116
|
+
["Name", detail.name],
|
|
117
|
+
["Type", detail.type],
|
|
118
|
+
["Visibility", detail.shared ? "shared" : "personal"],
|
|
119
|
+
["Owner", detail.owner?.displayName],
|
|
120
|
+
["Creator", detail.creator?.displayName],
|
|
121
|
+
["Team", detail.team ? `${detail.team.key} ${detail.team.name}` : null],
|
|
122
|
+
["Description", detail.description],
|
|
123
|
+
["Filter", JSON.stringify(detail.filter)],
|
|
124
|
+
["Color", detail.color],
|
|
125
|
+
["Icon", detail.icon],
|
|
126
|
+
["Slug", detail.slugId],
|
|
127
|
+
["Created", detail.createdAt],
|
|
128
|
+
["Updated", detail.updatedAt],
|
|
129
|
+
["Archived", detail.archivedAt],
|
|
130
|
+
["ID", detail.id],
|
|
131
|
+
]);
|
|
132
|
+
}),
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
customView
|
|
136
|
+
.command("results <id>")
|
|
137
|
+
.alias("items")
|
|
138
|
+
.description("List issues, projects, or initiatives matched by a view UUID")
|
|
139
|
+
.action(
|
|
140
|
+
action(async (ctx: Context, _opts, id: string) => {
|
|
141
|
+
const rows = await svc.listCustomViewResults(ctx.client, id, ctx.limit);
|
|
142
|
+
ctx.output.list(rows, RESULT_COLUMNS, rows, { empty: "(view matches no entities)" });
|
|
143
|
+
}),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
let create = customView
|
|
147
|
+
.command("create")
|
|
148
|
+
.alias("new")
|
|
149
|
+
.description("Create an issue, project, or initiative custom view")
|
|
150
|
+
.option("--name <name>", "view name")
|
|
151
|
+
.addOption(new Option("--type <type>", "entity type").choices([...svc.CUSTOM_VIEW_TYPES]))
|
|
152
|
+
.option("--owner <who>", "owner (me|email|name|id)")
|
|
153
|
+
.option("--color <hex>", "icon color")
|
|
154
|
+
.option("--icon <icon>", "emoji or decorative icon identifier")
|
|
155
|
+
.addOption(
|
|
156
|
+
new Option("--scope-team <key|name|id>", "attach the view to a team").conflicts([
|
|
157
|
+
"scopeProject",
|
|
158
|
+
"scopeInitiative",
|
|
159
|
+
]),
|
|
160
|
+
)
|
|
161
|
+
.addOption(
|
|
162
|
+
new Option("--scope-project <id|name>", "attach the view to a project").conflicts([
|
|
163
|
+
"scopeTeam",
|
|
164
|
+
"scopeInitiative",
|
|
165
|
+
]),
|
|
166
|
+
)
|
|
167
|
+
.addOption(
|
|
168
|
+
new Option("--scope-initiative <id|name>", "attach the view to an initiative").conflicts([
|
|
169
|
+
"scopeTeam",
|
|
170
|
+
"scopeProject",
|
|
171
|
+
]),
|
|
172
|
+
);
|
|
173
|
+
create = addVisibilityOptions(addFilterOptions(addDescriptionOptions(create)));
|
|
174
|
+
create
|
|
175
|
+
.addHelpText(
|
|
176
|
+
"after",
|
|
177
|
+
`\nExamples:\n linear custom-view create --name "Urgent" --type issue --filter '{"priority":{"eq":1}}' --shared\n linear cv create --name "At risk" --type project --filter-file project-filter.json --scope-team ENG\n`,
|
|
178
|
+
)
|
|
179
|
+
.action(
|
|
180
|
+
action(async (ctx: Context, opts) => {
|
|
181
|
+
const name = opts.name ?? (await promptInput(ctx, "Name:", { required: true }));
|
|
182
|
+
const type: svc.CustomViewType =
|
|
183
|
+
opts.type ??
|
|
184
|
+
(await promptSelect(ctx, "View type:", [
|
|
185
|
+
{ name: "Issues", value: "issue" as const },
|
|
186
|
+
{ name: "Projects", value: "project" as const },
|
|
187
|
+
{ name: "Initiatives", value: "initiative" as const },
|
|
188
|
+
]));
|
|
189
|
+
const description = resolveBody({
|
|
190
|
+
arg: opts.description,
|
|
191
|
+
file: opts.descriptionFile,
|
|
192
|
+
interactive: false,
|
|
193
|
+
});
|
|
194
|
+
const created = await svc.createCustomView(ctx.client, {
|
|
195
|
+
name,
|
|
196
|
+
type,
|
|
197
|
+
filter: filterFrom(opts),
|
|
198
|
+
description,
|
|
199
|
+
color: opts.color,
|
|
200
|
+
icon: opts.icon,
|
|
201
|
+
owner: opts.owner,
|
|
202
|
+
shared: visibility(opts),
|
|
203
|
+
scopeTeam: opts.scopeTeam,
|
|
204
|
+
scopeProject: opts.scopeProject,
|
|
205
|
+
scopeInitiative: opts.scopeInitiative,
|
|
206
|
+
});
|
|
207
|
+
ctx.output.emit(receipt(created), () =>
|
|
208
|
+
ctx.output.success(`Created custom view ${created.name} (${created.id})`),
|
|
209
|
+
);
|
|
210
|
+
}),
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
let update = customView
|
|
214
|
+
.command("update <id>")
|
|
215
|
+
.alias("edit")
|
|
216
|
+
.description("Update mutable fields on a custom view UUID")
|
|
217
|
+
.option("--name <name>", "new name")
|
|
218
|
+
.option("--owner <who>", "new owner (me|email|name|id)")
|
|
219
|
+
.addOption(new Option("--color <hex>", "new icon color").conflicts("clearColor"))
|
|
220
|
+
.addOption(new Option("--clear-color", "clear the icon color").conflicts("color"))
|
|
221
|
+
.addOption(new Option("--icon <icon>", "new icon").conflicts("clearIcon"))
|
|
222
|
+
.addOption(new Option("--clear-icon", "clear the icon").conflicts("icon"))
|
|
223
|
+
.addOption(
|
|
224
|
+
new Option("--clear-description", "clear the description").conflicts([
|
|
225
|
+
"description",
|
|
226
|
+
"descriptionFile",
|
|
227
|
+
]),
|
|
228
|
+
)
|
|
229
|
+
.addOption(
|
|
230
|
+
new Option("--scope-team <key|name|id>", "set the public team scope").conflicts(
|
|
231
|
+
"clearTeamScope",
|
|
232
|
+
),
|
|
233
|
+
)
|
|
234
|
+
.addOption(
|
|
235
|
+
new Option("--clear-team-scope", "clear the public team scope").conflicts("scopeTeam"),
|
|
236
|
+
);
|
|
237
|
+
update = addVisibilityOptions(addFilterOptions(addDescriptionOptions(update, true)));
|
|
238
|
+
update.action(
|
|
239
|
+
action(async (ctx: Context, opts, id: string) => {
|
|
240
|
+
const description = resolveBody({
|
|
241
|
+
arg: opts.description,
|
|
242
|
+
file: opts.descriptionFile,
|
|
243
|
+
interactive: false,
|
|
244
|
+
});
|
|
245
|
+
const changed = await svc.updateCustomView(ctx.client, id, {
|
|
246
|
+
name: opts.name,
|
|
247
|
+
filter: filterFrom(opts),
|
|
248
|
+
description,
|
|
249
|
+
clearDescription: opts.clearDescription,
|
|
250
|
+
color: opts.color,
|
|
251
|
+
clearColor: opts.clearColor,
|
|
252
|
+
icon: opts.icon,
|
|
253
|
+
clearIcon: opts.clearIcon,
|
|
254
|
+
owner: opts.owner,
|
|
255
|
+
shared: visibility(opts),
|
|
256
|
+
scopeTeam: opts.scopeTeam,
|
|
257
|
+
clearTeamScope: opts.clearTeamScope,
|
|
258
|
+
});
|
|
259
|
+
ctx.output.emit(receipt(changed), () =>
|
|
260
|
+
ctx.output.success(`Updated custom view ${changed.name}`),
|
|
261
|
+
);
|
|
262
|
+
}),
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
customView
|
|
266
|
+
.command("delete <id>")
|
|
267
|
+
.alias("rm")
|
|
268
|
+
.description("Delete a custom view by UUID")
|
|
269
|
+
.action(
|
|
270
|
+
action(async (ctx: Context, _opts, id: string) => {
|
|
271
|
+
const detail = await svc.getCustomViewDetail(ctx.client, id);
|
|
272
|
+
if (!(await confirmDestructive(ctx, `Delete custom view ${detail.name}?`))) return;
|
|
273
|
+
const deleted = await svc.deleteCustomView(ctx.client, id);
|
|
274
|
+
ctx.output.emit({ id: deleted.id, name: deleted.name, deleted: true }, () =>
|
|
275
|
+
ctx.output.success(`Deleted custom view ${deleted.name}`),
|
|
276
|
+
);
|
|
277
|
+
}),
|
|
278
|
+
);
|
|
279
|
+
}
|
|
@@ -10,14 +10,26 @@ import { Command, Option } from "commander";
|
|
|
10
10
|
import { action } from "../lib/action.js";
|
|
11
11
|
import { resolveBody } from "../lib/body.js";
|
|
12
12
|
import { confirmDestructive, promptInput } from "../lib/prompt.js";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
parseList,
|
|
15
|
+
parseIntOption,
|
|
16
|
+
addAliasOption,
|
|
17
|
+
readAlias,
|
|
18
|
+
addIncludeArchivedOption,
|
|
19
|
+
} from "../lib/options.js";
|
|
14
20
|
import type { Context } from "../context.js";
|
|
15
21
|
import * as svc from "../services/initiative.js";
|
|
16
22
|
import type { Column } from "../output/table.js";
|
|
23
|
+
import { lifecycleSuffix } from "../output/lifecycle.js";
|
|
17
24
|
|
|
18
25
|
const ROW_COLUMNS: Column<svc.InitiativeRow>[] = [
|
|
19
26
|
{ key: "name", header: "Name", value: (r) => r.name, max: 40 },
|
|
20
|
-
{
|
|
27
|
+
{
|
|
28
|
+
key: "status",
|
|
29
|
+
header: "Status",
|
|
30
|
+
value: (r) => `${r.status ?? "—"}${lifecycleSuffix(r)}`,
|
|
31
|
+
max: 24,
|
|
32
|
+
},
|
|
21
33
|
{
|
|
22
34
|
key: "priority",
|
|
23
35
|
header: "Pri",
|
|
@@ -34,7 +46,7 @@ export function registerInitiative(program: Command): void {
|
|
|
34
46
|
.description("Work with initiatives");
|
|
35
47
|
|
|
36
48
|
// list --------------------------------------------------------------------
|
|
37
|
-
initiative
|
|
49
|
+
const list = initiative
|
|
38
50
|
.command("list")
|
|
39
51
|
.alias("ls")
|
|
40
52
|
.description("List workspace initiatives (every status unless --status narrows)")
|
|
@@ -42,7 +54,6 @@ export function registerInitiative(program: Command): void {
|
|
|
42
54
|
// in the tree, and a short flag means one thing everywhere here.
|
|
43
55
|
.option("--status <name>", "filter by status (Planned, Active, Completed, Canceled, Proposed)")
|
|
44
56
|
.option("--owner <who>", "filter by owner (me|email|name|id)")
|
|
45
|
-
.option("--archived", "include archived initiatives")
|
|
46
57
|
// The reference CLI lists Active only and needs `--all-statuses` to widen;
|
|
47
58
|
// this list is every status already, so the flag is accepted as the no-op
|
|
48
59
|
// it is here rather than failing a transplanted script.
|
|
@@ -52,11 +63,13 @@ export function registerInitiative(program: Command): void {
|
|
|
52
63
|
const rows = await svc.listInitiatives(ctx.client, ctx.limit, {
|
|
53
64
|
status: opts.status,
|
|
54
65
|
owner: opts.owner,
|
|
55
|
-
|
|
66
|
+
includeArchived: !!readAlias<boolean>(opts, "--include-archived", "--archived"),
|
|
56
67
|
});
|
|
57
68
|
ctx.output.list(rows, ROW_COLUMNS, rows);
|
|
58
69
|
}),
|
|
59
70
|
);
|
|
71
|
+
addIncludeArchivedOption(list);
|
|
72
|
+
list.addOption(new Option("--archived", "compatibility alias of --include-archived"));
|
|
60
73
|
|
|
61
74
|
// view --------------------------------------------------------------------
|
|
62
75
|
initiative
|
|
@@ -68,7 +81,11 @@ export function registerInitiative(program: Command): void {
|
|
|
68
81
|
const detail = await svc.getInitiativeDetail(ctx.client, idArg);
|
|
69
82
|
ctx.output.detail(detail, [
|
|
70
83
|
["Initiative", detail.name],
|
|
71
|
-
[
|
|
84
|
+
[
|
|
85
|
+
"Trashed",
|
|
86
|
+
detail.trashed ? `YES (deleted ${detail.archivedAt ?? "at an unknown time"})` : null,
|
|
87
|
+
],
|
|
88
|
+
["Archived", !detail.trashed && detail.archivedAt ? `YES (${detail.archivedAt})` : null],
|
|
72
89
|
["Status", detail.status],
|
|
73
90
|
["Priority", detail.priority ? detail.priorityLabel : null],
|
|
74
91
|
["Labels", detail.labels.length ? detail.labels.join(", ") : null],
|