@bdsqqq/lnr-cli 1.6.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/package.json +2 -3
  2. package/src/bench-lnr-overhead.ts +160 -0
  3. package/src/e2e-mutations.test.ts +378 -0
  4. package/src/e2e-readonly.test.ts +103 -0
  5. package/src/generated/doc.ts +270 -0
  6. package/src/generated/issue.ts +807 -0
  7. package/src/generated/label.ts +273 -0
  8. package/src/generated/project.ts +596 -0
  9. package/src/generated/template.ts +157 -0
  10. package/src/hand-crafted/issue.ts +27 -0
  11. package/src/lib/adapters/doc.ts +14 -0
  12. package/src/lib/adapters/index.ts +4 -0
  13. package/src/lib/adapters/issue.ts +32 -0
  14. package/src/lib/adapters/label.ts +20 -0
  15. package/src/lib/adapters/project.ts +23 -0
  16. package/src/lib/arktype-config.ts +18 -0
  17. package/src/lib/command-introspection.ts +97 -0
  18. package/src/lib/dispatch-effects.test.ts +297 -0
  19. package/src/lib/error.ts +37 -1
  20. package/src/lib/operation-spec.test.ts +317 -0
  21. package/src/lib/operation-spec.ts +11 -0
  22. package/src/lib/operation-specs.ts +21 -0
  23. package/src/lib/output.test.ts +3 -1
  24. package/src/lib/output.ts +1 -296
  25. package/src/lib/renderers/comments.ts +300 -0
  26. package/src/lib/renderers/detail.ts +61 -0
  27. package/src/lib/renderers/index.ts +2 -0
  28. package/src/router/agent-sessions.ts +253 -0
  29. package/src/router/auth.ts +9 -5
  30. package/src/router/config.ts +7 -6
  31. package/src/router/contract.test.ts +364 -0
  32. package/src/router/cycles.ts +372 -95
  33. package/src/router/git-automation-states.ts +355 -0
  34. package/src/router/git-automation-target-branches.ts +309 -0
  35. package/src/router/index.ts +26 -8
  36. package/src/router/initiatives.ts +260 -0
  37. package/src/router/me.ts +8 -7
  38. package/src/router/notifications.ts +176 -0
  39. package/src/router/roadmaps.ts +172 -0
  40. package/src/router/search.ts +7 -6
  41. package/src/router/teams.ts +82 -24
  42. package/src/router/users.ts +126 -0
  43. package/src/router/views.ts +399 -0
  44. package/src/router/docs.ts +0 -153
  45. package/src/router/issues.ts +0 -606
  46. package/src/router/labels.ts +0 -192
  47. package/src/router/projects.ts +0 -220
@@ -2,25 +2,43 @@ import { router } from "./trpc";
2
2
  import { authRouter } from "./auth";
3
3
  import { configRouter } from "./config";
4
4
  import { cyclesRouter } from "./cycles";
5
- import { docsRouter } from "./docs";
6
- import { issuesRouter } from "./issues";
7
- import { labelsRouter } from "./labels";
5
+ import { viewsRouter } from "./views";
6
+ import { generatedDocsRouter } from "../generated/doc";
7
+ import { generatedIssuesRouter } from "../generated/issue";
8
+ import { generatedProjectsRouter } from "../generated/project";
9
+ import { generatedLabelsRouter } from "../generated/label";
10
+ import { generatedTemplatesRouter } from "../generated/template";
8
11
  import { meRouter } from "./me";
9
- import { projectsRouter } from "./projects";
10
12
  import { searchRouter } from "./search";
11
13
  import { teamsRouter } from "./teams";
14
+ import { usersRouter } from "./users";
15
+ import { notificationsRouter } from "./notifications";
16
+ import { initiativesRouter } from "./initiatives";
17
+ import { roadmapsRouter } from "./roadmaps";
18
+ import { gitAutomationStatesRouter } from "./git-automation-states";
19
+ import { gitAutomationTargetBranchesRouter } from "./git-automation-target-branches";
20
+ import { agentSessionsRouter } from "./agent-sessions";
12
21
 
