@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
@@ -0,0 +1,596 @@
1
+ /**
2
+ * GENERATED FILE - DO NOT EDIT
3
+ * Regenerate with: bun run packages/codegen/generate-commands.ts
4
+ */
5
+
6
+ import "../lib/arktype-config";
7
+ import { type } from "arktype";
8
+ import {
9
+ getClient,
10
+ listProjects,
11
+ getProject,
12
+ getProjectIssues,
13
+ createProject,
14
+ deleteProject,
15
+ updateProject,
16
+ findTeamByKeyOrName,
17
+ getAvailableTeamKeys,
18
+ resolveAssignee,
19
+ resolveTeamByKey,
20
+ createReaction,
21
+ deleteReaction,
22
+ createSubscription,
23
+ deleteSubscription,
24
+ findUserSubscription,
25
+ getProjectUpdates,
26
+ getProjectLabels,
27
+ getProjectStatus,
28
+ getProjectExternalLinks,
29
+ listMilestones,
30
+ createMilestone,
31
+ updateMilestone,
32
+ deleteMilestone,
33
+ resolveMilestoneByName,
34
+ resolveProjectByName,
35
+ type Project,
36
+ } from "@bdsqqq/lnr-core";
37
+ import { router, procedure } from "../router/trpc";
38
+ import { handleApiError, exitWithError, EXIT_CODES } from "../lib/error";
39
+ import type { OperationSpec } from "../lib/operation-spec";
40
+ import {
41
+ outputJson,
42
+ outputQuiet,
43
+ outputTable,
44
+ getOutputFormat,
45
+ truncate,
46
+ formatDate,
47
+ type OutputOptions,
48
+ type TableColumn,
49
+ } from "../lib/output";
50
+ import { outputDetail } from "../lib/renderers/detail";
51
+ import { projectToDetail } from "../lib/adapters";
52
+
53
+
54
+ export const listProjectsInput = type({
55
+ "team?": type("string").describe("filter by team key"),
56
+ "status?": type("string").describe("filter by status (planned, started, completed, etc)"),
57
+ "json?": type("boolean").describe("output as json"),
58
+ "quiet?": type("boolean").describe("output ids only"),
59
+ "verbose?": type("boolean").describe("show all columns"),
60
+ });
61
+
62
+ export const projectInput = type({
63
+ name: type("string").configure({ positional: true }).describe("project name or 'new'"),
64
+ "issues?": type("boolean").describe("list issues in project"),
65
+ "json?": type("boolean").describe("output as json"),
66
+ "quiet?": type("boolean").describe("output ids only"),
67
+ "verbose?": type("boolean").describe("show all columns"),
68
+ "delete?": type("boolean").describe("delete the project"),
69
+ "status?": type("string").describe("set project status"),
70
+ "newName?": type("string").describe("new name for the project"),
71
+ "description?": type("string").describe("project description"),
72
+ "content?": type("string").describe("set project content as markdown"),
73
+ "team?": type("string").describe("team key to associate project with"),
74
+ "lead?": type("string").describe("set lead by email or @me"),
75
+ "startDate?": type("string").describe("set start date (YYYY-MM-DD)"),
76
+ "targetDate?": type("string").describe("set target date (YYYY-MM-DD)"),
77
+ "priority?": type("number").describe("set priority (0=none, 1=urgent, 2=high, 3=normal, 4=low)"),
78
+ "updates?": type("boolean").describe("list project updates"),
79
+ "labels?": type("boolean").describe("list project labels"),
80
+ "showStatus?": type("boolean").describe("show project status details"),
81
+ "links?": type("boolean").describe("list project external links"),
82
+ "milestones?": type("boolean").describe("list project milestones"),
83
+ "react?": type("string").describe("entity id to add reaction (requires --emoji)"),
84
+ "emoji?": type("string").describe("emoji for --react"),
85
+ "unreact?": type("string").describe("reaction id to remove"),
86
+ "subscribe?": type("boolean").describe("subscribe to notifications"),
87
+ "unsubscribe?": type("boolean").describe("unsubscribe from notifications"),
88
+ });
89
+
90
+ type ProjectInput = typeof projectInput.infer;
91
+
92
+ export const projectMilestoneInput = type({
93
+ nameOrNew: type("string").configure({ positional: true }).describe("milestone name or 'new'"),
94
+ project: type("string").describe("project name (required)"),
95
+ "newName?": type("string").describe("new name for the milestone"),
96
+ "description?": type("string").describe("milestone description"),
97
+ "targetDate?": type("string").describe("target date (YYYY-MM-DD)"),
98
+ "delete?": type("boolean").describe("delete the milestone"),
99
+ "json?": type("boolean").describe("output as json"),
100
+ });
101
+
102
+ type ProjectMilestoneInput = typeof projectMilestoneInput.infer;
103
+
104
+ const projectColumns: TableColumn<Project>[] = [
105
+ { header: "NAME", value: (p) => truncate(p.name, 30), width: 30 },
106
+ { header: "STATE", value: (p) => p.state ?? "-", width: 12 },
107
+ { header: "PROGRESS", value: (p) => `${Math.round((p.progress ?? 0) * 100)}%`, width: 10 },
108
+ { header: "TARGET", value: (p) => formatDate(p.targetDate), width: 12 },
109
+ ];
110
+
111
+ export const projectOperations = ["create", "read", "update", "delete"] as const;
112
+ type Operation = (typeof projectOperations)[number];
113
+
114
+ export const projectMutationFlags: readonly (keyof ProjectInput)[] = [
115
+ "newName", "description", "content", "status", "startDate", "targetDate", "priority", "lead", "team", "react", "emoji", "unreact", "subscribe", "unsubscribe"
116
+ ] as const;
117
+
118
+ export function inferOperation(input: ProjectInput): Operation {
119
+ if (input.name === "new") return "create";
120
+ if (input.delete) return "delete";
121
+
122
+ for (const flag of projectMutationFlags) {
123
+ if (input[flag] !== undefined) return "update";
124
+ }
125
+
126
+ return "read";
127
+ }
128
+
129
+ export const projectOperationSpec: OperationSpec<ProjectInput, Operation> = {
130
+ command: "project",
131
+ operations: projectOperations,
132
+ mutationFlags: projectMutationFlags,
133
+ inferOperation,
134
+ };
135
+
136
+ async function handleListProjects(
137
+ input: typeof listProjectsInput.infer
138
+ ): Promise<void> {
139
+ try {
140
+ const client = getClient();
141
+
142
+ const outputOpts: OutputOptions = {
143
+ format: input.json ? "json" : input.quiet ? "quiet" : undefined,
144
+ verbose: input.verbose,
145
+ };
146
+ const format = getOutputFormat(outputOpts);
147
+
148
+ const projects = await listProjects(client, { team: input.team, status: input.status });
149
+
150
+ if (format === "json") {
151
+ outputJson(projects);
152
+ return;
153
+ }
154
+
155
+ if (format === "quiet") {
156
+ outputQuiet(projects.map((p) => p.id));
157
+ return;
158
+ }
159
+
160
+ outputTable(projects, projectColumns, outputOpts);
161
+ } catch (error) {
162
+ handleApiError(error);
163
+ }
164
+ }
165
+
166
+ async function handleShowProject(
167
+ name: string,
168
+ input: ProjectInput
169
+ ): Promise<void> {
170
+ try {
171
+ const client = getClient();
172
+
173
+ const outputOpts: OutputOptions = {
174
+ format: input.json ? "json" : input.quiet ? "quiet" : undefined,
175
+ verbose: input.verbose,
176
+ };
177
+ const format = getOutputFormat(outputOpts);
178
+
179
+ const project = await getProject(client, name);
180
+
181
+ if (!project) {
182
+ exitWithError(`project "${name}" not found`, undefined, EXIT_CODES.NOT_FOUND);
183
+ }
184
+
185
+ if (input.issues) {
186
+ const issues = await getProjectIssues(client, project.id);
187
+ if (format === "json") {
188
+ outputJson(issues);
189
+ } else if (format === "quiet") {
190
+ outputQuiet(issues.map((i) => i.identifier));
191
+ } else {
192
+ for (const issue of issues) {
193
+ console.log(`${issue.identifier}: ${issue.title}`);
194
+ }
195
+ }
196
+ return;
197
+ }
198
+
199
+ // scoped entity handlers (injected from entity-definitions)
200
+ if (input.updates) {
201
+ const updates = await getProjectUpdates(client, project.id);
202
+ if (format === "json") {
203
+ outputJson(updates);
204
+ } else if (format === "quiet") {
205
+ outputQuiet(updates.map((u) => u.id));
206
+ } else {
207
+ for (const u of updates) {
208
+ console.log(`[${u.health}] ${formatDate(u.createdAt)} - ${truncate(u.body.replace(/\n/g, " "), 60)}`);
209
+ }
210
+ }
211
+ return;
212
+ }
213
+
214
+ if (input.labels) {
215
+ const labels = await getProjectLabels(client, project.id);
216
+ if (format === "json") {
217
+ outputJson(labels);
218
+ } else if (format === "quiet") {
219
+ outputQuiet(labels.map((l) => l.id));
220
+ } else {
221
+ for (const l of labels) {
222
+ console.log(`${l.name} (${l.color})`);
223
+ }
224
+ }
225
+ return;
226
+ }
227
+
228
+ if (input.showStatus) {
229
+ const status = await getProjectStatus(client, project.id);
230
+ if (!status) {
231
+ console.log("no status set");
232
+ return;
233
+ }
234
+ if (format === "json") {
235
+ outputJson(status);
236
+ } else if (format === "quiet") {
237
+ console.log(status.id);
238
+ } else {
239
+ console.log(`${status.name} (${status.type}) - ${status.color}`);
240
+ }
241
+ return;
242
+ }
243
+
244
+ if (input.milestones) {
245
+ const milestones = await listMilestones(client, { projectId: project.id });
246
+ if (format === "json") {
247
+ outputJson(milestones);
248
+ } else if (format === "quiet") {
249
+ outputQuiet(milestones.map((m) => m.id));
250
+ } else {
251
+ if (milestones.length === 0) {
252
+ console.log("no milestones");
253
+ return;
254
+ }
255
+ for (const m of milestones) {
256
+ console.log(`${m.name}${m.targetDate ? ` (target: ${formatDate(m.targetDate)})` : ""}`);
257
+ }
258
+ }
259
+ return;
260
+ }
261
+
262
+ if (input.links) {
263
+ const links = await getProjectExternalLinks(client, project.id);
264
+ if (format === "json") {
265
+ outputJson(links);
266
+ } else if (format === "quiet") {
267
+ outputQuiet(links.map((l) => l.id));
268
+ } else {
269
+ if (links.length === 0) {
270
+ console.log("no external links");
271
+ return;
272
+ }
273
+ for (const l of links) {
274
+ console.log(`${l.label}: ${l.url}`);
275
+ }
276
+ }
277
+ return;
278
+ }
279
+
280
+ if (format === "json") {
281
+ outputJson(project);
282
+ return;
283
+ }
284
+
285
+ if (format === "quiet") {
286
+ console.log(project.id);
287
+ return;
288
+ }
289
+
290
+ outputDetail(projectToDetail(project));
291
+ } catch (error) {
292
+ handleApiError(error);
293
+ }
294
+ }
295
+
296
+ async function handleUpdateProject(
297
+ name: string,
298
+ input: ProjectInput
299
+ ): Promise<void> {
300
+ try {
301
+ const client = getClient();
302
+ const project = await getProject(client, name);
303
+
304
+ if (!project) {
305
+ exitWithError(`project "${name}" not found`, undefined, EXIT_CODES.NOT_FOUND);
306
+ }
307
+
308
+ const updatePayload: {
309
+ name?: string;
310
+ description?: string;
311
+ content?: string;
312
+ statusId?: string;
313
+ startDate?: string;
314
+ targetDate?: string;
315
+ priority?: number;
316
+ leadId?: string;
317
+ teamIds?: string[];
318
+ } = {};
319
+
320
+ if (input.newName) updatePayload.name = input.newName;
321
+ if (input.description !== undefined) updatePayload.description = input.description;
322
+ if (input.content !== undefined) updatePayload.content = input.content;
323
+ if (input.status !== undefined) updatePayload.statusId = input.status;
324
+ if (input.startDate !== undefined) updatePayload.startDate = input.startDate;
325
+ if (input.targetDate !== undefined) updatePayload.targetDate = input.targetDate;
326
+ if (input.priority !== undefined) updatePayload.priority = input.priority;
327
+ if (input.lead !== undefined) updatePayload.leadId = await resolveAssignee(client, input.lead);
328
+ if (input.team !== undefined) updatePayload.teamIds = [await resolveTeamByKey(client, input.team)];
329
+
330
+ if (Object.keys(updatePayload).length > 0) {
331
+ await updateProject(client, project.id, updatePayload);
332
+ console.log(`updated ${name}`);
333
+ }
334
+
335
+ // flag entity handlers (injected from entity-definitions)
336
+ if (input.react) {
337
+ if (!input.emoji) {
338
+ exitWithError("--emoji is required when using --react");
339
+ }
340
+ const success = await createReaction(client, { type: "projectUpdate", id: input.react }, input.emoji);
341
+ if (!success) {
342
+ exitWithError(`failed to add reaction to project update ${input.react.slice(0, 8)}`);
343
+ }
344
+ console.log(`added reaction ${input.emoji} to project update ${input.react.slice(0, 8)}`);
345
+ }
346
+
347
+ if (input.unreact) {
348
+ const success = await deleteReaction(client, input.unreact);
349
+ if (!success) {
350
+ exitWithError(`reaction ${input.unreact.slice(0, 8)} not found`, undefined, EXIT_CODES.NOT_FOUND);
351
+ }
352
+ console.log(`removed reaction ${input.unreact.slice(0, 8)}`);
353
+ }
354
+
355
+ if (input.subscribe) {
356
+ const subscriptionId = await createSubscription(client, { type: "project", projectId: project.id });
357
+ console.log(`subscribed to ${name} (subscription: ${subscriptionId.slice(0, 8)})`);
358
+ }
359
+
360
+ if (input.unsubscribe) {
361
+ const subscriptionId = await findUserSubscription(client, { type: "project", projectId: project.id });
362
+ if (!subscriptionId) {
363
+ exitWithError(`no subscription found for ${name}`, "you may not be subscribed to this project");
364
+ }
365
+ const success = await deleteSubscription(client, subscriptionId);
366
+ if (!success) {
367
+ exitWithError(`failed to remove subscription`, undefined, EXIT_CODES.NOT_FOUND);
368
+ }
369
+ console.log(`unsubscribed from ${name}`);
370
+ }
371
+ } catch (error) {
372
+ handleApiError(error);
373
+ }
374
+ }
375
+
376
+ async function handleCreateProject(input: ProjectInput): Promise<void> {
377
+ if (!input.newName && !input.description) {
378
+ exitWithError("--new-name is required", 'usage: lnr project new --new-name "..."');
379
+ }
380
+
381
+ const projectName = input.newName;
382
+ if (!projectName) {
383
+ exitWithError("--new-name is required", 'usage: lnr project new --new-name "..."');
384
+ }
385
+
386
+ try {
387
+ const client = getClient();
388
+
389
+ const createPayload: {
390
+ name: string;
391
+ description?: string;
392
+ content?: string;
393
+ teamIds?: string[];
394
+ leadId?: string;
395
+ startDate?: string;
396
+ targetDate?: string;
397
+ priority?: number;
398
+ } = {
399
+ name: projectName,
400
+ };
401
+
402
+ if (input.description) createPayload.description = input.description;
403
+ if (input.content) createPayload.content = input.content;
404
+ if (input.team) createPayload.teamIds = [await resolveTeamByKey(client, input.team)];
405
+ if (input.lead) createPayload.leadId = await resolveAssignee(client, input.lead);
406
+ if (input.startDate) createPayload.startDate = input.startDate;
407
+ if (input.targetDate) createPayload.targetDate = input.targetDate;
408
+ if (input.priority !== undefined) createPayload.priority = input.priority;
409
+
410
+ const project = await createProject(client, createPayload);
411
+
412
+ if (project) {
413
+ console.log(`created project: ${project.name}`);
414
+ } else {
415
+ console.log("created project");
416
+ }
417
+ } catch (error) {
418
+ handleApiError(error);
419
+ }
420
+ }
421
+
422
+ async function handleDeleteProject(
423
+ name: string,
424
+ _input: ProjectInput
425
+ ): Promise<void> {
426
+ try {
427
+ const client = getClient();
428
+ const success = await deleteProject(client, name);
429
+
430
+ if (!success) {
431
+ exitWithError(`project "${name}" not found`, undefined, EXIT_CODES.NOT_FOUND);
432
+ }
433
+
434
+ console.log(`deleted project: ${name}`);
435
+ } catch (error) {
436
+ handleApiError(error);
437
+ }
438
+ }
439
+
440
+
441
+
442
+ async function handleProjectMilestone(input: ProjectMilestoneInput): Promise<void> {
443
+ try {
444
+ const client = getClient();
445
+
446
+ const outputOpts: OutputOptions = {
447
+ format: input.json ? "json" : undefined,
448
+ };
449
+ const format = getOutputFormat(outputOpts);
450
+
451
+ const projectId = await resolveProjectByName(client, input.project);
452
+
453
+ // determine operation
454
+ const isCreate = input.nameOrNew === "new";
455
+ const isDelete = input.delete === true;
456
+ const isUpdate = !isCreate && !isDelete && (
457
+ input.newName !== undefined ||
458
+ input.description !== undefined ||
459
+ input.targetDate !== undefined
460
+ );
461
+ const isRead = !isCreate && !isDelete && !isUpdate;
462
+
463
+ if (isCreate) {
464
+ if (!input.newName) {
465
+ exitWithError("--new-name is required", 'usage: lnr project milestone new --project "..." --new-name "v1.0"');
466
+ }
467
+
468
+ const milestone = await createMilestone(client, {
469
+ name: input.newName,
470
+ projectId,
471
+ description: input.description,
472
+ targetDate: input.targetDate,
473
+ });
474
+
475
+ if (!milestone) {
476
+ exitWithError("failed to create milestone");
477
+ }
478
+
479
+ if (format === "json") {
480
+ outputJson(milestone);
481
+ } else {
482
+ console.log(`created milestone: ${milestone.name}`);
483
+ }
484
+ return;
485
+ }
486
+
487
+ const milestoneId = await resolveMilestoneByName(client, projectId, input.nameOrNew);
488
+
489
+ if (isDelete) {
490
+ const success = await deleteMilestone(client, milestoneId);
491
+ if (!success) {
492
+ exitWithError(`milestone "${input.nameOrNew}" not found`, undefined, EXIT_CODES.NOT_FOUND);
493
+ }
494
+ console.log(`deleted milestone: ${input.nameOrNew}`);
495
+ return;
496
+ }
497
+
498
+ if (isUpdate) {
499
+ const updatePayload: {
500
+ name?: string;
501
+ description?: string;
502
+ targetDate?: string;
503
+ } = {};
504
+
505
+ if (input.newName !== undefined) updatePayload.name = input.newName;
506
+ if (input.description !== undefined) updatePayload.description = input.description;
507
+ if (input.targetDate !== undefined) updatePayload.targetDate = input.targetDate;
508
+
509
+ const updated = await updateMilestone(client, milestoneId, updatePayload);
510
+ if (!updated) {
511
+ exitWithError(`failed to update milestone "${input.nameOrNew}"`);
512
+ }
513
+
514
+ if (format === "json") {
515
+ outputJson(updated);
516
+ } else {
517
+ console.log(`updated milestone: ${updated.name}`);
518
+ }
519
+ return;
520
+ }
521
+
522
+ // read: show milestone details
523
+ const milestone = await client.projectMilestone(milestoneId);
524
+ if (!milestone) {
525
+ exitWithError(`milestone "${input.nameOrNew}" not found`, undefined, EXIT_CODES.NOT_FOUND);
526
+ }
527
+
528
+ if (format === "json") {
529
+ outputJson({
530
+ id: milestone.id,
531
+ name: milestone.name,
532
+ description: milestone.description,
533
+ targetDate: milestone.targetDate,
534
+ createdAt: milestone.createdAt,
535
+ updatedAt: milestone.updatedAt,
536
+ });
537
+ } else {
538
+ console.log(`${milestone.name}`);
539
+ if (milestone.description) {
540
+ console.log(` ${milestone.description}`);
541
+ }
542
+ if (milestone.targetDate) {
543
+ console.log(` target: ${formatDate(milestone.targetDate)}`);
544
+ }
545
+ }
546
+ } catch (error) {
547
+ handleApiError(error);
548
+ }
549
+ }
550
+
551
+ export const generatedProjectsRouter = router({
552
+ projects: procedure
553
+ .meta({
554
+ description: "list projects",
555
+ aliases: { command: ["p"] },
556
+ })
557
+ .input(listProjectsInput)
558
+ .query(async ({ input }) => {
559
+ await handleListProjects(input);
560
+ }),
561
+
562
+ project: procedure
563
+ .meta({
564
+ description: "show or update a project, or create with 'new'",
565
+ })
566
+ .input(projectInput)
567
+ .mutation(async ({ input }) => {
568
+ const operation = inferOperation(input);
569
+
570
+ switch (operation) {
571
+ case "create":
572
+ await handleCreateProject(input);
573
+ break;
574
+ case "delete":
575
+ await handleDeleteProject(input.name, input);
576
+ break;
577
+
578
+ case "update":
579
+ await handleUpdateProject(input.name, input);
580
+ break;
581
+ case "read":
582
+ default:
583
+ await handleShowProject(input.name, input);
584
+ break;
585
+ }
586
+ }),
587
+
588
+ "project milestone": procedure
589
+ .meta({
590
+ description: "create, show, update, or delete a milestone",
591
+ })
592
+ .input(projectMilestoneInput)
593
+ .mutation(async ({ input }) => {
594
+ await handleProjectMilestone(input);
595
+ }),
596
+ });