@bdsqqq/lnr-cli 1.1.2 → 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.
@@ -0,0 +1,192 @@
1
+ import { z } from "zod";
2
+ import {
3
+ getClient,
4
+ listLabels,
5
+ getLabel,
6
+ createLabel,
7
+ updateLabel,
8
+ deleteLabel,
9
+ findTeamByKeyOrName,
10
+ getAvailableTeamKeys,
11
+ } from "@bdsqqq/lnr-core";
12
+ import { router, procedure } from "./trpc";
13
+ import { exitWithError, handleApiError, EXIT_CODES } from "../lib/error";
14
+ import {
15
+ outputJson,
16
+ outputQuiet,
17
+ outputTable,
18
+ getOutputFormat,
19
+ truncate,
20
+ type OutputOptions,
21
+ } from "../lib/output";
22
+
23
+ const listLabelsInput = z.object({
24
+ team: z.string().optional().describe("filter by team key"),
25
+ json: z.boolean().optional().describe("output as json"),
26
+ quiet: z.boolean().optional().describe("output ids only"),
27
+ verbose: z.boolean().optional().describe("show all columns"),
28
+ });
29
+
30
+ const labelInput = z.object({
31
+ id: z.string().meta({ positional: true }).describe("label id or 'new'"),
32
+ name: z.string().optional().describe("label name (required for new)"),
33
+ color: z.string().optional().describe("hex color code"),
34
+ description: z.string().optional().describe("label description"),
35
+ team: z.string().optional().describe("team key (required for new)"),
36
+ delete: z.boolean().optional().describe("delete the label"),
37
+ json: z.boolean().optional().describe("output as json"),
38
+ });
39
+
40
+ export const labelsRouter = router({
41
+ labels: procedure
42
+ .meta({
43
+ description: "list labels",
44
+ })
45
+ .input(listLabelsInput)
46
+ .query(async ({ input }) => {
47
+ try {
48
+ const client = getClient();
49
+
50
+ const outputOpts: OutputOptions = {
51
+ format: input.json ? "json" : input.quiet ? "quiet" : undefined,
52
+ verbose: input.verbose,
53
+ };
54
+ const format = getOutputFormat(outputOpts);
55
+
56
+ let teamId: string | undefined;
57
+ if (input.team) {
58
+ const team = await findTeamByKeyOrName(client, input.team);
59
+ if (!team) {
60
+ const available = await getAvailableTeamKeys(client);
61
+ exitWithError(
62
+ `team not found: ${input.team}`,
63
+ `available teams: ${available.join(", ")}`,
64
+ EXIT_CODES.NOT_FOUND
65
+ );
66
+ }
67
+ teamId = team.id;
68
+ }
69
+
70
+ const labels = await listLabels(client, teamId);
71
+
72
+ if (format === "json") {
73
+ outputJson(labels);
74
+ return;
75
+ }
76
+
77
+ if (format === "quiet") {
78
+ outputQuiet(labels.map((l) => l.id));
79
+ return;
80
+ }
81
+
82
+ outputTable(
83
+ labels,
84
+ [
85
+ { header: "ID", value: (l) => l.id.slice(0, 8), width: 10 },
86
+ { header: "NAME", value: (l) => truncate(l.name, 30), width: 30 },
87
+ { header: "COLOR", value: (l) => l.color ?? "-", width: 10 },
88
+ { header: "DESCRIPTION", value: (l) => truncate(l.description ?? "-", 40), width: 40 },
89
+ ],
90
+ outputOpts
91
+ );
92
+ } catch (error) {
93
+ handleApiError(error);
94
+ }
95
+ }),
96
+
97
+ label: procedure
98
+ .meta({
99
+ description: "show label details, create with 'new', update, or delete with --delete",
100
+ })
101
+ .input(labelInput)
102
+ .query(async ({ input }) => {
103
+ try {
104
+ const client = getClient();
105
+
106
+ if (input.id === "new") {
107
+ if (!input.name) {
108
+ exitWithError("--name is required", "usage: lnr label new --name \"...\" --team <key>");
109
+ }
110
+ if (!input.team) {
111
+ exitWithError("--team is required", "usage: lnr label new --name \"...\" --team <key>");
112
+ }
113
+
114
+ const team = await findTeamByKeyOrName(client, input.team);
115
+ if (!team) {
116
+ const available = (await getAvailableTeamKeys(client)).join(", ");
117
+ exitWithError(
118
+ `team "${input.team}" not found`,
119
+ `available teams: ${available}`,
120
+ EXIT_CODES.NOT_FOUND
121
+ );
122
+ }
123
+
124
+ const label = await createLabel(client, {
125
+ name: input.name,
126
+ teamId: team.id,
127
+ color: input.color,
128
+ description: input.description,
129
+ });
130
+
131
+ if (label) {
132
+ console.log(`created label: ${label.name}`);
133
+ } else {
134
+ exitWithError("failed to create label");
135
+ }
136
+ return;
137
+ }
138
+
139
+ if (input.delete) {
140
+ const success = await deleteLabel(client, input.id);
141
+
142
+ if (!success) {
143
+ exitWithError(`label "${input.id}" not found`, undefined, EXIT_CODES.NOT_FOUND);
144
+ }
145
+
146
+ console.log(`deleted label: ${input.id}`);
147
+ return;
148
+ }
149
+
150
+ if (input.name || input.color || input.description) {
151
+ const success = await updateLabel(client, input.id, {
152
+ name: input.name,
153
+ color: input.color,
154
+ description: input.description,
155
+ });
156
+
157
+ if (!success) {
158
+ exitWithError(`label "${input.id}" not found`, undefined, EXIT_CODES.NOT_FOUND);
159
+ }
160
+
161
+ console.log(`updated label: ${input.id}`);
162
+ return;
163
+ }
164
+
165
+ const outputOpts: OutputOptions = {
166
+ format: input.json ? "json" : undefined,
167
+ };
168
+ const format = getOutputFormat(outputOpts);
169
+
170
+ const label = await getLabel(client, input.id);
171
+
172
+ if (!label) {
173
+ exitWithError(`label "${input.id}" not found`, undefined, EXIT_CODES.NOT_FOUND);
174
+ }
175
+
176
+ if (format === "json") {
177
+ outputJson(label);
178
+ return;
179
+ }
180
+
181
+ console.log(`${label.name}`);
182
+ if (label.description) {
183
+ console.log(` ${truncate(label.description, 80)}`);
184
+ }
185
+ console.log();
186
+ console.log(`id: ${label.id}`);
187
+ console.log(`color: ${label.color ?? "-"}`);
188
+ } catch (error) {
189
+ handleApiError(error);
190
+ }
191
+ }),
192
+ });
@@ -1,47 +1,43 @@
1
- import type { Command } from "commander";
1
+ import { z } from "zod";
2
2
  import {
3
3
  getClient,
4
4
  getViewer,
5
5
  getMyIssues,
6
6
  getMyCreatedIssues,
7
+ getMyActivity,
7
8
  } from "@bdsqqq/lnr-core";