13
22
  export const appRouter = router({
14
23
  ...authRouter._def.procedures,
15
24
  ...configRouter._def.procedures,
16
25
  ...cyclesRouter._def.procedures,
17
- ...docsRouter._def.procedures,
18
- ...issuesRouter._def.procedures,
19
- ...labelsRouter._def.procedures,
26
+ ...viewsRouter._def.procedures,
27
+ ...generatedDocsRouter._def.procedures,
28
+ ...generatedIssuesRouter._def.procedures,
29
+ ...generatedProjectsRouter._def.procedures,
30
+ ...generatedLabelsRouter._def.procedures,
31
+ ...generatedTemplatesRouter._def.procedures,
20
32
  ...meRouter._def.procedures,
21
- ...projectsRouter._def.procedures,
22
33
  ...searchRouter._def.procedures,
23
34
  ...teamsRouter._def.procedures,
35
+ ...usersRouter._def.procedures,
36
+ ...notificationsRouter._def.procedures,
37
+ ...initiativesRouter._def.procedures,
38
+ ...roadmapsRouter._def.procedures,
39
+ ...gitAutomationStatesRouter._def.procedures,
40
+ ...gitAutomationTargetBranchesRouter._def.procedures,
41
+ ...agentSessionsRouter._def.procedures,
24
42
  });
25
43
 
26
44
  export type AppRouter = typeof appRouter;
