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,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
|
+
}
|