@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,253 @@
1
+ import "../lib/arktype-config";
2
+ import { type } from "arktype";
3
+ import {
4
+ getClient,
5
+ listAgentSessions,
6
+ getAgentSession,
7
+ updateAgentSession,
8
+ getAgentSessionActivities,
9
+ type AgentSession,
10
+ type AgentActivity,
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 listAgentSessionsInput = 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
+ "status?": type("string").describe("filter by status (active, pending, complete, error, stale)"),
28
+ });
29
+
30
+ export const agentSessionInput = type({
31
+ id: type("string").configure({ positional: true }).describe("agent session id"),
32
+ "json?": type("boolean").describe("output as json"),
33
+ "quiet?": type("boolean").describe("output id only"),
34
+ "verbose?": type("boolean").describe("show all fields"),
35
+ "externalLink?": type("string").describe("set external link url"),
36
+ "activities?": type("boolean").describe("show session activities"),
37
+ });
38
+
39
+ const sessionColumns: TableColumn<AgentSession>[] = [
40
+ { header: "STATUS", value: (s) => s.status, width: 14 },
41
+ { header: "TYPE", value: (s) => s.type, width: 14 },
42
+ { header: "ISSUE", value: (s) => s.issueIdentifier ?? "-", width: 12 },
43
+ { header: "CREATOR", value: (s) => s.creatorName ?? "-", width: 16 },
44
+ {
45
+ header: "DATE",
46
+ value: (s) => s.createdAt.toISOString().split("T")[0] ?? "",
47
+ width: 12,
48
+ },
49
+ ];
50
+
51
+ const verboseSessionColumns: TableColumn<AgentSession>[] = [
52
+ ...sessionColumns,
53
+ { header: "APP USER", value: (s) => s.appUserName ?? "-", width: 16 },
54
+ {
55
+ header: "STARTED",
56
+ value: (s) =>
57
+ s.startedAt ? s.startedAt.toISOString().split("T")[0] ?? "-" : "-",
58
+ width: 12,
59
+ },
60
+ {
61
+ header: "ENDED",
62
+ value: (s) =>
63
+ s.endedAt ? s.endedAt.toISOString().split("T")[0] ?? "-" : "-",
64
+ width: 12,
65
+ },
66
+ { header: "ID", value: (s) => s.id, width: 36 },
67
+ ];
68
+
69
+ const activityColumns: TableColumn<AgentActivity>[] = [
70
+ { header: "TYPE", value: (a) => a.type, width: 12 },
71
+ {
72
+ header: "CONTENT",
73
+ value: (a) => {
74
+ if (a.content.action) {
75
+ return `${a.content.action}: ${a.content.parameter ?? ""}`.slice(0, 40);
76
+ }
77
+ return (a.content.body ?? "").slice(0, 40);
78
+ },
79
+ width: 42,
80
+ },
81
+ { header: "USER", value: (a) => a.userName ?? "-", width: 16 },
82
+ {
83
+ header: "DATE",
84
+ value: (a) => a.createdAt.toISOString().split("T")[0] ?? "",
85
+ width: 12,
86
+ },
87
+ ];
88
+
89
+ const verboseActivityColumns: TableColumn<AgentActivity>[] = [
90
+ ...activityColumns,
91
+ { header: "SIGNAL", value: (a) => a.signal ?? "-", width: 10 },
92
+ { header: "EPHEMERAL", value: (a) => (a.ephemeral ? "yes" : "no"), width: 10 },
93
+ { header: "ID", value: (a) => a.id, width: 36 },
94
+ ];
95
+
96
+ export const agentSessionsRouter = router({
97
+ "agent-sessions": procedure
98
+ .meta({
99
+ aliases: { command: ["as"] },
100
+ description: "list agent sessions (experimental)",
101
+ })
102
+ .input(listAgentSessionsInput)
103
+ .query(async ({ input }) => {
104
+ try {
105
+ const client = getClient();
106
+ const sessions = await listAgentSessions(client, {
107
+ status: input.status,
108
+ });
109
+
110
+ const outputOpts: OutputOptions = {
111
+ format: input.json ? "json" : input.quiet ? "quiet" : undefined,
112
+ verbose: input.verbose,
113
+ };
114
+ const format = getOutputFormat(outputOpts);
115
+
116
+ if (format === "json") {
117
+ outputJson(sessions);
118
+ return;
119
+ }
120
+
121
+ if (format === "quiet") {
122
+ outputQuiet(sessions.map((s) => s.id));
123
+ return;
124
+ }
125
+
126
+ const columns = input.verbose ? verboseSessionColumns : sessionColumns;
127
+ outputTable(sessions, columns, outputOpts);
128
+ } catch (error) {
129
+ handleApiError(error);
130
+ }
131
+ }),
132
+
133
+ "agent-session": procedure
134
+ .meta({ description: "show agent session details (experimental)" })
135
+ .input(agentSessionInput)
136
+ .mutation(async ({ input }) => {
137
+ try {
138
+ const client = getClient();
139
+
140
+ if (input.externalLink !== undefined) {
141
+ const success = await updateAgentSession(client, input.id, {
142
+ externalLink: input.externalLink,
143
+ });
144
+ if (!success) {
145
+ exitWithError(
146
+ `failed to update agent session "${input.id}"`,
147
+ "check the session id and permissions",
148
+ EXIT_CODES.GENERAL_ERROR
149
+ );
150
+ }
151
+ console.log("updated");
152
+ return;
153
+ }
154
+
155
+ if (input.activities) {
156
+ const activities = await getAgentSessionActivities(client, input.id);
157
+
158
+ const outputOpts: OutputOptions = {
159
+ format: input.json ? "json" : input.quiet ? "quiet" : undefined,
160
+ verbose: input.verbose,
161
+ };
162
+ const format = getOutputFormat(outputOpts);
163
+
164
+ if (format === "json") {
165
+ outputJson(activities);
166
+ return;
167
+ }
168
+
169
+ if (format === "quiet") {
170
+ outputQuiet(activities.map((a) => a.id));
171
+ return;
172
+ }
173
+
174
+ const columns = input.verbose
175
+ ? verboseActivityColumns
176
+ : activityColumns;
177
+ outputTable(activities, columns, outputOpts);
178
+ return;
179
+ }
180
+
181
+ const session = await getAgentSession(client, input.id);
182
+
183
+ if (!session) {
184
+ exitWithError(
185
+ `agent session "${input.id}" not found`,
186
+ "try: lnr agent-sessions",
187
+ EXIT_CODES.NOT_FOUND
188
+ );
189
+ }
190
+
191
+ const outputOpts: OutputOptions = {
192
+ format: input.json ? "json" : input.quiet ? "quiet" : undefined,
193
+ verbose: input.verbose,
194
+ };
195
+ const format = getOutputFormat(outputOpts);
196
+
197
+ if (format === "json") {
198
+ outputJson(session);
199
+ return;
200
+ }
201
+
202
+ if (format === "quiet") {
203
+ console.log(session.id);
204
+ return;
205
+ }
206
+
207
+ console.log(`status: ${session.status}`);
208
+ console.log(`type: ${session.type}`);
209
+ console.log(`created: ${session.createdAt.toISOString()}`);
210
+ if (session.issueIdentifier) {
211
+ console.log(`issue: ${session.issueIdentifier}`);
212
+ }
213
+ if (session.creatorName) {
214
+ console.log(`creator: ${session.creatorName}`);
215
+ }
216
+ if (session.appUserName) {
217
+ console.log(`app user: ${session.appUserName}`);
218
+ }
219
+ if (session.summary) {
220
+ console.log(`summary: ${session.summary}`);
221
+ }
222
+ if (session.externalLink) {
223
+ console.log(`external link: ${session.externalLink}`);
224
+ }
225
+ if (session.startedAt) {
226
+ console.log(`started: ${session.startedAt.toISOString()}`);
227
+ }
228
+ if (session.endedAt) {
229
+ console.log(`ended: ${session.endedAt.toISOString()}`);
230
+ }
231
+ if (input.verbose) {
232
+ console.log(`id: ${session.id}`);
233
+ if (session.issueId) {
234
+ console.log(`issue id: ${session.issueId}`);
235
+ }
236
+ if (session.commentId) {
237
+ console.log(`comment id: ${session.commentId}`);
238
+ }
239
+ if (session.dismissedAt) {
240
+ console.log(`dismissed: ${session.dismissedAt.toISOString()}`);
241
+ }
242
+ if (session.archivedAt) {
243
+ console.log(`archived: ${session.archivedAt.toISOString()}`);
244
+ }
245
+ if (session.plan) {
246
+ console.log(`plan: ${JSON.stringify(session.plan)}`);
247
+ }
248
+ }
249
+ } catch (error) {
250
+ handleApiError(error);
251
+ }
252
+ }),
253
+ });
@@ -1,4 +1,5 @@
1
- import { z } from "zod";
1
+ import "../lib/arktype-config";
2
+ import { type } from "arktype";
2
3
  import {
3
4
  setApiKey,
4
5
  clearApiKey,
@@ -9,10 +10,10 @@ import {
9
10
  import { router, procedure } from "./trpc";
10
11
  import { exitWithError, EXIT_CODES } from "../lib/error";
11
12
 
12
- const authInput = z.object({
13
- apiKey: z.string().optional().meta({ positional: true }).describe("Linear API key"),
14
- whoami: z.boolean().optional().describe("show current authenticated user"),
15
- logout: z.boolean().optional().describe("clear stored credentials"),
13
+ export const authInput = type({
14
+ "apiKey?": type("string").configure({ positional: true }).describe("Linear API key"),
15
+ "whoami?": type("boolean").describe("show current authenticated user"),
16
+ "logout?": type("boolean").describe("clear stored credentials"),
16
17
  });
17
18
 
18
19
  export const authRouter = router({
@@ -1,4 +1,5 @@
1
- import { z } from "zod";
1
+ import "../lib/arktype-config";
2
+ import { type } from "arktype";
2
3
  import {
3
4
  loadConfig,
4
5
  getConfigValue,
@@ -8,13 +9,13 @@ import {
8
9
  import { router, procedure } from "./trpc";
9
10
  import { exitWithError } from "../lib/error";
10
11
 
11
- const getInput = z.object({
12
- key: z.enum(["api_key", "default_team", "output_format"]).meta({ positional: true }).describe("config key to get"),
12
+ export const getInput = type({
13
+ key: type("'api_key' | 'default_team' | 'output_format'").configure({ positional: true }).describe("config key to get"),
13
14
  });
14
15
 
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"),
16
+ export const setInput = type({
17
+ key: type("'api_key' | 'default_team' | 'output_format'").configure({ positional: true }).describe("config key to set"),
18
+ value: type("string").configure({ positional: true }).describe("value to set"),
18
19
  });
19
20
 
20
21
  export const configRouter = router({
@@ -0,0 +1,364 @@
1
+ /**
2
+ * contract tests: CLI input → core function parameters
3
+ *
4
+ * these tests verify the transformation from CLI flags to API request payloads.
5
+ */
6
+
7
+ import { describe, test, expect } from "bun:test";
8
+ import { type } from "arktype";
9
+
10
+ // import schemas directly from router files
11
+ import { listIssuesInput, issueInput } from "../generated/issue";
12
+ import { listProjectsInput, projectInput } from "../generated/project";
13
+ import { listTeamsInput, teamInput } from "../router/teams";
14
+ import { listCyclesInput, cycleInput } from "../router/cycles";
15
+ import { listDocsInput, docInput } from "../generated/doc";
16
+ import { listLabelsInput, labelInput } from "../generated/label";
17
+ import { meInput } from "../router/me";
18
+ import { searchInput } from "../router/search";
19
+ import { authInput } from "../router/auth";
20
+ import { getInput as configGetInput, setInput as configSetInput } from "../router/config";
21
+
22
+ /**
23
+ * unified validation helper that works with both zod and arktype schemas.
24
+ * provides consistent safeParse-like interface during migration.
25
+ */
26
+ function safeParse<T>(schema: unknown, data: unknown): { success: boolean; data?: T; error?: unknown } {
27
+ // zod schema check
28
+ if (typeof schema === "object" && schema !== null && "safeParse" in schema) {
29
+ const zodResult = (schema as { safeParse: (d: unknown) => { success: boolean; data?: T; error?: unknown } }).safeParse(data);
30
+ return zodResult;
31
+ }
32
+ // arktype schema (callable)
33
+ if (typeof schema === "function") {
34
+ const result = (schema as (d: unknown) => unknown)(data);
35
+ if (result instanceof type.errors) {
36
+ return { success: false, error: result };
37
+ }
38
+ return { success: true, data: result as T };
39
+ }
40
+ throw new Error("unknown schema type");
41
+ }
42
+
43
+ // operation inference - mirrors router logic
44
+ function inferOperation(command: string, input: Record<string, unknown>): "create" | "update" | "delete" | "show" {
45
+ const positionalNewCommands = ["issue", "project", "doc", "label"];
46
+ if (positionalNewCommands.includes(command)) {
47
+ const positional = input.idOrNew ?? input.name ?? input.id;
48
+ if (positional === "new") return "create";
49
+ }
50
+ if (input.delete || input.archive) return "delete";
51
+ const mutationFlags = [
52
+ "state", "assignee", "priority", "label", "comment",
53
+ "editComment", "replyTo", "deleteComment", "react", "unreact",
54
+ "parent", "blocks", "blockedBy", "relatesTo", "title",
55
+ "content", "projectName"
56
+ ];
57
+ for (const flag of mutationFlags) {
58
+ if (input[flag] !== undefined) return "update";
59
+ }
60
+ return "show";
61
+ }
62
+
63
+ describe("projects", () => {
64
+ describe("projects", () => {
65
+ test("valid input parses", () => {
66
+ const result = safeParse(listProjectsInput, {});
67
+ expect(result.success).toBe(true);
68
+ });
69
+
70
+ test("infers SHOW with no mutation flags", () => {
71
+ expect(inferOperation("projects", {})).toBe("show");
72
+ });
73
+
74
+ });
75
+
76
+ describe("project", () => {
77
+ test("valid input parses", () => {
78
+ const result = safeParse(projectInput, {"name":"test-value"});
79
+ expect(result.success).toBe(true);
80
+ });
81
+
82
+ test("rejects missing required flags", () => {
83
+ const result = safeParse(projectInput, {});
84
+ expect(result.success).toBe(false);
85
+ });
86
+
87
+ test("infers CREATE when name='new'", () => {
88
+ expect(inferOperation("project", { name: "new" })).toBe("create");
89
+ });
90
+
91
+ test("infers DELETE when --delete", () => {
92
+ expect(inferOperation("project", {"name":"test-value","delete":true})).toBe("delete");
93
+ });
94
+
95
+ test("infers SHOW with no mutation flags", () => {
96
+ expect(inferOperation("project", {"name":"test-value"})).toBe("show");
97
+ });
98
+
99
+ });
100
+
101
+ });
102
+
103
+ describe("search", () => {
104
+ describe("search", () => {
105
+ test("valid input parses", () => {
106
+ const result = safeParse(searchInput, {"query":"test-value"});
107
+ expect(result.success).toBe(true);
108
+ });
109
+
110
+ test("rejects missing required flags", () => {
111
+ const result = safeParse(searchInput, {});
112
+ expect(result.success).toBe(false);
113
+ });
114
+
115
+ });
116
+
117
+ });
118
+
119
+ describe("cycles", () => {
120
+ describe("cycles", () => {
121
+ test("valid input parses", () => {
122
+ const result = safeParse(listCyclesInput, {"team":"test-value"});
123
+ expect(result.success).toBe(true);
124
+ });
125
+
126
+ test("rejects missing required flags", () => {
127
+ const result = safeParse(listCyclesInput, {});
128
+ expect(result.success).toBe(false);
129
+ });
130
+
131
+ });
132
+
133
+ describe("cycle", () => {
134
+ test("valid input parses", () => {
135
+ const result = safeParse(cycleInput, {"nameOrNumber":"1","team":"test-value"});
136
+ expect(result.success).toBe(true);
137
+ });
138
+
139
+ test("rejects missing required flags", () => {
140
+ const result = safeParse(cycleInput, {});
141
+ expect(result.success).toBe(false);
142
+ });
143
+
144
+ test("infers CREATE when nameOrNumber='new'", () => {
145
+ const result = safeParse(cycleInput, {"nameOrNumber":"new","team":"ENG"});
146
+ expect(result.success).toBe(true);
147
+ });
148
+
149
+ test("infers DELETE when --delete", () => {
150
+ const result = safeParse(cycleInput, {"nameOrNumber":"1","team":"ENG","delete":true});
151
+ expect(result.success).toBe(true);
152
+ });
153
+
154
+ test("infers SHOW with no mutation flags", () => {
155
+ const result = safeParse(cycleInput, {"nameOrNumber":"Sprint 1","team":"ENG"});
156
+ expect(result.success).toBe(true);
157
+ });
158
+
159
+ });
160
+
161
+ });
162
+
163
+ describe("docs", () => {
164
+ describe("docs", () => {
165
+ test("valid input parses", () => {
166
+ const result = safeParse(listDocsInput, {});
167
+ expect(result.success).toBe(true);
168
+ });
169
+
170
+ test("infers SHOW with no mutation flags", () => {
171
+ expect(inferOperation("docs", {})).toBe("show");
172
+ });
173
+
174
+ });
175
+
176
+ describe("doc", () => {
177
+ test("valid input parses", () => {
178
+ const result = safeParse(docInput, {"id":"test-value"});
179
+ expect(result.success).toBe(true);
180
+ });
181
+
182
+ test("rejects missing required flags", () => {
183
+ const result = safeParse(docInput, {});
184
+ expect(result.success).toBe(false);
185
+ });
186
+
187
+ test("infers CREATE when id='new'", () => {
188
+ expect(inferOperation("doc", { id: "new" })).toBe("create");
189
+ });
190
+
191
+ test("infers DELETE when --delete", () => {
192
+ expect(inferOperation("doc", {"id":"test-value","delete":true})).toBe("delete");
193
+ });
194
+
195
+ test("infers SHOW with no mutation flags", () => {
196
+ expect(inferOperation("doc", {"id":"test-value"})).toBe("show");
197
+ });
198
+
199
+ });
200
+
201
+ });
202
+
203
+ describe("teams", () => {
204
+ describe("teams", () => {
205
+ test("valid input parses", () => {
206
+ const result = safeParse(listTeamsInput, {});
207
+ expect(result.success).toBe(true);
208
+ });
209
+
210
+ });
211
+
212
+ describe("team", () => {
213
+ test("valid input parses", () => {
214
+ const result = safeParse(teamInput, {"key":"test-value"});
215
+ expect(result.success).toBe(true);
216
+ });
217
+
218
+ test("rejects missing required flags", () => {
219
+ const result = safeParse(teamInput, {});
220
+ expect(result.success).toBe(false);
221
+ });
222
+
223
+ });
224
+
225
+ });
226
+
227
+ describe("config", () => {
228
+ describe("get", () => {
229
+ test("valid input parses", () => {
230
+ const result = safeParse(configGetInput, {"key":"api_key"});
231
+ expect(result.success).toBe(true);
232
+ });
233
+
234
+ test("rejects missing required flags", () => {
235
+ const result = safeParse(configGetInput, {});
236
+ expect(result.success).toBe(false);
237
+ });
238
+
239
+ });
240
+
241
+ describe("set", () => {
242
+ test("valid input parses", () => {
243
+ const result = safeParse(configSetInput, {"key":"api_key","value":"test-value"});
244
+ expect(result.success).toBe(true);
245
+ });
246
+
247
+ test("rejects missing required flags", () => {
248
+ const result = safeParse(configSetInput, {});
249
+ expect(result.success).toBe(false);
250
+ });
251
+
252
+ });
253
+
254
+ });
255
+
256
+ describe("me", () => {
257
+ describe("me", () => {
258
+ test("valid input parses", () => {
259
+ const result = safeParse(meInput, {});
260
+ expect(result.success).toBe(true);
261
+ });
262
+
263
+ });
264
+
265
+ });
266
+
267
+ describe("labels", () => {
268
+ describe("labels", () => {
269
+ test("valid input parses", () => {
270
+ const result = safeParse(listLabelsInput, {});
271
+ expect(result.success).toBe(true);
272
+ });
273
+
274
+ test("infers SHOW with no mutation flags", () => {
275
+ expect(inferOperation("labels", {})).toBe("show");
276
+ });
277
+
278
+ });
279
+
280
+ describe("label", () => {
281
+ test("valid input parses", () => {
282
+ const result = safeParse(labelInput, {"id":"test-value"});
283
+ expect(result.success).toBe(true);
284
+ });
285
+
286
+ test("rejects missing required flags", () => {
287
+ const result = safeParse(labelInput, {});
288
+ expect(result.success).toBe(false);
289
+ });
290
+
291
+ test("infers CREATE when id='new'", () => {
292
+ expect(inferOperation("label", { id: "new" })).toBe("create");
293
+ });
294
+
295
+ test("infers DELETE when --delete", () => {
296
+ expect(inferOperation("label", {"id":"test-value","delete":true})).toBe("delete");
297
+ });
298
+
299
+ test("infers SHOW with no mutation flags", () => {
300
+ expect(inferOperation("label", {"id":"test-value"})).toBe("show");
301
+ });
302
+
303
+ });
304
+
305
+ });
306
+
307
+ describe("auth", () => {
308
+ describe("auth", () => {
309
+ test("valid input parses", () => {
310
+ const result = safeParse(authInput, {});
311
+ expect(result.success).toBe(true);
312
+ });
313
+
314
+ });
315
+
316
+ });
317
+
318
+ describe("issues", () => {
319
+ describe("issues", () => {
320
+ test("valid input parses", () => {
321
+ const result = safeParse(listIssuesInput, {});
322
+ expect(result.success).toBe(true);
323
+ });
324
+
325
+ test("infers UPDATE when --state", () => {
326
+ expect(inferOperation("issues", {"state":"test"})).toBe("update");
327
+ });
328
+
329
+ test("infers SHOW with no mutation flags", () => {
330
+ expect(inferOperation("issues", {})).toBe("show");
331
+ });
332
+
333
+ });
334
+
335
+ describe("issue", () => {
336
+ test("valid input parses", () => {
337
+ const result = safeParse(issueInput, {"idOrNew":"test-value"});
338
+ expect(result.success).toBe(true);
339
+ });
340
+
341
+ test("rejects missing required flags", () => {
342
+ const result = safeParse(issueInput, {});
343
+ expect(result.success).toBe(false);
344
+ });
345
+
346
+ test("infers CREATE when idOrNew='new'", () => {
347
+ expect(inferOperation("issue", { idOrNew: "new" })).toBe("create");
348
+ });
349
+
350
+ test("infers DELETE when --archive", () => {
351
+ expect(inferOperation("issue", {"idOrNew":"test-value","archive":true})).toBe("delete");
352
+ });
353
+
354
+ test("infers UPDATE when --state", () => {
355
+ expect(inferOperation("issue", {"idOrNew":"test-value","state":"test"})).toBe("update");
356
+ });
357
+
358
+ test("infers SHOW with no mutation flags", () => {
359
+ expect(inferOperation("issue", {"idOrNew":"test-value"})).toBe("show");
360
+ });
361
+
362
+ });
363
+
364
+ });