zinkee 0.1.23 → 0.1.25
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/.claude/settings.local.json +5 -1
- package/AGENTS.md +1 -0
- package/README.md +82 -4
- package/dist/index.js +1238 -404
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/api/automations.ts +1 -1
- package/src/api/displays.ts +48 -0
- package/src/api/document-templates.ts +110 -0
- package/src/api/navigation.ts +0 -1
- package/src/command-registry.ts +91 -0
- package/src/commands/automations.test.ts +32 -0
- package/src/commands/automations.ts +9 -3
- package/src/commands/displays.test.ts +74 -0
- package/src/commands/displays.ts +143 -1
- package/src/commands/document-templates.test.ts +569 -0
- package/src/commands/document-templates.ts +563 -0
- package/src/commands/navigation.test.ts +2 -5
- package/src/commands/navigation.ts +11 -11
- package/src/commands/teamspace.ts +19 -13
- package/src/program.ts +2 -0
- package/src/utils/examples.test.ts +13 -4
- package/src/utils/examples.ts +280 -50
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
import type { Writable } from "node:stream";
|
|
4
|
+
|
|
5
|
+
import { Command, InvalidArgumentError } from "commander";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
DocumentTemplatesApi,
|
|
9
|
+
type ApiDocumentTemplate,
|
|
10
|
+
type CreateDocumentTemplateInput,
|
|
11
|
+
type UpdateDocumentTemplateInput,
|
|
12
|
+
} from "../api/document-templates.js";
|
|
13
|
+
import { ApiClient } from "../client.js";
|
|
14
|
+
import { defaultConfigPath, readRuntimeConfig } from "../config.js";
|
|
15
|
+
import {
|
|
16
|
+
normalizeUuidOrSlug,
|
|
17
|
+
type NormalizedUuidOrSlug,
|
|
18
|
+
} from "../parsers/selectors.js";
|
|
19
|
+
import { resolveRuntimeContext } from "../runtime-context.js";
|
|
20
|
+
import type { JsonObject, RuntimeOverrides } from "../types.js";
|
|
21
|
+
import { CliError } from "../utils/errors.js";
|
|
22
|
+
import { buildJsonSuccess, printTable } from "../utils/output.js";
|
|
23
|
+
|
|
24
|
+
interface CommandIo {
|
|
25
|
+
write(chunk: string | Uint8Array): unknown;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface RegisterDocumentTemplatesCommandsOptions {
|
|
29
|
+
configPath?: string;
|
|
30
|
+
stdout?: CommandIo;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface GlobalOptions extends RuntimeOverrides {}
|
|
34
|
+
|
|
35
|
+
interface TemplateCreateCommandOptions {
|
|
36
|
+
name?: string;
|
|
37
|
+
raw?: string;
|
|
38
|
+
rawFile?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface TemplateUpdateCommandOptions {
|
|
42
|
+
name?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface AssetAddCommandOptions {
|
|
46
|
+
name?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface AssetDownloadCommandOptions {
|
|
50
|
+
output?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getCommandIo(stdout: CommandIo | undefined): CommandIo {
|
|
54
|
+
return stdout ?? (process.stdout as Writable);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function writeLine(stdout: CommandIo, value: string): void {
|
|
58
|
+
stdout.write(`${value}\n`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function writeBinary(stdout: CommandIo, value: ArrayBuffer): void {
|
|
62
|
+
stdout.write(Buffer.from(value));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getGlobalOptions(command: Command): GlobalOptions {
|
|
66
|
+
return command.optsWithGlobals<GlobalOptions>();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function emitJson(
|
|
70
|
+
stdout: CommandIo,
|
|
71
|
+
commandName: string,
|
|
72
|
+
data: unknown,
|
|
73
|
+
configPath: string,
|
|
74
|
+
meta: Record<string, unknown> = {},
|
|
75
|
+
): void {
|
|
76
|
+
writeLine(
|
|
77
|
+
stdout,
|
|
78
|
+
JSON.stringify(buildJsonSuccess(data, { command: commandName, configPath, ...meta })),
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function toConfigurationError(filePath: string, error: unknown): never {
|
|
83
|
+
if (error instanceof CliError) {
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (
|
|
88
|
+
typeof error === "object" &&
|
|
89
|
+
error !== null &&
|
|
90
|
+
"code" in error &&
|
|
91
|
+
error.code === "ENOENT"
|
|
92
|
+
) {
|
|
93
|
+
throw new CliError("configuration_error", `Config file not found at ${filePath}.`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (error instanceof Error) {
|
|
97
|
+
throw new CliError("configuration_error", error.message);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function createDocumentTemplatesApi(
|
|
104
|
+
command: Command,
|
|
105
|
+
filePath: string,
|
|
106
|
+
): { api: DocumentTemplatesApi; json: boolean; readOnly: boolean } {
|
|
107
|
+
try {
|
|
108
|
+
const config = readRuntimeConfig(filePath);
|
|
109
|
+
const runtime = resolveRuntimeContext(config, getGlobalOptions(command));
|
|
110
|
+
|
|
111
|
+
if (!runtime.baseUrl) {
|
|
112
|
+
throw new CliError(
|
|
113
|
+
"configuration_error",
|
|
114
|
+
`Resolved profile "${runtime.profile}" is missing a base URL.`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
api: new DocumentTemplatesApi(
|
|
120
|
+
new ApiClient({
|
|
121
|
+
baseUrl: runtime.baseUrl,
|
|
122
|
+
apiKey: runtime.apiKey,
|
|
123
|
+
}),
|
|
124
|
+
),
|
|
125
|
+
json: runtime.json,
|
|
126
|
+
readOnly: runtime.readOnly,
|
|
127
|
+
};
|
|
128
|
+
} catch (error) {
|
|
129
|
+
toConfigurationError(filePath, error);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function ensureWritable(
|
|
134
|
+
commandName: string,
|
|
135
|
+
readOnly: boolean,
|
|
136
|
+
commandMode: "write" | "destructive",
|
|
137
|
+
): void {
|
|
138
|
+
if (readOnly) {
|
|
139
|
+
throw new CliError(
|
|
140
|
+
"command_not_allowed_in_read_only_mode",
|
|
141
|
+
`Command "${commandName}" is not allowed in read-only mode.`,
|
|
142
|
+
{
|
|
143
|
+
commandMode,
|
|
144
|
+
executionMode: "read-only",
|
|
145
|
+
},
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function parseUuidOrSlugSelector(input: string): NormalizedUuidOrSlug {
|
|
151
|
+
try {
|
|
152
|
+
return normalizeUuidOrSlug(input);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
const message = error instanceof Error ? error.message : "Invalid selector.";
|
|
155
|
+
throw new InvalidArgumentError(message);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function isJsonObject(value: unknown): value is JsonObject {
|
|
160
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function isEmptyObject(value: object): boolean {
|
|
164
|
+
return Object.keys(value).length === 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function parseJsonValue(input: string, sourceLabel: string): unknown {
|
|
168
|
+
try {
|
|
169
|
+
return JSON.parse(input);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
const message = error instanceof Error ? error.message : "Invalid JSON.";
|
|
172
|
+
throw new CliError(
|
|
173
|
+
"invalid_cli_usage",
|
|
174
|
+
`Failed to parse JSON from ${sourceLabel}: ${message}`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function readTextFile(filePath: string, label: string): string {
|
|
180
|
+
try {
|
|
181
|
+
return readFileSync(filePath, "utf8");
|
|
182
|
+
} catch (error) {
|
|
183
|
+
const message = error instanceof Error ? error.message : "Unable to read file.";
|
|
184
|
+
throw new CliError(
|
|
185
|
+
"invalid_cli_usage",
|
|
186
|
+
`Failed to read ${label} from ${filePath}: ${message}`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function readJsonObjectOption(
|
|
192
|
+
raw: string | undefined,
|
|
193
|
+
rawFile: string | undefined,
|
|
194
|
+
rawLabel: string,
|
|
195
|
+
rawFileLabel: string,
|
|
196
|
+
): JsonObject {
|
|
197
|
+
if (raw && rawFile) {
|
|
198
|
+
throw new CliError(
|
|
199
|
+
"invalid_cli_usage",
|
|
200
|
+
`Provide either "${rawLabel}" or "${rawFileLabel}", not both.`,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
let parsed: unknown;
|
|
205
|
+
|
|
206
|
+
if (raw) {
|
|
207
|
+
parsed = parseJsonValue(raw, rawLabel);
|
|
208
|
+
} else if (rawFile) {
|
|
209
|
+
parsed = parseJsonValue(readTextFile(rawFile, rawFileLabel), `${rawFileLabel} ${rawFile}`);
|
|
210
|
+
} else {
|
|
211
|
+
return {};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!isJsonObject(parsed)) {
|
|
215
|
+
throw new CliError(
|
|
216
|
+
"invalid_cli_usage",
|
|
217
|
+
`Expected ${raw ? rawLabel : rawFileLabel} to contain a JSON object.`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return parsed;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function readLocalFile(filePath: string): Buffer {
|
|
225
|
+
try {
|
|
226
|
+
return readFileSync(filePath);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
const message = error instanceof Error ? error.message : "Unable to read file.";
|
|
229
|
+
throw new CliError("invalid_cli_usage", `Failed to read file at ${filePath}: ${message}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function writeDownloadedFile(outputPath: string, value: ArrayBuffer): void {
|
|
234
|
+
try {
|
|
235
|
+
writeFileSync(outputPath, Buffer.from(value));
|
|
236
|
+
} catch (error) {
|
|
237
|
+
const message = error instanceof Error ? error.message : "Unable to write file.";
|
|
238
|
+
throw new CliError(
|
|
239
|
+
"invalid_cli_usage",
|
|
240
|
+
`Failed to write downloaded file to ${outputPath}: ${message}`,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function templateRows(templates: readonly ApiDocumentTemplate[]): Record<string, unknown>[] {
|
|
246
|
+
return templates.map((template) => ({
|
|
247
|
+
id: template.id ?? "",
|
|
248
|
+
name: template.name ?? "",
|
|
249
|
+
assetCount: Array.isArray(template.assets) ? template.assets.length : 0,
|
|
250
|
+
updatedAt: template.updatedAt ?? "",
|
|
251
|
+
}));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function resolveTemplate(
|
|
255
|
+
templates: readonly ApiDocumentTemplate[],
|
|
256
|
+
selector: NormalizedUuidOrSlug,
|
|
257
|
+
): ApiDocumentTemplate {
|
|
258
|
+
if (selector.kind === "uuid") {
|
|
259
|
+
const match = templates.find((template) => template.id === selector.value);
|
|
260
|
+
|
|
261
|
+
if (match) {
|
|
262
|
+
return match;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
throw new CliError(
|
|
266
|
+
"invalid_cli_usage",
|
|
267
|
+
`Document template "${selector.value}" was not found.`,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const matches = templates.filter((template) => template.name === selector.value);
|
|
272
|
+
|
|
273
|
+
if (matches.length === 1) {
|
|
274
|
+
return matches[0] as ApiDocumentTemplate;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (matches.length > 1) {
|
|
278
|
+
throw new CliError(
|
|
279
|
+
"invalid_cli_usage",
|
|
280
|
+
`Document template "${selector.value}" is ambiguous. Found ${matches.length} matches.`,
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
throw new CliError(
|
|
285
|
+
"invalid_cli_usage",
|
|
286
|
+
`Document template "${selector.value}" was not found.`,
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function resolveTemplateSelector(
|
|
291
|
+
api: DocumentTemplatesApi,
|
|
292
|
+
selector: NormalizedUuidOrSlug,
|
|
293
|
+
): Promise<NormalizedUuidOrSlug> {
|
|
294
|
+
if (selector.kind === "uuid") {
|
|
295
|
+
return selector;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const templates = await api.listTemplates();
|
|
299
|
+
const resolved = resolveTemplate(templates, selector);
|
|
300
|
+
|
|
301
|
+
if (typeof resolved.id !== "string" || resolved.id.length === 0) {
|
|
302
|
+
throw new CliError(
|
|
303
|
+
"invalid_cli_usage",
|
|
304
|
+
`Document template "${selector.value}" is missing an id in the API response.`,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return { kind: "uuid", value: resolved.id };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function buildCreateInput(
|
|
312
|
+
options: TemplateCreateCommandOptions,
|
|
313
|
+
): CreateDocumentTemplateInput {
|
|
314
|
+
const raw = readJsonObjectOption(options.raw, options.rawFile, "--raw", "--raw-file");
|
|
315
|
+
const input: JsonObject = { ...raw };
|
|
316
|
+
|
|
317
|
+
if (options.name !== undefined) {
|
|
318
|
+
input.name = options.name;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (typeof input.name !== "string" || input.name.trim().length === 0) {
|
|
322
|
+
throw new CliError("invalid_cli_usage", 'Provide template name via "--name".');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (input.assets === undefined) {
|
|
326
|
+
input.assets = [];
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return input as unknown as CreateDocumentTemplateInput;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function buildUpdateInput(
|
|
333
|
+
options: TemplateUpdateCommandOptions,
|
|
334
|
+
): UpdateDocumentTemplateInput {
|
|
335
|
+
const input: UpdateDocumentTemplateInput = {};
|
|
336
|
+
|
|
337
|
+
if (options.name !== undefined) {
|
|
338
|
+
input.name = options.name;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (isEmptyObject(input as object)) {
|
|
342
|
+
throw new CliError("invalid_cli_usage", 'Provide template updates via "--name".');
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return input;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function registerDocumentTemplatesCommands(
|
|
349
|
+
program: Command,
|
|
350
|
+
options: RegisterDocumentTemplatesCommandsOptions = {},
|
|
351
|
+
): void {
|
|
352
|
+
const filePath = options.configPath ?? defaultConfigPath;
|
|
353
|
+
const stdout = getCommandIo(options.stdout);
|
|
354
|
+
const templates = program
|
|
355
|
+
.command("document-templates")
|
|
356
|
+
.description("Manage document templates and their assets");
|
|
357
|
+
|
|
358
|
+
templates
|
|
359
|
+
.command("list")
|
|
360
|
+
.description("List document templates")
|
|
361
|
+
.action(async function action(this: Command) {
|
|
362
|
+
const { api, json } = createDocumentTemplatesApi(this, filePath);
|
|
363
|
+
const data = await api.listTemplates();
|
|
364
|
+
|
|
365
|
+
if (json) {
|
|
366
|
+
emitJson(stdout, "document-templates list", data, filePath);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
printTable(templateRows(data), ["id", "name", "assetCount", "updatedAt"], stdout);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
templates
|
|
374
|
+
.command("get")
|
|
375
|
+
.description("Get a document template by UUID or exact name")
|
|
376
|
+
.argument("<template>", "template UUID or exact name", parseUuidOrSlugSelector)
|
|
377
|
+
.action(async function action(this: Command, template: NormalizedUuidOrSlug) {
|
|
378
|
+
const { api, json } = createDocumentTemplatesApi(this, filePath);
|
|
379
|
+
const list = await api.listTemplates();
|
|
380
|
+
const data = resolveTemplate(list, template);
|
|
381
|
+
|
|
382
|
+
if (json) {
|
|
383
|
+
emitJson(stdout, "document-templates get", data, filePath);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
printTable([data as Record<string, unknown>], undefined, stdout);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
templates
|
|
391
|
+
.command("create")
|
|
392
|
+
.description("Create a document template")
|
|
393
|
+
.option("--name <name>", "template name")
|
|
394
|
+
.option("--raw <json>", "raw create JSON payload")
|
|
395
|
+
.option("--raw-file <path>", "path to raw create JSON payload")
|
|
396
|
+
.action(async function action(
|
|
397
|
+
this: Command,
|
|
398
|
+
commandOptions: TemplateCreateCommandOptions,
|
|
399
|
+
) {
|
|
400
|
+
const { api, json, readOnly } = createDocumentTemplatesApi(this, filePath);
|
|
401
|
+
ensureWritable("document-templates create", readOnly, "write");
|
|
402
|
+
const data = await api.createTemplate(buildCreateInput(commandOptions));
|
|
403
|
+
|
|
404
|
+
if (json) {
|
|
405
|
+
emitJson(stdout, "document-templates create", data, filePath);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
writeLine(stdout, `Created document template "${String(data.name ?? commandOptions.name)}".`);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
templates
|
|
413
|
+
.command("update")
|
|
414
|
+
.description("Rename a document template")
|
|
415
|
+
.argument("<template>", "template UUID or exact name", parseUuidOrSlugSelector)
|
|
416
|
+
.option("--name <name>", "new template name")
|
|
417
|
+
.action(async function action(
|
|
418
|
+
this: Command,
|
|
419
|
+
template: NormalizedUuidOrSlug,
|
|
420
|
+
commandOptions: TemplateUpdateCommandOptions,
|
|
421
|
+
) {
|
|
422
|
+
const { api, json, readOnly } = createDocumentTemplatesApi(this, filePath);
|
|
423
|
+
ensureWritable("document-templates update", readOnly, "write");
|
|
424
|
+
const resolved = await resolveTemplateSelector(api, template);
|
|
425
|
+
const data = await api.updateTemplate(resolved, buildUpdateInput(commandOptions));
|
|
426
|
+
|
|
427
|
+
if (json) {
|
|
428
|
+
emitJson(stdout, "document-templates update", data, filePath);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
writeLine(stdout, `Updated document template "${String(data.name ?? template.value)}".`);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
templates
|
|
436
|
+
.command("delete")
|
|
437
|
+
.description("Delete a document template (and its assets)")
|
|
438
|
+
.argument("<template>", "template UUID or exact name", parseUuidOrSlugSelector)
|
|
439
|
+
.action(async function action(this: Command, template: NormalizedUuidOrSlug) {
|
|
440
|
+
const { api, json, readOnly } = createDocumentTemplatesApi(this, filePath);
|
|
441
|
+
ensureWritable("document-templates delete", readOnly, "destructive");
|
|
442
|
+
const resolved = await resolveTemplateSelector(api, template);
|
|
443
|
+
const data = await api.deleteTemplate(resolved);
|
|
444
|
+
|
|
445
|
+
if (json) {
|
|
446
|
+
emitJson(stdout, "document-templates delete", data, filePath);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
writeLine(stdout, `Deleted document template "${template.value}".`);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
const assets = templates.command("assets").description("Manage document template assets");
|
|
454
|
+
|
|
455
|
+
assets
|
|
456
|
+
.command("add")
|
|
457
|
+
.description("Upload an asset file to a document template")
|
|
458
|
+
.argument("<template>", "template UUID or exact name", parseUuidOrSlugSelector)
|
|
459
|
+
.argument("<path>", "path to the local file")
|
|
460
|
+
.option("--name <name>", "override the asset display name")
|
|
461
|
+
.action(async function action(
|
|
462
|
+
this: Command,
|
|
463
|
+
template: NormalizedUuidOrSlug,
|
|
464
|
+
localPath: string,
|
|
465
|
+
commandOptions: AssetAddCommandOptions,
|
|
466
|
+
) {
|
|
467
|
+
const { api, json, readOnly } = createDocumentTemplatesApi(this, filePath);
|
|
468
|
+
ensureWritable("document-templates assets add", readOnly, "write");
|
|
469
|
+
|
|
470
|
+
const contents = readLocalFile(localPath);
|
|
471
|
+
const formData = new FormData();
|
|
472
|
+
formData.append("file", new Blob([contents]), basename(localPath));
|
|
473
|
+
|
|
474
|
+
if (commandOptions.name !== undefined) {
|
|
475
|
+
formData.append("name", commandOptions.name);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const resolved = await resolveTemplateSelector(api, template);
|
|
479
|
+
const data = await api.addAsset(resolved, formData);
|
|
480
|
+
|
|
481
|
+
if (json) {
|
|
482
|
+
emitJson(stdout, "document-templates assets add", data, filePath);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
writeLine(
|
|
487
|
+
stdout,
|
|
488
|
+
`Added asset "${commandOptions.name ?? basename(localPath)}" to template "${template.value}".`,
|
|
489
|
+
);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
assets
|
|
493
|
+
.command("delete")
|
|
494
|
+
.description("Delete one asset from a document template")
|
|
495
|
+
.argument("<template>", "template UUID or exact name", parseUuidOrSlugSelector)
|
|
496
|
+
.argument("<asset>", "asset UUID")
|
|
497
|
+
.action(async function action(
|
|
498
|
+
this: Command,
|
|
499
|
+
template: NormalizedUuidOrSlug,
|
|
500
|
+
assetId: string,
|
|
501
|
+
) {
|
|
502
|
+
const { api, json, readOnly } = createDocumentTemplatesApi(this, filePath);
|
|
503
|
+
ensureWritable("document-templates assets delete", readOnly, "destructive");
|
|
504
|
+
const resolved = await resolveTemplateSelector(api, template);
|
|
505
|
+
const data = await api.deleteAsset(resolved, assetId);
|
|
506
|
+
|
|
507
|
+
if (json) {
|
|
508
|
+
emitJson(stdout, "document-templates assets delete", data, filePath);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
writeLine(stdout, `Deleted asset "${assetId}" from template "${template.value}".`);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
assets
|
|
516
|
+
.command("download")
|
|
517
|
+
.description("Download the raw bytes of a document template asset")
|
|
518
|
+
.argument("<template>", "template UUID or exact name", parseUuidOrSlugSelector)
|
|
519
|
+
.argument("<asset>", "asset UUID")
|
|
520
|
+
.option("--output <path>", "write the downloaded bytes to a path")
|
|
521
|
+
.action(async function action(
|
|
522
|
+
this: Command,
|
|
523
|
+
template: NormalizedUuidOrSlug,
|
|
524
|
+
assetId: string,
|
|
525
|
+
commandOptions: AssetDownloadCommandOptions,
|
|
526
|
+
) {
|
|
527
|
+
const { api, json } = createDocumentTemplatesApi(this, filePath);
|
|
528
|
+
|
|
529
|
+
if (json && !commandOptions.output) {
|
|
530
|
+
throw new CliError(
|
|
531
|
+
"invalid_cli_usage",
|
|
532
|
+
'Provide "--output" when using "--json" with "document-templates assets download".',
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const resolved = await resolveTemplateSelector(api, template);
|
|
537
|
+
const data = await api.downloadAsset(resolved, assetId);
|
|
538
|
+
|
|
539
|
+
if (commandOptions.output) {
|
|
540
|
+
writeDownloadedFile(commandOptions.output, data);
|
|
541
|
+
|
|
542
|
+
if (json) {
|
|
543
|
+
emitJson(
|
|
544
|
+
stdout,
|
|
545
|
+
"document-templates assets download",
|
|
546
|
+
{
|
|
547
|
+
template: resolved.value,
|
|
548
|
+
asset: assetId,
|
|
549
|
+
output: commandOptions.output,
|
|
550
|
+
bytes: data.byteLength,
|
|
551
|
+
},
|
|
552
|
+
filePath,
|
|
553
|
+
);
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
writeLine(stdout, `Downloaded asset to "${commandOptions.output}".`);
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
writeBinary(stdout, data);
|
|
562
|
+
});
|
|
563
|
+
}
|
|
@@ -157,7 +157,7 @@ describe("navigation commands", () => {
|
|
|
157
157
|
).rejects.toThrowError('Folder "Finance" is ambiguous. Found 2 matches.');
|
|
158
158
|
});
|
|
159
159
|
|
|
160
|
-
it("shapes move payloads using the
|
|
160
|
+
it("shapes move payloads using the resolved folder id (schemas only, no resource type)", async () => {
|
|
161
161
|
const configPath = createApiConfig();
|
|
162
162
|
const output = createStdoutBuffer();
|
|
163
163
|
const fetchMock = createFetchMock();
|
|
@@ -171,7 +171,7 @@ describe("navigation commands", () => {
|
|
|
171
171
|
{
|
|
172
172
|
id: "resource-1",
|
|
173
173
|
name: "Invoices",
|
|
174
|
-
type: "
|
|
174
|
+
type: "SCHEMA",
|
|
175
175
|
isInSpace: false,
|
|
176
176
|
},
|
|
177
177
|
],
|
|
@@ -189,8 +189,6 @@ describe("navigation commands", () => {
|
|
|
189
189
|
"resources",
|
|
190
190
|
"move",
|
|
191
191
|
"resource-1",
|
|
192
|
-
"--type",
|
|
193
|
-
"display",
|
|
194
192
|
"--to",
|
|
195
193
|
"Finance",
|
|
196
194
|
]);
|
|
@@ -207,7 +205,6 @@ describe("navigation commands", () => {
|
|
|
207
205
|
expect(moveInit?.method).toBe("POST");
|
|
208
206
|
expect(JSON.parse(String(moveInit?.body))).toEqual({
|
|
209
207
|
resourceId: "resource-1",
|
|
210
|
-
resourceType: "DISPLAY",
|
|
211
208
|
targetFolderId: "aaaabbbb-cccc-dddd-eeee-ffffffffffff",
|
|
212
209
|
});
|
|
213
210
|
});
|
|
@@ -251,9 +251,9 @@ export function registerNavigationCommands(
|
|
|
251
251
|
.command("create")
|
|
252
252
|
.description("Create a navigation folder")
|
|
253
253
|
.requiredOption("--name <name>", "folder name")
|
|
254
|
-
.option("--order <n>", "
|
|
255
|
-
.option("--props <json>", "
|
|
256
|
-
.option("--props-file <path>", "
|
|
254
|
+
.option("--order <n>", "display order (lower values appear first)")
|
|
255
|
+
.option("--props <json>", "additionalProperties JSON: free-form client metadata, e.g. {\"color\":\"blue\",\"icon\":\"chart-line\"}")
|
|
256
|
+
.option("--props-file <path>", "path to a file with the additionalProperties JSON")
|
|
257
257
|
.action(async function action(this: Command, commandOptions: FolderMutationOptions) {
|
|
258
258
|
const { api, json, readOnly } = createNavigationApi(this, filePath);
|
|
259
259
|
ensureWritable("navigation folders create", readOnly, "write");
|
|
@@ -272,9 +272,9 @@ export function registerNavigationCommands(
|
|
|
272
272
|
.description("Update a navigation folder")
|
|
273
273
|
.argument("<folder>", "folder UUID or exact name", parseUuidOrSlugSelector)
|
|
274
274
|
.option("--name <name>", "folder name")
|
|
275
|
-
.option("--order <n>", "
|
|
276
|
-
.option("--props <json>", "
|
|
277
|
-
.option("--props-file <path>", "
|
|
275
|
+
.option("--order <n>", "display order (lower values appear first)")
|
|
276
|
+
.option("--props <json>", "additionalProperties JSON: free-form client metadata, e.g. {\"color\":\"blue\",\"icon\":\"chart-line\"}")
|
|
277
|
+
.option("--props-file <path>", "path to a file with the additionalProperties JSON")
|
|
278
278
|
.action(async function action(this: Command, folder: NormalizedUuidOrSlug, commandOptions: FolderMutationOptions) {
|
|
279
279
|
const { api, json, readOnly } = createNavigationApi(this, filePath);
|
|
280
280
|
ensureWritable("navigation folders update", readOnly, "write");
|
|
@@ -319,14 +319,15 @@ export function registerNavigationCommands(
|
|
|
319
319
|
.command("resources")
|
|
320
320
|
.description("Manage navigation resources")
|
|
321
321
|
.command("move")
|
|
322
|
-
.description(
|
|
323
|
-
|
|
324
|
-
|
|
322
|
+
.description(
|
|
323
|
+
"Move a schema into a navigation folder. Only schemas can be moved (the navigation menu holds databases); schedules and sub-schemas follow their parent schema. Use the root folder UUID 00000000-0000-0000-0000-000000000000 to move it back to the workspace root.",
|
|
324
|
+
)
|
|
325
|
+
.argument("<resource>", "schema UUID")
|
|
325
326
|
.requiredOption("--to <folder>", "target folder UUID or exact name")
|
|
326
327
|
.action(async function action(
|
|
327
328
|
this: Command,
|
|
328
329
|
resourceId: string,
|
|
329
|
-
commandOptions: {
|
|
330
|
+
commandOptions: { to: string },
|
|
330
331
|
) {
|
|
331
332
|
const { api, json, readOnly } = createNavigationApi(this, filePath);
|
|
332
333
|
ensureWritable("navigation resources move", readOnly, "write");
|
|
@@ -334,7 +335,6 @@ export function registerNavigationCommands(
|
|
|
334
335
|
const resolved = resolveFolder(allFolders, parseUuidOrSlugSelector(commandOptions.to));
|
|
335
336
|
const data = await api.moveResource({
|
|
336
337
|
resourceId,
|
|
337
|
-
resourceType: commandOptions.type.toUpperCase(),
|
|
338
338
|
targetFolderId: resolved.id,
|
|
339
339
|
});
|
|
340
340
|
|