zinkee 0.1.43 → 0.1.45

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 +9 -4
  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-E6FFEVBE.js} +856 -1141
  5. package/dist/examples-E6FFEVBE.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,883 +0,0 @@
1
- import { readFileSync } from "node:fs";
2
- import type { Writable } from "node:stream";
3
-
4
- import { Command, InvalidArgumentError } from "commander";
5
-
6
- import {
7
- type ApiRecord,
8
- type CreateRecordValues,
9
- type RecordQueryExpression,
10
- type RecordQueryInput,
11
- type RecordQueryResult,
12
- RecordsApi,
13
- type UpdateRecordInput,
14
- } from "../api/records.js";
15
- import { SchemasApi } from "../api/schemas.js";
16
- import { ApiClient } from "../client.js";
17
- import { defaultConfigPath, readRuntimeConfig } from "../config.js";
18
- import {
19
- type ParsedExpression,
20
- parseExpression,
21
- } from "../parsers/expressions.js";
22
- import {
23
- parseJsonAssignment,
24
- parseKeyValue,
25
- type JsonKeyValueAssignment,
26
- type KeyValueAssignment,
27
- } from "../parsers/kv.js";
28
- import {
29
- normalizeUuidOrSlug,
30
- type NormalizedUuidOrSlug,
31
- } from "../parsers/selectors.js";
32
- import { resolveRuntimeContext } from "../runtime-context.js";
33
- import type { JsonObject, RuntimeOverrides } from "../types.js";
34
- import { CliError } from "../utils/errors.js";
35
- import { buildJsonSuccess, printTable } from "../utils/output.js";
36
-
37
- interface CommandIo {
38
- write(chunk: string): unknown;
39
- }
40
-
41
- export interface RegisterRecordsCommandsOptions {
42
- configPath?: string;
43
- stdout?: CommandIo;
44
- }
45
-
46
- interface GlobalOptions extends RuntimeOverrides {}
47
-
48
- interface RecordsQueryCommandOptions {
49
- select?: string[];
50
- where?: ParsedExpression[];
51
- search?: string;
52
- sort?: string[];
53
- limit?: number;
54
- offset?: number;
55
- raw?: string;
56
- rawFile?: string;
57
- }
58
-
59
- interface RecordsListCommandOptions {
60
- limit?: number;
61
- offset?: number;
62
- }
63
-
64
- interface RecordsCreateCommandOptions {
65
- set?: KeyValueAssignment[];
66
- setJson?: JsonKeyValueAssignment[];
67
- raw?: string;
68
- rawFile?: string;
69
- recordsFile?: string;
70
- }
71
-
72
- interface RecordsUpdateCommandOptions extends RecordsCreateCommandOptions {
73
- unset?: string[];
74
- }
75
-
76
- interface RecordsDeleteCommandOptions {
77
- record?: string[];
78
- recordsFile?: string;
79
- }
80
-
81
- function getCommandIo(stdout: CommandIo | undefined): CommandIo {
82
- return stdout ?? (process.stdout as Writable);
83
- }
84
-
85
- function writeLine(stdout: CommandIo, value: string): void {
86
- stdout.write(`${value}\n`);
87
- }
88
-
89
- function getGlobalOptions(command: Command): GlobalOptions {
90
- return command.optsWithGlobals<GlobalOptions>();
91
- }
92
-
93
- function emitJson(
94
- stdout: CommandIo,
95
- commandName: string,
96
- data: unknown,
97
- configPath: string,
98
- meta: Record<string, unknown> = {},
99
- ): void {
100
- writeLine(
101
- stdout,
102
- JSON.stringify(buildJsonSuccess(data, { command: commandName, configPath, ...meta })),
103
- );
104
- }
105
-
106
- function toConfigurationError(filePath: string, error: unknown): never {
107
- if (error instanceof CliError) {
108
- throw error;
109
- }
110
-
111
- if (
112
- typeof error === "object" &&
113
- error !== null &&
114
- "code" in error &&
115
- error.code === "ENOENT"
116
- ) {
117
- throw new CliError("configuration_error", `Config file not found at ${filePath}.`);
118
- }
119
-
120
- if (error instanceof Error) {
121
- throw new CliError("configuration_error", error.message);
122
- }
123
-
124
- throw error;
125
- }
126
-
127
- function createRecordsApi(
128
- command: Command,
129
- filePath: string,
130
- ): {
131
- api: RecordsApi;
132
- schemasApi: SchemasApi;
133
- json: boolean;
134
- profile: string;
135
- readOnly: boolean;
136
- } {
137
- try {
138
- const config = readRuntimeConfig(filePath);
139
- const runtime = resolveRuntimeContext(config, getGlobalOptions(command));
140
-
141
- if (!runtime.baseUrl) {
142
- throw new CliError(
143
- "configuration_error",
144
- `Resolved profile "${runtime.profile}" is missing a base URL.`,
145
- );
146
- }
147
-
148
- const client = new ApiClient({
149
- baseUrl: runtime.baseUrl,
150
- apiKey: runtime.apiKey,
151
- });
152
-
153
- return {
154
- api: new RecordsApi(client),
155
- schemasApi: new SchemasApi(client),
156
- json: runtime.json,
157
- profile: runtime.profile,
158
- readOnly: runtime.readOnly,
159
- };
160
- } catch (error) {
161
- toConfigurationError(filePath, error);
162
- }
163
- }
164
-
165
- function ensureWritable(
166
- commandName: string,
167
- readOnly: boolean,
168
- commandMode: "write" | "destructive",
169
- ): void {
170
- if (readOnly) {
171
- throw new CliError(
172
- "command_not_allowed_in_read_only_mode",
173
- `Command "${commandName}" is not allowed in read-only mode.`,
174
- {
175
- commandMode,
176
- executionMode: "read-only",
177
- },
178
- );
179
- }
180
- }
181
-
182
- function parseUuidOrSlugSelector(input: string): NormalizedUuidOrSlug {
183
- try {
184
- return normalizeUuidOrSlug(input);
185
- } catch (error) {
186
- const message = error instanceof Error ? error.message : "Invalid selector.";
187
- throw new InvalidArgumentError(message);
188
- }
189
- }
190
-
191
- function parseNonNegativeInteger(input: string): number {
192
- if (!/^\d+$/.test(input.trim())) {
193
- throw new InvalidArgumentError(`Expected a non-negative integer, received "${input}".`);
194
- }
195
-
196
- return Number(input);
197
- }
198
-
199
- function parseCommaSeparatedValues(
200
- input: string,
201
- label: string,
202
- ): string[] {
203
- const values = input
204
- .split(",")
205
- .map((value) => value.trim())
206
- .filter(Boolean);
207
-
208
- if (values.length === 0) {
209
- throw new InvalidArgumentError(`Expected ${label} to contain at least one value.`);
210
- }
211
-
212
- return values;
213
- }
214
-
215
- function parseExpressionValue(input: string): ParsedExpression {
216
- try {
217
- return parseExpression(input);
218
- } catch (error) {
219
- const message = error instanceof Error ? error.message : "Invalid expression.";
220
- throw new InvalidArgumentError(message);
221
- }
222
- }
223
-
224
- function parseKeyValueAssignment(input: string): KeyValueAssignment {
225
- try {
226
- return parseKeyValue(input);
227
- } catch (error) {
228
- const message = error instanceof Error ? error.message : "Invalid assignment.";
229
- throw new InvalidArgumentError(message);
230
- }
231
- }
232
-
233
- function parseJsonKeyValueAssignment(input: string): JsonKeyValueAssignment {
234
- try {
235
- return parseJsonAssignment(input);
236
- } catch (error) {
237
- const message = error instanceof Error ? error.message : "Invalid JSON assignment.";
238
- throw new InvalidArgumentError(message);
239
- }
240
- }
241
-
242
- function dedupeStrings(values: string[]): string[] {
243
- return [...new Set(values)];
244
- }
245
-
246
- function toRecordUuid(value: string): string | null {
247
- try {
248
- const normalized = normalizeUuidOrSlug(value);
249
- return normalized.kind === "uuid" ? normalized.value : null;
250
- } catch {
251
- return null;
252
- }
253
- }
254
-
255
- function collectRecordIds(value: string, previous: string[] = []): string[] {
256
- const uuid = toRecordUuid(value.trim());
257
- if (!uuid) {
258
- throw new InvalidArgumentError(`Expected a record UUID, received "${value}".`);
259
- }
260
- return [...previous, uuid];
261
- }
262
-
263
- function readRecordIdsFromFile(path: string): string[] {
264
- let contents: string;
265
-
266
- try {
267
- contents = readFileSync(path, "utf8");
268
- } catch (error) {
269
- const message = error instanceof Error ? error.message : "Unable to read file.";
270
- throw new CliError(
271
- "invalid_cli_usage",
272
- `Failed to read record ids from ${path}: ${message}`,
273
- );
274
- }
275
-
276
- const ids: string[] = [];
277
-
278
- contents.split(/\r?\n/).forEach((line, index) => {
279
- const value = line.trim();
280
- if (!value) {
281
- return;
282
- }
283
- const uuid = toRecordUuid(value);
284
- if (!uuid) {
285
- throw new CliError(
286
- "invalid_cli_usage",
287
- `Invalid record UUID "${value}" at ${path}:${index + 1}.`,
288
- );
289
- }
290
- ids.push(uuid);
291
- });
292
-
293
- return ids;
294
- }
295
-
296
- function resolveDeleteRecordIds(options: RecordsDeleteCommandOptions): string[] {
297
- const flagIds = options.record ?? [];
298
- const fileIds = options.recordsFile ? readRecordIdsFromFile(options.recordsFile) : [];
299
- const recordIds = dedupeStrings([...flagIds, ...fileIds]);
300
-
301
- if (recordIds.length === 0) {
302
- throw new CliError(
303
- "invalid_cli_usage",
304
- 'Provide at least one record id via "--record <uuid>" (repeatable) or "--records-file <path>".',
305
- );
306
- }
307
-
308
- return recordIds;
309
- }
310
-
311
- function collectStringValues(value: string, previous: string[] = []): string[] {
312
- return [...previous, value];
313
- }
314
-
315
- function collectSelectValues(value: string, previous: string[] = []): string[] {
316
- return dedupeStrings([...previous, ...parseCommaSeparatedValues(value, "--select")]);
317
- }
318
-
319
- function collectSortValues(value: string, previous: string[] = []): string[] {
320
- return dedupeStrings([...previous, ...parseCommaSeparatedValues(value, "--sort")]);
321
- }
322
-
323
- function collectWhereExpressions(
324
- value: string,
325
- previous: ParsedExpression[] = [],
326
- ): ParsedExpression[] {
327
- return [...previous, parseExpressionValue(value)];
328
- }
329
-
330
- function collectKeyValueAssignments(
331
- value: string,
332
- previous: KeyValueAssignment[] = [],
333
- ): KeyValueAssignment[] {
334
- return [...previous, parseKeyValueAssignment(value)];
335
- }
336
-
337
- function collectJsonAssignments(
338
- value: string,
339
- previous: JsonKeyValueAssignment[] = [],
340
- ): JsonKeyValueAssignment[] {
341
- return [...previous, parseJsonKeyValueAssignment(value)];
342
- }
343
-
344
- function isJsonObject(value: unknown): value is JsonObject {
345
- return typeof value === "object" && value !== null && !Array.isArray(value);
346
- }
347
-
348
- function parseRawJsonObject(input: string, sourceLabel: string): JsonObject {
349
- let parsed: unknown;
350
-
351
- try {
352
- parsed = JSON.parse(input);
353
- } catch (error) {
354
- const message = error instanceof Error ? error.message : "Invalid JSON.";
355
- throw new CliError(
356
- "invalid_cli_usage",
357
- `Failed to parse JSON from ${sourceLabel}: ${message}`,
358
- );
359
- }
360
-
361
- if (!isJsonObject(parsed)) {
362
- throw new CliError(
363
- "invalid_cli_usage",
364
- `Expected ${sourceLabel} to contain a JSON object.`,
365
- );
366
- }
367
-
368
- return parsed;
369
- }
370
-
371
- function readRawObject(
372
- raw: string | undefined,
373
- rawFile: string | undefined,
374
- ): JsonObject {
375
- if (raw && rawFile) {
376
- throw new CliError(
377
- "invalid_cli_usage",
378
- 'Provide either "--raw" or "--raw-file", not both.',
379
- );
380
- }
381
-
382
- if (raw) {
383
- return parseRawJsonObject(raw, "--raw");
384
- }
385
-
386
- if (rawFile) {
387
- let contents: string;
388
-
389
- try {
390
- contents = readFileSync(rawFile, "utf8");
391
- } catch (error) {
392
- const message = error instanceof Error ? error.message : "Unable to read file.";
393
- throw new CliError(
394
- "invalid_cli_usage",
395
- `Failed to read raw payload from ${rawFile}: ${message}`,
396
- );
397
- }
398
-
399
- return parseRawJsonObject(contents, `--raw-file ${rawFile}`);
400
- }
401
-
402
- return {};
403
- }
404
-
405
- function isEmptyObject(value: object): boolean {
406
- return Object.keys(value).length === 0;
407
- }
408
-
409
- function buildAssignmentObject(
410
- assignments: readonly KeyValueAssignment[] = [],
411
- jsonAssignments: readonly JsonKeyValueAssignment[] = [],
412
- ): Record<string, unknown> {
413
- const output: Record<string, unknown> = {};
414
-
415
- for (const assignment of assignments) {
416
- output[assignment.key] = assignment.value;
417
- }
418
-
419
- for (const assignment of jsonAssignments) {
420
- output[assignment.key] = assignment.value;
421
- }
422
-
423
- return output;
424
- }
425
-
426
- function toQueryExpressions(
427
- expressions: readonly ParsedExpression[] | undefined,
428
- ): RecordQueryExpression[] | undefined {
429
- if (!expressions || expressions.length === 0) {
430
- return undefined;
431
- }
432
-
433
- return expressions.map((expression) => ({
434
- field: expression.field,
435
- comparator: expression.comparator,
436
- value: expression.value,
437
- }));
438
- }
439
-
440
- function buildRecordQueryInput(
441
- options: RecordsQueryCommandOptions,
442
- allowRaw: boolean,
443
- ): RecordQueryInput {
444
- const input: RecordQueryInput = allowRaw
445
- ? readRawObject(options.raw, options.rawFile)
446
- : {};
447
-
448
- if (options.select && options.select.length > 0) {
449
- input.select = [...options.select];
450
- }
451
-
452
- const where = toQueryExpressions(options.where);
453
-
454
- if (where && where.length > 0) {
455
- input.where = where;
456
- }
457
-
458
- if (options.search !== undefined) {
459
- input.search = { term: options.search };
460
- }
461
-
462
- if (options.sort && options.sort.length > 0) {
463
- input.sort = [...options.sort];
464
- }
465
-
466
- if (options.limit !== undefined) {
467
- input.limit = options.limit;
468
- }
469
-
470
- if (options.offset !== undefined) {
471
- input.offset = options.offset;
472
- }
473
-
474
- return input;
475
- }
476
-
477
- function readRecordsFromFile(path: string): CreateRecordValues[] {
478
- let contents: string;
479
-
480
- try {
481
- contents = readFileSync(path, "utf8");
482
- } catch (error) {
483
- const message = error instanceof Error ? error.message : "Unable to read file.";
484
- throw new CliError(
485
- "invalid_cli_usage",
486
- `Failed to read records from ${path}: ${message}`,
487
- );
488
- }
489
-
490
- let parsed: unknown;
491
- try {
492
- parsed = JSON.parse(contents);
493
- } catch (error) {
494
- const message = error instanceof Error ? error.message : "Invalid JSON.";
495
- throw new CliError(
496
- "invalid_cli_usage",
497
- `Failed to parse JSON from --records-file ${path}: ${message}`,
498
- );
499
- }
500
-
501
- if (!Array.isArray(parsed)) {
502
- throw new CliError(
503
- "invalid_cli_usage",
504
- `Expected --records-file ${path} to contain a JSON array of record objects.`,
505
- );
506
- }
507
-
508
- if (parsed.length === 0) {
509
- throw new CliError(
510
- "invalid_cli_usage",
511
- `--records-file ${path} must contain at least one record.`,
512
- );
513
- }
514
-
515
- return parsed.map((record, index) => {
516
- if (!isJsonObject(record)) {
517
- throw new CliError(
518
- "invalid_cli_usage",
519
- `Record at index ${index} in --records-file ${path} must be a JSON object.`,
520
- );
521
- }
522
- return record;
523
- });
524
- }
525
-
526
- function buildCreateRecords(options: RecordsCreateCommandOptions): CreateRecordValues[] {
527
- const hasSingleRecordFlags =
528
- (options.set?.length ?? 0) > 0 ||
529
- (options.setJson?.length ?? 0) > 0 ||
530
- options.raw !== undefined ||
531
- options.rawFile !== undefined;
532
-
533
- if (options.recordsFile !== undefined) {
534
- if (hasSingleRecordFlags) {
535
- throw new CliError(
536
- "invalid_cli_usage",
537
- 'Provide either "--records-file" or the single-record flags (--set/--set-json/--raw/--raw-file), not both.',
538
- );
539
- }
540
- return readRecordsFromFile(options.recordsFile);
541
- }
542
-
543
- const values = {
544
- ...readRawObject(options.raw, options.rawFile),
545
- ...buildAssignmentObject(options.set, options.setJson),
546
- };
547
-
548
- if (isEmptyObject(values)) {
549
- throw new CliError(
550
- "invalid_cli_usage",
551
- 'Provide record fields via "--set", "--set-json", "--raw", "--raw-file", or load multiple records with "--records-file".',
552
- );
553
- }
554
-
555
- return [values];
556
- }
557
-
558
- function buildRecordUpdateInput(
559
- options: RecordsUpdateCommandOptions,
560
- ): UpdateRecordInput {
561
- const raw = readRawObject(options.raw, options.rawFile);
562
- const input: UpdateRecordInput = {};
563
- const setPayload = {
564
- ...raw,
565
- ...buildAssignmentObject(options.set, options.setJson),
566
- };
567
- const unsetPayload = [...(options.unset ?? [])];
568
-
569
- for (const key of Object.keys(setPayload)) {
570
- const unsetIndex = unsetPayload.indexOf(key);
571
-
572
- if (unsetIndex !== -1) {
573
- unsetPayload.splice(unsetIndex, 1);
574
- }
575
- }
576
-
577
- if (Object.keys(setPayload).length > 0) {
578
- input.set = setPayload;
579
- } else {
580
- delete input.set;
581
- }
582
-
583
- if (unsetPayload.length > 0) {
584
- input.unset = [...new Set(unsetPayload)];
585
- } else {
586
- delete input.unset;
587
- }
588
-
589
- if (isEmptyObject(input)) {
590
- throw new CliError(
591
- "invalid_cli_usage",
592
- 'Provide record updates via "--set", "--set-json", "--unset", "--raw", or "--raw-file".',
593
- );
594
- }
595
-
596
- return input;
597
- }
598
-
599
- function toTableRows(data: unknown): Record<string, unknown>[] {
600
- if (Array.isArray(data)) {
601
- return data.map((entry) => (isJsonObject(entry) ? entry : { value: entry }));
602
- }
603
-
604
- if (isJsonObject(data)) {
605
- return [data];
606
- }
607
-
608
- return [{ value: data }];
609
- }
610
-
611
- function emitTable(stdout: CommandIo, data: unknown): void {
612
- printTable(toTableRows(data), undefined, stdout);
613
- }
614
-
615
- function getQueryRecords(result: RecordQueryResult): ApiRecord[] {
616
- return Array.isArray(result.records) ? result.records : [];
617
- }
618
-
619
- function getRecordIdentifier(record: ApiRecord): string {
620
- return typeof record.recordId === "string" ? record.recordId : "unknown";
621
- }
622
-
623
- async function resolveSchemaSelector(
624
- api: SchemasApi,
625
- selector: NormalizedUuidOrSlug,
626
- ): Promise<NormalizedUuidOrSlug> {
627
- if (selector.kind === "uuid") {
628
- return selector;
629
- }
630
-
631
- const schemas = await api.listSchemas();
632
- const match = schemas.find((entry) => entry.slug === selector.value);
633
-
634
- if (!match || typeof match.id !== "string") {
635
- throw new CliError("invalid_cli_usage", `Schema "${selector.value}" not found.`);
636
- }
637
-
638
- return {
639
- kind: "uuid",
640
- value: match.id,
641
- };
642
- }
643
-
644
- export function registerRecordsCommands(
645
- program: Command,
646
- options: RegisterRecordsCommandsOptions = {},
647
- ): void {
648
- const filePath = options.configPath ?? defaultConfigPath;
649
- const stdout = getCommandIo(options.stdout);
650
- const records = program.command("records").description("Manage schema records");
651
-
652
- records
653
- .command("list")
654
- .description("List records for a schema with simple pagination")
655
- .argument("<schema>", "schema uuid or slug", parseUuidOrSlugSelector)
656
- .option("--limit <number>", "limit results", parseNonNegativeInteger)
657
- .option("--offset <number>", "offset results", parseNonNegativeInteger)
658
- .action(async function action(
659
- this: Command,
660
- schema: NormalizedUuidOrSlug,
661
- commandOptions: RecordsListCommandOptions,
662
- ) {
663
- const { api, schemasApi, json } = createRecordsApi(this, filePath);
664
- const resolvedSchema = await resolveSchemaSelector(schemasApi, schema);
665
- const data = await api.listRecords(
666
- resolvedSchema,
667
- buildRecordQueryInput(commandOptions, false),
668
- );
669
- const recordsData = getQueryRecords(data);
670
-
671
- if (json) {
672
- emitJson(
673
- stdout,
674
- "records list",
675
- recordsData,
676
- filePath,
677
- data.page ? { page: data.page } : {},
678
- );
679
- return;
680
- }
681
-
682
- emitTable(stdout, recordsData);
683
- });
684
-
685
- records
686
- .command("query")
687
- .description("Query records for a schema")
688
- .argument("<schema>", "schema uuid or slug", parseUuidOrSlugSelector)
689
- .option("--select <fields>", "field selection (comma-separated, repeatable)", collectSelectValues, [])
690
- .option("--where <expression>", "where expression", collectWhereExpressions, [])
691
- .option(
692
- "--search <term>",
693
- "free-text search term (matched across the schema's searchable fields)",
694
- )
695
- .option("--sort <field:direction>", "sort field(s)", collectSortValues, [])
696
- .option("--limit <number>", "limit results", parseNonNegativeInteger)
697
- .option("--offset <number>", "offset results", parseNonNegativeInteger)
698
- .option("--raw <json>", "raw JSON object payload")
699
- .option("--raw-file <path>", "path to a raw JSON object payload")
700
- .action(async function action(
701
- this: Command,
702
- schema: NormalizedUuidOrSlug,
703
- commandOptions: RecordsQueryCommandOptions,
704
- ) {
705
- const { api, schemasApi, json } = createRecordsApi(this, filePath);
706
- const resolvedSchema = await resolveSchemaSelector(schemasApi, schema);
707
- const data = await api.queryRecords(
708
- resolvedSchema,
709
- buildRecordQueryInput(commandOptions, true),
710
- );
711
- const recordsData = getQueryRecords(data);
712
-
713
- if (json) {
714
- emitJson(
715
- stdout,
716
- "records query",
717
- recordsData,
718
- filePath,
719
- data.page ? { page: data.page } : {},
720
- );
721
- return;
722
- }
723
-
724
- emitTable(stdout, recordsData);
725
- });
726
-
727
- records
728
- .command("get")
729
- .description("Get a record by UUID or slug")
730
- .argument("<schema>", "schema uuid or slug", parseUuidOrSlugSelector)
731
- .argument("<record>", "record uuid or slug", parseUuidOrSlugSelector)
732
- .action(async function action(
733
- this: Command,
734
- schema: NormalizedUuidOrSlug,
735
- record: NormalizedUuidOrSlug,
736
- ) {
737
- const { api, schemasApi, json } = createRecordsApi(this, filePath);
738
- const resolvedSchema = await resolveSchemaSelector(schemasApi, schema);
739
- const data = await api.getRecord(resolvedSchema, record);
740
-
741
- if (json) {
742
- emitJson(stdout, "records get", data, filePath);
743
- return;
744
- }
745
-
746
- emitTable(stdout, data);
747
- });
748
-
749
- records
750
- .command("create")
751
- .description("Create one or more records")
752
- .argument("<schema>", "schema uuid or slug", parseUuidOrSlugSelector)
753
- .option(
754
- "--set <field=value>",
755
- "set a string field (reference fields: a record id, 'recordId::label', or an exact display value)",
756
- collectKeyValueAssignments,
757
- [],
758
- )
759
- .option("--set-json <field=json>", "set a structured field", collectJsonAssignments, [])
760
- .option("--raw <json>", "raw JSON object payload")
761
- .option("--raw-file <path>", "path to a raw JSON object payload")
762
- .option(
763
- "--records-file <path>",
764
- "path to a JSON array of record objects (mutually exclusive with --set/--set-json/--raw/--raw-file)",
765
- )
766
- .action(async function action(
767
- this: Command,
768
- schema: NormalizedUuidOrSlug,
769
- commandOptions: RecordsCreateCommandOptions,
770
- ) {
771
- const { api, schemasApi, json, readOnly } = createRecordsApi(this, filePath);
772
- ensureWritable("records create", readOnly, "write");
773
-
774
- const records = buildCreateRecords(commandOptions);
775
- const resolvedSchema = await resolveSchemaSelector(schemasApi, schema);
776
-
777
- const data = await api.createRecords(resolvedSchema, records);
778
-
779
- if (json) {
780
- emitJson(stdout, "records create", data, filePath);
781
- return;
782
- }
783
-
784
- writeLine(stdout, `Created ${data.created.length} record(s) in schema "${schema.value}".`);
785
- if (data.errors.length > 0) {
786
- writeLine(stdout, `${data.errors.length} record(s) rejected:`);
787
- for (const error of data.errors) {
788
- writeLine(stdout, ` - index ${error.index}: ${error.message}`);
789
- }
790
- }
791
- });
792
-
793
- records
794
- .command("update")
795
- .description("Update a record")
796
- .argument("<schema>", "schema uuid or slug", parseUuidOrSlugSelector)
797
- .argument("<record>", "record uuid or slug", parseUuidOrSlugSelector)
798
- .option(
799
- "--set <field=value>",
800
- "set a string field (reference fields: a record id, 'recordId::label', or an exact display value)",
801
- collectKeyValueAssignments,
802
- [],
803
- )
804
- .option("--set-json <field=json>", "set a structured field", collectJsonAssignments, [])
805
- .option("--unset <field>", "unset a field", collectStringValues, [])
806
- .option("--raw <json>", "raw JSON object of fields to set")
807
- .option("--raw-file <path>", "path to a JSON object of fields to set")
808
- .action(async function action(
809
- this: Command,
810
- schema: NormalizedUuidOrSlug,
811
- record: NormalizedUuidOrSlug,
812
- commandOptions: RecordsUpdateCommandOptions,
813
- ) {
814
- const { api, schemasApi, json, readOnly } = createRecordsApi(this, filePath);
815
- ensureWritable("records update", readOnly, "write");
816
- const resolvedSchema = await resolveSchemaSelector(schemasApi, schema);
817
-
818
- const data = await api.updateRecord(
819
- resolvedSchema,
820
- record,
821
- buildRecordUpdateInput(commandOptions),
822
- );
823
-
824
- if (json) {
825
- emitJson(stdout, "records update", data, filePath);
826
- return;
827
- }
828
-
829
- writeLine(stdout, `Updated record "${getRecordIdentifier(data)}".`);
830
- });
831
-
832
- records
833
- .command("delete")
834
- .description("Delete one or more records")
835
- .argument("<schema>", "schema uuid or slug", parseUuidOrSlugSelector)
836
- .option("--record <uuid>", "record uuid to delete (repeatable)", collectRecordIds, [])
837
- .option(
838
- "--records-file <path>",
839
- "path to a file with one record uuid per line (merged with --record)",
840
- )
841
- .action(async function action(
842
- this: Command,
843
- schema: NormalizedUuidOrSlug,
844
- commandOptions: RecordsDeleteCommandOptions,
845
- ) {
846
- const { api, schemasApi, json, readOnly } = createRecordsApi(this, filePath);
847
- ensureWritable("records delete", readOnly, "destructive");
848
-
849
- const recordIds = resolveDeleteRecordIds(commandOptions);
850
- const resolvedSchema = await resolveSchemaSelector(schemasApi, schema);
851
-
852
- const data = await api.deleteRecords(resolvedSchema, recordIds);
853
-
854
- if (json) {
855
- emitJson(stdout, "records delete", data, filePath);
856
- return;
857
- }
858
-
859
- writeLine(
860
- stdout,
861
- `Deleted ${recordIds.length} record(s) from schema "${schema.value}".`,
862
- );
863
- });
864
-
865
- records
866
- .command("clear")
867
- .description("Delete all records from a schema's table")
868
- .argument("<schema>", "schema uuid or slug", parseUuidOrSlugSelector)
869
- .action(async function action(this: Command, schema: NormalizedUuidOrSlug) {
870
- const { api, schemasApi, json, readOnly } = createRecordsApi(this, filePath);
871
- ensureWritable("records clear", readOnly, "destructive");
872
- const resolvedSchema = await resolveSchemaSelector(schemasApi, schema);
873
-
874
- const data = await api.clearRecords(resolvedSchema);
875
-
876
- if (json) {
877
- emitJson(stdout, "records clear", data, filePath);
878
- return;
879
- }
880
-
881
- writeLine(stdout, `Cleared all records from schema "${schema.value}".`);
882
- });
883
- }