@@ -0,0 +1,260 @@
1
+ import "../lib/arktype-config";
2
+ import { type } from "arktype";
3
+ import {
4
+ getClient,
5
+ listInitiatives,
6
+ getInitiative,
7
+ findInitiativeByName,
8
+ getInitiativeUpdates,
9
+ getInitiativeExternalLinks,
10
+ createReaction,
11
+ deleteReaction,
12
+ createSubscription,
13
+ deleteSubscription,
14
+ findUserSubscription,
15
+ type Initiative,
16
+ type InitiativeUpdate,
17
+ type EntityExternalLink,
18
+ } from "@bdsqqq/lnr-core";
19
+ import { router, procedure } from "./trpc";
20
+ import { exitWithError, handleApiError, EXIT_CODES } from "../lib/error";
21
+ import {
22
+ outputJson,
23
+ outputQuiet,
24
+ outputTable,
25
+ getOutputFormat,
26
+ type OutputOptions,
27
+ type TableColumn,
28
+ } from "../lib/output";
29
+
30
+ export const listInitiativesInput = type({
31
+ "json?": type("boolean").describe("output as json"),
32
+ "quiet?": type("boolean").describe("output ids only"),
33
+ "verbose?": type("boolean").describe("show all columns"),
34
+ });
35
+
36
+ export const initiativeInput = type({
37
+ nameOrId: type("string").configure({ positional: true }).describe("initiative name, slugId, or id"),
38
+ "json?": type("boolean").describe("output as json"),
39
+ "quiet?": type("boolean").describe("output id only"),
40
+ "verbose?": type("boolean").describe("show all columns"),
41
+ "updates?": type("boolean").describe("show initiative updates"),
42
+ "links?": type("boolean").describe("show initiative external links"),
43
+ "react?": type("string").describe("initiative update id to add reaction (requires --emoji)"),
44
+ "emoji?": type("string").describe("emoji for --react"),
45
+ "unreact?": type("string").describe("reaction id to remove"),
46
+ "subscribe?": type("boolean").describe("subscribe to initiative notifications"),
47
+ "unsubscribe?": type("boolean").describe("unsubscribe from initiative notifications"),
48
+ });
49
+
50
+ const initiativeColumns: TableColumn<Initiative>[] = [
51
+ { header: "NAME", value: (i) => i.name, width: 32 },
52
+ { header: "STATUS", value: (i) => i.status, width: 12 },
53
+ { header: "HEALTH", value: (i) => i.health ?? "-", width: 10 },
54
+ { header: "TARGET", value: (i) => i.targetDate ?? "-", width: 12 },
55
+ ];
56
+
57
+ const verboseInitiativeColumns: TableColumn<Initiative>[] = [
58
+ ...initiativeColumns,
59
+ { header: "SLUG", value: (i) => i.slugId, width: 16 },
60
+ { header: "ID", value: (i) => i.id, width: 36 },
61
+ ];
62
+
63
+ const updateColumns: TableColumn<InitiativeUpdate>[] = [
64
+ { header: "DATE", value: (u) => u.createdAt.toISOString().slice(0, 10), width: 12 },
65
+ { header: "HEALTH", value: (u) => u.health, width: 10 },
66
+ { header: "AUTHOR", value: (u) => u.userName ?? "-", width: 20 },
67
+ { header: "CONTENT", value: (u) => u.body.slice(0, 50).replace(/\n/g, " "), width: 52 },
68
+ ];
69
+
70
+ const verboseUpdateColumns: TableColumn<InitiativeUpdate>[] = [
71
+ ...updateColumns,
72
+ { header: "ID", value: (u) => u.id, width: 36 },
73
+ ];
74
+
75
+ const linkColumns: TableColumn<EntityExternalLink>[] = [
76
+ { header: "LABEL", value: (l) => l.label.slice(0, 30), width: 30 },
77
+ { header: "URL", value: (l) => l.url.slice(0, 50), width: 50 },
78
+ ];
79
+
80
+ const verboseLinkColumns: TableColumn<EntityExternalLink>[] = [
81
+ ...linkColumns,
82
+ { header: "ID", value: (l) => l.id, width: 36 },
83
+ { header: "CREATOR", value: (l) => l.creatorName ?? "-", width: 20 },
84
+ ];
85
+
86
+ export const initiativesRouter = router({
87
+ initiatives: procedure
88
+ .meta({ aliases: { command: ["init"] }, description: "list initiatives" })
89
+ .input(listInitiativesInput)
90
+ .query(async ({ input }) => {
91
+ try {
92
+ const client = getClient();
93
+ const initiatives = await listInitiatives(client);
94
+
95
+ const outputOpts: OutputOptions = {
96
+ format: input.json ? "json" : input.quiet ? "quiet" : undefined,
97
+ verbose: input.verbose,
98
+ };
99
+ const format = getOutputFormat(outputOpts);
100
+
101
+ if (format === "json") {
102
+ outputJson(initiatives);
103
+ return;
104
+ }
105
+
106
+ if (format === "quiet") {
107
+ outputQuiet(initiatives.map((i) => i.id));
108
+ return;
109
+ }
110
+
111
+ const columns = input.verbose ? verboseInitiativeColumns : initiativeColumns;
112
+ outputTable(initiatives, columns, outputOpts);
113
+ } catch (error) {
114
+ handleApiError(error, "initiatives");
115
+ }
116
+ }),
117
+
118
+ initiative: procedure
119
+ .meta({ description: "show or update initiative" })
120
+ .input(initiativeInput)
121
+ .mutation(async ({ input }) => {
122
+ try {
123
+ const client = getClient();
124
+ let initiative = await getInitiative(client, input.nameOrId);
125
+
126
+ if (!initiative) {
127
+ initiative = await findInitiativeByName(client, input.nameOrId);
128
+ }
129
+
130
+ if (!initiative) {
131
+ exitWithError(
132
+ `initiative "${input.nameOrId}" not found`,
133
+ "try: lnr initiatives",
134
+ EXIT_CODES.NOT_FOUND
135
+ );
136
+ }
137
+
138
+ const outputOpts: OutputOptions = {
139
+ format: input.json ? "json" : input.quiet ? "quiet" : undefined,
140
+ verbose: input.verbose,
141
+ };
142
+ const format = getOutputFormat(outputOpts);
143
+
144
+ if (input.react) {
145
+ if (!input.emoji) {
146
+ exitWithError("--emoji is required when using --react");
147
+ }
148
+ const success = await createReaction(client, { type: "initiativeUpdate", id: input.react }, input.emoji);
149
+ if (!success) {
150
+ exitWithError(`failed to add reaction to initiative update ${input.react.slice(0, 8)}`);
151
+ }
152
+ console.log(`added reaction ${input.emoji} to initiative update ${input.react.slice(0, 8)}`);
153
+ return;
154
+ }
155
+
156
+ if (input.unreact) {
157
+ const success = await deleteReaction(client, input.unreact);
158
+ if (!success) {
159
+ exitWithError(`reaction ${input.unreact.slice(0, 8)} not found`, undefined, EXIT_CODES.NOT_FOUND);
160
+ }
161
+ console.log(`removed reaction ${input.unreact.slice(0, 8)}`);
162
+ return;
163
+ }
164
+
165
+ if (input.subscribe) {
166
+ const subscriptionId = await createSubscription(client, { type: "initiative", initiativeId: initiative.id });
167
+ console.log(`subscribed to ${initiative.name} (subscription: ${subscriptionId.slice(0, 8)})`);
168
+ return;
169
+ }
170
+
171
+ if (input.unsubscribe) {
172
+ const subscriptionId = await findUserSubscription(client, { type: "initiative", initiativeId: initiative.id });
173
+ if (!subscriptionId) {
174
+ exitWithError(`no subscription found for ${initiative.name}`, "you may not be subscribed to this initiative");
175
+ }
176
+ const success = await deleteSubscription(client, subscriptionId);
177
+ if (!success) {
178
+ exitWithError(`failed to remove subscription`, undefined, EXIT_CODES.NOT_FOUND);
179
+ }
180
+ console.log(`unsubscribed from ${initiative.name}`);
181
+ return;
182
+ }
183
+
184
+ if (input.updates) {
185
+ const updates = await getInitiativeUpdates(client, initiative.id);
186
+
187
+ if (format === "json") {
188
+ outputJson(updates);
189
+ return;
190
+ }
191
+
192
+ if (format === "quiet") {
193
+ outputQuiet(updates.map((u) => u.id));
194
+ return;
195
+ }
196
+
197
+ if (updates.length === 0) {
198
+ console.log("no updates");
199
+ return;
200
+ }
201
+
202
+ const columns = input.verbose ? verboseUpdateColumns : updateColumns;
203
+ outputTable(updates, columns, outputOpts);
204
+ return;
205
+ }
206
+
207
+ if (input.links) {
208
+ const links = await getInitiativeExternalLinks(client, initiative.id);
209
+
210
+ if (format === "json") {
211
+ outputJson(links);
212
+ return;
213
+ }
214
+
215
+ if (format === "quiet") {
216
+ outputQuiet(links.map((l) => l.id));
217
+ return;
218
+ }
219
+
220
+ if (links.length === 0) {
221
+ console.log("no external links");
222
+ return;
223
+ }
224
+
225
+ const columns = input.verbose ? verboseLinkColumns : linkColumns;
226
+ outputTable(links, columns, outputOpts);
227
+ return;
228
+ }
229
+
230
+ if (format === "json") {
231
+ outputJson(initiative);
232
+ return;
233
+ }
234
+
235
+ if (format === "quiet") {
236
+ console.log(initiative.id);
237
+ return;
238
+ }
239
+
240
+ console.log(`${initiative.name}`);
241
+ console.log(`status: ${initiative.status}`);
242
+ if (initiative.health) {
243
+ console.log(`health: ${initiative.health}`);
244
+ }
245
+ if (initiative.description) {
246
+ console.log(`description: ${initiative.description}`);
247
+ }
248
+ if (initiative.targetDate) {
249
+ console.log(`target: ${initiative.targetDate}`);
250
+ }
251
+ console.log(`url: ${initiative.url}`);
252
+ if (input.verbose) {
253
+ console.log(`slugId: ${initiative.slugId}`);
254
+ console.log(`id: ${initiative.id}`);
255
+ }
256
+ } catch (error) {
257
+ handleApiError(error, "initiatives");
258
+ }
259
+ }),
260
+ });
package/src/router/me.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { z } from "zod";
1
+ import "../lib/arktype-config";
2
+ import { type } from "arktype";
2
3
  import {
3
4
  getClient,
4
5
  getViewer,
@@ -17,12 +18,12 @@ import {
17
18
  } from "../lib/output";
18
19
  import { handleApiError } from "../lib/error";
19
20
 
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"),
21
+ export const meInput = type({
22
+ "issues?": type("boolean").describe("list issues assigned to me"),
23
+ "created?": type("boolean").describe("list issues created by me"),
24
+ "activity?": type("boolean").describe("show recent activity"),
25
+ "json?": type("boolean").describe("output as json"),
26
+ "quiet?": type("boolean").describe("output ids only"),
26
27
  });
27
28
 
28
29
  export const meRouter = router({
@@ -0,0 +1,176 @@
1
+ import "../lib/arktype-config";
2
+ import { type } from "arktype";
3
+ import {
4
+ getClient,
5
+ listNotifications,
6
+ getNotification,
7
+ markNotificationRead,
8
+ archiveNotification,
9
+ type Notification,
10
+ } from "@bdsqqq/lnr-core";
11
+ import { router, procedure } from "./trpc";
12
+ import { exitWithError, handleApiError, EXIT_CODES } from "../lib/error";
13
+ import {
14
+ outputJson,
15
+ outputQuiet,
16
+ outputTable,
17
+ getOutputFormat,
18
+ type OutputOptions,
19
+ type TableColumn,
20
+ } from "../lib/output";
21
+
22
+ export const listNotificationsInput = type({
23
+ "json?": type("boolean").describe("output as json"),
24
+ "quiet?": type("boolean").describe("output ids only"),
25
+ "verbose?": type("boolean").describe("show all columns"),
26
+ "unread?": type("boolean").describe("show unread only"),
27
+ });
28
+
29
+ export const notificationInput = type({
30
+ id: type("string").configure({ positional: true }).describe("notification id"),
31
+ "json?": type("boolean").describe("output as json"),
32
+ "quiet?": type("boolean").describe("output id only"),
33
+ "verbose?": type("boolean").describe("show all fields"),
34
+ "read?": type("boolean").describe("mark as read"),
35
+ "archive?": type("boolean").describe("archive notification"),
36
+ });
37
+
38
+ const notificationColumns: TableColumn<Notification>[] = [
39
+ { header: "TYPE", value: (n) => n.type, width: 20 },
40
+ { header: "CATEGORY", value: (n) => n.category, width: 16 },
41
+ { header: "ACTOR", value: (n) => n.actorName ?? "-", width: 20 },
42
+ { header: "READ", value: (n) => (n.readAt ? "yes" : "no"), width: 6 },
43
+ {
44
+ header: "DATE",
45
+ value: (n) => n.createdAt.toISOString().split("T")[0] ?? "",
46
+ width: 12,
47
+ },
48
+ ];
49
+
50
+ const verboseNotificationColumns: TableColumn<Notification>[] = [
51
+ ...notificationColumns,
52
+ {
53
+ header: "SNOOZED",
54
+ value: (n) =>
55
+ n.snoozedUntilAt ? n.snoozedUntilAt.toISOString().split("T")[0] ?? "-" : "-",
56
+ width: 12,
57
+ },
58
+ { header: "ID", value: (n) => n.id, width: 36 },
59
+ ];
60
+
61
+ export const notificationsRouter = router({
62
+ notifications: procedure
63
+ .meta({ aliases: { command: ["n"] }, description: "list notifications" })
64
+ .input(listNotificationsInput)
65
+ .query(async ({ input }) => {
66
+ try {
67
+ const client = getClient();
68
+ const notifications = await listNotifications(client, {
69
+ unreadOnly: input.unread,
70
+ });
71
+
72
+ const outputOpts: OutputOptions = {
73
+ format: input.json ? "json" : input.quiet ? "quiet" : undefined,
74
+ verbose: input.verbose,
75
+ };
76
+ const format = getOutputFormat(outputOpts);
77
+
78
+ if (format === "json") {
79
+ outputJson(notifications);
80
+ return;
81
+ }
82
+
83
+ if (format === "quiet") {
84
+ outputQuiet(notifications.map((n) => n.id));
85
+ return;
86
+ }
87
+
88
+ const columns = input.verbose
89
+ ? verboseNotificationColumns
90
+ : notificationColumns;
91
+ outputTable(notifications, columns, outputOpts);
92
+ } catch (error) {
93
+ handleApiError(error);
94
+ }
95
+ }),
96
+
97
+ notification: procedure
98
+ .meta({ description: "show notification details" })
99
+ .input(notificationInput)
100
+ .mutation(async ({ input }) => {
101
+ try {
102
+ const client = getClient();
103
+
104
+ if (input.read) {
105
+ const success = await markNotificationRead(client, input.id);
106
+ if (!success) {
107
+ exitWithError(
108
+ `failed to mark notification "${input.id}" as read`,
109
+ "check the notification id",
110
+ EXIT_CODES.GENERAL_ERROR
111
+ );
112
+ }
113
+ console.log("marked as read");
114
+ return;
115
+ }
116
+
117
+ if (input.archive) {
118
+ const success = await archiveNotification(client, input.id);
119
+ if (!success) {
120
+ exitWithError(
121
+ `failed to archive notification "${input.id}"`,
122
+ "check the notification id",
123
+ EXIT_CODES.GENERAL_ERROR
124
+ );
125
+ }
126
+ console.log("archived");
127
+ return;
128
+ }
129
+
130
+ const notification = await getNotification(client, input.id);
131
+
132
+ if (!notification) {
133
+ exitWithError(
134
+ `notification "${input.id}" not found`,
135
+ "try: lnr notifications",
136
+ EXIT_CODES.NOT_FOUND
137
+ );
138
+ }
139
+
140
+ const outputOpts: OutputOptions = {
141
+ format: input.json ? "json" : input.quiet ? "quiet" : undefined,
142
+ verbose: input.verbose,
143
+ };
144
+ const format = getOutputFormat(outputOpts);
145
+
146
+ if (format === "json") {
147
+ outputJson(notification);
148
+ return;
149
+ }
150
+
151
+ if (format === "quiet") {
152
+ console.log(notification.id);
153
+ return;
154
+ }
155
+
156
+ console.log(`type: ${notification.type}`);
157
+ console.log(`category: ${notification.category}`);
158
+ console.log(`date: ${notification.createdAt.toISOString()}`);
159
+ console.log(`read: ${notification.readAt ? "yes" : "no"}`);
160
+ if (notification.actorName) {
161
+ console.log(`actor: ${notification.actorName}`);
162
+ }
163
+ if (notification.snoozedUntilAt) {
164
+ console.log(`snoozed until: ${notification.snoozedUntilAt.toISOString()}`);
165
+ }
166
+ if (input.verbose) {
167
+ console.log(`id: ${notification.id}`);
168
+ if (notification.archivedAt) {
169
+ console.log(`archived: ${notification.archivedAt.toISOString()}`);
170
+ }
171
+ }
172
+ } catch (error) {
173
+ handleApiError(error);
174
+ }
175
+ }),
176
+ });
@@ -0,0 +1,172 @@
1
+ import "../lib/arktype-config";
2
+ import { type } from "arktype";
3
+ import {
4
+ getClient,
5
+ listRoadmaps,
6
+ getRoadmap,
7
+ findRoadmapByName,
8
+ getRoadmapProjects,
9
+ type Roadmap,
10
+ type Project,
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
+ type OutputOptions,
20
+ type TableColumn,
21
+ } from "../lib/output";
22
+
23
+ export const listRoadmapsInput = type({
24
+ "json?": type("boolean").describe("output as json"),
25
+ "quiet?": type("boolean").describe("output ids only"),
26
+ "verbose?": type("boolean").describe("show all columns"),
27
+ });
28
+
29
+ export const roadmapInput = type({
30
+ nameOrId: type("string").configure({ positional: true }).describe("roadmap name, slugId, or id"),
31
+ "json?": type("boolean").describe("output as json"),
32
+ "quiet?": type("boolean").describe("output id only"),
33
+ "verbose?": type("boolean").describe("show all columns"),
34
+ "projects?": type("boolean").describe("show roadmap projects"),
35
+ });
36
+
37
+ const roadmapColumns: TableColumn<Roadmap>[] = [
38
+ { header: "NAME", value: (r) => r.name, width: 32 },
39
+ { header: "OWNER", value: (r) => r.ownerName ?? "-", width: 20 },
40
+ { header: "SLUG", value: (r) => r.slugId, width: 16 },
41
+ ];
42
+
43
+ const verboseRoadmapColumns: TableColumn<Roadmap>[] = [
44
+ ...roadmapColumns,
45
+ { header: "COLOR", value: (r) => r.color ?? "-", width: 10 },
46
+ { header: "ID", value: (r) => r.id, width: 36 },
47
+ ];
48
+
49
+ const projectColumns: TableColumn<Project>[] = [
50
+ { header: "NAME", value: (p) => p.name, width: 32 },
51
+ { header: "STATE", value: (p) => p.state ?? "-", width: 12 },
52
+ { header: "PROGRESS", value: (p) => p.progress != null ? `${Math.round(p.progress * 100)}%` : "-", width: 10 },
53
+ { header: "TARGET", value: (p) => p.targetDate?.toISOString().slice(0, 10) ?? "-", width: 12 },
54
+ ];
55
+
56
+ const verboseProjectColumns: TableColumn<Project>[] = [
57
+ ...projectColumns,
58
+ { header: "START", value: (p) => p.startDate?.toISOString().slice(0, 10) ?? "-", width: 12 },
59
+ { header: "ID", value: (p) => p.id, width: 36 },
60
+ ];
61
+
62
+ export const roadmapsRouter = router({
63
+ roadmaps: procedure
64
+ .meta({ aliases: { command: ["rm"] }, description: "list roadmaps" })
65
+ .input(listRoadmapsInput)
66
+ .query(async ({ input }) => {
67
+ try {
68
+ const client = getClient();
69
+ const roadmaps = await listRoadmaps(client);
70
+
71
+ const outputOpts: OutputOptions = {
72
+ format: input.json ? "json" : input.quiet ? "quiet" : undefined,
73
+ verbose: input.verbose,
74
+ };
75
+ const format = getOutputFormat(outputOpts);
76
+
77
+ if (format === "json") {
78
+ outputJson(roadmaps);
79
+ return;
80
+ }
81
+
82
+ if (format === "quiet") {
83
+ outputQuiet(roadmaps.map((r) => r.id));
84
+ return;
85
+ }
86
+
87
+ const columns = input.verbose ? verboseRoadmapColumns : roadmapColumns;
88
+ outputTable(roadmaps, columns, outputOpts);
89
+ } catch (error) {
90
+ handleApiError(error, "roadmaps");
91
+ }
92
+ }),
93
+
94
+ roadmap: procedure
95
+ .meta({ description: "show roadmap details" })
96
+ .input(roadmapInput)
97
+ .query(async ({ input }) => {
98
+ try {
99
+ const client = getClient();
100
+ let roadmap = await getRoadmap(client, input.nameOrId);
101
+
102
+ if (!roadmap) {
103
+ roadmap = await findRoadmapByName(client, input.nameOrId);
104
+ }
105
+
106
+ if (!roadmap) {
107
+ exitWithError(
108
+ `roadmap "${input.nameOrId}" not found`,
109
+ "try: lnr roadmaps",
110
+ EXIT_CODES.NOT_FOUND
111
+ );
112
+ }
113
+
114
+ const outputOpts: OutputOptions = {
115
+ format: input.json ? "json" : input.quiet ? "quiet" : undefined,
116
+ verbose: input.verbose,
117
+ };
118
+ const format = getOutputFormat(outputOpts);
119
+
120
+ if (input.projects) {
121
+ const projects = await getRoadmapProjects(client, roadmap.id);
122
+
123
+ if (format === "json") {
124
+ outputJson(projects);
125
+ return;
126
+ }
127
+
128
+ if (format === "quiet") {
129
+ outputQuiet(projects.map((p) => p.id));
130
+ return;
131
+ }
132
+
133
+ if (projects.length === 0) {
134
+ console.log("no projects");
135
+ return;
136
+ }
137
+
138
+ const columns = input.verbose ? verboseProjectColumns : projectColumns;
139
+ outputTable(projects, columns, outputOpts);
140
+ return;
141
+ }
142
+
143
+ if (format === "json") {
144
+ outputJson(roadmap);
145
+ return;
146
+ }
147
+
148
+ if (format === "quiet") {
149
+ console.log(roadmap.id);
150
+ return;
151
+ }
152
+
153
+ console.log(`${roadmap.name}`);
154
+ if (roadmap.ownerName) {
155
+ console.log(`owner: ${roadmap.ownerName}`);
156
+ }
157
+ if (roadmap.description) {
158
+ console.log(`description: ${roadmap.description}`);
159
+ }
160
+ console.log(`url: ${roadmap.url}`);
161
+ if (input.verbose) {
162
+ console.log(`slugId: ${roadmap.slugId}`);
163
+ if (roadmap.color) {
164
+ console.log(`color: ${roadmap.color}`);
165
+ }
166
+ console.log(`id: ${roadmap.id}`);
167
+ }
168
+ } catch (error) {
169
+ handleApiError(error, "roadmaps");
170
+ }
171
+ }),
172
+ });