@bdsqqq/lnr-cli 1.5.0 → 2.0.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.
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 +6 -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 -600
  46. package/src/router/labels.ts +0 -192
  47. package/src/router/projects.ts +0 -220
@@ -0,0 +1,399 @@
1
+ import type { OperationSpec } from "../lib/operation-spec";
2
+ import "../lib/arktype-config";
3
+ import { type } from "arktype";
4
+ import {
5
+ getClient,
6
+ listViews,
7
+ getView,
8
+ createView,
9
+ updateView,
10
+ deleteView,
11
+ getViewPreferences,
12
+ type CustomView,
13
+ type ViewPreferencesResult,
14
+ } from "@bdsqqq/lnr-core";
15
+ import { router, procedure } from "./trpc";
16
+ import { exitWithError, handleApiError, EXIT_CODES } from "../lib/error";
17
+ import {
18
+ outputJson,
19
+ outputQuiet,
20
+ outputTable,
21
+ getOutputFormat,
22
+ formatDate,
23
+ truncate,
24
+ type OutputOptions,
25
+ type TableColumn,
26
+ } from "../lib/output";
27
+
28
+ export const listViewsInput = type({
29
+ "json?": type("boolean").describe("output as json"),
30
+ "quiet?": type("boolean").describe("output ids only"),
31
+ "verbose?": type("boolean").describe("show all columns"),
32
+ });
33
+
34
+ export const viewInput = type({
35
+ nameOrId: type("string").configure({ positional: true }).describe("view name, id, or 'new'"),
36
+ "name?": type("string").describe("view name"),
37
+ "description?": type("string").describe("view description"),
38
+ "icon?": type("string").describe("view icon"),
39
+ "color?": type("string").describe("view color"),
40
+ "shared?": type("boolean").describe("make view shared"),
41
+ "delete?": type("boolean").describe("delete the view"),
42
+ "preferences?": type("boolean").describe("show view preferences"),
43
+ "json?": type("boolean").describe("output as json"),
44
+ "quiet?": type("boolean").describe("output ids only"),
45
+ "verbose?": type("boolean").describe("show all columns"),
46
+ });
47
+
48
+ type ViewInput = typeof viewInput.infer;
49
+
50
+ const viewColumns: TableColumn<CustomView>[] = [
51
+ { header: "NAME", value: (v) => v.name, width: 25 },
52
+ { header: "SHARED", value: (v) => (v.shared ? "yes" : "no"), width: 8 },
53
+ { header: "UPDATED", value: (v) => formatDate(v.updatedAt), width: 12 },
54
+ ];
55
+
56
+ const verboseViewColumns: TableColumn<CustomView>[] = [
57
+ ...viewColumns,
58
+ {
59
+ header: "DESCRIPTION",
60
+ value: (v) => truncate(v.description ?? "-", 30),
61
+ width: 30,
62
+ },
63
+ { header: "ICON", value: (v) => v.icon ?? "-", width: 8 },
64
+ { header: "COLOR", value: (v) => v.color ?? "-", width: 10 },
65
+ { header: "ID", value: (v) => v.id, width: 36 },
66
+ ];
67
+
68
+ export const viewOperations = ["create", "read", "update", "delete", "preferences"] as const;
69
+ type Operation = (typeof viewOperations)[number];
70
+
71
+ export const viewMutationFlags: readonly (keyof ViewInput)[] = [
72
+ "name", "description", "icon", "color", "shared"
73
+ ] as const;
74
+
75
+ export function inferOperation(input: ViewInput): Operation {
76
+ if (input.nameOrId === "new") return "create";
77
+ if (input.delete) return "delete";
78
+ if (input.preferences) return "preferences";
79
+
80
+ for (const flag of viewMutationFlags) {
81
+ if (input[flag] !== undefined) return "update";
82
+ }
83
+
84
+ return "read";
85
+ }
86
+
87
+ export const viewOperationSpec: OperationSpec<ViewInput, Operation> = {
88
+ command: "view",
89
+ operations: viewOperations,
90
+ mutationFlags: viewMutationFlags,
91
+ inferOperation,
92
+ };
93
+
94
+ async function handleListViews(
95
+ input: typeof listViewsInput.infer
96
+ ): Promise<void> {
97
+ try {
98
+ const client = getClient();
99
+ const views = await listViews(client);
100
+
101
+ const outputOpts: OutputOptions = {
102
+ format: input.json ? "json" : input.quiet ? "quiet" : undefined,
103
+ verbose: input.verbose,
104
+ };
105
+ const format = getOutputFormat(outputOpts);
106
+
107
+ if (format === "json") {
108
+ outputJson(views);
109
+ return;
110
+ }
111
+
112
+ if (format === "quiet") {
113
+ outputQuiet(views.map((v) => v.id));
114
+ return;
115
+ }
116
+
117
+ const columns = input.verbose ? verboseViewColumns : viewColumns;
118
+ outputTable(views, columns, outputOpts);
119
+ } catch (error) {
120
+ handleApiError(error);
121
+ }
122
+ }
123
+
124
+ async function handleShowView(
125
+ nameOrId: string,
126
+ input: ViewInput
127
+ ): Promise<void> {
128
+ try {
129
+ const client = getClient();
130
+
131
+ const outputOpts: OutputOptions = {
132
+ format: input.json ? "json" : input.quiet ? "quiet" : undefined,
133
+ verbose: input.verbose,
134
+ };
135
+ const format = getOutputFormat(outputOpts);
136
+
137
+ const view = await getView(client, nameOrId);
138
+
139
+ if (!view) {
140
+ exitWithError(
141
+ `view "${nameOrId}" not found`,
142
+ "try: lnr views",
143
+ EXIT_CODES.NOT_FOUND
144
+ );
145
+ }
146
+
147
+ if (format === "json") {
148
+ outputJson(view);
149
+ return;
150
+ }
151
+
152
+ if (format === "quiet") {
153
+ console.log(view.id);
154
+ return;
155
+ }
156
+
157
+ console.log(`view: ${view.name}`);
158
+ console.log(` shared: ${view.shared ? "yes" : "no"}`);
159
+ console.log(` updated: ${formatDate(view.updatedAt)}`);
160
+ if (view.description) {
161
+ console.log(` description: ${view.description}`);
162
+ }
163
+ if (view.icon) {
164
+ console.log(` icon: ${view.icon}`);
165
+ }
166
+ if (view.color) {
167
+ console.log(` color: ${view.color}`);
168
+ }
169
+ } catch (error) {
170
+ handleApiError(error);
171
+ }
172
+ }
173
+
174
+ async function handleCreateView(input: ViewInput): Promise<void> {
175
+ if (!input.name) {
176
+ exitWithError(
177
+ "--name is required",
178
+ 'usage: lnr view new --name "My View"'
179
+ );
180
+ }
181
+
182
+ try {
183
+ const client = getClient();
184
+
185
+ const view = await createView(client, {
186
+ name: input.name,
187
+ description: input.description,
188
+ icon: input.icon,
189
+ color: input.color,
190
+ filterData: {},
191
+ shared: input.shared,
192
+ });
193
+
194
+ if (view) {
195
+ console.log(`created view: ${view.name}`);
196
+ } else {
197
+ exitWithError("failed to create view");
198
+ }
199
+ } catch (error) {
200
+ handleApiError(error);
201
+ }
202
+ }
203
+
204
+ async function handleUpdateView(
205
+ nameOrId: string,
206
+ input: ViewInput
207
+ ): Promise<void> {
208
+ try {
209
+ const client = getClient();
210
+
211
+ const view = await getView(client, nameOrId);
212
+
213
+ if (!view) {
214
+ exitWithError(
215
+ `view "${nameOrId}" not found`,
216
+ "try: lnr views",
217
+ EXIT_CODES.NOT_FOUND
218
+ );
219
+ }
220
+
221
+ const success = await updateView(client, view.id, {
222
+ name: input.name,
223
+ description: input.description,
224
+ icon: input.icon,
225
+ color: input.color,
226
+ shared: input.shared,
227
+ });
228
+
229
+ if (!success) {
230
+ exitWithError(
231
+ `failed to update view "${nameOrId}"`,
232
+ undefined,
233
+ EXIT_CODES.NOT_FOUND
234
+ );
235
+ }
236
+
237
+ console.log(`updated view: ${view.name}`);
238
+ } catch (error) {
239
+ handleApiError(error);
240
+ }
241
+ }
242
+
243
+ async function handleDeleteView(
244
+ nameOrId: string,
245
+ input: ViewInput
246
+ ): Promise<void> {
247
+ try {
248
+ const client = getClient();
249
+
250
+ const view = await getView(client, nameOrId);
251
+
252
+ if (!view) {
253
+ exitWithError(
254
+ `view "${nameOrId}" not found`,
255
+ "try: lnr views",
256
+ EXIT_CODES.NOT_FOUND
257
+ );
258
+ }
259
+
260
+ const success = await deleteView(client, view.id);
261
+
262
+ if (!success) {
263
+ exitWithError(
264
+ `failed to delete view "${nameOrId}"`,
265
+ undefined,
266
+ EXIT_CODES.NOT_FOUND
267
+ );
268
+ }
269
+
270
+ console.log(`deleted view: ${view.name}`);
271
+ } catch (error) {
272
+ handleApiError(error);
273
+ }
274
+ }
275
+
276
+ async function handleShowPreferences(
277
+ nameOrId: string,
278
+ input: ViewInput
279
+ ): Promise<void> {
280
+ try {
281
+ const client = getClient();
282
+
283
+ const outputOpts: OutputOptions = {
284
+ format: input.json ? "json" : input.quiet ? "quiet" : undefined,
285
+ verbose: input.verbose,
286
+ };
287
+ const format = getOutputFormat(outputOpts);
288
+
289
+ const view = await getView(client, nameOrId);
290
+
291
+ if (!view) {
292
+ exitWithError(
293
+ `view "${nameOrId}" not found`,
294
+ "try: lnr views",
295
+ EXIT_CODES.NOT_FOUND
296
+ );
297
+ }
298
+
299
+ const prefs = await getViewPreferences(client, view.id);
300
+
301
+ if (!prefs) {
302
+ exitWithError(`no preferences found for view "${nameOrId}"`);
303
+ }
304
+
305
+ if (format === "json") {
306
+ outputJson(prefs);
307
+ return;
308
+ }
309
+
310
+ if (format === "quiet") {
311
+ const ids: string[] = [];
312
+ if (prefs.user) ids.push(prefs.user.id);
313
+ if (prefs.organization) ids.push(prefs.organization.id);
314
+ outputQuiet(ids);
315
+ return;
316
+ }
317
+
318
+ console.log(`preferences for view: ${view.name}`);
319
+ console.log();
320
+
321
+ console.log("effective preferences:");
322
+ console.log(` grouping: ${prefs.effective.issueGrouping ?? "-"}`);
323
+ console.log(` ordering: ${prefs.effective.viewOrdering ?? "-"}`);
324
+ console.log(
325
+ ` show completed: ${prefs.effective.showCompletedIssues ?? "-"}`
326
+ );
327
+
328
+ if (input.verbose) {
329
+ if (prefs.user) {
330
+ console.log();
331
+ console.log("user preferences:");
332
+ console.log(` id: ${prefs.user.id}`);
333
+ console.log(` type: ${prefs.user.type}`);
334
+ console.log(` grouping: ${prefs.user.preferences.issueGrouping ?? "-"}`);
335
+ console.log(` ordering: ${prefs.user.preferences.viewOrdering ?? "-"}`);
336
+ console.log(
337
+ ` show completed: ${prefs.user.preferences.showCompletedIssues ?? "-"}`
338
+ );
339
+ }
340
+ if (prefs.organization) {
341
+ console.log();
342
+ console.log("organization preferences:");
343
+ console.log(` id: ${prefs.organization.id}`);
344
+ console.log(` type: ${prefs.organization.type}`);
345
+ console.log(
346
+ ` grouping: ${prefs.organization.preferences.issueGrouping ?? "-"}`
347
+ );
348
+ console.log(
349
+ ` ordering: ${prefs.organization.preferences.viewOrdering ?? "-"}`
350
+ );
351
+ console.log(
352
+ ` show completed: ${prefs.organization.preferences.showCompletedIssues ?? "-"}`
353
+ );
354
+ }
355
+ }
356
+ } catch (error) {
357
+ handleApiError(error);
358
+ }
359
+ }
360
+
361
+ export const viewsRouter = router({
362
+ views: procedure
363
+ .meta({
364
+ aliases: { command: ["v"] },
365
+ description: "list custom views",
366
+ })
367
+ .input(listViewsInput)
368
+ .query(async ({ input }) => {
369
+ await handleListViews(input);
370
+ }),
371
+
372
+ view: procedure
373
+ .meta({
374
+ description: "show, create, update, or delete a custom view",
375
+ })
376
+ .input(viewInput)
377
+ .mutation(async ({ input }) => {
378
+ const operation = inferOperation(input);
379
+
380
+ switch (operation) {
381
+ case "create":
382
+ await handleCreateView(input);
383
+ break;
384
+ case "delete":
385
+ await handleDeleteView(input.nameOrId, input);
386
+ break;
387
+ case "preferences":
388
+ await handleShowPreferences(input.nameOrId, input);
389
+ break;
390
+ case "update":
391
+ await handleUpdateView(input.nameOrId, input);
392
+ break;
393
+ case "read":
394
+ default:
395
+ await handleShowView(input.nameOrId, input);
396
+ break;
397
+ }
398
+ }),
399
+ });
@@ -1,153 +0,0 @@
1
- import { z } from "zod";
2
- import {
3
- getClient,
4
- listDocuments,
5
- getDocument,
6
- createDocument,
7
- updateDocument,
8
- deleteDocument,
9
- } from "@bdsqqq/lnr-core";
10
- import { router, procedure } from "./trpc";
11
- import { exitWithError, handleApiError, EXIT_CODES } from "../lib/error";
12
- import {
13
- outputJson,
14
- outputQuiet,
15
- outputTable,
16
- getOutputFormat,
17
- truncate,
18
- type OutputOptions,
19
- } from "../lib/output";
20
-
21
- const listDocsInput = z.object({
22
- project: z.string().optional().describe("filter by project id"),
23
- json: z.boolean().optional().describe("output as json"),
24
- quiet: z.boolean().optional().describe("output ids only"),
25
- verbose: z.boolean().optional().describe("show all columns"),
26
- });
27
-
28
- const docInput = z.object({
29
- id: z.string().meta({ positional: true }).describe("document id or 'new'"),
30
- title: z.string().optional().describe("document title (required for new)"),
31
- content: z.string().optional().describe("document content"),
32
- project: z.string().optional().describe("project id to attach document to"),
33
- delete: z.boolean().optional().describe("delete the document"),
34
- json: z.boolean().optional().describe("output as json"),
35
- });
36
-
37
- export const docsRouter = router({
38
- docs: procedure
39
- .meta({
40
- description: "list documents",
41
- })
42
- .input(listDocsInput)
43
- .query(async ({ input }) => {
44
- try {
45
- const client = getClient();
46
-
47
- const outputOpts: OutputOptions = {
48
- format: input.json ? "json" : input.quiet ? "quiet" : undefined,
49
- verbose: input.verbose,
50
- };
51
- const format = getOutputFormat(outputOpts);
52
-
53
- const documents = await listDocuments(client, input.project);
54
-
55
- if (format === "json") {
56
- outputJson(documents);
57
- return;
58
- }
59
-
60
- if (format === "quiet") {
61
- outputQuiet(documents.map((d) => d.id));
62
- return;
63
- }
64
-
65
- outputTable(
66
- documents,
67
- [
68
- { header: "ID", value: (d) => d.id, width: 20 },
69
- { header: "TITLE", value: (d) => truncate(d.title, 50), width: 50 },
70
- ],
71
- outputOpts
72
- );
73
- } catch (error) {
74
- handleApiError(error);
75
- }
76
- }),
77
-
78
- doc: procedure
79
- .meta({
80
- description: "show document details, create with 'new', update, or delete with --delete",
81
- })
82
- .input(docInput)
83
- .query(async ({ input }) => {
84
- try {
85
- const client = getClient();
86
-
87
- if (input.id === "new") {
88
- if (!input.title) {
89
- exitWithError("--title is required", "usage: lnr doc new --title \"...\"");
90
- }
91
-
92
- const doc = await createDocument(client, {
93
- title: input.title,
94
- content: input.content,
95
- projectId: input.project,
96
- });
97
-
98
- if (doc) {
99
- console.log(`created document: ${doc.title}`);
100
- } else {
101
- exitWithError("failed to create document");
102
- }
103
- return;
104
- }
105
-
106
- if (input.delete) {
107
- const success = await deleteDocument(client, input.id);
108
-
109
- if (!success) {
110
- exitWithError(`document "${input.id}" not found`, undefined, EXIT_CODES.NOT_FOUND);
111
- }
112
-
113
- console.log(`deleted document: ${input.id}`);
114
- return;
115
- }
116
-
117
- if (input.title || input.content) {
118
- const success = await updateDocument(client, input.id, {
119
- title: input.title,
120
- content: input.content,
121
- });
122
-
123
- if (!success) {
124
- exitWithError(`document "${input.id}" not found`, undefined, EXIT_CODES.NOT_FOUND);
125
- }
126
-
127
- console.log(`updated document: ${input.id}`);
128
- return;
129
- }
130
-
131
- const format = input.json ? "json" : undefined;
132
-
133
- const doc = await getDocument(client, input.id);
134
-
135
- if (!doc) {
136
- exitWithError(`document "${input.id}" not found`, undefined, EXIT_CODES.NOT_FOUND);
137
- }
138
-
139
- if (format === "json") {
140
- outputJson(doc);
141
- return;
142
- }
143
-
144
- console.log(`${doc.title}`);
145
- if (doc.content) {
146
- console.log();
147
- console.log(doc.content);
148
- }
149
- } catch (error) {
150
- handleApiError(error);
151
- }
152
- }),
153
- });