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,163 @@
1
+ /**
2
+ * `linear milestone` (alias `m`) — project milestone management.
3
+ *
4
+ * Milestones live inside a project: `list`/`create` take a project reference
5
+ * (name or id), while `view`/`update`/`delete` take a milestone id — or a
6
+ * milestone *name* together with `--project`, since names are unique only
7
+ * within a project.
8
+ */
9
+
10
+ import { Command } from "commander";
11
+ import { action } from "../lib/action.js";
12
+ import { resolveBody } from "../lib/body.js";
13
+ import { confirmDestructive, promptInput } from "../lib/prompt.js";
14
+ import { addAliasOption, readAlias } from "../lib/options.js";
15
+ import type { Context } from "../context.js";
16
+ import * as svc from "../services/milestone.js";
17
+ import type { Column } from "../output/table.js";
18
+
19
+ const ROW_COLUMNS: Column<svc.MilestoneRow>[] = [
20
+ { key: "name", header: "Name", value: (r) => r.name, max: 40 },
21
+ { key: "target", header: "Target", value: (r) => r.targetDate ?? "—" },
22
+ { key: "progress", header: "Progress", value: (r) => formatMilestoneProgress(r.progress) },
23
+ { key: "status", header: "Status", value: (r) => r.status, max: 14 },
24
+ { key: "id", header: "ID", value: (r) => r.id },
25
+ ];
26
+
27
+ /**
28
+ * `ProjectMilestone.progress` is already a percentage — `38.46` for 38%
29
+ * (verified live) — unlike `Project.progress`, which is a fraction. Multiplying
30
+ * it by 100 as the project renderer does printed `3846%`. Exported for tests.
31
+ */
32
+ export function formatMilestoneProgress(progress: number | null): string {
33
+ if (progress === null || progress === undefined) return "—";
34
+ return `${Math.round(progress)}%`;
35
+ }
36
+
37
+ const PROJECT_SCOPE = [
38
+ "-p, --project <name>",
39
+ "the milestone's project, when <id> is a name (names are unique per project only)",
40
+ ] as const;
41
+
42
+ export function registerMilestone(program: Command): void {
43
+ const milestone = program
44
+ .command("milestone")
45
+ .alias("m")
46
+ .description("Work with project milestones");
47
+
48
+ // list --------------------------------------------------------------------
49
+ milestone
50
+ .command("list <project>")
51
+ .alias("ls")
52
+ .description("List milestones in a project")
53
+ .action(
54
+ action(async (ctx: Context, _opts, project: string) => {
55
+ const rows = await svc.listMilestones(ctx.client, project, ctx.limit);
56
+ ctx.output.list(rows, ROW_COLUMNS, rows);
57
+ }),
58
+ );
59
+
60
+ // view --------------------------------------------------------------------
61
+ milestone
62
+ .command("view <id>")
63
+ .description("Show a milestone and the issues in it (by id, or by name with --project)")
64
+ .option(...PROJECT_SCOPE)
65
+ .action(
66
+ action(async (ctx: Context, opts, idArg: string) => {
67
+ const id = await svc.resolveMilestoneRef(ctx.client, idArg, opts.project);
68
+ const detail = await svc.getMilestoneDetail(ctx.client, id, ctx.limit);
69
+ const issueLines = detail.issues.map(
70
+ (i) => ` ${i.identifier} ${i.state ? `[${i.state.name}] ` : ""}${i.title}`,
71
+ );
72
+ if (detail.issuesTruncated) issueLines.push(" … more (use --all)");
73
+ ctx.output.detail(detail, [
74
+ ["Milestone", detail.name],
75
+ ["Project", detail.project?.name ?? null],
76
+ ["Target", detail.targetDate],
77
+ ["Progress", formatMilestoneProgress(detail.progress)],
78
+ ["Status", detail.status],
79
+ ["Updated", detail.updatedAt],
80
+ ["ID", detail.id],
81
+ ["Issues", issueLines.length ? `\n${issueLines.join("\n")}` : null],
82
+ ["Description", detail.description ? `\n${detail.description}` : null],
83
+ ]);
84
+ }),
85
+ );
86
+
87
+ // create ------------------------------------------------------------------
88
+ const create = milestone
89
+ .command("create <project>")
90
+ .alias("new")
91
+ .description("Create a milestone in a project")
92
+ .option("--name <name>", "milestone name")
93
+ .option("-d, --description <text>", "milestone description (body)")
94
+ .option("--description-file <path>", "read description from a file ('-' = stdin)")
95
+ .option("--target <date>", "target date (YYYY-MM-DD)")
96
+ .action(
97
+ action(async (ctx: Context, opts, project: string) => {
98
+ let name: string | undefined = opts.name;
99
+ if (!name) name = await promptInput(ctx, "Name:", { required: true });
100
+ const description = resolveBody({
101
+ arg: opts.description,
102
+ file: opts.descriptionFile,
103
+ interactive: false,
104
+ });
105
+ const created = await svc.createMilestone(ctx.client, project, {
106
+ name,
107
+ description,
108
+ targetDate: readAlias(opts, "--target", "--target-date"),
109
+ });
110
+ ctx.output.emit({ id: created.id, name: created.name }, () =>
111
+ ctx.output.success(`Created milestone ${created.name}`),
112
+ );
113
+ }),
114
+ );
115
+ addAliasOption(create, "--target-date <date>", "--target");
116
+
117
+ // update ------------------------------------------------------------------
118
+ const update = milestone
119
+ .command("update <id>")
120
+ .alias("edit")
121
+ .description("Update a milestone (by id, or by name with --project)")
122
+ .option(...PROJECT_SCOPE)
123
+ .option("--name <name>", "new name")
124
+ .option("-d, --description <text>", "new description")
125
+ .option("--description-file <path>", "read description from a file ('-' = stdin)")
126
+ .option("--target <date>", "target date (YYYY-MM-DD)")
127
+ .action(
128
+ action(async (ctx: Context, opts, idArg: string) => {
129
+ const description = resolveBody({
130
+ arg: opts.description,
131
+ file: opts.descriptionFile,
132
+ interactive: false,
133
+ });
134
+ const id = await svc.resolveMilestoneRef(ctx.client, idArg, opts.project);
135
+ const updated = await svc.updateMilestone(ctx.client, id, {
136
+ name: opts.name,
137
+ description,
138
+ targetDate: readAlias(opts, "--target", "--target-date"),
139
+ });
140
+ ctx.output.emit({ id: updated.id, name: updated.name }, () =>
141
+ ctx.output.success(`Updated milestone ${updated.name}`),
142
+ );
143
+ }),
144
+ );
145
+ addAliasOption(update, "--target-date <date>", "--target");
146
+
147
+ // delete ------------------------------------------------------------------
148
+ milestone
149
+ .command("delete <id>")
150
+ .alias("rm")
151
+ .description("Delete a milestone (by id, or by name with --project)")
152
+ .option(...PROJECT_SCOPE)
153
+ .action(
154
+ action(async (ctx: Context, opts, idArg: string) => {
155
+ const id = await svc.resolveMilestoneRef(ctx.client, idArg, opts.project);
156
+ if (!(await confirmDestructive(ctx, `Delete milestone ${idArg}?`))) return;
157
+ const deleted = await svc.deleteMilestone(ctx.client, id);
158
+ ctx.output.emit({ id: deleted.id, name: deleted.name, deleted: true }, () =>
159
+ ctx.output.success(`Deleted milestone ${deleted.name}`),
160
+ );
161
+ }),
162
+ );
163
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * `linear notification` (alias `notif`) — work with the viewer's notifications.
3
+ */
4
+
5
+ import { Command } from "commander";
6
+ import { action } from "../lib/action.js";
7
+ import { confirmDestructive } from "../lib/prompt.js";
8
+ import type { Context } from "../context.js";
9
+ import * as svc from "../services/notification.js";
10
+ import type { Column } from "../output/table.js";
11
+ import { ExitCode } from "../lib/errors.js";
12
+
13
+ const ROW_COLUMNS: Column<svc.NotificationRow>[] = [
14
+ { key: "type", header: "Type", value: (r) => r.type, max: 24 },
15
+ { key: "subject", header: "Subject", value: (r) => r.subject ?? "—", max: 50 },
16
+ { key: "read", header: "Read", value: (r) => (r.read ? "✓" : "•") },
17
+ { key: "createdAt", header: "Created", value: (r) => r.createdAt.slice(0, 10) },
18
+ ];
19
+
20
+ export function registerNotification(program: Command): void {
21
+ const notification = program
22
+ .command("notification")
23
+ .alias("notif")
24
+ .description("Work with your notifications");
25
+
26
+ // list --------------------------------------------------------------------
27
+ notification
28
+ .command("list")
29
+ .alias("ls")
30
+ .description("List your notifications")
31
+ .option("--include-archived", "include archived notifications")
32
+ .action(
33
+ action(async (ctx: Context, opts) => {
34
+ const rows = await svc.listNotifications(ctx.client, ctx.limit, !!opts.includeArchived);
35
+ ctx.output.list(rows, ROW_COLUMNS, rows);
36
+ }),
37
+ );
38
+
39
+ // read --------------------------------------------------------------------
40
+ notification
41
+ .command("read <id>")
42
+ .description("Mark a notification as read")
43
+ .action(
44
+ action(async (ctx: Context, _opts, id: string) => {
45
+ // The service throws unless the API confirmed the write, so this
46
+ // receipt is what happened rather than what was asked for.
47
+ const n = await svc.setRead(ctx.client, id, true);
48
+ ctx.output.emit(n, () => ctx.output.success(`Marked ${n.id} read`));
49
+ }),
50
+ );
51
+
52
+ // unread ------------------------------------------------------------------
53
+ notification
54
+ .command("unread <id>")
55
+ .description("Mark a notification as unread")
56
+ .action(
57
+ action(async (ctx: Context, _opts, id: string) => {
58
+ const n = await svc.setRead(ctx.client, id, false);
59
+ ctx.output.emit(n, () => ctx.output.success(`Marked ${n.id} unread`));
60
+ }),
61
+ );
62
+
63
+ // read-all ----------------------------------------------------------------
64
+ notification
65
+ .command("read-all")
66
+ .description("Mark all your notifications as read")
67
+ .action(
68
+ action(async (ctx: Context) => {
69
+ const res = await svc.markAllRead(ctx.client);
70
+ ctx.output.emit(res, () => {
71
+ if (res.success) {
72
+ ctx.output.success(`Marked ${res.count} of ${res.attempted} notification(s) read`);
73
+ } else {
74
+ ctx.output.warn(
75
+ `Marked ${res.count} of ${res.attempted} notification(s) read; ${res.failed.length} failed`,
76
+ );
77
+ for (const f of res.failed) ctx.output.warn(`${f.id}: ${f.error}`);
78
+ }
79
+ });
80
+ // A batch receipt belongs on stdout even when only part of the batch
81
+ // succeeded, but exit zero would let `read-all && ...` treat that as a
82
+ // complete success. Preserve the receipt and make the process outcome
83
+ // unambiguous.
84
+ if (!res.success) process.exitCode = ExitCode.Runtime;
85
+ }),
86
+ );
87
+
88
+ // archive -----------------------------------------------------------------
89
+ notification
90
+ .command("archive <id>")
91
+ .description("Archive a notification")
92
+ .action(
93
+ action(async (ctx: Context, _opts, id: string) => {
94
+ if (!(await confirmDestructive(ctx, `Archive notification ${id}?`))) return;
95
+ const success = await svc.archiveNotification(ctx.client, id);
96
+ ctx.output.emit({ id, archived: success }, () => ctx.output.success(`Archived ${id}`));
97
+ }),
98
+ );
99
+
100
+ // snooze ------------------------------------------------------------------
101
+ notification
102
+ .command("snooze <id> <untilISO>")
103
+ .description("Snooze a notification until an ISO timestamp (e.g. 2026-07-01T09:00:00Z)")
104
+ .action(
105
+ action(async (ctx: Context, _opts, id: string, untilISO: string) => {
106
+ const n = await svc.snoozeNotification(ctx.client, id, untilISO);
107
+ ctx.output.emit(n, () => ctx.output.success(`Snoozed ${n.id} until ${untilISO}`));
108
+ }),
109
+ );
110
+ }
@@ -0,0 +1,84 @@
1
+ /** `linear open` — open a workspace, team, project, issue, or explicit URL. */
2
+
3
+ import { Command } from "commander";
4
+ import { action } from "../lib/action.js";
5
+ import { usageError } from "../lib/errors.js";
6
+ import { openUrl } from "../lib/open.js";
7
+ import { normalizeIssueReference, resolveProjectId, resolveTeam } from "../lib/resolve.js";
8
+ import type { Context } from "../context.js";
9
+ import { getIssueDetail } from "../services/issue.js";
10
+ import { getOrganizationDetail } from "../services/organization.js";
11
+ import { getProjectDetail } from "../services/project.js";
12
+
13
+ const ISSUE_REFERENCE = /^[a-zA-Z][a-zA-Z0-9]*-\d+$/;
14
+ const URL = /^https?:\/\//i;
15
+ type OpenTarget = "workspace" | "team" | "project" | "issue" | "url";
16
+
17
+ export function registerOpen(program: Command): void {
18
+ program
19
+ .command("open [target]")
20
+ .description("Open the workspace, an issue, team, project, or URL")
21
+ .option("--app", "open in Linear.app (macOS)")
22
+ .option("-w, --web", "open in the default browser (the default)")
23
+ .addHelpText(
24
+ "after",
25
+ [
26
+ "",
27
+ "Targets: an issue id (TES-123), `team:<key>`, `project:<name>`, or an https URL.",
28
+ "With no target, opens the current workspace. A bare team key opens that team.",
29
+ ].join("\n"),
30
+ )
31
+ .action(
32
+ action(async (ctx: Context, opts, target?: string) => {
33
+ if (opts.app && opts.web) throw usageError("Pass either --web or --app, not both.");
34
+ const resolved = await resolveOpenTarget(ctx, target);
35
+ await openUrl(resolved.url, { app: opts.app === true });
36
+ ctx.output.emit({ ...resolved, opened: true }, () =>
37
+ ctx.output.success(
38
+ `Opened ${resolved.label} in ${opts.app ? "Linear.app" : "the browser"}`,
39
+ ),
40
+ );
41
+ }),
42
+ );
43
+ }
44
+
45
+ async function resolveOpenTarget(
46
+ ctx: Context,
47
+ target: string | undefined,
48
+ ): Promise<{ target: OpenTarget; url: string; label: string }> {
49
+ if (!target) {
50
+ const org = await getOrganizationDetail(ctx.client);
51
+ return { target: "workspace", url: `https://linear.app/${org.urlKey}`, label: org.name };
52
+ }
53
+ if (URL.test(target)) return { target: "url", url: target, label: target };
54
+ if (target.startsWith("team:")) {
55
+ const team = await resolveTeam(ctx.client, target.slice("team:".length), ctx.defaultTeam);
56
+ const org = await getOrganizationDetail(ctx.client);
57
+ return {
58
+ target: "team",
59
+ url: `https://linear.app/${org.urlKey}/team/${team.key}/all`,
60
+ label: `${team.key} (${team.name})`,
61
+ };
62
+ }
63
+ if (target.startsWith("project:")) {
64
+ const project = await getProjectDetail(
65
+ ctx.client,
66
+ await resolveProjectId(ctx.client, target.slice("project:".length)),
67
+ );
68
+ return { target: "project", url: project.url, label: project.name };
69
+ }
70
+ const issueReference = /^\d+$/.test(target)
71
+ ? normalizeIssueReference(target, ctx.defaultTeam)
72
+ : target;
73
+ if (ISSUE_REFERENCE.test(issueReference)) {
74
+ const issue = await getIssueDetail(ctx.client, issueReference, { includeComments: false });
75
+ return { target: "issue", url: issue.url, label: issue.identifier };
76
+ }
77
+ const team = await resolveTeam(ctx.client, target, ctx.defaultTeam);
78
+ const org = await getOrganizationDetail(ctx.client);
79
+ return {
80
+ target: "team",
81
+ url: `https://linear.app/${org.urlKey}/team/${team.key}/all`,
82
+ label: `${team.key} (${team.name})`,
83
+ };
84
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * `linear organization` (alias `org`) — inspect the current workspace.
3
+ *
4
+ * A read-only group: `view` shows workspace details, `members` lists workspace
5
+ * users, and `invites` lists pending organization invites. Admin/destructive
6
+ * operations are intentionally out of scope.
7
+ */
8
+
9
+ import { Command } from "commander";
10
+ import { action } from "../lib/action.js";
11
+ import type { Context } from "../context.js";
12
+ import * as svc from "../services/organization.js";
13
+ import type { Column } from "../output/table.js";
14
+
15
+ const MEMBER_COLUMNS: Column<svc.MemberRow>[] = [
16
+ { key: "name", header: "Name", value: (r) => r.displayName, max: 24 },
17
+ { key: "email", header: "Email", value: (r) => r.email, max: 40 },
18
+ { key: "admin", header: "Admin", value: (r) => (r.admin ? "yes" : "no") },
19
+ { key: "active", header: "Active", value: (r) => (r.active ? "yes" : "no") },
20
+ ];
21
+
22
+ const INVITE_COLUMNS: Column<svc.InviteRow>[] = [
23
+ { key: "email", header: "Email", value: (r) => r.email, max: 40 },
24
+ { key: "status", header: "Status", value: (r) => r.status },
25
+ { key: "role", header: "Role", value: (r) => r.role },
26
+ ];
27
+
28
+ export function registerOrganization(program: Command): void {
29
+ const org = program
30
+ .command("organization")
31
+ .alias("org")
32
+ .description("Inspect the current workspace");
33
+
34
+ // view --------------------------------------------------------------------
35
+ org
36
+ .command("view", { isDefault: true })
37
+ .alias("show")
38
+ .description("Show the current workspace")
39
+ .action(
40
+ action(async (ctx: Context) => {
41
+ const detail = await svc.getOrganizationDetail(ctx.client);
42
+ ctx.output.detail(detail, [
43
+ ["Workspace", detail.name],
44
+ ["URL key", detail.urlKey],
45
+ ["ID", detail.id],
46
+ ["Users", detail.userCount],
47
+ ["Issues created", detail.createdIssueCount],
48
+ ["SAML", detail.samlEnabled ? "yes" : "no"],
49
+ ["SCIM", detail.scimEnabled ? "yes" : "no"],
50
+ ["Roadmaps", detail.roadmapEnabled ? "yes" : "no"],
51
+ ["Logo", detail.logoUrl],
52
+ ["Created", detail.createdAt],
53
+ ["Updated", detail.updatedAt],
54
+ ]);
55
+ }),
56
+ );
57
+
58
+ // members -----------------------------------------------------------------
59
+ org
60
+ .command("members")
61
+ .description("List workspace members")
62
+ .action(
63
+ action(async (ctx: Context) => {
64
+ const rows = await svc.listMembers(ctx.client, ctx.limit);
65
+ ctx.output.list(rows, MEMBER_COLUMNS, rows);
66
+ }),
67
+ );
68
+
69
+ // invites -----------------------------------------------------------------
70
+ org
71
+ .command("invites")
72
+ .description("List organization invites")
73
+ .action(
74
+ action(async (ctx: Context) => {
75
+ const rows = await svc.listInvites(ctx.client, ctx.limit);
76
+ ctx.output.list(rows, INVITE_COLUMNS, rows);
77
+ }),
78
+ );
79
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * `linear project-update` (alias `pu`) — post and list project 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/project-update.js";
10
+
11
+ export function registerProjectUpdate(program: Command): void {
12
+ const group = program
13
+ .command("project-update")
14
+ .alias("pu")
15
+ .description("Post and list project status updates");
16
+
17
+ // create ------------------------------------------------------------------
18
+ addUpdateFlags(
19
+ group
20
+ .command("create <project>")
21
+ .alias("new")
22
+ .description("Post a status update on a project (by name or id)"),
23
+ ).action(
24
+ action(async (ctx: Context, opts, projectArg: string) => {
25
+ const body = resolveUpdateBody(ctx, opts);
26
+ const created = await svc.createProjectUpdate(ctx.client, projectArg, {
27
+ body,
28
+ health: opts.health,
29
+ });
30
+ ctx.output.emit(created, () =>
31
+ ctx.output.success(`Posted project update${opts.health ? ` (${opts.health})` : ""}`),
32
+ );
33
+ }),
34
+ );
35
+
36
+ // list --------------------------------------------------------------------
37
+ group
38
+ .command("list <project>")
39
+ .alias("ls")
40
+ .description("List a project's status updates")
41
+ .action(
42
+ action(async (ctx: Context, _opts, projectArg: string) => {
43
+ const rows = await svc.listProjectUpdates(ctx.client, projectArg, ctx.limit);
44
+ ctx.output.list(rows, UPDATE_COLUMNS, rows);
45
+ }),
46
+ );
47
+ }