zinkee 0.1.44 → 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.
- package/README.md +4 -0
- package/dist/chunk-MHEVRMDX.js +1230 -0
- package/dist/chunk-MHEVRMDX.js.map +1 -0
- package/{src/utils/examples.ts → dist/examples-E6FFEVBE.js} +856 -1141
- package/dist/examples-E6FFEVBE.js.map +1 -0
- package/dist/index.js +1145 -8456
- package/dist/index.js.map +1 -1
- package/package.json +6 -3
- package/.github/workflows/npm-publish.yml +0 -77
- package/.github/workflows/pr-checks.yml +0 -36
- package/AGENTS.md +0 -110
- package/docs/cli-contract.md +0 -41
- package/docs/npm-release.md +0 -64
- package/docs/superpowers/plans/2026-03-24-zinkee-cli-implementation.md +0 -837
- package/docs/superpowers/plans/2026-03-25-cli-backend-error-contract.md +0 -503
- package/docs/superpowers/plans/2026-03-26-display-freeform-create.md +0 -389
- package/docs/superpowers/specs/2026-03-24-zinkee-cli-backend-blockers.md +0 -172
- package/docs/superpowers/specs/2026-03-24-zinkee-cli-design.md +0 -1576
- package/docs/superpowers/specs/2026-03-24-zinkee-cli-e2e-checklist.md +0 -215
- package/docs/superpowers/specs/2026-03-24-zinkee-cli-e2e-design.md +0 -492
- package/docs/superpowers/specs/2026-03-24-zinkee-cli-e2e-status.md +0 -307
- package/docs/superpowers/specs/2026-07-30-cli-pr-checks-design.md +0 -37
- package/src/api/automations.ts +0 -404
- package/src/api/comments.ts +0 -51
- package/src/api/displays.ts +0 -337
- package/src/api/document-templates.ts +0 -110
- package/src/api/files.ts +0 -50
- package/src/api/formulas.test.ts +0 -65
- package/src/api/formulas.ts +0 -67
- package/src/api/logs.ts +0 -34
- package/src/api/navigation.ts +0 -70
- package/src/api/records.ts +0 -184
- package/src/api/schemas.ts +0 -148
- package/src/api/teamspace.ts +0 -110
- package/src/cli-examples.ts +0 -130
- package/src/cli-runner.ts +0 -95
- package/src/client.test.ts +0 -189
- package/src/client.ts +0 -269
- package/src/command-registry.ts +0 -882
- package/src/commands/automations.test.ts +0 -1030
- package/src/commands/automations.ts +0 -2102
- package/src/commands/comments.test.ts +0 -214
- package/src/commands/comments.ts +0 -303
- package/src/commands/config.test.ts +0 -81
- package/src/commands/config.ts +0 -150
- package/src/commands/displays.test.ts +0 -1105
- package/src/commands/displays.ts +0 -1442
- package/src/commands/document-templates.test.ts +0 -569
- package/src/commands/document-templates.ts +0 -563
- package/src/commands/files.test.ts +0 -284
- package/src/commands/files.ts +0 -280
- package/src/commands/formulas.test.ts +0 -194
- package/src/commands/formulas.ts +0 -243
- package/src/commands/logs.test.ts +0 -123
- package/src/commands/logs.ts +0 -159
- package/src/commands/navigation.test.ts +0 -211
- package/src/commands/navigation.ts +0 -348
- package/src/commands/profiles.test.ts +0 -191
- package/src/commands/profiles.ts +0 -303
- package/src/commands/records.test.ts +0 -860
- package/src/commands/records.ts +0 -883
- package/src/commands/schemas.test.ts +0 -1252
- package/src/commands/schemas.ts +0 -890
- package/src/commands/teamspace.test.ts +0 -229
- package/src/commands/teamspace.ts +0 -546
- package/src/completion/engine.test.ts +0 -138
- package/src/completion/engine.ts +0 -168
- package/src/completion/install.test.ts +0 -179
- package/src/completion/install.ts +0 -260
- package/src/completion/runtime.ts +0 -150
- package/src/completion/scripts.ts +0 -91
- package/src/config.test.ts +0 -362
- package/src/config.ts +0 -294
- package/src/index.test.ts +0 -217
- package/src/index.ts +0 -8
- package/src/parsers/expressions.test.ts +0 -95
- package/src/parsers/expressions.ts +0 -128
- package/src/parsers/kv.test.ts +0 -35
- package/src/parsers/kv.ts +0 -49
- package/src/parsers/selectors.test.ts +0 -23
- package/src/parsers/selectors.ts +0 -29
- package/src/program.ts +0 -64
- package/src/runtime-context.ts +0 -103
- package/src/types.ts +0 -76
- package/src/utils/argv-rewrite.test.ts +0 -149
- package/src/utils/argv-rewrite.ts +0 -269
- package/src/utils/errors.test.ts +0 -85
- package/src/utils/errors.ts +0 -191
- package/src/utils/examples.test.ts +0 -374
- package/src/utils/output.test.ts +0 -65
- package/src/utils/output.ts +0 -154
- package/src/utils/schema-fields.ts +0 -1016
- package/tsconfig.json +0 -20
- package/tsup.config.ts +0 -13
package/src/commands/records.ts
DELETED
|
@@ -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
|
-
}
|