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.
Files changed (111) hide show
  1. package/.claude-plugin/marketplace.json +20 -0
  2. package/.claude-plugin/plugin.json +9 -0
  3. package/CHANGELOG.md +807 -0
  4. package/COVERAGE.md +495 -0
  5. package/LICENSE +21 -0
  6. package/MIGRATING.md +220 -0
  7. package/README.md +524 -0
  8. package/package.json +87 -0
  9. package/skills/linear-sdk-cli/SKILL.md +534 -0
  10. package/skills/linear-sdk-cli/references/api.md +27 -0
  11. package/skills/linear-sdk-cli/references/attachment.md +82 -0
  12. package/skills/linear-sdk-cli/references/auth.md +174 -0
  13. package/skills/linear-sdk-cli/references/commands.md +37 -0
  14. package/skills/linear-sdk-cli/references/comment.md +154 -0
  15. package/skills/linear-sdk-cli/references/completion.md +17 -0
  16. package/skills/linear-sdk-cli/references/config.md +99 -0
  17. package/skills/linear-sdk-cli/references/cycle.md +131 -0
  18. package/skills/linear-sdk-cli/references/document.md +180 -0
  19. package/skills/linear-sdk-cli/references/favorite.md +78 -0
  20. package/skills/linear-sdk-cli/references/initiative-update.md +67 -0
  21. package/skills/linear-sdk-cli/references/initiative.md +256 -0
  22. package/skills/linear-sdk-cli/references/issue.md +928 -0
  23. package/skills/linear-sdk-cli/references/label.md +112 -0
  24. package/skills/linear-sdk-cli/references/milestone.md +139 -0
  25. package/skills/linear-sdk-cli/references/notification.md +123 -0
  26. package/skills/linear-sdk-cli/references/open.md +29 -0
  27. package/skills/linear-sdk-cli/references/organization.md +99 -0
  28. package/skills/linear-sdk-cli/references/project-update.md +67 -0
  29. package/skills/linear-sdk-cli/references/project.md +234 -0
  30. package/skills/linear-sdk-cli/references/roadmap.md +150 -0
  31. package/skills/linear-sdk-cli/references/schema.md +21 -0
  32. package/skills/linear-sdk-cli/references/state.md +61 -0
  33. package/skills/linear-sdk-cli/references/team.md +227 -0
  34. package/skills/linear-sdk-cli/references/user.md +103 -0
  35. package/skills/linear-sdk-cli/references/webhook.md +151 -0
  36. package/skills/linear-sdk-cli/references/whoami.md +26 -0
  37. package/skills/linear-sdk-cli/scripts/generate-docs.ts +339 -0
  38. package/src/bin/linear.ts +97 -0
  39. package/src/cli.ts +232 -0
  40. package/src/client.ts +172 -0
  41. package/src/commands/agent-session.ts +121 -0
  42. package/src/commands/api.ts +317 -0
  43. package/src/commands/attach.ts +90 -0
  44. package/src/commands/attachment.ts +74 -0
  45. package/src/commands/comment.ts +225 -0
  46. package/src/commands/completion.ts +69 -0
  47. package/src/commands/cycle.ts +125 -0
  48. package/src/commands/discover.ts +170 -0
  49. package/src/commands/document.ts +211 -0
  50. package/src/commands/favorite.ts +72 -0
  51. package/src/commands/initiative-update.ts +47 -0
  52. package/src/commands/initiative.ts +261 -0
  53. package/src/commands/issue.ts +1263 -0
  54. package/src/commands/label.ts +119 -0
  55. package/src/commands/meta.ts +534 -0
  56. package/src/commands/milestone.ts +163 -0
  57. package/src/commands/notification.ts +110 -0
  58. package/src/commands/open.ts +84 -0
  59. package/src/commands/organization.ts +79 -0
  60. package/src/commands/project-update.ts +47 -0
  61. package/src/commands/project.ts +339 -0
  62. package/src/commands/roadmap.ts +131 -0
  63. package/src/commands/state.ts +58 -0
  64. package/src/commands/team.ts +243 -0
  65. package/src/commands/user.ts +107 -0
  66. package/src/commands/webhook.ts +156 -0
  67. package/src/config.ts +1236 -0
  68. package/src/context.ts +117 -0
  69. package/src/git.ts +174 -0
  70. package/src/index.ts +10 -0
  71. package/src/lib/action.ts +26 -0
  72. package/src/lib/body.ts +172 -0
  73. package/src/lib/errors.ts +195 -0
  74. package/src/lib/introspect.ts +150 -0
  75. package/src/lib/keyring.ts +270 -0
  76. package/src/lib/mutation.ts +53 -0
  77. package/src/lib/open.ts +32 -0
  78. package/src/lib/options.ts +640 -0
  79. package/src/lib/output-shapes.ts +562 -0
  80. package/src/lib/pagination.ts +145 -0
  81. package/src/lib/prompt.ts +91 -0
  82. package/src/lib/resolve.ts +596 -0
  83. package/src/lib/shape.ts +273 -0
  84. package/src/lib/status-update.ts +91 -0
  85. package/src/lib/upload.ts +329 -0
  86. package/src/output/color.ts +52 -0
  87. package/src/output/format.ts +238 -0
  88. package/src/output/markdown.ts +133 -0
  89. package/src/output/pager.ts +131 -0
  90. package/src/output/sanitize.ts +76 -0
  91. package/src/output/table.ts +198 -0
  92. package/src/services/agent-session.ts +289 -0
  93. package/src/services/attachment.ts +198 -0
  94. package/src/services/comment.ts +262 -0
  95. package/src/services/cycle.ts +224 -0
  96. package/src/services/document.ts +357 -0
  97. package/src/services/favorite.ts +147 -0
  98. package/src/services/initiative-update.ts +59 -0
  99. package/src/services/initiative.ts +449 -0
  100. package/src/services/issue.ts +1153 -0
  101. package/src/services/label.ts +193 -0
  102. package/src/services/milestone.ts +262 -0
  103. package/src/services/notification.ts +190 -0
  104. package/src/services/organization.ts +144 -0
  105. package/src/services/project-update.ts +36 -0
  106. package/src/services/project.ts +494 -0
  107. package/src/services/roadmap.ts +177 -0
  108. package/src/services/state.ts +127 -0
  109. package/src/services/team.ts +386 -0
  110. package/src/services/user.ts +137 -0
  111. 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
+ }