8
- import { handleApiError } from "../lib/error";
9
+ import { router, procedure } from "./trpc";
9
10
  import {
10
- getOutputFormat,
11
11
  outputJson,
12
12
  outputQuiet,
13
13
  outputTable,
14
14
  formatPriority,
15
+ formatDate,
15
16
  truncate,
16
- type OutputOptions,
17
17
  } from "../lib/output";
18
+ import { handleApiError } from "../lib/error";
18
19
 
19
- interface MeOptions extends OutputOptions {
20
- issues?: boolean;
21
- created?: boolean;
22
- json?: boolean;
23
- quiet?: boolean;
24
- }
25
-
26
- export function registerMeCommand(program: Command): void {
27
- program
28
- .command("me")
29
- .description("show current user info")
30
- .option("--issues", "show my assigned issues")
31
- .option("--created", "show issues i created")
32
- .option("--json", "output as json")
33
- .option("--quiet", "output ids only")
34
- .action(async (options: MeOptions) => {
35
- const format = options.json
36
- ? "json"
37
- : options.quiet
38
- ? "quiet"
39
- : getOutputFormat(options);
20
+ const meInput = z.object({
21
+ issues: z.boolean().optional().describe("list issues assigned to me"),
22
+ created: z.boolean().optional().describe("list issues created by me"),
23
+ activity: z.boolean().optional().describe("show recent activity"),
24
+ json: z.boolean().optional().describe("output as json"),
25
+ quiet: z.boolean().optional().describe("output ids only"),
26
+ });
40
27
 
28
+ export const meRouter = router({
29
+ me: procedure
30
+ .meta({
31
+ description: "show current user info",
32
+ })
33
+ .input(meInput)
34
+ .query(async ({ input }) => {
41
35
  try {
42
36
  const client = getClient();
43
37
 
44
- if (options.issues) {
38
+ const format = input.json ? "json" : input.quiet ? "quiet" : "table";
39
+
40
+ if (input.issues) {
45
41
  const issues = await getMyIssues(client);
46
42
 
47
43
  if (format === "json") {
@@ -63,7 +59,7 @@ export function registerMeCommand(program: Command): void {
63
59
  return;
64
60
  }
65
61
 
66
- if (options.created) {
62
+ if (input.created) {
67
63
  const issues = await getMyCreatedIssues(client);
68
64
 
69
65
  if (format === "json") {
@@ -85,6 +81,28 @@ export function registerMeCommand(program: Command): void {
85
81
  return;
86
82
  }
87
83
 
84
+ if (input.activity) {
85
+ const activity = await getMyActivity(client);
86
+
87
+ if (format === "json") {
88
+ outputJson(activity);
89
+ return;
90
+ }
91
+
92
+ if (format === "quiet") {
93
+ outputQuiet(activity.map((a) => a.identifier));
94
+ return;
95
+ }
96
+
97
+ outputTable(activity, [
98
+ { header: "ID", value: (a) => a.identifier, width: 12 },
99
+ { header: "TITLE", value: (a) => truncate(a.title, 40), width: 40 },
100
+ { header: "STATE", value: (a) => a.state ?? "-", width: 16 },
101
+ { header: "UPDATED", value: (a) => formatDate(a.updatedAt), width: 12 },
102
+ ]);
103
+ return;
104
+ }
105
+
88
106
  const viewer = await getViewer(client);
89
107
 
90
108
  if (format === "json") {
@@ -106,5 +124,5 @@ export function registerMeCommand(program: Command): void {
106
124
  } catch (error) {
107
125
  handleApiError(error);
108
126
  }
109
- });
110
- }
127
+ }),
128
+ });
@@ -0,0 +1,220 @@
1
+ import { z } from "zod";
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 { router, procedure } from "./trpc";
13
+ import { exitWithError, handleApiError, EXIT_CODES } from "../lib/error";
14
+ import {
15
+ outputJson,
16
+ outputQuiet,
17
+ outputTable,
18
+ getOutputFormat,
19
+ formatDate,
20
+ truncate,
21
+ type OutputOptions,
22
+ } from "../lib/output";
23
+
24
+ const listProjectsInput = z.object({
25
+ team: z.string().optional().describe("filter by team key"),
26
+ status: z.string().optional().describe("filter by status (planned, started, completed, etc)"),
27
+ json: z.boolean().optional().describe("output as json"),
28
+ quiet: z.boolean().optional().describe("output ids only"),
29
+ verbose: z.boolean().optional().describe("show all columns"),
30
+ });
31
+
32
+ const projectInput = z.object({
33
+ name: z.string().meta({ positional: true }).describe("project name or 'new'"),
34
+ issues: z.boolean().optional().describe("list issues in project"),
35
+ json: z.boolean().optional().describe("output as json"),
36
+ quiet: z.boolean().optional().describe("output ids only"),
37
+ verbose: z.boolean().optional().describe("show all columns"),
38
+ delete: z.boolean().optional().describe("delete the project"),
39
+ projectName: z.string().optional().describe("project name (required for new)"),
40
+ team: z.string().optional().describe("team key to associate project with"),
41
+ description: z.string().optional().describe("project description"),
42
+ });
43
+
44
+ export const projectsRouter = router({
45
+ projects: procedure
46
+ .meta({
47
+ aliases: { command: ["p"] },
48
+ description: "list projects",
49
+ })
50
+ .input(listProjectsInput)
51
+ .query(async ({ input }) => {
52
+ try {
53
+ const client = getClient();
54
+
55
+ const outputOpts: OutputOptions = {
56
+ format: input.json ? "json" : input.quiet ? "quiet" : undefined,
57
+ verbose: input.verbose,
58
+ };
59
+ const format = getOutputFormat(outputOpts);
60
+
61
+ const projects = await listProjects(client, {
62
+ team: input.team,
63
+ status: input.status,
64
+ });
65
+
66
+ if (format === "json") {
67
+ outputJson(projects);
68
+ return;
69
+ }
70
+
71
+ if (format === "quiet") {
72
+ outputQuiet(projects.map((p) => p.id));
73
+ return;
74
+ }
75
+
76
+ outputTable(
77
+ projects,
78
+ [
79
+ { header: "NAME", value: (p) => truncate(p.name, 30), width: 30 },
80
+ { header: "STATE", value: (p) => p.state ?? "-", width: 12 },
81
+ {
82
+ header: "PROGRESS",
83
+ value: (p) => `${Math.round((p.progress ?? 0) * 100)}%`,
84
+ width: 10,
85
+ },
86
+ { header: "TARGET", value: (p) => formatDate(p.targetDate), width: 12 },
87
+ ],
88
+ outputOpts
89
+ );
90
+ } catch (error) {
91
+ handleApiError(error);
92
+ }
93
+ }),
94
+
95
+ project: procedure
96
+ .meta({
97
+ description: "show project details, create with 'new', or delete with --delete",
98
+ })
99
+ .input(projectInput)
100
+ .query(async ({ input }) => {
101
+ if (input.name === "new") {
102
+ if (!input.projectName) {
103
+ exitWithError("--projectName is required", "usage: lnr project new --projectName \"...\"");
104
+ }
105
+
106
+ try {
107
+ const client = getClient();
108
+ let teamIds: string[] = [];
109
+
110
+ if (input.team) {
111
+ const team = await findTeamByKeyOrName(client, input.team);
112
+ if (!team) {
113
+ const available = (await getAvailableTeamKeys(client)).join(", ");
114
+ exitWithError(
115
+ `team "${input.team}" not found`,
116
+ `available teams: ${available}`,
117
+ EXIT_CODES.NOT_FOUND
118
+ );
119
+ }
120
+ teamIds = [team.id];
121
+ }
122
+
123
+ const project = await createProject(client, {
124
+ name: input.projectName,
125
+ description: input.description,
126
+ teamIds,
127
+ });
128
+
129
+ if (project) {
130
+ console.log(`created project: ${project.name}`);
131
+ } else {
132
+ console.log("created project");
133
+ }
134
+ } catch (error) {
135
+ handleApiError(error);
136
+ }
137
+ return;
138
+ }
139
+
140
+ if (input.delete) {
141
+ try {
142
+ const client = getClient();
143
+ const success = await deleteProject(client, input.name);
144
+
145
+ if (!success) {
146
+ exitWithError(`project "${input.name}" not found`, undefined, EXIT_CODES.NOT_FOUND);
147
+ }
148
+
149
+ console.log(`deleted project: ${input.name}`);
150
+ } catch (error) {
151
+ handleApiError(error);
152
+ }
153
+ return;
154
+ }
155
+
156
+ try {
157
+ const client = getClient();
158
+
159
+ const outputOpts: OutputOptions = {
160
+ format: input.json ? "json" : input.quiet ? "quiet" : undefined,
161
+ verbose: input.verbose,
162
+ };
163
+ const format = getOutputFormat(outputOpts);
164
+
165
+ const project = await getProject(client, input.name);
166
+
167
+ if (!project) {
168
+ exitWithError(`project "${input.name}" not found`, undefined, EXIT_CODES.NOT_FOUND);
169
+ }
170
+
171
+ if (input.issues) {
172
+ const issues = await getProjectIssues(client, input.name);
173
+
174
+ if (format === "json") {
175
+ outputJson(issues);
176
+ return;
177
+ }
178
+
179
+ if (format === "quiet") {
180
+ outputQuiet(issues.map((i) => i.identifier));
181
+ return;
182
+ }
183
+
184
+ outputTable(
185
+ issues,
186
+ [
187
+ { header: "ID", value: (i) => i.identifier, width: 12 },
188
+ { header: "TITLE", value: (i) => truncate(i.title, 50), width: 50 },
189
+ { header: "CREATED", value: (i) => formatDate(i.createdAt), width: 12 },
190
+ ],
191
+ outputOpts
192
+ );
193
+ return;
194
+ }
195
+
196
+ if (format === "json") {
197
+ outputJson(project);
198
+ return;
199
+ }
200
+
201
+ if (format === "quiet") {
202
+ console.log(project.id);
203
+ return;
204
+ }
205
+
206
+ console.log(`${project.name}`);
207
+ if (project.description) {
208
+ console.log(` ${truncate(project.description, 80)}`);
209
+ }
210
+ console.log();
211
+ console.log(`state: ${project.state ?? "-"}`);
212
+ console.log(`progress: ${Math.round((project.progress ?? 0) * 100)}%`);
213
+ console.log(`target: ${formatDate(project.targetDate)}`);
214
+ console.log(`started: ${formatDate(project.startDate)}`);
215
+ console.log(`created: ${formatDate(project.createdAt)}`);
216
+ } catch (error) {
217
+ handleApiError(error);
218
+ }
219
+ }),
220
+ });
@@ -1,5 +1,6 @@
1
- import type { Command } from "commander";
1
+ import { z } from "zod";
2
2
  import { getClient, searchIssues } from "@bdsqqq/lnr-core";
3
+ import { router, procedure } from "./trpc";
3
4
  import { handleApiError } from "../lib/error";
4
5
  import {
5
6
  getOutputFormat,
@@ -7,28 +8,28 @@ import {
7
8
  outputQuiet,
8
9
  outputTable,
9
10
  truncate,
10
- type OutputOptions,
11
11
  } from "../lib/output";
12
12
 
13
- interface SearchOptions extends OutputOptions {
14
- team?: string;
15
- json?: boolean;
16
- quiet?: boolean;
17
- }
13
+ const searchInput = z.object({
14
+ query: z.string().meta({ positional: true }).describe("search query"),
15
+ team: z.string().optional().describe("filter by team key"),
16
+ json: z.boolean().optional().describe("output as json"),
17
+ quiet: z.boolean().optional().describe("output ids only"),
18
+ });
18
19
 
19
- export function registerSearchCommand(program: Command): void {
20
- program
21
- .command("search <query>")
22
- .description("search issues")
23
- .option("--team <key>", "filter by team")
24
- .option("--json", "output as json")
25
- .option("--quiet", "output ids only")
26
- .action(async (query: string, options: SearchOptions) => {
27
- const format = options.json ? "json" : options.quiet ? "quiet" : getOutputFormat(options);
20
+ export const searchRouter = router({
21
+ search: procedure
22
+ .meta({
23
+ description: "search issues",
24
+ aliases: { command: ["s"] },
25
+ })
26
+ .input(searchInput)
27
+ .query(async ({ input }) => {
28
+ const format = input.json ? "json" : input.quiet ? "quiet" : getOutputFormat({});
28
29
 
29
30
  try {
30
31
  const client = getClient();
31
- const issues = await searchIssues(client, query, { team: options.team });
32
+ const issues = await searchIssues(client, input.query, { team: input.team });
32
33
 
33
34
  if (format === "json") {
34
35
  outputJson(issues);
@@ -48,5 +49,5 @@ export function registerSearchCommand(program: Command): void {
48
49
  } catch (error) {
49
50
  handleApiError(error);
50
51
  }
51
- });
52
- }
52
+ }),
53
+ });