@bdsqqq/lnr-cli 1.1.1 → 1.2.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.
@@ -1,390 +0,0 @@
1
- import type { Command } from "commander";
2
- import {
3
- getClient,
4
- listIssues,
5
- getIssue,
6
- createIssue,
7
- updateIssue,
8
- addComment,
9
- priorityFromString,
10
- getTeamStates,
11
- getTeamLabels,
12
- findTeamByKeyOrName,
13
- getAvailableTeamKeys,
14
- type Issue,
15
- type ListIssuesFilter,
16
- } from "@bdsqqq/lnr-core";
17
- import { handleApiError, exitWithError, EXIT_CODES } from "../lib/error";
18
- import {
19
- outputJson,
20
- outputQuiet,
21
- outputTable,
22
- getOutputFormat,
23
- formatDate,
24
- formatPriority,
25
- truncate,
26
- type OutputOptions,
27
- type TableColumn,
28
- } from "../lib/output";
29
-
30
- interface ListOptions extends OutputOptions {
31
- team?: string;
32
- state?: string;
33
- assignee?: string;
34
- label?: string;
35
- project?: string;
36
- json?: boolean;
37
- quiet?: boolean;
38
- verbose?: boolean;
39
- }
40
-
41
- interface ShowOptions extends OutputOptions {
42
- json?: boolean;
43
- open?: boolean;
44
- }
45
-
46
- interface UpdateOptions {
47
- state?: string;
48
- assignee?: string;
49
- priority?: string;
50
- label?: string;
51
- comment?: string;
52
- open?: boolean;
53
- }
54
-
55
- interface CreateOptions {
56
- team?: string;
57
- title?: string;
58
- description?: string;
59
- assignee?: string;
60
- label?: string;
61
- priority?: string;
62
- }
63
-
64
- const issueColumns: TableColumn<Issue>[] = [
65
- { header: "ID", value: (i) => i.identifier, width: 10 },
66
- { header: "STATE", value: (i) => i.state ?? "-", width: 15 },
67
- { header: "TITLE", value: (i) => truncate(i.title, 50), width: 50 },
68
- { header: "ASSIGNEE", value: (i) => i.assignee ?? "-", width: 15 },
69
- { header: "PRIORITY", value: (i) => formatPriority(i.priority), width: 8 },
70
- ];
71
-
72
- async function handleListIssues(options: ListOptions): Promise<void> {
73
- try {
74
- const client = getClient();
75
- const filter: ListIssuesFilter = {
76
- team: options.team,
77
- state: options.state,
78
- assignee: options.assignee,
79
- label: options.label,
80
- project: options.project,
81
- };
82
-
83
- const issues = await listIssues(client, filter);
84
- const format = options.json ? "json" : options.quiet ? "quiet" : getOutputFormat(options);
85
-
86
- if (format === "json") {
87
- outputJson(issues);
88
- return;
89
- }
90
-
91
- if (format === "quiet") {
92
- outputQuiet(issues.map((i) => i.identifier));
93
- return;
94
- }
95
-
96
- outputTable(issues, issueColumns, { verbose: options.verbose });
97
- } catch (error) {
98
- handleApiError(error);
99
- }
100
- }
101
-
102
- async function handleShowIssue(identifier: string, options: ShowOptions): Promise<void> {
103
- try {
104
- const client = getClient();
105
- const issue = await getIssue(client, identifier);
106
-
107
- if (!issue) {
108
- exitWithError(`issue ${identifier} not found`, undefined, EXIT_CODES.NOT_FOUND);
109
- }
110
-
111
- if (options.open) {
112
- const { exec } = await import("child_process");
113
- exec(`open "${issue.url}"`);
114
- console.log(`opened ${issue.url}`);
115
- return;
116
- }
117
-
118
- const format = options.json ? "json" : getOutputFormat(options);
119
-
120
- if (format === "json") {
121
- outputJson({
122
- ...issue,
123
- priority: formatPriority(issue.priority),
124
- createdAt: formatDate(issue.createdAt),
125
- updatedAt: formatDate(issue.updatedAt),
126
- });
127
- return;
128
- }
129
-
130
- console.log(`${issue.identifier}: ${issue.title}`);
131
- console.log();
132
- console.log(`state: ${issue.state ?? "-"}`);
133
- console.log(`assignee: ${issue.assignee ?? "-"}`);
134
- console.log(`priority: ${formatPriority(issue.priority)}`);
135
- console.log(`created: ${formatDate(issue.createdAt)}`);
136
- console.log(`updated: ${formatDate(issue.updatedAt)}`);
137
- console.log(`url: ${issue.url}`);
138
-
139
- if (issue.description) {
140
- console.log();
141
- console.log(issue.description);
142
- }
143
- } catch (error) {
144
- handleApiError(error);
145
- }
146
- }
147
-
148
- async function handleUpdateIssue(identifier: string, options: UpdateOptions): Promise<void> {
149
- try {
150
- const client = getClient();
151
- const issue = await getIssue(client, identifier);
152
-
153
- if (!issue) {
154
- exitWithError(`issue ${identifier} not found`, undefined, EXIT_CODES.NOT_FOUND);
155
- }
156
-
157
- if (options.open) {
158
- const { exec } = await import("child_process");
159
- exec(`open "${issue.url}"`);
160
- console.log(`opened ${issue.url}`);
161
- return;
162
- }
163
-
164
- if (options.comment) {
165
- await addComment(client, issue.id, options.comment);
166
- console.log(`commented on ${identifier}`);
167
- return;
168
- }
169
-
170
- const updatePayload: Record<string, unknown> = {};
171
-
172
- if (options.state) {
173
- const rawIssue = await (client as unknown as { issue: (id: string) => Promise<{ team?: { id: string } | null } | null> }).issue(identifier);
174
- const teamRef = rawIssue && 'team' in rawIssue ? await (rawIssue as unknown as { team: Promise<{ id: string } | null> }).team : null;
175
- if (!teamRef) {
176
- exitWithError("could not determine team for issue");
177
- }
178
- const states = await getTeamStates(client, teamRef.id);
179
- const targetState = states.find(
180
- (s) => s.name.toLowerCase() === options.state!.toLowerCase()
181
- );
182
- if (!targetState) {
183
- const available = states.map((s) => s.name).join(", ");
184
- exitWithError(
185
- `state "${options.state}" not found`,
186
- `available states: ${available}`
187
- );
188
- }
189
- updatePayload.stateId = targetState.id;
190
- }
191
-
192
- if (options.assignee) {
193
- if (options.assignee === "@me") {
194
- const viewer = await client.viewer;
195
- updatePayload.assigneeId = viewer.id;
196
- } else {
197
- const users = await client.users({ filter: { email: { eq: options.assignee } } });
198
- const user = users.nodes[0];
199
- if (!user) {
200
- exitWithError(`user "${options.assignee}" not found`);
201
- }
202
- updatePayload.assigneeId = user.id;
203
- }
204
- }
205
-
206
- if (options.priority) {
207
- updatePayload.priority = priorityFromString(options.priority);
208
- }
209
-
210
- if (options.label) {
211
- const isAdd = options.label.startsWith("+");
212
- const isRemove = options.label.startsWith("-");
213
- const labelName = isAdd || isRemove ? options.label.slice(1) : options.label;
214
-
215
- const rawIssue = await (client as unknown as { issue: (id: string) => Promise<{ team?: { id: string } | null; labels: () => Promise<{ nodes: { id: string }[] }> } | null> }).issue(identifier);
216
- const teamRef = rawIssue && 'team' in rawIssue ? await (rawIssue as unknown as { team: Promise<{ id: string } | null> }).team : null;
217
- if (!teamRef) {
218
- exitWithError("could not determine team for issue");
219
- }
220
-
221
- const labels = await getTeamLabels(client, teamRef.id);
222
- const targetLabel = labels.find(
223
- (l) => l.name.toLowerCase() === labelName.toLowerCase()
224
- );
225
- if (!targetLabel) {
226
- const available = labels.map((l) => l.name).join(", ");
227
- exitWithError(`label "${labelName}" not found`, `available labels: ${available}`);
228
- }
229
-
230
- const currentLabelsData = rawIssue ? await rawIssue.labels() : { nodes: [] };
231
- const currentLabelIds = currentLabelsData.nodes.map((l) => l.id);
232
-
233
- if (isRemove) {
234
- updatePayload.labelIds = currentLabelIds.filter((id) => id !== targetLabel.id);
235
- } else {
236
- if (!currentLabelIds.includes(targetLabel.id)) {
237
- updatePayload.labelIds = [...currentLabelIds, targetLabel.id];
238
- }
239
- }
240
- }
241
-
242
- if (Object.keys(updatePayload).length > 0) {
243
- await updateIssue(client, issue.id, updatePayload);
244
- console.log(`updated ${identifier}`);
245
- }
246
- } catch (error) {
247
- handleApiError(error);
248
- }
249
- }
250
-
251
- async function handleCreateIssue(options: CreateOptions): Promise<void> {
252
- if (!options.team) {
253
- exitWithError("--team is required", "usage: lnr issue new --team ENG --title \"...\"");
254
- }
255
-
256
- if (!options.title) {
257
- exitWithError("--title is required", "usage: lnr issue new --team ENG --title \"...\"");
258
- }
259
-
260
- try {
261
- const client = getClient();
262
- const team = await findTeamByKeyOrName(client, options.team);
263
-
264
- if (!team) {
265
- const available = (await getAvailableTeamKeys(client)).join(", ");
266
- exitWithError(`team "${options.team}" not found`, `available teams: ${available}`);
267
- }
268
-
269
- const createPayload: {
270
- teamId: string;
271
- title: string;
272
- description?: string;
273
- assigneeId?: string;
274
- priority?: number;
275
- labelIds?: string[];
276
- } = {
277
- teamId: team.id,
278
- title: options.title,
279
- };
280
-
281
- if (options.description) {
282
- createPayload.description = options.description;
283
- }
284
-
285
- if (options.assignee) {
286
- if (options.assignee === "@me") {
287
- const viewer = await client.viewer;
288
- createPayload.assigneeId = viewer.id;
289
- } else {
290
- const users = await client.users({ filter: { email: { eq: options.assignee } } });
291
- const user = users.nodes[0];
292
- if (!user) {
293
- exitWithError(`user "${options.assignee}" not found`);
294
- }
295
- createPayload.assigneeId = user.id;
296
- }
297
- }
298
-
299
- if (options.priority) {
300
- createPayload.priority = priorityFromString(options.priority);
301
- }
302
-
303
- if (options.label) {
304
- const labels = await getTeamLabels(client, team.id);
305
- const targetLabel = labels.find(
306
- (l) => l.name.toLowerCase() === options.label!.toLowerCase()
307
- );
308
- if (!targetLabel) {
309
- const available = labels.map((l) => l.name).join(", ");
310
- exitWithError(`label "${options.label}" not found`, `available labels: ${available}`);
311
- }
312
- createPayload.labelIds = [targetLabel.id];
313
- }
314
-
315
- const issue = await createIssue(client, createPayload);
316
- if (issue) {
317
- console.log(`created ${issue.identifier}: ${issue.title}`);
318
- } else {
319
- console.log("created issue");
320
- }
321
- } catch (error) {
322
- handleApiError(error);
323
- }
324
- }
325
-
326
- export function registerIssuesCommand(program: Command): void {
327
- program
328
- .command("issues")
329
- .description("list issues")
330
- .option("--team <key>", "filter by team key")
331
- .option("--state <state>", "filter by state name")
332
- .option("--assignee <email>", "filter by assignee (@me for self)")
333
- .option("--label <label>", "filter by label")
334
- .option("--project <project>", "filter by project name")
335
- .option("--json", "output as JSON")
336
- .option("--quiet", "output issue IDs only")
337
- .option("--verbose", "show table headers")
338
- .action(async (options: ListOptions) => {
339
- await handleListIssues(options);
340
- });
341
-
342
- program
343
- .command("issue <id>")
344
- .description("show or update an issue, or create with 'new'")
345
- .option("--json", "output as JSON")
346
- .option("--open", "open issue in browser")
347
- .option("--state <state>", "update state")
348
- .option("--assignee <email>", "update assignee (@me for self)")
349
- .option("--priority <priority>", "update priority (urgent, high, medium, low)")
350
- .option("--label <label>", "add (+label) or remove (-label) a label")
351
- .option("--comment <text>", "add a comment")
352
- .option("--team <key>", "team for new issue")
353
- .option("--title <title>", "title for new issue")
354
- .option("--description <description>", "description for new issue")
355
- .action(async (id: string, options: ShowOptions & UpdateOptions & CreateOptions) => {
356
- if (id === "new") {
357
- await handleCreateIssue(options);
358
- return;
359
- }
360
-
361
- const hasUpdate =
362
- options.state || options.assignee || options.priority || options.label || options.comment;
363
-
364
- if (hasUpdate) {
365
- await handleUpdateIssue(id, options);
366
- } else {
367
- await handleShowIssue(id, options);
368
- }
369
- });
370
-
371
- program
372
- .command("i")
373
- .description("alias for issues")
374
- .option("--team <key>", "filter by team key")
375
- .option("--state <state>", "filter by state name")
376
- .option("--assignee <email>", "filter by assignee (@me for self)")
377
- .option("--label <label>", "filter by label")
378
- .option("--project <project>", "filter by project name")
379
- .option("--json", "output as JSON")
380
- .option("--quiet", "output issue IDs only")
381
- .option("--verbose", "show table headers")
382
- .argument("[subcommand]", "subcommand (new)")
383
- .action(async (subcommand: string | undefined, options: ListOptions & CreateOptions) => {
384
- if (subcommand === "new") {
385
- await handleCreateIssue(options);
386
- return;
387
- }
388
- await handleListIssues(options);
389
- });
390
- }
@@ -1,214 +0,0 @@
1
- import type { Command } from "commander";
2
- import {
3
- getClient,
4
- listProjects,
5
- getProject,
6
- getProjectIssues,
7
- createProject,
8
- deleteProject,
9
- findTeamByKeyOrName,
10
- getAvailableTeamKeys,
11
- } from "@bdsqqq/lnr-core";
12
- import { handleApiError, exitWithError, EXIT_CODES } from "../lib/error";
13
- import {
14
- outputJson,
15
- outputQuiet,
16
- outputTable,
17
- getOutputFormat,
18
- formatDate,
19
- truncate,
20
- type OutputOptions,
21
- } from "../lib/output";
22
-
23
- interface CreateProjectOptions {
24
- name?: string;
25
- team?: string;
26
- description?: string;
27
- }
28
-
29
- async function handleCreateProject(options: CreateProjectOptions): Promise<void> {
30
- if (!options.name) {
31
- exitWithError("--name is required", "usage: lnr project new --name \"...\"");
32
- }
33
-
34
- try {
35
- const client = getClient();
36
- let teamIds: string[] = [];
37
-
38
- if (options.team) {
39
- const team = await findTeamByKeyOrName(client, options.team);
40
- if (!team) {
41
- const available = (await getAvailableTeamKeys(client)).join(", ");
42
- exitWithError(`team "${options.team}" not found`, `available teams: ${available}`, EXIT_CODES.NOT_FOUND);
43
- }
44
- teamIds = [team.id];
45
- }
46
-
47
- const project = await createProject(client, {
48
- name: options.name,
49
- description: options.description,
50
- teamIds,
51
- });
52
-
53
- if (project) {
54
- console.log(`created project: ${project.name}`);
55
- } else {
56
- console.log("created project");
57
- }
58
- } catch (error) {
59
- handleApiError(error);
60
- }
61
- }
62
-
63
- async function handleDeleteProject(name: string): Promise<void> {
64
- try {
65
- const client = getClient();
66
- const success = await deleteProject(client, name);
67
-
68
- if (!success) {
69
- exitWithError(`project "${name}" not found`, undefined, EXIT_CODES.NOT_FOUND);
70
- }
71
-
72
- console.log(`deleted project: ${name}`);
73
- } catch (error) {
74
- handleApiError(error);
75
- }
76
- }
77
-
78
- export function registerProjectsCommand(program: Command): void {
79
- program
80
- .command("projects")
81
- .description("list projects")
82
- .option("--team <team>", "filter by team")
83
- .option("--status <status>", "filter by status (active, completed, canceled, paused)")
84
- .option("--json", "output as json")
85
- .option("--quiet", "output ids only")
86
- .option("--verbose", "show detailed output")
87
- .action(async (options: { team?: string; status?: string; json?: boolean; quiet?: boolean; verbose?: boolean }) => {
88
- try {
89
- const client = getClient();
90
-
91
- const outputOpts: OutputOptions = {
92
- format: options.json ? "json" : options.quiet ? "quiet" : undefined,
93
- verbose: options.verbose,
94
- };
95
- const format = getOutputFormat(outputOpts);
96
-
97
- const projects = await listProjects(client, { team: options.team, status: options.status });
98
-
99
- if (format === "json") {
100
- outputJson(projects);
101
- return;
102
- }
103
-
104
- if (format === "quiet") {
105
- outputQuiet(projects.map((p) => p.id));
106
- return;
107
- }
108
-
109
- outputTable(projects, [
110
- { header: "NAME", value: (p) => truncate(p.name, 30), width: 30 },
111
- { header: "STATE", value: (p) => p.state ?? "-", width: 12 },
112
- { header: "PROGRESS", value: (p) => `${Math.round((p.progress ?? 0) * 100)}%`, width: 10 },
113
- { header: "TARGET", value: (p) => formatDate(p.targetDate), width: 12 },
114
- ], outputOpts);
115
- } catch (error) {
116
- handleApiError(error);
117
- }
118
- });
119
-
120
- program
121
- .command("project <name>")
122
- .description("show project details, create with 'new', or delete with --delete")
123
- .option("--issues", "list issues in project")
124
- .option("--json", "output as json")
125
- .option("--quiet", "output ids only")
126
- .option("--verbose", "show detailed output")
127
- .option("--delete", "delete/archive the project")
128
- .option("--name <name>", "name for new project")
129
- .option("--team <team>", "team for new project")
130
- .option("--description <description>", "description for new project")
131
- .action(async (name: string, options: { issues?: boolean; json?: boolean; quiet?: boolean; verbose?: boolean; delete?: boolean; name?: string; team?: string; description?: string }) => {
132
- if (name === "new") {
133
- await handleCreateProject(options);
134
- return;
135
- }
136
-
137
- if (options.delete) {
138
- await handleDeleteProject(name);
139
- return;
140
- }
141
-
142
- try {
143
- const client = getClient();
144
-
145
- const outputOpts: OutputOptions = {
146
- format: options.json ? "json" : options.quiet ? "quiet" : undefined,
147
- verbose: options.verbose,
148
- };
149
- const format = getOutputFormat(outputOpts);
150
-
151
- const project = await getProject(client, name);
152
-
153
- if (!project) {
154
- exitWithError(`project "${name}" not found`, undefined, EXIT_CODES.NOT_FOUND);
155
- }
156
-
157
- if (options.issues) {
158
- const issues = await getProjectIssues(client, name);
159
-
160
- if (format === "json") {
161
- outputJson(issues);
162
- return;
163
- }
164
-
165
- if (format === "quiet") {
166
- outputQuiet(issues.map((i) => i.identifier));
167
- return;
168
- }
169
-
170
- outputTable(issues, [
171
- { header: "ID", value: (i) => i.identifier, width: 12 },
172
- { header: "TITLE", value: (i) => truncate(i.title, 50), width: 50 },
173
- { header: "CREATED", value: (i) => formatDate(i.createdAt), width: 12 },
174
- ], outputOpts);
175
- return;
176
- }
177
-
178
- if (format === "json") {
179
- outputJson(project);
180
- return;
181
- }
182
-
183
- if (format === "quiet") {
184
- console.log(project.id);
185
- return;
186
- }
187
-
188
- console.log(`${project.name}`);
189
- if (project.description) {
190
- console.log(` ${truncate(project.description, 80)}`);
191
- }
192
- console.log();
193
- console.log(`state: ${project.state ?? "-"}`);
194
- console.log(`progress: ${Math.round((project.progress ?? 0) * 100)}%`);
195
- console.log(`target: ${formatDate(project.targetDate)}`);
196
- console.log(`started: ${formatDate(project.startDate)}`);
197
- console.log(`created: ${formatDate(project.createdAt)}`);
198
- } catch (error) {
199
- handleApiError(error);
200
- }
201
- });
202
-
203
- program
204
- .command("p")
205
- .description("alias for projects")
206
- .option("--team <team>", "filter by team")
207
- .option("--status <status>", "filter by status")
208
- .option("--json", "output as json")
209
- .option("--quiet", "output ids only")
210
- .option("--verbose", "show detailed output")
211
- .action(async (options: { team?: string; status?: string; json?: boolean; quiet?: boolean; verbose?: boolean }) => {
212
- await program.commands.find((c) => c.name() === "projects")?.parseAsync(["projects", ...process.argv.slice(3)], { from: "user" });
213
- });
214
- }