linear-sdk-cli 0.1.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/.claude-plugin/marketplace.json +20 -0
- package/.claude-plugin/plugin.json +9 -0
- package/CHANGELOG.md +807 -0
- package/COVERAGE.md +495 -0
- package/LICENSE +21 -0
- package/MIGRATING.md +220 -0
- package/README.md +524 -0
- package/package.json +87 -0
- package/skills/linear-sdk-cli/SKILL.md +534 -0
- package/skills/linear-sdk-cli/references/api.md +27 -0
- package/skills/linear-sdk-cli/references/attachment.md +82 -0
- package/skills/linear-sdk-cli/references/auth.md +174 -0
- package/skills/linear-sdk-cli/references/commands.md +37 -0
- package/skills/linear-sdk-cli/references/comment.md +154 -0
- package/skills/linear-sdk-cli/references/completion.md +17 -0
- package/skills/linear-sdk-cli/references/config.md +99 -0
- package/skills/linear-sdk-cli/references/cycle.md +131 -0
- package/skills/linear-sdk-cli/references/document.md +180 -0
- package/skills/linear-sdk-cli/references/favorite.md +78 -0
- package/skills/linear-sdk-cli/references/initiative-update.md +67 -0
- package/skills/linear-sdk-cli/references/initiative.md +256 -0
- package/skills/linear-sdk-cli/references/issue.md +928 -0
- package/skills/linear-sdk-cli/references/label.md +112 -0
- package/skills/linear-sdk-cli/references/milestone.md +139 -0
- package/skills/linear-sdk-cli/references/notification.md +123 -0
- package/skills/linear-sdk-cli/references/open.md +29 -0
- package/skills/linear-sdk-cli/references/organization.md +99 -0
- package/skills/linear-sdk-cli/references/project-update.md +67 -0
- package/skills/linear-sdk-cli/references/project.md +234 -0
- package/skills/linear-sdk-cli/references/roadmap.md +150 -0
- package/skills/linear-sdk-cli/references/schema.md +21 -0
- package/skills/linear-sdk-cli/references/state.md +61 -0
- package/skills/linear-sdk-cli/references/team.md +227 -0
- package/skills/linear-sdk-cli/references/user.md +103 -0
- package/skills/linear-sdk-cli/references/webhook.md +151 -0
- package/skills/linear-sdk-cli/references/whoami.md +26 -0
- package/skills/linear-sdk-cli/scripts/generate-docs.ts +339 -0
- package/src/bin/linear.ts +97 -0
- package/src/cli.ts +232 -0
- package/src/client.ts +172 -0
- package/src/commands/agent-session.ts +121 -0
- package/src/commands/api.ts +317 -0
- package/src/commands/attach.ts +90 -0
- package/src/commands/attachment.ts +74 -0
- package/src/commands/comment.ts +225 -0
- package/src/commands/completion.ts +69 -0
- package/src/commands/cycle.ts +125 -0
- package/src/commands/discover.ts +170 -0
- package/src/commands/document.ts +211 -0
- package/src/commands/favorite.ts +72 -0
- package/src/commands/initiative-update.ts +47 -0
- package/src/commands/initiative.ts +261 -0
- package/src/commands/issue.ts +1263 -0
- package/src/commands/label.ts +119 -0
- package/src/commands/meta.ts +534 -0
- package/src/commands/milestone.ts +163 -0
- package/src/commands/notification.ts +110 -0
- package/src/commands/open.ts +84 -0
- package/src/commands/organization.ts +79 -0
- package/src/commands/project-update.ts +47 -0
- package/src/commands/project.ts +339 -0
- package/src/commands/roadmap.ts +131 -0
- package/src/commands/state.ts +58 -0
- package/src/commands/team.ts +243 -0
- package/src/commands/user.ts +107 -0
- package/src/commands/webhook.ts +156 -0
- package/src/config.ts +1236 -0
- package/src/context.ts +117 -0
- package/src/git.ts +174 -0
- package/src/index.ts +10 -0
- package/src/lib/action.ts +26 -0
- package/src/lib/body.ts +172 -0
- package/src/lib/errors.ts +195 -0
- package/src/lib/introspect.ts +150 -0
- package/src/lib/keyring.ts +270 -0
- package/src/lib/mutation.ts +53 -0
- package/src/lib/open.ts +32 -0
- package/src/lib/options.ts +640 -0
- package/src/lib/output-shapes.ts +562 -0
- package/src/lib/pagination.ts +145 -0
- package/src/lib/prompt.ts +91 -0
- package/src/lib/resolve.ts +596 -0
- package/src/lib/shape.ts +273 -0
- package/src/lib/status-update.ts +91 -0
- package/src/lib/upload.ts +329 -0
- package/src/output/color.ts +52 -0
- package/src/output/format.ts +238 -0
- package/src/output/markdown.ts +133 -0
- package/src/output/pager.ts +131 -0
- package/src/output/sanitize.ts +76 -0
- package/src/output/table.ts +198 -0
- package/src/services/agent-session.ts +289 -0
- package/src/services/attachment.ts +198 -0
- package/src/services/comment.ts +262 -0
- package/src/services/cycle.ts +224 -0
- package/src/services/document.ts +357 -0
- package/src/services/favorite.ts +147 -0
- package/src/services/initiative-update.ts +59 -0
- package/src/services/initiative.ts +449 -0
- package/src/services/issue.ts +1153 -0
- package/src/services/label.ts +193 -0
- package/src/services/milestone.ts +262 -0
- package/src/services/notification.ts +190 -0
- package/src/services/organization.ts +144 -0
- package/src/services/project-update.ts +36 -0
- package/src/services/project.ts +494 -0
- package/src/services/roadmap.ts +177 -0
- package/src/services/state.ts +127 -0
- package/src/services/team.ts +386 -0
- package/src/services/user.ts +137 -0
- package/src/services/webhook.ts +203 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `linear project` (alias `p`) — work with projects.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Command, Option } from "commander";
|
|
6
|
+
import { action } from "../lib/action.js";
|
|
7
|
+
import { parseList, parseIntOption, addAliasOption, readAlias } from "../lib/options.js";
|
|
8
|
+
import { usageError } from "../lib/errors.js";
|
|
9
|
+
import { resolveBody } from "../lib/body.js";
|
|
10
|
+
import { confirmDestructive, promptInput } from "../lib/prompt.js";
|
|
11
|
+
import type { Context } from "../context.js";
|
|
12
|
+
import * as svc from "../services/project.js";
|
|
13
|
+
import { formatMilestoneProgress } from "./milestone.js";
|
|
14
|
+
import type { Column } from "../output/table.js";
|
|
15
|
+
|
|
16
|
+
const ROW_COLUMNS: Column<svc.ProjectRow>[] = [
|
|
17
|
+
{ key: "name", header: "Name", value: (r) => r.name, max: 36 },
|
|
18
|
+
{ key: "state", header: "State", value: (r) => r.status?.name ?? r.state ?? "—", max: 14 },
|
|
19
|
+
{ key: "progress", header: "Progress", value: (r) => formatProgress(r.progress) },
|
|
20
|
+
{ key: "lead", header: "Lead", value: (r) => r.lead?.displayName ?? "—", max: 16 },
|
|
21
|
+
{ key: "target", header: "Target", value: (r) => r.targetDate ?? "—" },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
function formatProgress(p: number | null): string {
|
|
25
|
+
if (p === null || p === undefined) return "—";
|
|
26
|
+
return `${Math.round(p * 100)}%`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A team-scoped listing with no team to scope to — none passed, none
|
|
31
|
+
* configured, not `--all-teams` — lists the whole workspace. That is by design
|
|
32
|
+
* (a command named "list" should list; see MIGRATING.md §6). But
|
|
33
|
+
* schpet/linear-cli *errors* in that situation (`No default team…`, T
|
|
34
|
+
* `issue-mine.ts:184-193`), so someone arriving from it can read a
|
|
35
|
+
* workspace-wide result as the team's (TES-637 item 8). Say what happened, on
|
|
36
|
+
* stderr, once; `--quiet` silences it, `--json` stdout never carries it.
|
|
37
|
+
* Shared by `project list` and the issue queries.
|
|
38
|
+
*/
|
|
39
|
+
export function noteWorkspaceWide(
|
|
40
|
+
ctx: Context,
|
|
41
|
+
opts: { team?: unknown; allTeams?: boolean },
|
|
42
|
+
): void {
|
|
43
|
+
const teamGiven = Array.isArray(opts.team) ? opts.team.length > 0 : opts.team !== undefined;
|
|
44
|
+
if (!teamGiven && !opts.allTeams && !ctx.defaultTeam) {
|
|
45
|
+
ctx.output.info(
|
|
46
|
+
"No default team configured; listing every team's. Pass --team <KEY> (or set `team` in .linear.toml) to narrow.",
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function registerProject(program: Command): void {
|
|
52
|
+
const project = program.command("project").alias("p").description("Work with projects");
|
|
53
|
+
|
|
54
|
+
// list --------------------------------------------------------------------
|
|
55
|
+
const list = project
|
|
56
|
+
.command("list")
|
|
57
|
+
.alias("ls")
|
|
58
|
+
.description("List projects with filters (the default team's unless --all-teams)")
|
|
59
|
+
.option("--state <name>", "filter by status name or type (e.g. 'In QA', started)")
|
|
60
|
+
.option("--all-teams", "every team's projects, ignoring the default team")
|
|
61
|
+
.action(
|
|
62
|
+
action(async (ctx: Context, opts) => {
|
|
63
|
+
// `--team` here is the global; passing it alongside `--all-teams` asks
|
|
64
|
+
// for two different scopes at once.
|
|
65
|
+
if (opts.allTeams && opts.team !== undefined) {
|
|
66
|
+
throw usageError("Pass either --team or --all-teams, not both.");
|
|
67
|
+
}
|
|
68
|
+
noteWorkspaceWide(ctx, opts);
|
|
69
|
+
const rows = await svc.listProjects(
|
|
70
|
+
ctx.client,
|
|
71
|
+
{
|
|
72
|
+
team: opts.allTeams ? undefined : (opts.team ?? ctx.defaultTeam),
|
|
73
|
+
allTeams: !!opts.allTeams,
|
|
74
|
+
state: readAlias(opts, "--state", "--status"),
|
|
75
|
+
},
|
|
76
|
+
ctx.limit,
|
|
77
|
+
ctx.defaultTeam,
|
|
78
|
+
);
|
|
79
|
+
ctx.output.list(rows, ROW_COLUMNS, rows);
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
82
|
+
// `--status` is the reference CLI's spelling for the same thing; it shipped
|
|
83
|
+
// earlier as a visible duplicate and now goes through the shared alias
|
|
84
|
+
// mechanism, so `--help` shows one canonical spelling here like everywhere else.
|
|
85
|
+
addAliasOption(list, "--status <name>", "--state");
|
|
86
|
+
|
|
87
|
+
// view --------------------------------------------------------------------
|
|
88
|
+
project
|
|
89
|
+
.command("view <id>", { isDefault: true })
|
|
90
|
+
.alias("show")
|
|
91
|
+
.description("Show a project (by name or id)")
|
|
92
|
+
.action(
|
|
93
|
+
action(async (ctx: Context, _opts, idArg: string) => {
|
|
94
|
+
const detail = await svc.getProjectDetail(ctx.client, idArg);
|
|
95
|
+
ctx.output.detail(detail, [
|
|
96
|
+
["Project", detail.name],
|
|
97
|
+
["Archived", detail.archivedAt ? `YES (${detail.archivedAt})` : null],
|
|
98
|
+
["State", detail.status?.name ?? detail.state],
|
|
99
|
+
["Health", detail.health],
|
|
100
|
+
["Progress", detail.progress !== null ? formatProgress(detail.progress) : null],
|
|
101
|
+
["Priority", detail.priorityLabel],
|
|
102
|
+
["Lead", detail.lead?.displayName ?? null],
|
|
103
|
+
[
|
|
104
|
+
"Teams",
|
|
105
|
+
detail.teams.length ? detail.teams.map((t) => `${t.key} ${t.name}`).join(", ") : null,
|
|
106
|
+
],
|
|
107
|
+
[
|
|
108
|
+
"Members",
|
|
109
|
+
detail.members.length ? detail.members.map((m) => m.displayName).join(", ") : null,
|
|
110
|
+
],
|
|
111
|
+
["Labels", detail.labels.length ? detail.labels.map((l) => l.name).join(", ") : null],
|
|
112
|
+
["Start", detail.startDate],
|
|
113
|
+
["Target", detail.targetDate],
|
|
114
|
+
["URL", detail.url],
|
|
115
|
+
["Updated", detail.updatedAt],
|
|
116
|
+
["Description", detail.description ? `\n${detail.description}` : null],
|
|
117
|
+
["Content", detail.content ? `\n${detail.content}` : null],
|
|
118
|
+
]);
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
// create ------------------------------------------------------------------
|
|
123
|
+
const create = project
|
|
124
|
+
.command("create")
|
|
125
|
+
.alias("new")
|
|
126
|
+
.description("Create a new project")
|
|
127
|
+
.option("--name <name>", "project name")
|
|
128
|
+
.option("-d, --description <text>", "project description (one-line summary)")
|
|
129
|
+
.option("--description-file <path>", "read description from a file ('-' = stdin)")
|
|
130
|
+
.option("--content <text>", "project content (markdown body)")
|
|
131
|
+
.option("--content-file <path>", "read content from a file ('-' = stdin)")
|
|
132
|
+
.option("--teams <key>", "team (repeatable / comma-separated)", parseList)
|
|
133
|
+
// The global `-t/--team` is single-valued (last one wins), and on this
|
|
134
|
+
// command it named the project's team, so schpet's repeatable `--team A
|
|
135
|
+
// --team B` created the project in B alone (TES-637 item 3). Declared
|
|
136
|
+
// locally — which keeps addGlobalOptions from injecting the global — as
|
|
137
|
+
// the same repeatable list `--teams` is, so both spellings collect. Both
|
|
138
|
+
// at once is a usage error (readAlias), not a merge.
|
|
139
|
+
.addOption(
|
|
140
|
+
new Option("-t, --team <key>", "same as --teams (repeatable / comma-separated)").argParser(
|
|
141
|
+
parseList,
|
|
142
|
+
),
|
|
143
|
+
)
|
|
144
|
+
.option("--lead <who>", "project lead (me|email|name|id)")
|
|
145
|
+
.option("--member <who>", "project member (repeatable / comma-separated)", parseList)
|
|
146
|
+
.option("--state <name>", "initial status (name, type, or id)")
|
|
147
|
+
.option("--start <date>", "planned start date (YYYY-MM-DD)")
|
|
148
|
+
.option("--target <date>", "planned target date (YYYY-MM-DD)")
|
|
149
|
+
.option("-P, --priority <0-4>", "priority (0 none, 1 urgent … 4 low)", parseIntOption)
|
|
150
|
+
.option("-l, --label <name>", "project label (repeatable / comma-separated)", parseList)
|
|
151
|
+
.option("--icon <name>", "Linear icon name, capitalized (e.g. Rocket)")
|
|
152
|
+
.option("--color <hex>", "project color (e.g. #EB5757)")
|
|
153
|
+
.addHelpText(
|
|
154
|
+
"after",
|
|
155
|
+
[
|
|
156
|
+
"",
|
|
157
|
+
"Examples:",
|
|
158
|
+
" linear project create --name 'Q3 Launch' --teams TES --lead me",
|
|
159
|
+
" linear project create --name Roadmap --teams TES,ENG --target 2026-09-30",
|
|
160
|
+
" linear project create --name API --team TES --team ENG # same as --teams TES,ENG",
|
|
161
|
+
" linear project create --name API --teams TES --json | jq -r '.id'",
|
|
162
|
+
"",
|
|
163
|
+
"Files: --description-file / --content-file read from a file. -f is the global",
|
|
164
|
+
"--fields (a column selector) everywhere in this CLI and is refused here.",
|
|
165
|
+
].join("\n"),
|
|
166
|
+
)
|
|
167
|
+
.action(
|
|
168
|
+
action(async (ctx: Context, opts) => {
|
|
169
|
+
// `--team` (local, repeatable) and `--teams` are one list under two
|
|
170
|
+
// spellings; validated before the name prompt so a bad pair fails first.
|
|
171
|
+
const teams = readAlias<string[]>(opts, "--teams", "--team");
|
|
172
|
+
let name: string | undefined = opts.name;
|
|
173
|
+
if (!name) name = await promptInput(ctx, "Name:", { required: true });
|
|
174
|
+
const description = resolveBody({
|
|
175
|
+
arg: opts.description,
|
|
176
|
+
file: opts.descriptionFile,
|
|
177
|
+
interactive: false,
|
|
178
|
+
});
|
|
179
|
+
const content = resolveBody({
|
|
180
|
+
arg: opts.content,
|
|
181
|
+
file: opts.contentFile,
|
|
182
|
+
interactive: false,
|
|
183
|
+
});
|
|
184
|
+
const created = await svc.createProject(
|
|
185
|
+
ctx.client,
|
|
186
|
+
{
|
|
187
|
+
name,
|
|
188
|
+
description,
|
|
189
|
+
content,
|
|
190
|
+
team: teams,
|
|
191
|
+
lead: opts.lead,
|
|
192
|
+
member: opts.member,
|
|
193
|
+
state: opts.state,
|
|
194
|
+
startDate: readAlias(opts, "--start", "--start-date"),
|
|
195
|
+
targetDate: readAlias(opts, "--target", "--target-date"),
|
|
196
|
+
priority: opts.priority,
|
|
197
|
+
label: opts.label,
|
|
198
|
+
icon: opts.icon,
|
|
199
|
+
color: opts.color,
|
|
200
|
+
},
|
|
201
|
+
ctx.defaultTeam,
|
|
202
|
+
);
|
|
203
|
+
ctx.output.emit({ id: created.id, name: created.name, url: created.url }, () =>
|
|
204
|
+
ctx.output.success(`Created ${created.name}: ${created.url}`),
|
|
205
|
+
);
|
|
206
|
+
}),
|
|
207
|
+
);
|
|
208
|
+
addAliasOption(create, "--start-date <date>", "--start");
|
|
209
|
+
addAliasOption(create, "--target-date <date>", "--target");
|
|
210
|
+
|
|
211
|
+
// update ------------------------------------------------------------------
|
|
212
|
+
const update = project
|
|
213
|
+
.command("update <id>")
|
|
214
|
+
.alias("edit")
|
|
215
|
+
.description("Update a project")
|
|
216
|
+
.option("--name <name>", "new name")
|
|
217
|
+
.option("-d, --description <text>", "new description (one-line summary)")
|
|
218
|
+
.option("--description-file <path>", "read description from a file ('-' = stdin)")
|
|
219
|
+
.option("--content <text>", "new content (markdown body)")
|
|
220
|
+
.option("--content-file <path>", "read content from a file ('-' = stdin)")
|
|
221
|
+
.option("--teams <key>", "set teams (repeatable / comma-separated)", parseList)
|
|
222
|
+
.option("--lead <who>", "project lead (me|email|name|id)")
|
|
223
|
+
.option("--member <who>", "replace the members (repeatable / comma-separated)", parseList)
|
|
224
|
+
.option("--state <name>", "status (name, type, or id)")
|
|
225
|
+
.option("--start <date>", "planned start date (YYYY-MM-DD)")
|
|
226
|
+
.option("--target <date>", "planned target date (YYYY-MM-DD)")
|
|
227
|
+
.option("-P, --priority <0-4>", "priority (0 none, 1 urgent … 4 low)", parseIntOption)
|
|
228
|
+
.option("-l, --label <name>", "replace the labels (repeatable / comma-separated)", parseList)
|
|
229
|
+
.option("--icon <name>", "Linear icon name, capitalized (e.g. Rocket)")
|
|
230
|
+
.option("--color <hex>", "project color (e.g. #EB5757)")
|
|
231
|
+
// `-t/--team` is a global on every other command, so it is the flag a user
|
|
232
|
+
// reaches for here too — and it did nothing at all: `project update --team X`
|
|
233
|
+
// alone reported "Nothing to update", and alongside another flag it was
|
|
234
|
+
// dropped without a word. Declaring it locally stops addGlobalOptions from
|
|
235
|
+
// injecting the global, which lets the action reject it by name.
|
|
236
|
+
//
|
|
237
|
+
// Rejecting rather than implementing: a project belongs to *several* teams,
|
|
238
|
+
// and `--teams` REPLACES that whole set. Quietly treating `--team TES` as
|
|
239
|
+
// `--teams TES` would therefore remove every other team from the project —
|
|
240
|
+
// a destructive reading of a flag the user almost certainly meant as "also
|
|
241
|
+
// this team". Hidden, so `--help` and `linear commands --json` advertise
|
|
242
|
+
// only `--teams`, the flag that works.
|
|
243
|
+
.addOption(new Option("-t, --team <key>", "not supported here; use --teams").hideHelp())
|
|
244
|
+
.action(
|
|
245
|
+
action(async (ctx: Context, opts, idArg: string) => {
|
|
246
|
+
if (opts.team !== undefined) {
|
|
247
|
+
throw usageError(
|
|
248
|
+
"--team does not apply to `project update`: a project belongs to several teams. " +
|
|
249
|
+
"Use --teams <key,...> to set them (this replaces the project's current teams).",
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
const description = resolveBody({
|
|
253
|
+
arg: opts.description,
|
|
254
|
+
file: opts.descriptionFile,
|
|
255
|
+
interactive: false,
|
|
256
|
+
});
|
|
257
|
+
const content = resolveBody({
|
|
258
|
+
arg: opts.content,
|
|
259
|
+
file: opts.contentFile,
|
|
260
|
+
interactive: false,
|
|
261
|
+
});
|
|
262
|
+
const updated = await svc.updateProject(ctx.client, idArg, {
|
|
263
|
+
name: opts.name,
|
|
264
|
+
description,
|
|
265
|
+
content,
|
|
266
|
+
team: opts.teams,
|
|
267
|
+
lead: opts.lead,
|
|
268
|
+
member: opts.member,
|
|
269
|
+
state: opts.state,
|
|
270
|
+
startDate: readAlias(opts, "--start", "--start-date"),
|
|
271
|
+
targetDate: readAlias(opts, "--target", "--target-date"),
|
|
272
|
+
priority: opts.priority,
|
|
273
|
+
label: opts.label,
|
|
274
|
+
icon: opts.icon,
|
|
275
|
+
color: opts.color,
|
|
276
|
+
});
|
|
277
|
+
ctx.output.emit({ id: updated.id, name: updated.name, url: updated.url }, () =>
|
|
278
|
+
ctx.output.success(`Updated ${updated.name}`),
|
|
279
|
+
);
|
|
280
|
+
}),
|
|
281
|
+
);
|
|
282
|
+
addAliasOption(update, "--start-date <date>", "--start");
|
|
283
|
+
addAliasOption(update, "--target-date <date>", "--target");
|
|
284
|
+
|
|
285
|
+
// archive -----------------------------------------------------------------
|
|
286
|
+
project
|
|
287
|
+
.command("archive <id>")
|
|
288
|
+
.description("Archive a project")
|
|
289
|
+
.action(
|
|
290
|
+
action(async (ctx: Context, _opts, idArg: string) => {
|
|
291
|
+
const proj = await svc.getProjectDetail(ctx.client, idArg);
|
|
292
|
+
if (!(await confirmDestructive(ctx, `Archive project ${proj.name}?`))) return;
|
|
293
|
+
const archived = await svc.archiveProject(ctx.client, proj.id);
|
|
294
|
+
ctx.output.emit({ id: archived.id, name: archived.name, archived: true }, () =>
|
|
295
|
+
ctx.output.success(`Archived ${archived.name}`),
|
|
296
|
+
);
|
|
297
|
+
}),
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
// delete ------------------------------------------------------------------
|
|
301
|
+
project
|
|
302
|
+
.command("delete <id>")
|
|
303
|
+
.alias("rm")
|
|
304
|
+
.description("Delete (trash) a project — `archive` keeps it, read-only")
|
|
305
|
+
.action(
|
|
306
|
+
action(async (ctx: Context, _opts, idArg: string) => {
|
|
307
|
+
const proj = await svc.getProjectDetail(ctx.client, idArg);
|
|
308
|
+
if (!(await confirmDestructive(ctx, `Delete project ${proj.name}?`))) return;
|
|
309
|
+
const deleted = await svc.deleteProject(ctx.client, proj.id);
|
|
310
|
+
ctx.output.emit({ id: deleted.id, name: deleted.name, deleted: true }, () =>
|
|
311
|
+
ctx.output.success(`Deleted ${deleted.name}`),
|
|
312
|
+
);
|
|
313
|
+
}),
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
// milestones --------------------------------------------------------------
|
|
317
|
+
project
|
|
318
|
+
.command("milestones <id>")
|
|
319
|
+
.description("List a project's milestones")
|
|
320
|
+
.action(
|
|
321
|
+
action(async (ctx: Context, _opts, idArg: string) => {
|
|
322
|
+
const rows = await svc.listMilestones(ctx.client, idArg, ctx.limit);
|
|
323
|
+
ctx.output.list(
|
|
324
|
+
rows,
|
|
325
|
+
[
|
|
326
|
+
{ key: "name", header: "Name", value: (m) => m.name, max: 40 },
|
|
327
|
+
{ key: "target", header: "Target", value: (m) => m.targetDate ?? "—" },
|
|
328
|
+
// A milestone's progress is already a percentage (TES-648), unlike a project's.
|
|
329
|
+
{
|
|
330
|
+
key: "progress",
|
|
331
|
+
header: "Progress",
|
|
332
|
+
value: (m) => formatMilestoneProgress(m.progress),
|
|
333
|
+
},
|
|
334
|
+
],
|
|
335
|
+
rows,
|
|
336
|
+
);
|
|
337
|
+
}),
|
|
338
|
+
);
|
|
339
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `linear roadmap` (alias `rm`) — work with workspace roadmaps.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
import { action } from "../lib/action.js";
|
|
7
|
+
import { resolveBody } from "../lib/body.js";
|
|
8
|
+
import { confirmDestructive, promptInput } from "../lib/prompt.js";
|
|
9
|
+
import type { Context } from "../context.js";
|
|
10
|
+
import * as svc from "../services/roadmap.js";
|
|
11
|
+
import type { Column } from "../output/table.js";
|
|
12
|
+
|
|
13
|
+
const ROW_COLUMNS: Column<svc.RoadmapRow>[] = [
|
|
14
|
+
{ key: "name", header: "Name", value: (r) => r.name, max: 36 },
|
|
15
|
+
{ key: "description", header: "Description", value: (r) => r.description ?? "—", max: 60 },
|
|
16
|
+
{ key: "id", header: "ID", value: (r) => r.id },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
export function registerRoadmap(program: Command): void {
|
|
20
|
+
const roadmap = program.command("roadmap").alias("rm").description("Work with roadmaps");
|
|
21
|
+
|
|
22
|
+
// list --------------------------------------------------------------------
|
|
23
|
+
roadmap
|
|
24
|
+
.command("list")
|
|
25
|
+
.alias("ls")
|
|
26
|
+
.description("List roadmaps")
|
|
27
|
+
.action(
|
|
28
|
+
action(async (ctx: Context, _opts) => {
|
|
29
|
+
const rows = await svc.listRoadmaps(ctx.client, ctx.limit);
|
|
30
|
+
ctx.output.list(rows, ROW_COLUMNS, rows);
|
|
31
|
+
}),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// view --------------------------------------------------------------------
|
|
35
|
+
roadmap
|
|
36
|
+
.command("view <id>", { isDefault: true })
|
|
37
|
+
.alias("show")
|
|
38
|
+
.description("Show a roadmap (by name or id)")
|
|
39
|
+
.action(
|
|
40
|
+
action(async (ctx: Context, _opts, idArg: string) => {
|
|
41
|
+
const detail = await svc.getRoadmapDetail(ctx.client, idArg);
|
|
42
|
+
ctx.output.detail(detail, [
|
|
43
|
+
["Roadmap", detail.name],
|
|
44
|
+
["Owner", detail.owner],
|
|
45
|
+
["Creator", detail.creator],
|
|
46
|
+
["Color", detail.color],
|
|
47
|
+
["Projects", detail.projects.length ? detail.projects.join(", ") : null],
|
|
48
|
+
["URL", detail.url],
|
|
49
|
+
["Updated", detail.updatedAt],
|
|
50
|
+
["ID", detail.id],
|
|
51
|
+
["Description", detail.description ? `\n${detail.description}` : null],
|
|
52
|
+
]);
|
|
53
|
+
}),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// create ------------------------------------------------------------------
|
|
57
|
+
roadmap
|
|
58
|
+
.command("create")
|
|
59
|
+
.alias("new")
|
|
60
|
+
.description("Create a new roadmap")
|
|
61
|
+
.option("--name <name>", "roadmap name")
|
|
62
|
+
.option("-d, --description <text>", "roadmap description")
|
|
63
|
+
.option("--description-file <path>", "read description from a file ('-' = stdin)")
|
|
64
|
+
.option("--owner <who>", "roadmap owner (me|email|name|id)")
|
|
65
|
+
.option("--color <hex>", "roadmap color (e.g. #5e6ad2)")
|
|
66
|
+
.action(
|
|
67
|
+
action(async (ctx: Context, opts) => {
|
|
68
|
+
let name: string | undefined = opts.name;
|
|
69
|
+
if (!name) name = await promptInput(ctx, "Name:", { required: true });
|
|
70
|
+
const description = resolveBody({
|
|
71
|
+
arg: opts.description,
|
|
72
|
+
file: opts.descriptionFile,
|
|
73
|
+
interactive: false,
|
|
74
|
+
});
|
|
75
|
+
const created = await svc.createRoadmap(ctx.client, {
|
|
76
|
+
name,
|
|
77
|
+
description,
|
|
78
|
+
owner: opts.owner,
|
|
79
|
+
color: opts.color,
|
|
80
|
+
});
|
|
81
|
+
ctx.output.emit({ id: created.id, name: created.name, url: created.url }, () =>
|
|
82
|
+
ctx.output.success(`Created ${created.name}: ${created.url}`),
|
|
83
|
+
);
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// update ------------------------------------------------------------------
|
|
88
|
+
roadmap
|
|
89
|
+
.command("update <id>")
|
|
90
|
+
.alias("edit")
|
|
91
|
+
.description("Update a roadmap")
|
|
92
|
+
.option("--name <name>", "new name")
|
|
93
|
+
.option("-d, --description <text>", "new description")
|
|
94
|
+
.option("--description-file <path>", "read description from a file ('-' = stdin)")
|
|
95
|
+
.option("--owner <who>", "roadmap owner (me|email|name|id)")
|
|
96
|
+
.option("--color <hex>", "roadmap color (e.g. #5e6ad2)")
|
|
97
|
+
.action(
|
|
98
|
+
action(async (ctx: Context, opts, idArg: string) => {
|
|
99
|
+
const description = resolveBody({
|
|
100
|
+
arg: opts.description,
|
|
101
|
+
file: opts.descriptionFile,
|
|
102
|
+
interactive: false,
|
|
103
|
+
});
|
|
104
|
+
const updated = await svc.updateRoadmap(ctx.client, idArg, {
|
|
105
|
+
name: opts.name,
|
|
106
|
+
description,
|
|
107
|
+
owner: opts.owner,
|
|
108
|
+
color: opts.color,
|
|
109
|
+
});
|
|
110
|
+
ctx.output.emit({ id: updated.id, name: updated.name, url: updated.url }, () =>
|
|
111
|
+
ctx.output.success(`Updated ${updated.name}`),
|
|
112
|
+
);
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
// delete ------------------------------------------------------------------
|
|
117
|
+
roadmap
|
|
118
|
+
.command("delete <id>")
|
|
119
|
+
.alias("rm")
|
|
120
|
+
.description("Delete a roadmap")
|
|
121
|
+
.action(
|
|
122
|
+
action(async (ctx: Context, _opts, idArg: string) => {
|
|
123
|
+
const rm = await svc.getRoadmapDetail(ctx.client, idArg);
|
|
124
|
+
if (!(await confirmDestructive(ctx, `Delete roadmap ${rm.name}?`))) return;
|
|
125
|
+
const deleted = await svc.deleteRoadmap(ctx.client, rm.id);
|
|
126
|
+
ctx.output.emit({ id: deleted.id, name: deleted.name, deleted: true }, () =>
|
|
127
|
+
ctx.output.success(`Deleted ${deleted.name}`),
|
|
128
|
+
);
|
|
129
|
+
}),
|
|
130
|
+
);
|
|
131
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `linear state` (alias `st`) — inspect a team's workflow states (read-only).
|
|
3
|
+
*
|
|
4
|
+
* The team argument is optional on `list` and falls back to the configured
|
|
5
|
+
* default team (ctx.defaultTeam) when omitted. `view` takes a state id, or a
|
|
6
|
+
* state name/type resolved against `--team` / the default team.
|
|
7
|
+
* Creating/editing workflow states is admin-ish and intentionally out of scope.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Command } from "commander";
|
|
11
|
+
import { action } from "../lib/action.js";
|
|
12
|
+
import type { Context } from "../context.js";
|
|
13
|
+
import * as svc from "../services/state.js";
|
|
14
|
+
import type { Column } from "../output/table.js";
|
|
15
|
+
|
|
16
|
+
const ROW_COLUMNS: Column<svc.StateRow>[] = [
|
|
17
|
+
{ key: "name", header: "Name", value: (r) => r.name, max: 24 },
|
|
18
|
+
{ key: "type", header: "Type", value: (r) => r.type },
|
|
19
|
+
{ key: "position", header: "Pos", value: (r) => r.position },
|
|
20
|
+
{ key: "color", header: "Color", value: (r) => r.color },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
export function registerState(program: Command): void {
|
|
24
|
+
const state = program.command("state").alias("st").description("Inspect workflow states");
|
|
25
|
+
|
|
26
|
+
// list --------------------------------------------------------------------
|
|
27
|
+
state
|
|
28
|
+
.command("list [team]")
|
|
29
|
+
.alias("ls")
|
|
30
|
+
.description("List a team's workflow states (defaults to the configured team)")
|
|
31
|
+
.action(
|
|
32
|
+
action(async (ctx: Context, _opts, teamArg?: string) => {
|
|
33
|
+
const rows = await svc.listStates(ctx.client, teamArg, ctx.defaultTeam, ctx.limit);
|
|
34
|
+
ctx.output.list(rows, ROW_COLUMNS, rows);
|
|
35
|
+
}),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
// view --------------------------------------------------------------------
|
|
39
|
+
state
|
|
40
|
+
.command("view <id>")
|
|
41
|
+
.description("Show a workflow state (by id, or by name/type within --team / the default team)")
|
|
42
|
+
.action(
|
|
43
|
+
action(async (ctx: Context, _opts, id: string) => {
|
|
44
|
+
const detail = await svc.getStateDetail(ctx.client, id, ctx.defaultTeam);
|
|
45
|
+
ctx.output.detail(detail, [
|
|
46
|
+
["State", detail.name],
|
|
47
|
+
["ID", detail.id],
|
|
48
|
+
["Type", detail.type],
|
|
49
|
+
["Position", detail.position],
|
|
50
|
+
["Color", detail.color],
|
|
51
|
+
["Team", detail.team],
|
|
52
|
+
["Description", detail.description ? `\n${detail.description}` : null],
|
|
53
|
+
["Created", detail.createdAt],
|
|
54
|
+
["Updated", detail.updatedAt],
|
|
55
|
+
]);
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
}
|