@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
@@ -1,4 +1,5 @@
1
- import { z } from "zod";
1
+ import "../lib/arktype-config";
2
+ import { type } from "arktype";
2
3
  import { getClient, searchIssues } from "@bdsqqq/lnr-core";
3
4
  import { router, procedure } from "./trpc";
4
5
  import { handleApiError } from "../lib/error";
@@ -10,11 +11,11 @@ import {
10
11
  truncate,
11
12
  } from "../lib/output";
12
13
 
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"),
14
+ export const searchInput = type({
15
+ query: type("string").configure({ positional: true }).describe("search query"),
16
+ "team?": type("string").describe("filter by team key"),
17
+ "json?": type("boolean").describe("output as json"),
18
+ "quiet?": type("boolean").describe("output ids only"),
18
19
  });
19
20
 
20
21
  export const searchRouter = router({
@@ -1,50 +1,92 @@
1
- import { z } from "zod";
1
+ import "../lib/arktype-config";
2
+ import { type } from "arktype";
2
3
  import {
3
4
  getClient,
4
5
  listTeams,
5
6
  getTeam,
6
7
  getTeamMembers,
7
8
  getAvailableTeamKeys,
9
+ type Team,
10
+ type TeamMember,
8
11
  } from "@bdsqqq/lnr-core";
9
12
  import { router, procedure } from "./trpc";
10
13
  import { exitWithError, handleApiError, EXIT_CODES } from "../lib/error";
11
- import { outputJson, outputQuiet, outputTable } from "../lib/output";
14
+ import {
15
+ outputJson,
16
+ outputQuiet,
17
+ outputTable,
18
+ getOutputFormat,
19
+ truncate,
20
+ type OutputOptions,
21
+ type TableColumn,
22
+ } from "../lib/output";
12
23
 
13
- const teamsInput = z.object({
14
- json: z.boolean().optional().describe("output as json"),
15
- quiet: z.boolean().optional().describe("output keys only"),
24
+ export const listTeamsInput = type({
25
+ "json?": type("boolean").describe("output as json"),
26
+ "quiet?": type("boolean").describe("output keys only"),
27
+ "verbose?": type("boolean").describe("show all columns"),
16
28
  });
17
29
 
18
- const teamInput = z.object({
19
- key: z.string().meta({ positional: true }).describe("team key"),
20
- members: z.boolean().optional().describe("list team members"),
21
- json: z.boolean().optional().describe("output as json"),
30
+ export const teamInput = type({
31
+ key: type("string").configure({ positional: true }).describe("team key"),
32
+ "members?": type("boolean").describe("list team members"),
33
+ "json?": type("boolean").describe("output as json"),
34
+ "quiet?": type("boolean").describe("output id only"),
35
+ "verbose?": type("boolean").describe("show all columns"),
22
36
  });
23
37
 
38
+ const teamColumns: TableColumn<Team>[] = [
39
+ { header: "KEY", value: (t) => t.key, width: 8 },
40
+ { header: "NAME", value: (t) => t.name, width: 24 },
41
+ { header: "DESCRIPTION", value: (t) => truncate(t.description ?? "-", 40), width: 40 },
42
+ ];
43
+
44
+ const verboseTeamColumns: TableColumn<Team>[] = [
45
+ ...teamColumns,
46
+ { header: "PRIVATE", value: (t) => (t.private ? "yes" : "no"), width: 8 },
47
+ { header: "TIMEZONE", value: (t) => t.timezone ?? "-", width: 20 },
48
+ { header: "ID", value: (t) => t.id, width: 36 },
49
+ ];
50
+
51
+ const memberColumns: TableColumn<TeamMember>[] = [
52
+ { header: "NAME", value: (m) => m.name, width: 24 },
53
+ { header: "EMAIL", value: (m) => m.email ?? "-", width: 32 },
54
+ { header: "ACTIVE", value: (m) => (m.active ? "yes" : "no"), width: 8 },
55
+ ];
56
+
57
+ const verboseMemberColumns: TableColumn<TeamMember>[] = [
58
+ ...memberColumns,
59
+ { header: "DISPLAY NAME", value: (m) => m.displayName ?? "-", width: 24 },
60
+ { header: "ID", value: (m) => m.id, width: 36 },
61
+ ];
62
+
24
63
  export const teamsRouter = router({
25
64
  teams: procedure
26
65
  .meta({ aliases: { command: ["t"] }, description: "list teams" })
27
- .input(teamsInput)
66
+ .input(listTeamsInput)
28
67
  .query(async ({ input }) => {
29
68
  try {
30
69
  const client = getClient();
31
70
  const teams = await listTeams(client);
32
71
 
33
- if (input.json) {
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") {
34
79
  outputJson(teams);
35
80
  return;
36
81
  }
37
82
 
38
- if (input.quiet) {
83
+ if (format === "quiet") {
39
84
  outputQuiet(teams.map((t) => t.key));
40
85
  return;
41
86
  }
42
87
 
43
- outputTable(teams, [
44
- { header: "KEY", value: (t) => t.key, width: 8 },
45
- { header: "NAME", value: (t) => t.name, width: 24 },
46
- { header: "DESCRIPTION", value: (t) => t.description ?? "-", width: 40 },
47
- ]);
88
+ const columns = input.verbose ? verboseTeamColumns : teamColumns;
89
+ outputTable(teams, columns, outputOpts);
48
90
  } catch (error) {
49
91
  handleApiError(error);
50
92
  }
@@ -67,34 +109,50 @@ export const teamsRouter = router({
67
109
  );
68
110
  }
69
111
 
112
+ const outputOpts: OutputOptions = {
113
+ format: input.json ? "json" : input.quiet ? "quiet" : undefined,
114
+ verbose: input.verbose,
115
+ };
116
+ const format = getOutputFormat(outputOpts);
117
+
70
118
  if (input.members) {
71
119
  const members = await getTeamMembers(client, input.key);
72
120
 
73
- if (input.json) {
121
+ if (format === "json") {
74
122
  outputJson(members);
75
123
  return;
76
124
  }
77
125
 
126
+ if (format === "quiet") {
127
+ outputQuiet(members.map((m) => m.id));
128
+ return;
129
+ }
130
+
78
131
  console.log(`${team.name} (${team.key}) members:\n`);
79
- outputTable(members, [
80
- { header: "NAME", value: (m) => m.name, width: 24 },
81
- { header: "EMAIL", value: (m) => m.email ?? "-", width: 32 },
82
- { header: "ACTIVE", value: (m) => (m.active ? "yes" : "no"), width: 8 },
83
- ]);
132
+ const cols = input.verbose ? verboseMemberColumns : memberColumns;
133
+ outputTable(members, cols, outputOpts);
84
134
  return;
85
135
  }
86
136
 
87
- if (input.json) {
137
+ if (format === "json") {
88
138
  outputJson(team);
89
139
  return;
90
140
  }
91
141
 
142
+ if (format === "quiet") {
143
+ console.log(team.id);
144
+ return;
145
+ }
146
+
92
147
  console.log(`${team.name} (${team.key})`);
93
148
  if (team.description) {
94
149
  console.log(team.description);
95
150
  }
96
151
  console.log(`timezone: ${team.timezone ?? "-"}`);
97
152
  console.log(`private: ${team.private ? "yes" : "no"}`);
153
+ if (input.verbose) {
154
+ console.log(`id: ${team.id}`);
155
+ }
98
156
  } catch (error) {
99
157
  handleApiError(error);
100
158
  }
@@ -0,0 +1,126 @@
1
+ import "../lib/arktype-config";
2
+ import { type } from "arktype";
3
+ import {
4
+ getClient,
5
+ listUsers,
6
+ findUserByNameOrEmail,
7
+ type User,
8
+ } from "@bdsqqq/lnr-core";
9
+ import { router, procedure } from "./trpc";
10
+ import { exitWithError, handleApiError, EXIT_CODES } from "../lib/error";
11
+ import {
12
+ outputJson,
13
+ outputQuiet,
14
+ outputTable,
15
+ getOutputFormat,
16
+ type OutputOptions,
17
+ type TableColumn,
18
+ } from "../lib/output";
19
+
20
+ export const listUsersInput = type({
21
+ "json?": type("boolean").describe("output as json"),
22
+ "quiet?": type("boolean").describe("output ids only"),
23
+ "verbose?": type("boolean").describe("show all columns"),
24
+ });
25
+
26
+ export const userInput = type({
27
+ nameOrEmail: type("string").configure({ positional: true }).describe("user name or email"),
28
+ "json?": type("boolean").describe("output as json"),
29
+ "quiet?": type("boolean").describe("output id only"),
30
+ "verbose?": type("boolean").describe("show all columns"),
31
+ });
32
+
33
+ const userColumns: TableColumn<User>[] = [
34
+ { header: "NAME", value: (u) => u.name, width: 24 },
35
+ { header: "EMAIL", value: (u) => u.email ?? "-", width: 32 },
36
+ { header: "ACTIVE", value: (u) => (u.active ? "yes" : "no"), width: 8 },
37
+ { header: "ADMIN", value: (u) => (u.admin ? "yes" : "no"), width: 8 },
38
+ ];
39
+
40
+ const verboseUserColumns: TableColumn<User>[] = [
41
+ ...userColumns,
42
+ { header: "DISPLAY NAME", value: (u) => u.displayName ?? "-", width: 24 },
43
+ { header: "ID", value: (u) => u.id, width: 36 },
44
+ ];
45
+
46
+ export const usersRouter = router({
47
+ users: procedure
48
+ .meta({ aliases: { command: ["u"] }, description: "list users" })
49
+ .input(listUsersInput)
50
+ .query(async ({ input }) => {
51
+ try {
52
+ const client = getClient();
53
+ const users = await listUsers(client);
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
+ if (format === "json") {
62
+ outputJson(users);
63
+ return;
64
+ }
65
+
66
+ if (format === "quiet") {
67
+ outputQuiet(users.map((u) => u.id));
68
+ return;
69
+ }
70
+
71
+ const columns = input.verbose ? verboseUserColumns : userColumns;
72
+ outputTable(users, columns, outputOpts);
73
+ } catch (error) {
74
+ handleApiError(error);
75
+ }
76
+ }),
77
+
78
+ user: procedure
79
+ .meta({ description: "show user details" })
80
+ .input(userInput)
81
+ .query(async ({ input }) => {
82
+ try {
83
+ const client = getClient();
84
+ const user = await findUserByNameOrEmail(client, input.nameOrEmail);
85
+
86
+ if (!user) {
87
+ exitWithError(
88
+ `user "${input.nameOrEmail}" not found`,
89
+ "try: lnr users",
90
+ EXIT_CODES.NOT_FOUND
91
+ );
92
+ }
93
+
94
+ const outputOpts: OutputOptions = {
95
+ format: input.json ? "json" : input.quiet ? "quiet" : undefined,
96
+ verbose: input.verbose,
97
+ };
98
+ const format = getOutputFormat(outputOpts);
99
+
100
+ if (format === "json") {
101
+ outputJson(user);
102
+ return;
103
+ }
104
+
105
+ if (format === "quiet") {
106
+ console.log(user.id);
107
+ return;
108
+ }
109
+
110
+ console.log(`${user.name}`);
111
+ if (user.email) {
112
+ console.log(`email: ${user.email}`);
113
+ }
114
+ if (user.displayName) {
115
+ console.log(`display name: ${user.displayName}`);
116
+ }
117
+ console.log(`active: ${user.active ? "yes" : "no"}`);
118
+ console.log(`admin: ${user.admin ? "yes" : "no"}`);
119
+ if (input.verbose) {
120
+ console.log(`id: ${user.id}`);
121
+ }
122
+ } catch (error) {
123
+ handleApiError(error);
124
+ }
125
+ }),
126
+ });