zinkee 0.1.44 → 0.1.46

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 (94) hide show
  1. package/README.md +4 -0
  2. package/dist/chunk-MHEVRMDX.js +1230 -0
  3. package/dist/chunk-MHEVRMDX.js.map +1 -0
  4. package/{src/utils/examples.ts → dist/examples-67U5IRBS.js} +866 -1141
  5. package/dist/examples-67U5IRBS.js.map +1 -0
  6. package/dist/index.js +1145 -8456
  7. package/dist/index.js.map +1 -1
  8. package/package.json +6 -3
  9. package/.github/workflows/npm-publish.yml +0 -77
  10. package/.github/workflows/pr-checks.yml +0 -36
  11. package/AGENTS.md +0 -110
  12. package/docs/cli-contract.md +0 -41
  13. package/docs/npm-release.md +0 -64
  14. package/docs/superpowers/plans/2026-03-24-zinkee-cli-implementation.md +0 -837
  15. package/docs/superpowers/plans/2026-03-25-cli-backend-error-contract.md +0 -503
  16. package/docs/superpowers/plans/2026-03-26-display-freeform-create.md +0 -389
  17. package/docs/superpowers/specs/2026-03-24-zinkee-cli-backend-blockers.md +0 -172
  18. package/docs/superpowers/specs/2026-03-24-zinkee-cli-design.md +0 -1576
  19. package/docs/superpowers/specs/2026-03-24-zinkee-cli-e2e-checklist.md +0 -215
  20. package/docs/superpowers/specs/2026-03-24-zinkee-cli-e2e-design.md +0 -492
  21. package/docs/superpowers/specs/2026-03-24-zinkee-cli-e2e-status.md +0 -307
  22. package/docs/superpowers/specs/2026-07-30-cli-pr-checks-design.md +0 -37
  23. package/src/api/automations.ts +0 -404
  24. package/src/api/comments.ts +0 -51
  25. package/src/api/displays.ts +0 -337
  26. package/src/api/document-templates.ts +0 -110
  27. package/src/api/files.ts +0 -50
  28. package/src/api/formulas.test.ts +0 -65
  29. package/src/api/formulas.ts +0 -67
  30. package/src/api/logs.ts +0 -34
  31. package/src/api/navigation.ts +0 -70
  32. package/src/api/records.ts +0 -184
  33. package/src/api/schemas.ts +0 -148
  34. package/src/api/teamspace.ts +0 -110
  35. package/src/cli-examples.ts +0 -130
  36. package/src/cli-runner.ts +0 -95
  37. package/src/client.test.ts +0 -189
  38. package/src/client.ts +0 -269
  39. package/src/command-registry.ts +0 -882
  40. package/src/commands/automations.test.ts +0 -1030
  41. package/src/commands/automations.ts +0 -2102
  42. package/src/commands/comments.test.ts +0 -214
  43. package/src/commands/comments.ts +0 -303
  44. package/src/commands/config.test.ts +0 -81
  45. package/src/commands/config.ts +0 -150
  46. package/src/commands/displays.test.ts +0 -1105
  47. package/src/commands/displays.ts +0 -1442
  48. package/src/commands/document-templates.test.ts +0 -569
  49. package/src/commands/document-templates.ts +0 -563
  50. package/src/commands/files.test.ts +0 -284
  51. package/src/commands/files.ts +0 -280
  52. package/src/commands/formulas.test.ts +0 -194
  53. package/src/commands/formulas.ts +0 -243
  54. package/src/commands/logs.test.ts +0 -123
  55. package/src/commands/logs.ts +0 -159
  56. package/src/commands/navigation.test.ts +0 -211
  57. package/src/commands/navigation.ts +0 -348
  58. package/src/commands/profiles.test.ts +0 -191
  59. package/src/commands/profiles.ts +0 -303
  60. package/src/commands/records.test.ts +0 -860
  61. package/src/commands/records.ts +0 -883
  62. package/src/commands/schemas.test.ts +0 -1252
  63. package/src/commands/schemas.ts +0 -890
  64. package/src/commands/teamspace.test.ts +0 -229
  65. package/src/commands/teamspace.ts +0 -546
  66. package/src/completion/engine.test.ts +0 -138
  67. package/src/completion/engine.ts +0 -168
  68. package/src/completion/install.test.ts +0 -179
  69. package/src/completion/install.ts +0 -260
  70. package/src/completion/runtime.ts +0 -150
  71. package/src/completion/scripts.ts +0 -91
  72. package/src/config.test.ts +0 -362
  73. package/src/config.ts +0 -294
  74. package/src/index.test.ts +0 -217
  75. package/src/index.ts +0 -8
  76. package/src/parsers/expressions.test.ts +0 -95
  77. package/src/parsers/expressions.ts +0 -128
  78. package/src/parsers/kv.test.ts +0 -35
  79. package/src/parsers/kv.ts +0 -49
  80. package/src/parsers/selectors.test.ts +0 -23
  81. package/src/parsers/selectors.ts +0 -29
  82. package/src/program.ts +0 -64
  83. package/src/runtime-context.ts +0 -103
  84. package/src/types.ts +0 -76
  85. package/src/utils/argv-rewrite.test.ts +0 -149
  86. package/src/utils/argv-rewrite.ts +0 -269
  87. package/src/utils/errors.test.ts +0 -85
  88. package/src/utils/errors.ts +0 -191
  89. package/src/utils/examples.test.ts +0 -374
  90. package/src/utils/output.test.ts +0 -65
  91. package/src/utils/output.ts +0 -154
  92. package/src/utils/schema-fields.ts +0 -1016
  93. package/tsconfig.json +0 -20
  94. package/tsup.config.ts +0 -13
