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