@bdsqqq/lnr-cli 1.1.1 → 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,71 @@
1
+ import { z } from "zod";
2
+ import {
3
+ loadConfig,
4
+ getConfigValue,
5
+ setConfigValue,
6
+ type Config,
7
+ } from "@bdsqqq/lnr-core";
8
+ import { router, procedure } from "./trpc";
9
+ import { exitWithError } from "../lib/error";
10
+
11
+ const getInput = z.object({
12
+ key: z.enum(["api_key", "default_team", "output_format"]).meta({ positional: true }).describe("config key to get"),
13
+ });
14
+
15
+ const setInput = z.object({
16
+ key: z.enum(["api_key", "default_team", "output_format"]).meta({ positional: true }).describe("config key to set"),
17
+ value: z.string().meta({ positional: true }).describe("value to set"),
18
+ });
19
+
20
+ export const configRouter = router({
21
+ config: router({
22
+ get: procedure
23
+ .meta({
24
+ description: "get a config value",
25
+ })
26
+ .input(getInput)
27
+ .query(({ input }) => {
28
+ const value = getConfigValue(input.key as keyof Config);
29
+ if (value === undefined) {
30
+ console.log("(not set)");
31
+ } else {
32
+ console.log(value);
33
+ }
34
+ }),
35
+
36
+ set: procedure
37
+ .meta({
38
+ description: "set a config value",
39
+ })
40
+ .input(setInput)
41
+ .mutation(({ input }) => {
42
+ if (input.key === "output_format" && !["table", "json", "quiet"].includes(input.value)) {
43
+ exitWithError(`invalid output_format: ${input.value}`, "valid values: table, json, quiet");
44
+ }
45
+
46
+ setConfigValue(input.key as keyof Config, input.value as Config[keyof Config]);
47
+ console.log(`${input.key} = ${input.value}`);
48
+ }),
49
+
50
+ list: procedure
51
+ .meta({
52
+ description: "view and manage configuration",
53
+ default: true,
54
+ })
55
+ .query(() => {
56
+ const config = loadConfig();
57
+ if (Object.keys(config).length === 0) {
58
+ console.log("(no configuration set)");
59
+ return;
60
+ }
61
+
62
+ for (const [key, value] of Object.entries(config)) {
63
+ if (key === "api_key" && value) {
64
+ console.log(`${key} = ${(value as string).slice(0, 10)}...`);
65
+ } else {
66
+ console.log(`${key} = ${value}`);
67
+ }
68
+ }
69
+ }),
70
+ }),
71
+ });
@@ -0,0 +1,131 @@
1
+ import { z } from "zod";
2
+ import {
3
+ getClient,
4
+ listCycles,
5
+ getCurrentCycle,
6
+ getCycleIssues,
7
+ } from "@bdsqqq/lnr-core";
8
+ import { router, procedure } from "./trpc";
9
+ import { exitWithError, handleApiError } from "../lib/error";
10
+ import {
11
+ outputJson,
12
+ outputQuiet,
13
+ outputTable,
14
+ getOutputFormat,
15
+ formatDate,
16
+ truncate,
17
+ } from "../lib/output";
18
+
19
+ const outputOptions = z.object({
20
+ json: z.boolean().optional().describe("output as json"),
21
+ quiet: z.boolean().optional().describe("output ids only"),
22
+ verbose: z.boolean().optional().describe("show all columns"),
23
+ });
24
+
25
+ const cyclesInput = z.object({
26
+ team: z.string().describe("team key"),
27
+ }).merge(outputOptions);
28
+
29
+ const cycleInput = z.object({
30
+ team: z.string().describe("team key"),
31
+ current: z.boolean().optional().describe("show current active cycle"),
32
+ issues: z.boolean().optional().describe("list issues in cycle"),
33
+ }).merge(outputOptions);
34
+
35
+ export const cyclesRouter = router({
36
+ cycles: procedure
37
+ .meta({
38
+ aliases: { command: ["c"] },
39
+ description: "list cycles for a team",
40
+ })
41
+ .input(cyclesInput)
42
+ .query(async ({ input }) => {
43
+ try {
44
+ const client = getClient();
45
+ const cycles = await listCycles(client, input.team);
46
+
47
+ if (cycles.length === 0) {
48
+ exitWithError(`team "${input.team}" not found`);
49
+ }
50
+
51
+ const format = input.json ? "json" : input.quiet ? "quiet" : getOutputFormat(input);
52
+
53
+ if (format === "json") {
54
+ outputJson(cycles);
55
+ return;
56
+ }
57
+
58
+ if (format === "quiet") {
59
+ outputQuiet(cycles.map((c) => c.id));
60
+ return;
61
+ }
62
+
63
+ outputTable(cycles, [
64
+ { header: "#", value: (c) => String(c.number), width: 4 },
65
+ { header: "NAME", value: (c) => c.name ?? `Cycle ${c.number}`, width: 20 },
66
+ { header: "START", value: (c) => formatDate(c.startsAt), width: 12 },
67
+ { header: "END", value: (c) => formatDate(c.endsAt), width: 12 },
68
+ ], input);
69
+ } catch (error) {
70
+ handleApiError(error);
71
+ }
72
+ }),
73
+
74
+ cycle: procedure
75
+ .meta({
76
+ description: "show cycle details",
77
+ })
78
+ .input(cycleInput)
79
+ .query(async ({ input }) => {
80
+ try {
81
+ if (!input.current) {
82
+ exitWithError("cycle identifier required", "use --current to show active cycle");
83
+ }
84
+
85
+ const client = getClient();
86
+ const cycle = await getCurrentCycle(client, input.team);
87
+
88
+ if (!cycle) {
89
+ exitWithError("no active cycle", `team "${input.team}" has no current cycle`);
90
+ }
91
+
92
+ const format = input.json ? "json" : input.quiet ? "quiet" : getOutputFormat(input);
93
+
94
+ if (input.issues) {
95
+ const issues = await getCycleIssues(client, input.team);
96
+
97
+ if (format === "json") {
98
+ outputJson(issues);
99
+ return;
100
+ }
101
+
102
+ if (format === "quiet") {
103
+ outputQuiet(issues.map((i) => i.identifier));
104
+ return;
105
+ }
106
+
107
+ outputTable(issues, [
108
+ { header: "ID", value: (i) => i.identifier, width: 10 },
109
+ { header: "TITLE", value: (i) => truncate(i.title, 50), width: 50 },
110
+ ], input);
111
+ return;
112
+ }
113
+
114
+ if (format === "json") {
115
+ outputJson(cycle);
116
+ return;
117
+ }
118
+
119
+ if (format === "quiet") {
120
+ console.log(cycle.id);
121
+ return;
122
+ }
123
+
124
+ console.log(`cycle ${cycle.number}: ${cycle.name ?? `Cycle ${cycle.number}`}`);
125
+ console.log(` start: ${formatDate(cycle.startsAt)}`);
126
+ console.log(` end: ${formatDate(cycle.endsAt)}`);
127
+ } catch (error) {
128
+ handleApiError(error);
129
+ }
130
+ }),
131
+ });
@@ -0,0 +1,153 @@
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
+ });
@@ -0,0 +1,28 @@
1
+ import { router } from "./trpc";
2
+ import { authRouter } from "./auth";
3
+ import { configRouter } from "./config";
4
+ import { cyclesRouter } from "./cycles";
5
+ import { docsRouter } from "./docs";
6
+ import { issuesRouter } from "./issues";
7
+ import { labelsRouter } from "./labels";
8
+ import { meRouter } from "./me";
9
+ import { projectsRouter } from "./projects";
10
+ import { searchRouter } from "./search";
11
+ import { teamsRouter } from "./teams";
12
+
13
+ export const appRouter = router({
14
+ ...authRouter._def.procedures,
15
+ ...configRouter._def.procedures,
16
+ ...cyclesRouter._def.procedures,
17
+ ...docsRouter._def.procedures,
18
+ ...issuesRouter._def.procedures,
19
+ ...labelsRouter._def.procedures,
20
+ ...meRouter._def.procedures,
21
+ ...projectsRouter._def.procedures,
22
+ ...searchRouter._def.procedures,
23
+ ...teamsRouter._def.procedures,
24
+ });
25
+
26
+ export type AppRouter = typeof appRouter;
27
+
28
+ export { router, procedure } from "./trpc";