@@ -1,374 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- import { commandRegistry } from "../command-registry.js";
4
- import { getCommandExamples, getResolvedCommandExamples } from "./examples.js";
5
-
6
- describe("getCommandExamples", () => {
7
- it("returns registered examples for complex commands", () => {
8
- const examples = getCommandExamples("records query");
9
-
10
- expect(examples.length).toBeGreaterThan(0);
11
- expect(examples[0]?.command).toContain("records query");
12
-
13
- const searchExample = examples.find((example) =>
14
- example.command.includes("--search"),
15
- );
16
- expect(searchExample?.command).toContain("--search garcia");
17
- expect(searchExample?.raw).toContain('"search": { "term": "garcia" }');
18
- });
19
-
20
- it("returns fallback examples for simple commands", () => {
21
- const examples = getCommandExamples("schemas list");
22
-
23
- expect(examples.length).toBeGreaterThan(0);
24
- expect(examples[0]?.command).toContain("schemas list");
25
- });
26
-
27
- it("returns schema field examples filtered by member type", () => {
28
- const examples = getCommandExamples("schemas fields create", { type: "member" });
29
-
30
- expect(examples).toHaveLength(1);
31
- expect(examples[0]?.command).toContain('"type":"MemberField"');
32
- expect(examples[0]?.description).toBe("Member field");
33
- });
34
-
35
- it("returns monthly and daily variants for schedule fields", () => {
36
- const examples = getCommandExamples("schemas fields create", { type: "schedule" });
37
-
38
- expect(examples).toHaveLength(2);
39
- expect(examples.map((example) => example.description)).toEqual([
40
- "Schedule field (monthly)",
41
- "Schedule field (daily)",
42
- ]);
43
- expect(examples[0]?.command).toContain('"type":"ScheduleField"');
44
- expect(examples[0]?.command).toContain('"frequency":"monthly"');
45
- expect(examples[1]?.command).toContain('"frequency":"daily"');
46
- });
47
-
48
- it("covers every backend number field variant", () => {
49
- const examples = getCommandExamples("schemas fields create", { type: "number" });
50
-
51
- expect(examples).toHaveLength(6);
52
-
53
- const descriptions = examples.map((example) => example.description);
54
- expect(descriptions).toEqual([
55
- "Number field (plain integer)",
56
- "Number field (decimal with thousands)",
57
- "Number field (currency € prefix)",
58
- "Number field (currency $ suffix)",
59
- "Number field (percentage)",
60
- "Number field (duration HH:MM)",
61
- ]);
62
-
63
- expect(examples[0]?.command).toContain('"slug":"quantity"');
64
- expect(examples[0]?.command).toContain('"format":"number"');
65
-
66
- const eurCurrency = examples[2]?.command ?? "";
67
- expect(eurCurrency).toContain('"format":"currency"');
68
- expect(eurCurrency).toContain('"symbolEnabled":true');
69
- expect(eurCurrency).toContain('"symbolPosition":"before"');
70
- expect(eurCurrency).toContain('"symbolValue":"€"');
71
-
72
- const usdCurrency = examples[3]?.command ?? "";
73
- expect(usdCurrency).toContain('"symbolPosition":"right"');
74
- expect(usdCurrency).toContain('"symbolValue":" $"');
75
-
76
- expect(examples[4]?.command).toContain('"format":"percentage"');
77
- expect(examples[5]?.command).toContain('"format":"duration"');
78
- expect(examples[5]?.command).toContain('"durationFormat":"HH:MM"');
79
- });
80
-
81
- it("returns canonical automation creation examples", () => {
82
- const examples = getCommandExamples("automations create");
83
-
84
- expect(examples.length).toBeGreaterThanOrEqual(3);
85
- expect(examples[0]?.command).toContain("--trigger-type scheduled");
86
- expect(examples.some((example) => example.command.includes("--trigger-type record_changed"))).toBe(
87
- true,
88
- );
89
- });
90
-
91
- it("returns automation trigger, webhook, and connection examples", () => {
92
- expect(getCommandExamples("automations trigger set")).not.toHaveLength(0);
93
- expect(getCommandExamples("automations webhook set")).not.toHaveLength(0);
94
- expect(getCommandExamples("automations connections create")).not.toHaveLength(0);
95
- expect(getCommandExamples("automations connections update")).not.toHaveLength(0);
96
- });
97
-
98
- it("includes raw payloads for key automation examples", () => {
99
- const createExamples = getCommandExamples("automations create");
100
- const actionExamples = getCommandExamples("automations actions add");
101
- const webhookExamples = getCommandExamples("automations webhook set");
102
-
103
- expect(createExamples.every((example) => typeof example.raw === "string" && example.raw.length > 0)).toBe(
104
- true,
105
- );
106
- expect(actionExamples.some((example) => example.command.includes("--condition-json"))).toBe(true);
107
- expect(actionExamples.some((example) => example.raw?.includes('"rawValue"'))).toBe(true);
108
- expect(webhookExamples[0]?.raw).toContain("idempotencyKeyJsonPath");
109
- });
110
-
111
- it("covers every action type documented by the backend", () => {
112
- const actionExamples = getCommandExamples("automations actions add");
113
- const backendActionTypes = [
114
- "create_record",
115
- "update_record",
116
- "search_records",
117
- "send_message",
118
- "http_request",
119
- "execute_plugin",
120
- "generate_document",
121
- ];
122
-
123
- for (const type of backendActionTypes) {
124
- expect(
125
- actionExamples.some((example) => example.command.includes(`--type ${type}`)),
126
- ).toBe(true);
127
- }
128
- });
129
-
130
- it("documents send_message with wrapped {rawValue, source} values keyed by recipients/subject/body", () => {
131
- const actionExamples = getCommandExamples("automations actions add");
132
- const sendMessage = actionExamples.find((example) =>
133
- example.command.includes("--type send_message"),
134
- );
135
-
136
- expect(sendMessage?.command).toContain("--to customer@example.com");
137
- expect(sendMessage?.command).toContain("--subject");
138
- expect(sendMessage?.command).toContain("--message");
139
- expect(sendMessage?.raw).toContain(`"recipients"`);
140
- expect(sendMessage?.raw).toContain(`"rawValue"`);
141
- expect(sendMessage?.raw).not.toContain(`"channel"`);
142
- });
143
-
144
- it("documents generate_document with template, schema, and file-field ids", () => {
145
- const actionExamples = getCommandExamples("automations actions add");
146
- const generateDocument = actionExamples.find((example) =>
147
- example.command.includes("--type generate_document"),
148
- );
149
-
150
- expect(generateDocument?.command).toContain("--document-template");
151
- expect(generateDocument?.command).toContain("--target-schema");
152
- expect(generateDocument?.command).toContain("--target-file-field");
153
- expect(generateDocument?.raw).toContain("documentTemplateId");
154
- expect(generateDocument?.raw).toContain("targetFileFieldId");
155
- });
156
-
157
- it("covers every records query operator documented by the backend", () => {
158
- const examples = getCommandExamples("records query");
159
- const backendOperators = [
160
- "eq",
161
- "neq",
162
- "gt",
163
- "gte",
164
- "lt",
165
- "lte",
166
- "contains",
167
- "in",
168
- "is_empty",
169
- "is_not_empty",
170
- ];
171
-
172
- for (const op of backendOperators) {
173
- expect(
174
- examples.some((example) => example.raw?.includes(`"op": "${op}"`)),
175
- `expected an example with op "${op}"`,
176
- ).toBe(true);
177
- }
178
- });
179
-
180
- it("offers minimal, multi-type, and reference records create examples", () => {
181
- const examples = getCommandExamples("records create");
182
-
183
- expect(examples).toHaveLength(3);
184
- expect(examples[0]?.raw).toContain('"contact-name"');
185
- expect(examples[1]?.raw).toContain('"customer-ref"');
186
- expect(examples[1]?.raw).toContain('"owner"');
187
- expect(examples[2]?.raw).toContain('"customer-ref"');
188
- expect(examples[2]?.response).toContain("data_field_reference_not_found");
189
- });
190
-
191
- it("provides full/minimal/folder variants for schemas create", () => {
192
- const examples = getCommandExamples("schemas create");
193
-
194
- expect(examples).toHaveLength(3);
195
- expect(examples.some((example) => example.raw?.includes('"folderId"'))).toBe(true);
196
- expect(examples.every((example) => example.command.startsWith("zinkee --json schemas create"))).toBe(
197
- true,
198
- );
199
- });
200
-
201
- it("covers all widget types in displays widgets create", () => {
202
- const examples = getCommandExamples("displays widgets create");
203
- const rawPayloads = examples.map((example) => example.raw ?? "");
204
-
205
- expect(examples.length).toBeGreaterThanOrEqual(7);
206
- for (const widgetType of ["TABLE_OR_SUBTABLE", "DETAIL", "KPI", "CHART", "TIMELINE", "KANBAN", "HIERARCHY"]) {
207
- expect(rawPayloads.some((raw) => raw.includes(`"type": "${widgetType}"`))).toBe(true);
208
- }
209
- });
210
-
211
- it("does not teach legacy system columns in display widget examples", () => {
212
- const legacyColumns = ["id", "createdat", "updatedat", "createdby", "updatedby"];
213
- const commands = ["displays widgets create", "displays widgets update", "displays list", "displays get"];
214
-
215
- for (const command of commands) {
216
- for (const example of getCommandExamples(command)) {
217
- for (const column of legacyColumns) {
218
- expect(example.command).not.toContain(`--field ${column}`);
219
- }
220
-
221
- const examplePayloads = [example.raw, example.response].filter(Boolean);
222
- for (const payload of examplePayloads) {
223
- expect(payload).not.toMatch(/"fields"\s*:\s*\[[^\]]*"(id|createdat|updatedat|createdby|updatedby)"/);
224
- expect(payload).not.toMatch(/"fieldCalc"\s*:\s*"(id|createdat|updatedat|createdby|updatedby)"/);
225
- }
226
- }
227
- }
228
- });
229
-
230
- it("covers REFERENCE, MEMBER and DATE variants in displays variables create", () => {
231
- const examples = getCommandExamples("displays variables create");
232
- const rawPayloads = examples.map((example) => example.raw ?? "");
233
-
234
- expect(examples.length).toBeGreaterThanOrEqual(6);
235
- for (const variableType of ["REFERENCE", "MEMBER", "DATE"]) {
236
- expect(rawPayloads.some((raw) => raw.includes(`"type": "${variableType}"`))).toBe(true);
237
- }
238
- expect(rawPayloads.some((raw) => raw.includes('"dynamicOption": "thisMonth"'))).toBe(true);
239
- });
240
-
241
- it("documents navigation resources move as schema-only with no resourceType", () => {
242
- const examples = getCommandExamples("navigation resources move");
243
- const rawPayloads = examples.map((example) => example.raw ?? "");
244
-
245
- expect(examples.length).toBeGreaterThanOrEqual(2);
246
- // Navigation only moves schemas now, so the payload no longer carries a
247
- // resourceType and the example commands must not pass --type.
248
- for (const raw of rawPayloads) {
249
- expect(raw).not.toContain("resourceType");
250
- }
251
- for (const example of examples) {
252
- expect(example.command).not.toContain("--type");
253
- }
254
- // Covers moving into a folder and back to the root folder.
255
- expect(
256
- rawPayloads.some((raw) => raw.includes('"targetFolderId": "00000000-0000-0000-0000-000000000000"')),
257
- ).toBe(true);
258
- });
259
-
260
- it("covers slug and UUID variants in teamspace resources publish", () => {
261
- const examples = getCommandExamples("teamspace resources publish");
262
-
263
- expect(examples.length).toBeGreaterThanOrEqual(4);
264
- expect(examples.some((example) => example.command.includes("customer-records"))).toBe(true);
265
- expect(examples.some((example) => example.command.includes("2eb8bdcf-6d9c-4fbb-9018-7fa8180f4c5e"))).toBe(
266
- true,
267
- );
268
- });
269
-
270
- it("enriches displays list with the documented response body", () => {
271
- const examples = getCommandExamples("displays list");
272
-
273
- expect(examples).toHaveLength(1);
274
- expect(examples[0]?.response).toContain('"layoutTemplate": "widget_row_tabs"');
275
- expect(examples[0]?.response).toContain('"layoutTemplate": "freeform"');
276
- expect(examples[0]?.response).toContain('"layoutTemplate": "grid"');
277
- expect(examples[0]?.response).toContain('"folder"');
278
- expect(examples[0]?.response).toContain('"isRoot": true');
279
- });
280
-
281
- it("attaches a response body to schemas create and teamspace resources publish", () => {
282
- const schemaExamples = getCommandExamples("schemas create");
283
- const publishExamples = getCommandExamples("teamspace resources publish");
284
-
285
- expect(schemaExamples[0]?.response).toContain('"schemaId"');
286
- expect(publishExamples[0]?.response).toContain('"targetFolder"');
287
- expect(publishExamples[0]?.response).toContain('"previousFolder"');
288
- });
289
-
290
- it("attaches a rich response body to displays get", () => {
291
- const examples = getCommandExamples("displays get");
292
-
293
- expect(examples).toHaveLength(1);
294
- expect(examples[0]?.response).toContain('"widgets"');
295
- expect(examples[0]?.response).toContain('"variables"');
296
- expect(examples[0]?.response).toContain('"crossWidgetFilter"');
297
- expect(examples[0]?.response).toContain('"navigationTargets"');
298
- });
299
-
300
- it("attaches responses to automations reads and mutations", () => {
301
- expect(getCommandExamples("automations list")[0]?.response).toContain('"triggerType"');
302
- expect(getCommandExamples("automations get")[0]?.response).toContain('"trigger"');
303
- expect(getCommandExamples("automations create")[0]?.response).toContain('"automationId"');
304
- expect(getCommandExamples("automations create")[0]?.response).toContain('"webhookEndpoint"');
305
- expect(getCommandExamples("automations webhook set")[0]?.response).toContain('"endpointUrl"');
306
- expect(getCommandExamples("automations folders create")[0]?.response).toContain('"automationCount": 0');
307
- expect(getCommandExamples("automations connections create")[0]?.response).toContain(
308
- '"ownerMemberId"',
309
- );
310
- });
311
-
312
- it("covers every profiles and config subcommand with an example that emits JSON", () => {
313
- const profileAndConfigCommands = [
314
- "profiles list",
315
- "profiles get",
316
- "profiles use",
317
- "profiles add",
318
- "profiles update",
319
- "profiles remove",
320
- "config path",
321
- "config show",
322
- "config validate",
323
- ];
324
-
325
- for (const commandName of profileAndConfigCommands) {
326
- const examples = getCommandExamples(commandName);
327
- expect(examples.length, `${commandName} has no examples`).toBeGreaterThan(0);
328
- for (const example of examples) {
329
- expect(example.command.startsWith("zinkee "), `${commandName}: ${example.command}`).toBe(true);
330
- expect(example.response, `${commandName} missing response`).toBeTruthy();
331
- }
332
- }
333
-
334
- expect(getCommandExamples("profiles list")[0]?.response).toContain('"apiKey": "********cdef"');
335
- expect(getCommandExamples("config show")[0]?.response).toContain('"defaultProfile": "prod"');
336
- expect(getCommandExamples("config validate")[0]?.response).toContain('"valid": true');
337
- });
338
-
339
- it("wraps example responses in the { data, meta } JSON envelope", () => {
340
- const listExample = getCommandExamples("schemas list")[0];
341
- const parsed = JSON.parse(listExample?.response ?? "{}");
342
-
343
- expect(parsed).toHaveProperty("data");
344
- expect(parsed).toHaveProperty("meta");
345
- expect(parsed.meta).toMatchObject({ command: "schemas list" });
346
- expect(Array.isArray(parsed.data)).toBe(true);
347
-
348
- // get/detail commands wrap a single object under data, not an array
349
- const getExample = getCommandExamples("schemas get")[0];
350
- const parsedGet = JSON.parse(getExample?.response ?? "{}");
351
- expect(Array.isArray(parsedGet.data)).toBe(false);
352
- expect(parsedGet.data).toHaveProperty("fields");
353
- });
354
-
355
- it.each(Object.keys(commandRegistry))(
356
- "returns at least one example for %s",
357
- (commandName) => {
358
- const examples = getCommandExamples(commandName);
359
- expect(examples.length, `${commandName} has no examples`).toBeGreaterThan(0);
360
- for (const example of examples) {
361
- expect(example.command, `${commandName} example missing command`).toBeTruthy();
362
- expect(example.description, `${commandName} example missing description`).toBeTruthy();
363
- }
364
- },
365
- );
366
-
367
- it("getResolvedCommandExamples is synchronous and matches getCommandExamples", () => {
368
- for (const commandName of Object.keys(commandRegistry)) {
369
- const resolved = getResolvedCommandExamples(commandName);
370
- expect(resolved, `${commandName} resolved must not be a thenable`).not.toHaveProperty("then");
371
- expect(resolved).toEqual(getCommandExamples(commandName));
372
- }
373
- });
374
- });
@@ -1,65 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- import { getCommandMeta } from "../command-registry.js";
4
-
5
- import { buildJsonError, buildJsonSuccess, folderColumns } from "./output.js";
6
-
7
- describe("folderColumns", () => {
8
- it("flattens a named folder into id/name/isRoot columns", () => {
9
- expect(folderColumns({ id: "f1", name: "Finance", isRoot: false })).toEqual({
10
- folderId: "f1",
11
- folderName: "Finance",
12
- folderIsRoot: "false",
13
- });
14
- });
15
-
16
- it("renders the root folder (null name) as (root)", () => {
17
- expect(folderColumns({ id: "root-1", name: null, isRoot: true })).toEqual({
18
- folderId: "root-1",
19
- folderName: "(root)",
20
- folderIsRoot: "true",
21
- });
22
- });
23
-
24
- it("leaves every column empty when there is no folder", () => {
25
- expect(folderColumns(undefined)).toEqual({
26
- folderId: "",
27
- folderName: "",
28
- folderIsRoot: "",
29
- });
30
- });
31
- });
32
-
33
- describe("buildJsonSuccess", () => {
34
- it("wraps data in the standard success envelope", () => {
35
- expect(buildJsonSuccess([{ id: "1" }], { command: "schemas list" })).toEqual({
36
- data: [{ id: "1" }],
37
- meta: { command: "schemas list" },
38
- });
39
- });
40
- });
41
-
42
- describe("buildJsonError", () => {
43
- it("wraps error details in the standard error envelope", () => {
44
- expect(
45
- buildJsonError({ reason: "invalid_request", message: "Bad request" }, {}),
46
- ).toMatchObject({
47
- error: { reason: "invalid_request" },
48
- meta: {},
49
- });
50
- });
51
- });
52
-
53
- describe("command registry", () => {
54
- it("looks up registered command metadata cleanly", () => {
55
- const meta = getCommandMeta("profiles add");
56
-
57
- expect(meta).toEqual({
58
- command: "profiles add",
59
- accessLevel: "write",
60
- supportsExample: true,
61
- supportsRaw: false,
62
- output: "json-only",
63
- });
64
- });
65
- });
@@ -1,154 +0,0 @@
1
- import Table from "cli-table3";
2
-
3
- import type {
4
- JsonEnvelopeMeta,
5
- JsonErrorEnvelope,
6
- JsonSuccessEnvelope,
7
- OutputFormat,
8
- } from "../types.js";
9
-
10
- export interface TableColumn {
11
- key: string;
12
- label: string;
13
- }
14
-
15
- export interface TableCell {
16
- key: string;
17
- value: string;
18
- }
19
-
20
- export interface TableRow {
21
- cells: TableCell[];
22
- }
23
-
24
- export interface OutputWriter {
25
- write(chunk: string): unknown;
26
- }
27
-
28
- function stringifyCellValue(value: unknown): string {
29
- if (value === null || value === undefined) {
30
- return "";
31
- }
32
-
33
- if (typeof value === "string") {
34
- return value;
35
- }
36
-
37
- if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
38
- return String(value);
39
- }
40
-
41
- return JSON.stringify(value);
42
- }
43
-
44
- export function buildJsonSuccess<TData, TMeta extends JsonEnvelopeMeta = JsonEnvelopeMeta>(
45
- data: TData,
46
- meta: TMeta = {} as TMeta,
47
- ): JsonSuccessEnvelope<TData, TMeta> {
48
- return { data, meta };
49
- }
50
-
51
- export function buildJsonError<TErr, TMeta extends JsonEnvelopeMeta = JsonEnvelopeMeta>(
52
- error: TErr,
53
- meta: TMeta = {} as TMeta,
54
- ): JsonErrorEnvelope<TErr, TMeta> {
55
- return { error, meta };
56
- }
57
-
58
- export function buildTableColumns(keys: readonly string[]): readonly TableColumn[] {
59
- return keys.map((key) => ({ key, label: key }));
60
- }
61
-
62
- export interface FolderLocation {
63
- id?: string;
64
- name?: string | null;
65
- isRoot?: boolean;
66
- }
67
-
68
- /** Column keys, in order, produced by {@link folderColumns}. Append them to a table's key list. */
69
- export const FOLDER_TABLE_KEYS = ["folderId", "folderName", "folderIsRoot"] as const;
70
-
71
- /**
72
- * Flattens the API `folder` object (`{ id, name, isRoot }`) into the folderId/folderName/folderIsRoot table columns.
73
- * The root folder arrives with a null name, so folderName renders it as "(root)". A missing folder (e.g. a workspace
74
- * without a default group) leaves every column empty.
75
- */
76
- export function folderColumns(folder: FolderLocation | null | undefined): {
77
- folderId: string;
78
- folderName: string;
79
- folderIsRoot: string;
80
- } {
81
- if (!folder) {
82
- return { folderId: "", folderName: "", folderIsRoot: "" };
83
- }
84
-
85
- return {
86
- folderId: folder.id ?? "",
87
- folderName: folder.name ?? (folder.isRoot === true ? "(root)" : ""),
88
- folderIsRoot: folder.isRoot === undefined ? "" : String(folder.isRoot),
89
- };
90
- }
91
-
92
- export function buildTableRows(
93
- rows: readonly Record<string, unknown>[],
94
- columns: readonly TableColumn[],
95
- ): readonly TableRow[] {
96
- return rows.map((row) => ({
97
- cells: columns.map((column) => ({
98
- key: column.key,
99
- value: stringifyCellValue(row[column.key]),
100
- })),
101
- }));
102
- }
103
-
104
- function getWriter(writer?: OutputWriter): OutputWriter {
105
- return writer ?? process.stdout;
106
- }
107
-
108
- export function printJson(value: unknown, writer?: OutputWriter): void {
109
- getWriter(writer).write(`${JSON.stringify(value, null, 2)}\n`);
110
- }
111
-
112
- export function printTable(
113
- rows: readonly Record<string, unknown>[],
114
- keys?: readonly string[],
115
- writer?: OutputWriter,
116
- ): void {
117
- if (rows.length === 0) {
118
- getWriter(writer).write("No results.\n");
119
- return;
120
- }
121
-
122
- const firstRow = rows[0];
123
-
124
- if (!firstRow) {
125
- getWriter(writer).write("No results.\n");
126
- return;
127
- }
128
-
129
- const headings = [...(keys ?? Object.keys(firstRow))];
130
- const table = new Table({
131
- head: headings,
132
- wordWrap: true,
133
- });
134
-
135
- for (const row of rows) {
136
- table.push(headings.map((heading) => stringifyCellValue(row[heading])));
137
- }
138
-
139
- getWriter(writer).write(`${table.toString()}\n`);
140
- }
141
-
142
- export function printOutput(
143
- format: OutputFormat,
144
- rows: readonly Record<string, unknown>[],
145
- keys?: readonly string[],
146
- writer?: OutputWriter,
147
- ): void {
148
- if (format === "json") {
149
- printJson(rows, writer);
150
- return;
151
- }
152
-
153
- printTable(rows, keys, writer);
154
- }