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,211 @@
1
+ /**
2
+ * `linear document` (alias `doc`) — work with documents.
3
+ *
4
+ * A document is attached to exactly one target: a project, issue, initiative,
5
+ * team, cycle or release. `create` needs one (`--project`, `--issue`,
6
+ * `--initiative`, `--team`, `--cycle`, `--release`; the configured team is the
7
+ * fallback), `list` can filter by one, and `update` can re-point to one.
8
+ * `--team` is the global flag: alone it names the team; with `--cycle` it
9
+ * scopes the cycle lookup, as on the issue commands.
10
+ */
11
+
12
+ import { Command } from "commander";
13
+ import { action } from "../lib/action.js";
14
+ import { resolveBody } from "../lib/body.js";
15
+ import { confirmDestructive, promptInput } from "../lib/prompt.js";
16
+ import { CYCLE_FLAG } from "../lib/options.js";
17
+ import { firstTeam, type Context } from "../context.js";
18
+ import * as svc from "../services/document.js";
19
+ import type { Column } from "../output/table.js";
20
+
21
+ const ROW_COLUMNS: Column<svc.DocumentRow>[] = [
22
+ { key: "title", header: "Title", value: (r) => r.title, max: 50 },
23
+ { key: "target", header: "Attached to", value: (r) => svc.describeTarget(r) ?? "—", max: 40 },
24
+ { key: "updatedAt", header: "Updated", value: (r) => r.updatedAt.slice(0, 10) },
25
+ ];
26
+
27
+ /**
28
+ * The six target flags, on `create`/`list`/`update` alike, worded for the verb.
29
+ * `--team` is not declared here: it is the global `-t, --team`, read back off
30
+ * the merged options so it works in either position (`linear -t ENG doc …`).
31
+ */
32
+ function addTargetOptions(cmd: Command, verb: string): Command {
33
+ return cmd
34
+ .option("-p, --project <name>", `${verb} a project (name or id)`)
35
+ .option("--issue <id>", `${verb} an issue (identifier or id)`)
36
+ .option("--initiative <name>", `${verb} an initiative (name or id)`)
37
+ .option(
38
+ CYCLE_FLAG,
39
+ `${verb} a cycle (number, name, id, or 'current'; team from --team or config)`,
40
+ )
41
+ .option("--release <name>", `${verb} a release (name, version, or id)`);
42
+ }
43
+
44
+ /** The target flags as typed, with `--team` taken from wherever on the command line it sat. */
45
+ function targetOptions(ctx: Context, opts: Record<string, any>): svc.DocumentTargetOptions {
46
+ return {
47
+ project: opts.project,
48
+ issue: opts.issue,
49
+ initiative: opts.initiative,
50
+ team: firstTeam(ctx.options.team),
51
+ cycle: opts.cycle,
52
+ release: opts.release,
53
+ };
54
+ }
55
+
56
+ export function registerDocument(program: Command): void {
57
+ const document = program.command("document").alias("doc").description("Work with documents");
58
+
59
+ // list --------------------------------------------------------------------
60
+ const list = document
61
+ .command("list")
62
+ .alias("ls")
63
+ .description("List workspace documents (optionally only those attached to one target)")
64
+ .addHelpText(
65
+ "after",
66
+ [
67
+ "",
68
+ "Examples:",
69
+ " linear document list --project Roadmap",
70
+ " linear document list --issue TES-42",
71
+ " linear document list --team ENG # the team's own documents",
72
+ " linear document list --cycle current # in the configured team's active cycle",
73
+ ].join("\n"),
74
+ )
75
+ .action(
76
+ action(async (ctx: Context, opts) => {
77
+ // A configured default team does not filter here — documents are
78
+ // workspace-wide, and hiding project documents behind a repo's team
79
+ // would be a surprise. Only an explicit --team narrows.
80
+ const rows = await svc.listDocuments(
81
+ ctx.client,
82
+ ctx.limit,
83
+ targetOptions(ctx, opts),
84
+ ctx.config.team,
85
+ );
86
+ ctx.output.list(rows, ROW_COLUMNS, rows);
87
+ }),
88
+ );
89
+ addTargetOptions(list, "only documents attached to");
90
+
91
+ // view --------------------------------------------------------------------
92
+ document
93
+ .command("view <id>", { isDefault: true })
94
+ .alias("show")
95
+ .description("Show a document, including its markdown content")
96
+ .action(
97
+ action(async (ctx: Context, _opts, idArg: string) => {
98
+ const detail = await svc.getDocumentDetail(ctx.client, idArg);
99
+ ctx.output.detail(detail, [
100
+ ["Document", detail.title],
101
+ ["Attached to", svc.describeTarget(detail)],
102
+ ["Creator", detail.creator?.displayName ?? null],
103
+ ["URL", detail.url],
104
+ ["Updated", detail.updatedAt],
105
+ ["Content", detail.content ? `\n${detail.content}` : null],
106
+ ]);
107
+ }),
108
+ );
109
+
110
+ // create ------------------------------------------------------------------
111
+ const create = document
112
+ .command("create")
113
+ .alias("new")
114
+ .description(
115
+ "Create a new document, attached to one target (--project, --issue, --initiative, --team, --cycle, or --release; default: the configured team)",
116
+ )
117
+ .option("--title <title>", "document title")
118
+ .option("--content <text>", "document content (markdown body)")
119
+ .option("--content-file <path>", "read content from a file ('-' = stdin)")
120
+ .addHelpText(
121
+ "after",
122
+ [
123
+ "",
124
+ "Examples:",
125
+ " linear document create --title 'Spec' --project Roadmap --content-file spec.md",
126
+ " linear document create --title 'Notes' --issue TES-42",
127
+ " linear document create --title 'Retro' --cycle current --team ENG",
128
+ " linear document create --title 'Handbook' # attached to the configured team",
129
+ ].join("\n"),
130
+ )
131
+ .action(
132
+ action(async (ctx: Context, opts) => {
133
+ // Validate the target flags before the title prompt or any file read.
134
+ const target = targetOptions(ctx, opts);
135
+ // The configured team is the fallback target — for create only — when
136
+ // no target flag names one; it never overrides one that does.
137
+ if (!svc.selectTarget(target) && ctx.config.team) target.team = ctx.config.team;
138
+ let title: string | undefined = opts.title;
139
+ if (!title) title = await promptInput(ctx, "Title:", { required: true });
140
+ const content = resolveBody({
141
+ arg: opts.content,
142
+ file: opts.contentFile,
143
+ interactive: false,
144
+ });
145
+ const created = await svc.createDocument(
146
+ ctx.client,
147
+ { title, content, ...target },
148
+ ctx.config.team,
149
+ );
150
+ ctx.output.emit({ id: created.id, title: created.title, url: created.url }, () =>
151
+ ctx.output.success(`Created ${created.title}: ${created.url}`),
152
+ );
153
+ }),
154
+ );
155
+ addTargetOptions(create, "attach to");
156
+
157
+ // update ------------------------------------------------------------------
158
+ const update = document
159
+ .command("update <id>")
160
+ .alias("edit")
161
+ .description("Update a document's title or content, or re-point it to another target")
162
+ .option("--title <title>", "new title")
163
+ .option("--content <text>", "new content (markdown body)")
164
+ .option("--content-file <path>", "read content from a file ('-' = stdin)")
165
+ .addHelpText(
166
+ "after",
167
+ [
168
+ "",
169
+ "Examples:",
170
+ " linear document update <id> --title 'Spec v2'",
171
+ " linear document update <id> --content-file spec.md",
172
+ " linear document update <id> --issue TES-42 # move it onto the issue (replaces the old target)",
173
+ ].join("\n"),
174
+ )
175
+ .action(
176
+ action(async (ctx: Context, opts, idArg: string) => {
177
+ const content = resolveBody({
178
+ arg: opts.content,
179
+ file: opts.contentFile,
180
+ interactive: false,
181
+ });
182
+ // Only the explicit --team re-points; a configured team never does.
183
+ const updated = await svc.updateDocument(
184
+ ctx.client,
185
+ idArg,
186
+ { title: opts.title, content, ...targetOptions(ctx, opts) },
187
+ ctx.config.team,
188
+ );
189
+ ctx.output.emit({ id: updated.id, title: updated.title, url: updated.url }, () =>
190
+ ctx.output.success(`Updated ${updated.title}`),
191
+ );
192
+ }),
193
+ );
194
+ addTargetOptions(update, "re-point to");
195
+
196
+ // delete ------------------------------------------------------------------
197
+ document
198
+ .command("delete <id>")
199
+ .alias("rm")
200
+ .description("Delete (trash) a document")
201
+ .action(
202
+ action(async (ctx: Context, _opts, idArg: string) => {
203
+ const doc = await svc.getDocumentDetail(ctx.client, idArg);
204
+ if (!(await confirmDestructive(ctx, `Delete document ${doc.title}?`))) return;
205
+ const deleted = await svc.deleteDocument(ctx.client, doc.id);
206
+ ctx.output.emit({ id: deleted.id, title: deleted.title, deleted: true }, () =>
207
+ ctx.output.success(`Deleted ${deleted.title}`),
208
+ );
209
+ }),
210
+ );
211
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * `linear favorite` (alias `fav`) — manage the viewer's favorites.
3
+ *
4
+ * `list` shows each favorite's type and the referenced thing's title. `add`
5
+ * takes exactly one of --issue / --project / --document. `remove` deletes a
6
+ * favorite by its id (confirmed when destructive).
7
+ */
8
+
9
+ import { Command } from "commander";
10
+ import { action } from "../lib/action.js";
11
+ import { confirmDestructive } from "../lib/prompt.js";
12
+ import type { Context } from "../context.js";
13
+ import * as svc from "../services/favorite.js";
14
+ import type { Column } from "../output/table.js";
15
+
16
+ const ROW_COLUMNS: Column<svc.FavoriteRow>[] = [
17
+ { key: "type", header: "Type", value: (r) => r.type, max: 16 },
18
+ { key: "name", header: "Name", value: (r) => r.name, max: 60 },
19
+ { key: "id", header: "ID", value: (r) => r.id },
20
+ ];
21
+
22
+ export function registerFavorite(program: Command): void {
23
+ const favorite = program.command("favorite").alias("fav").description("Manage your favorites");
24
+
25
+ // list --------------------------------------------------------------------
26
+ favorite
27
+ .command("list")
28
+ .alias("ls")
29
+ .description("List your favorites")
30
+ .action(
31
+ action(async (ctx: Context) => {
32
+ const rows = await svc.listFavorites(ctx.client, ctx.limit);
33
+ ctx.output.list(rows, ROW_COLUMNS, rows);
34
+ }),
35
+ );
36
+
37
+ // add ---------------------------------------------------------------------
38
+ favorite
39
+ .command("add")
40
+ .alias("new")
41
+ .description("Favorite an issue, project, or document (exactly one)")
42
+ .option("--issue <id>", "issue id or identifier (e.g. TES-123)")
43
+ .option("--project <id|name>", "project name or id")
44
+ .option("--document <id>", "document id (UUID)")
45
+ .action(
46
+ action(async (ctx: Context, opts) => {
47
+ const created = await svc.addFavorite(ctx.client, {
48
+ issue: opts.issue,
49
+ project: opts.project,
50
+ document: opts.document,
51
+ });
52
+ ctx.output.emit({ id: created.id, type: created.type }, () =>
53
+ ctx.output.success(`Favorited ${created.type} (${created.id})`),
54
+ );
55
+ }),
56
+ );
57
+
58
+ // remove ------------------------------------------------------------------
59
+ favorite
60
+ .command("remove <favoriteId>")
61
+ .alias("rm")
62
+ .description("Remove a favorite by id")
63
+ .action(
64
+ action(async (ctx: Context, _opts, favoriteId: string) => {
65
+ if (!(await confirmDestructive(ctx, `Remove favorite ${favoriteId}?`))) return;
66
+ const removed = await svc.removeFavorite(ctx.client, favoriteId);
67
+ ctx.output.emit({ id: removed.id, removed: true }, () =>
68
+ ctx.output.success(`Removed favorite ${removed.id}`),
69
+ );
70
+ }),
71
+ );
72
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * `linear initiative-update` (alias `iu`) — post and list initiative status updates.
3
+ */
4
+
5
+ import { Command } from "commander";
6
+ import { action } from "../lib/action.js";
7
+ import { addUpdateFlags, resolveUpdateBody, UPDATE_COLUMNS } from "../lib/status-update.js";
8
+ import type { Context } from "../context.js";
9
+ import * as svc from "../services/initiative-update.js";
10
+
11
+ export function registerInitiativeUpdate(program: Command): void {
12
+ const group = program
13
+ .command("initiative-update")
14
+ .alias("iu")
15
+ .description("Post and list initiative status updates");
16
+
17
+ // create ------------------------------------------------------------------
18
+ addUpdateFlags(
19
+ group
20
+ .command("create <initiative>")
21
+ .alias("new")
22
+ .description("Post a status update on an initiative (by name or id)"),
23
+ ).action(
24
+ action(async (ctx: Context, opts, initiativeArg: string) => {
25
+ const body = resolveUpdateBody(ctx, opts);
26
+ const created = await svc.createInitiativeUpdate(ctx.client, initiativeArg, {
27
+ body,
28
+ health: opts.health,
29
+ });
30
+ ctx.output.emit(created, () =>
31
+ ctx.output.success(`Posted initiative update${opts.health ? ` (${opts.health})` : ""}`),
32
+ );
33
+ }),
34
+ );
35
+
36
+ // list --------------------------------------------------------------------
37
+ group
38
+ .command("list <initiative>")
39
+ .alias("ls")
40
+ .description("List an initiative's status updates")
41
+ .action(
42
+ action(async (ctx: Context, _opts, initiativeArg: string) => {
43
+ const rows = await svc.listInitiativeUpdates(ctx.client, initiativeArg, ctx.limit);
44
+ ctx.output.list(rows, UPDATE_COLUMNS, rows);
45
+ }),
46
+ );
47
+ }
@@ -0,0 +1,261 @@
1
+ /**
2
+ * `linear initiative` (alias `init`) — work with workspace initiatives.
3
+ *
4
+ * Initiatives are workspace-scoped (no team). `view`/`update`/`archive`/`delete`
5
+ * take an initiative id (UUID) or name; `create` needs a name and optionally a
6
+ * description, target date, owner, and status.
7
+ */
8
+
9
+ import { Command, Option } from "commander";
10
+ import { action } from "../lib/action.js";
11
+ import { resolveBody } from "../lib/body.js";
12
+ import { confirmDestructive, promptInput } from "../lib/prompt.js";
13
+ import { parseList, parseIntOption, addAliasOption, readAlias } from "../lib/options.js";
14
+ import type { Context } from "../context.js";
15
+ import * as svc from "../services/initiative.js";
16
+ import type { Column } from "../output/table.js";
17
+
18
+ const ROW_COLUMNS: Column<svc.InitiativeRow>[] = [
19
+ { key: "name", header: "Name", value: (r) => r.name, max: 40 },
20
+ { key: "status", header: "Status", value: (r) => r.status ?? "—", max: 12 },
21
+ {
22
+ key: "priority",
23
+ header: "Pri",
24
+ value: (r) => (r.priority ? svc.priorityLabel(r.priority) : "—"),
25
+ },
26
+ { key: "targetDate", header: "Target", value: (r) => r.targetDate ?? "—" },
27
+ { key: "health", header: "Health", value: (r) => r.health ?? "—", max: 12 },
28
+ ];
29
+
30
+ export function registerInitiative(program: Command): void {
31
+ const initiative = program
32
+ .command("initiative")
33
+ .alias("init")
34
+ .description("Work with initiatives");
35
+
36
+ // list --------------------------------------------------------------------
37
+ initiative
38
+ .command("list")
39
+ .alias("ls")
40
+ .description("List workspace initiatives (every status unless --status narrows)")
41
+ // No `-s`/`-o` shorts: `-s` is `--state` and `-o` is `--output` elsewhere
42
+ // in the tree, and a short flag means one thing everywhere here.
43
+ .option("--status <name>", "filter by status (Planned, Active, Completed, Canceled, Proposed)")
44
+ .option("--owner <who>", "filter by owner (me|email|name|id)")
45
+ .option("--archived", "include archived initiatives")
46
+ // The reference CLI lists Active only and needs `--all-statuses` to widen;
47
+ // this list is every status already, so the flag is accepted as the no-op
48
+ // it is here rather than failing a transplanted script.
49
+ .addOption(new Option("--all-statuses", "no-op: the list is all statuses").hideHelp())
50
+ .action(
51
+ action(async (ctx: Context, opts) => {
52
+ const rows = await svc.listInitiatives(ctx.client, ctx.limit, {
53
+ status: opts.status,
54
+ owner: opts.owner,
55
+ archived: !!opts.archived,
56
+ });
57
+ ctx.output.list(rows, ROW_COLUMNS, rows);
58
+ }),
59
+ );
60
+
61
+ // view --------------------------------------------------------------------
62
+ initiative
63
+ .command("view <id>", { isDefault: true })
64
+ .alias("show")
65
+ .description("Show an initiative (by name or id)")
66
+ .action(
67
+ action(async (ctx: Context, _opts, idArg: string) => {
68
+ const detail = await svc.getInitiativeDetail(ctx.client, idArg);
69
+ ctx.output.detail(detail, [
70
+ ["Initiative", detail.name],
71
+ ["Archived", detail.archivedAt ? `YES (${detail.archivedAt})` : null],
72
+ ["Status", detail.status],
73
+ ["Priority", detail.priority ? detail.priorityLabel : null],
74
+ ["Labels", detail.labels.length ? detail.labels.join(", ") : null],
75
+ ["Health", detail.health],
76
+ ["Owner", detail.owner],
77
+ ["Creator", detail.creator],
78
+ ["Target", detail.targetDate],
79
+ ["Icon", detail.icon],
80
+ ["Color", detail.color],
81
+ [
82
+ "Projects",
83
+ detail.projects.length
84
+ ? detail.projects
85
+ .map((p) => `${p.name}${p.status ? ` (${p.status.name})` : ""}`)
86
+ .join(", ")
87
+ : null,
88
+ ],
89
+ ["URL", detail.url],
90
+ ["Updated", detail.updatedAt],
91
+ ["Description", detail.description ? `\n${detail.description}` : null],
92
+ ]);
93
+ }),
94
+ );
95
+
96
+ // create ------------------------------------------------------------------
97
+ const create = initiative
98
+ .command("create")
99
+ .alias("new")
100
+ .description("Create a new initiative")
101
+ .option("--name <name>", "initiative name")
102
+ .option("-d, --description <text>", "initiative description")
103
+ .option("--description-file <path>", "read description from a file ('-' = stdin)")
104
+ .option("--target <date>", "estimated completion date (YYYY-MM-DD)")
105
+ .option("--owner <who>", "initiative owner (me|email|name|id)")
106
+ .option("--status <name>", "status (Planned, Active, Completed, Canceled, Proposed)")
107
+ .option("-P, --priority <0-4>", "priority (0 none, 1 urgent … 4 low)", parseIntOption)
108
+ .option("-l, --label <name>", "initiative label (repeatable / comma-separated)", parseList)
109
+ .option("--icon <name>", "Linear icon name, capitalized (e.g. Rocket)")
110
+ .option("--color <hex>", "initiative color (e.g. #5E6AD2)")
111
+ .action(
112
+ action(async (ctx: Context, opts) => {
113
+ let name: string | undefined = opts.name;
114
+ if (!name) name = await promptInput(ctx, "Name:", { required: true });
115
+ const description = resolveBody({
116
+ arg: opts.description,
117
+ file: opts.descriptionFile,
118
+ interactive: false,
119
+ });
120
+ const created = await svc.createInitiative(ctx.client, {
121
+ name,
122
+ description,
123
+ targetDate: readAlias(opts, "--target", "--target-date"),
124
+ owner: opts.owner,
125
+ status: opts.status,
126
+ priority: opts.priority,
127
+ label: opts.label,
128
+ icon: opts.icon,
129
+ color: opts.color,
130
+ });
131
+ ctx.output.emit({ id: created.id, name: created.name, url: created.url }, () =>
132
+ ctx.output.success(`Created ${created.name}: ${created.url}`),
133
+ );
134
+ }),
135
+ );
136
+ addAliasOption(create, "--target-date <date>", "--target");
137
+
138
+ // update ------------------------------------------------------------------
139
+ const update = initiative
140
+ .command("update <id>")
141
+ .alias("edit")
142
+ .description("Update an initiative")
143
+ .option("--name <name>", "new name")
144
+ .option("-d, --description <text>", "new description")
145
+ .option("--description-file <path>", "read description from a file ('-' = stdin)")
146
+ .option("--target <date>", "estimated completion date (YYYY-MM-DD)")
147
+ .option("--owner <who>", "initiative owner (me|email|name|id)")
148
+ .option("--status <name>", "status (Planned, Active, Completed, Canceled, Proposed)")
149
+ .option("-P, --priority <0-4>", "priority (0 none, 1 urgent … 4 low)", parseIntOption)
150
+ .option("-l, --label <name>", "replace the labels (repeatable / comma-separated)", parseList)
151
+ .option("--icon <name>", "Linear icon name, capitalized (e.g. Rocket)")
152
+ .option("--color <hex>", "initiative color (e.g. #5E6AD2)")
153
+ .action(
154
+ action(async (ctx: Context, opts, idArg: string) => {
155
+ const description = resolveBody({
156
+ arg: opts.description,
157
+ file: opts.descriptionFile,
158
+ interactive: false,
159
+ });
160
+ const updated = await svc.updateInitiative(ctx.client, idArg, {
161
+ name: opts.name,
162
+ description,
163
+ targetDate: readAlias(opts, "--target", "--target-date"),
164
+ owner: opts.owner,
165
+ status: opts.status,
166
+ priority: opts.priority,
167
+ label: opts.label,
168
+ icon: opts.icon,
169
+ color: opts.color,
170
+ });
171
+ ctx.output.emit({ id: updated.id, name: updated.name, url: updated.url }, () =>
172
+ ctx.output.success(`Updated ${updated.name}`),
173
+ );
174
+ }),
175
+ );
176
+ addAliasOption(update, "--target-date <date>", "--target");
177
+
178
+ // archive -----------------------------------------------------------------
179
+ initiative
180
+ .command("archive <id>")
181
+ .description("Archive an initiative")
182
+ .action(
183
+ action(async (ctx: Context, _opts, idArg: string) => {
184
+ const init = await svc.getInitiativeDetail(ctx.client, idArg);
185
+ if (!(await confirmDestructive(ctx, `Archive initiative ${init.name}?`))) return;
186
+ const archived = await svc.archiveInitiative(ctx.client, init.id);
187
+ ctx.output.emit({ id: archived.id, name: archived.name, archived: true }, () =>
188
+ ctx.output.success(`Archived ${archived.name}`),
189
+ );
190
+ }),
191
+ );
192
+
193
+ // unarchive ---------------------------------------------------------------
194
+ initiative
195
+ .command("unarchive <id>")
196
+ .description("Unarchive an initiative")
197
+ .action(
198
+ action(async (ctx: Context, _opts, idArg: string) => {
199
+ const restored = await svc.unarchiveInitiative(ctx.client, idArg);
200
+ ctx.output.emit({ id: restored.id, name: restored.name, archived: false }, () =>
201
+ ctx.output.success(`Unarchived ${restored.name}`),
202
+ );
203
+ }),
204
+ );
205
+
206
+ // add-project / remove-project --------------------------------------------
207
+ initiative
208
+ .command("add-project <initiative> <project>")
209
+ .description("Link a project to an initiative")
210
+ .option("--sort-order <n>", "position among the initiative's projects", parseIntOption)
211
+ .addHelpText(
212
+ "after",
213
+ ["", "Examples:", " linear initiative add-project 'Q3 Bets' 'API v2'"].join("\n"),
214
+ )
215
+ .action(
216
+ action(async (ctx: Context, opts, initiativeArg: string, projectArg: string) => {
217
+ const link = await svc.addProject(ctx.client, initiativeArg, projectArg, {
218
+ sortOrder: opts.sortOrder,
219
+ });
220
+ ctx.output.emit(link, () =>
221
+ ctx.output.success(`Linked ${link.project.name} to ${link.initiative.name}`),
222
+ );
223
+ }),
224
+ );
225
+
226
+ initiative
227
+ .command("remove-project <initiative> <project>")
228
+ .description("Unlink a project from an initiative")
229
+ .action(
230
+ action(async (ctx: Context, _opts, initiativeArg: string, projectArg: string) => {
231
+ const link = await svc.findProjectLink(ctx.client, initiativeArg, projectArg);
232
+ if (
233
+ !(await confirmDestructive(
234
+ ctx,
235
+ `Remove ${link.project.name} from initiative ${link.initiative.name}?`,
236
+ ))
237
+ )
238
+ return;
239
+ await svc.removeProjectLink(ctx.client, link);
240
+ ctx.output.emit({ ...link, removed: true }, () =>
241
+ ctx.output.success(`Removed ${link.project.name} from ${link.initiative.name}`),
242
+ );
243
+ }),
244
+ );
245
+
246
+ // delete ------------------------------------------------------------------
247
+ initiative
248
+ .command("delete <id>")
249
+ .alias("rm")
250
+ .description("Delete (trash) an initiative")
251
+ .action(
252
+ action(async (ctx: Context, _opts, idArg: string) => {
253
+ const init = await svc.getInitiativeDetail(ctx.client, idArg);
254
+ if (!(await confirmDestructive(ctx, `Delete initiative ${init.name}?`))) return;
255
+ const deleted = await svc.deleteInitiative(ctx.client, init.id);
256
+ ctx.output.emit({ id: deleted.id, name: deleted.name, deleted: true }, () =>
257
+ ctx.output.success(`Deleted ${deleted.name}`),
258
+ );
259
+ }),
260
+ );
261
+ }