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
package/package.json
CHANGED
package/src/api/automations.ts
CHANGED
|
@@ -180,7 +180,7 @@ export class AutomationsApi {
|
|
|
180
180
|
|
|
181
181
|
moveAutomation(input: MoveAutomationInput): Promise<Record<string, unknown>> {
|
|
182
182
|
return this.client.requestJson<Record<string, unknown>>(
|
|
183
|
-
|
|
183
|
+
"/api/v2/automation-folders/automations:move",
|
|
184
184
|
jsonOptions(input, "POST"),
|
|
185
185
|
);
|
|
186
186
|
}
|
package/src/api/displays.ts
CHANGED
|
@@ -45,6 +45,19 @@ export interface ApiDisplayNavigationTarget {
|
|
|
45
45
|
label?: string;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
export interface ApiDisplayFolder {
|
|
49
|
+
[key: string]: unknown;
|
|
50
|
+
id?: string;
|
|
51
|
+
name?: string | null;
|
|
52
|
+
isRoot?: boolean;
|
|
53
|
+
displayCount?: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface MoveDisplayInput {
|
|
57
|
+
displayId: string;
|
|
58
|
+
targetFolderId: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
48
61
|
export type DisplaySummary = ApiDisplay;
|
|
49
62
|
export type DisplayResource = ApiDisplay;
|
|
50
63
|
|
|
@@ -273,4 +286,39 @@ export class DisplaysApi {
|
|
|
273
286
|
{ method: "DELETE" },
|
|
274
287
|
);
|
|
275
288
|
}
|
|
289
|
+
|
|
290
|
+
listFolders(): Promise<ApiDisplayFolder[]> {
|
|
291
|
+
return this.client.getJson<ApiDisplayFolder[]>("/api/v2/display-folders");
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
createFolder(body: Record<string, unknown>): Promise<ApiDisplayFolder> {
|
|
295
|
+
return this.client.requestJson<ApiDisplayFolder>(
|
|
296
|
+
"/api/v2/display-folders",
|
|
297
|
+
jsonOptions(body, "POST"),
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
updateFolder(
|
|
302
|
+
folder: NormalizedUuidOrSlug,
|
|
303
|
+
body: Record<string, unknown>,
|
|
304
|
+
): Promise<ApiDisplayFolder> {
|
|
305
|
+
return this.client.requestJson<ApiDisplayFolder>(
|
|
306
|
+
`/api/v2/display-folders/${selectorValue(folder)}`,
|
|
307
|
+
jsonOptions(body, "PATCH"),
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
deleteFolder(folder: NormalizedUuidOrSlug): Promise<Record<string, unknown>> {
|
|
312
|
+
return this.client.requestJson<Record<string, unknown>>(
|
|
313
|
+
`/api/v2/display-folders/${selectorValue(folder)}`,
|
|
314
|
+
{ method: "DELETE" },
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
moveDisplay(input: MoveDisplayInput): Promise<Record<string, unknown>> {
|
|
319
|
+
return this.client.requestJson<Record<string, unknown>>(
|
|
320
|
+
"/api/v2/display-folders/displays:move",
|
|
321
|
+
jsonOptions(input, "POST"),
|
|
322
|
+
);
|
|
323
|
+
}
|
|
276
324
|
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { ApiClient } from "../client.js";
|
|
2
|
+
import type { NormalizedUuidOrSlug } from "../parsers/selectors.js";
|
|
3
|
+
|
|
4
|
+
export interface ApiDocumentTemplateAsset {
|
|
5
|
+
[key: string]: unknown;
|
|
6
|
+
id?: string;
|
|
7
|
+
name?: string;
|
|
8
|
+
link?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ApiDocumentTemplate {
|
|
12
|
+
[key: string]: unknown;
|
|
13
|
+
id?: string;
|
|
14
|
+
name?: string;
|
|
15
|
+
assets?: ApiDocumentTemplateAsset[];
|
|
16
|
+
createdAt?: string;
|
|
17
|
+
updatedAt?: string;
|
|
18
|
+
createdBy?: string;
|
|
19
|
+
updatedBy?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CreateDocumentTemplateInput {
|
|
23
|
+
[key: string]: unknown;
|
|
24
|
+
name: string;
|
|
25
|
+
assets?: ApiDocumentTemplateAsset[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface UpdateDocumentTemplateInput {
|
|
29
|
+
[key: string]: unknown;
|
|
30
|
+
name?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function jsonOptions(body: unknown, method: string): RequestInit {
|
|
34
|
+
return {
|
|
35
|
+
method,
|
|
36
|
+
headers: {
|
|
37
|
+
"content-type": "application/json",
|
|
38
|
+
},
|
|
39
|
+
body: JSON.stringify(body),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function selectorValue(selector: NormalizedUuidOrSlug): string {
|
|
44
|
+
return encodeURIComponent(selector.value);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function segment(value: string): string {
|
|
48
|
+
return encodeURIComponent(value);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class DocumentTemplatesApi {
|
|
52
|
+
constructor(private readonly client: ApiClient) {}
|
|
53
|
+
|
|
54
|
+
listTemplates(): Promise<ApiDocumentTemplate[]> {
|
|
55
|
+
return this.client.getJson<ApiDocumentTemplate[]>("/api/v2/document-templates");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
createTemplate(input: CreateDocumentTemplateInput): Promise<ApiDocumentTemplate> {
|
|
59
|
+
return this.client.requestJson<ApiDocumentTemplate>(
|
|
60
|
+
"/api/v2/document-templates",
|
|
61
|
+
jsonOptions(input, "POST"),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
updateTemplate(
|
|
66
|
+
template: NormalizedUuidOrSlug,
|
|
67
|
+
input: UpdateDocumentTemplateInput,
|
|
68
|
+
): Promise<ApiDocumentTemplate> {
|
|
69
|
+
return this.client.requestJson<ApiDocumentTemplate>(
|
|
70
|
+
`/api/v2/document-templates/${selectorValue(template)}`,
|
|
71
|
+
jsonOptions(input, "PATCH"),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
deleteTemplate(template: NormalizedUuidOrSlug): Promise<Record<string, unknown>> {
|
|
76
|
+
return this.client.requestJson<Record<string, unknown>>(
|
|
77
|
+
`/api/v2/document-templates/${selectorValue(template)}`,
|
|
78
|
+
{ method: "DELETE" },
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
addAsset(
|
|
83
|
+
template: NormalizedUuidOrSlug,
|
|
84
|
+
input: FormData,
|
|
85
|
+
): Promise<ApiDocumentTemplate> {
|
|
86
|
+
return this.client.requestJson<ApiDocumentTemplate>(
|
|
87
|
+
`/api/v2/document-templates/${selectorValue(template)}/assets`,
|
|
88
|
+
{ method: "POST", body: input },
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
deleteAsset(
|
|
93
|
+
template: NormalizedUuidOrSlug,
|
|
94
|
+
assetId: string,
|
|
95
|
+
): Promise<Record<string, unknown>> {
|
|
96
|
+
return this.client.requestJson<Record<string, unknown>>(
|
|
97
|
+
`/api/v2/document-templates/${selectorValue(template)}/assets/${segment(assetId)}`,
|
|
98
|
+
{ method: "DELETE" },
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
downloadAsset(
|
|
103
|
+
template: NormalizedUuidOrSlug,
|
|
104
|
+
assetId: string,
|
|
105
|
+
): Promise<ArrayBuffer> {
|
|
106
|
+
return this.client.requestBinary(
|
|
107
|
+
`/api/v2/document-templates/${selectorValue(template)}/assets/${segment(assetId)}/download`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
package/src/api/navigation.ts
CHANGED
package/src/command-registry.ts
CHANGED
|
@@ -280,6 +280,41 @@ export const commandRegistry = {
|
|
|
280
280
|
supportsRaw: false,
|
|
281
281
|
output: writeOutput,
|
|
282
282
|
},
|
|
283
|
+
"displays folders list": {
|
|
284
|
+
command: "displays folders list",
|
|
285
|
+
accessLevel: "read",
|
|
286
|
+
supportsExample: true,
|
|
287
|
+
supportsRaw: false,
|
|
288
|
+
output: readOutput,
|
|
289
|
+
},
|
|
290
|
+
"displays folders create": {
|
|
291
|
+
command: "displays folders create",
|
|
292
|
+
accessLevel: "write",
|
|
293
|
+
supportsExample: true,
|
|
294
|
+
supportsRaw: false,
|
|
295
|
+
output: writeOutput,
|
|
296
|
+
},
|
|
297
|
+
"displays folders update": {
|
|
298
|
+
command: "displays folders update",
|
|
299
|
+
accessLevel: "write",
|
|
300
|
+
supportsExample: true,
|
|
301
|
+
supportsRaw: false,
|
|
302
|
+
output: writeOutput,
|
|
303
|
+
},
|
|
304
|
+
"displays folders delete": {
|
|
305
|
+
command: "displays folders delete",
|
|
306
|
+
accessLevel: "destructive",
|
|
307
|
+
supportsExample: true,
|
|
308
|
+
supportsRaw: false,
|
|
309
|
+
output: writeOutput,
|
|
310
|
+
},
|
|
311
|
+
"displays folders move": {
|
|
312
|
+
command: "displays folders move",
|
|
313
|
+
accessLevel: "write",
|
|
314
|
+
supportsExample: true,
|
|
315
|
+
supportsRaw: false,
|
|
316
|
+
output: writeOutput,
|
|
317
|
+
},
|
|
283
318
|
"displays layout get": {
|
|
284
319
|
command: "displays layout get",
|
|
285
320
|
accessLevel: "read",
|
|
@@ -441,6 +476,62 @@ export const commandRegistry = {
|
|
|
441
476
|
supportsRaw: false,
|
|
442
477
|
output: writeOutput,
|
|
443
478
|
},
|
|
479
|
+
"document-templates list": {
|
|
480
|
+
command: "document-templates list",
|
|
481
|
+
accessLevel: "read",
|
|
482
|
+
supportsExample: true,
|
|
483
|
+
supportsRaw: false,
|
|
484
|
+
output: readOutput,
|
|
485
|
+
},
|
|
486
|
+
"document-templates get": {
|
|
487
|
+
command: "document-templates get",
|
|
488
|
+
accessLevel: "read",
|
|
489
|
+
supportsExample: true,
|
|
490
|
+
supportsRaw: false,
|
|
491
|
+
output: readOutput,
|
|
492
|
+
},
|
|
493
|
+
"document-templates create": {
|
|
494
|
+
command: "document-templates create",
|
|
495
|
+
accessLevel: "write",
|
|
496
|
+
supportsExample: true,
|
|
497
|
+
supportsRaw: true,
|
|
498
|
+
output: writeOutput,
|
|
499
|
+
},
|
|
500
|
+
"document-templates update": {
|
|
501
|
+
command: "document-templates update",
|
|
502
|
+
accessLevel: "write",
|
|
503
|
+
supportsExample: true,
|
|
504
|
+
supportsRaw: false,
|
|
505
|
+
output: writeOutput,
|
|
506
|
+
},
|
|
507
|
+
"document-templates delete": {
|
|
508
|
+
command: "document-templates delete",
|
|
509
|
+
accessLevel: "destructive",
|
|
510
|
+
supportsExample: true,
|
|
511
|
+
supportsRaw: false,
|
|
512
|
+
output: writeOutput,
|
|
513
|
+
},
|
|
514
|
+
"document-templates assets add": {
|
|
515
|
+
command: "document-templates assets add",
|
|
516
|
+
accessLevel: "write",
|
|
517
|
+
supportsExample: true,
|
|
518
|
+
supportsRaw: false,
|
|
519
|
+
output: writeOutput,
|
|
520
|
+
},
|
|
521
|
+
"document-templates assets delete": {
|
|
522
|
+
command: "document-templates assets delete",
|
|
523
|
+
accessLevel: "destructive",
|
|
524
|
+
supportsExample: true,
|
|
525
|
+
supportsRaw: false,
|
|
526
|
+
output: writeOutput,
|
|
527
|
+
},
|
|
528
|
+
"document-templates assets download": {
|
|
529
|
+
command: "document-templates assets download",
|
|
530
|
+
accessLevel: "read",
|
|
531
|
+
supportsExample: true,
|
|
532
|
+
supportsRaw: false,
|
|
533
|
+
output: "binary",
|
|
534
|
+
},
|
|
444
535
|
"files upload": {
|
|
445
536
|
command: "files upload",
|
|
446
537
|
accessLevel: "write",
|
|
@@ -87,6 +87,38 @@ describe("automations commands", () => {
|
|
|
87
87
|
expect(program.commands.some((command) => command.name() === "automations")).toBe(true);
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
+
it("moves an automation with the folder id in the body, not the path", async () => {
|
|
91
|
+
const configPath = createApiConfig();
|
|
92
|
+
const output = createStdoutBuffer();
|
|
93
|
+
const fetchMock = createFetchMock();
|
|
94
|
+
fetchMock.mockResolvedValueOnce(
|
|
95
|
+
jsonResponse({ id: "folder-1", name: "Ops", isRoot: false }),
|
|
96
|
+
);
|
|
97
|
+
const program = buildAutomationsProgram({ configPath, stdout: output.stdout });
|
|
98
|
+
|
|
99
|
+
await program.parseAsync([
|
|
100
|
+
"node",
|
|
101
|
+
"zinkee",
|
|
102
|
+
"--json",
|
|
103
|
+
"automations",
|
|
104
|
+
"folders",
|
|
105
|
+
"move",
|
|
106
|
+
"11111111-1111-4111-a111-111111111111",
|
|
107
|
+
"--to",
|
|
108
|
+
"22222222-2222-4222-a222-222222222222",
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
expect(fetchMock.mock.calls[0]?.[0]).toBe(
|
|
112
|
+
"https://api.zinkee.test/api/v2/automation-folders/automations:move",
|
|
113
|
+
);
|
|
114
|
+
const init = fetchMock.mock.calls[0]?.[1];
|
|
115
|
+
expect(init?.method).toBe("POST");
|
|
116
|
+
expect(JSON.parse(String(init?.body))).toEqual({
|
|
117
|
+
automationId: "11111111-1111-4111-a111-111111111111",
|
|
118
|
+
targetFolderId: "22222222-2222-4222-a222-222222222222",
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
90
122
|
it("sets trigger payloads through the generic trigger flags", async () => {
|
|
91
123
|
const configPath = createApiConfig();
|
|
92
124
|
const output = createStdoutBuffer();
|
|
@@ -1408,11 +1408,13 @@ export function registerAutomationsCommands(
|
|
|
1408
1408
|
.command("automations")
|
|
1409
1409
|
.description("Manage automations, triggers, actions, and connections");
|
|
1410
1410
|
|
|
1411
|
-
const folders = automations
|
|
1411
|
+
const folders = automations
|
|
1412
|
+
.command("folders")
|
|
1413
|
+
.description("Manage automation folders (flat: a folder only has a name, no order or properties)");
|
|
1412
1414
|
|
|
1413
1415
|
folders
|
|
1414
1416
|
.command("list")
|
|
1415
|
-
.description("List automation folders")
|
|
1417
|
+
.description("List automation folders (non-root folders plus the root bucket, whose name is null)")
|
|
1416
1418
|
.action(async function action(this: Command) {
|
|
1417
1419
|
const { api, json } = createAutomationsApi(this, filePath);
|
|
1418
1420
|
const data = await api.listFolders();
|
|
@@ -1500,7 +1502,11 @@ export function registerAutomationsCommands(
|
|
|
1500
1502
|
.command("move")
|
|
1501
1503
|
.description("Move an automation into a folder")
|
|
1502
1504
|
.argument("<automation>", "automation UUID", parseUuidOrSlugSelector)
|
|
1503
|
-
.requiredOption(
|
|
1505
|
+
.requiredOption(
|
|
1506
|
+
"--to <folder>",
|
|
1507
|
+
"target folder UUID or exact name (use the root folder to move it out of any folder)",
|
|
1508
|
+
parseUuidOrSlugSelector,
|
|
1509
|
+
)
|
|
1504
1510
|
.action(async function action(
|
|
1505
1511
|
this: Command,
|
|
1506
1512
|
automation: NormalizedUuidOrSlug,
|
|
@@ -486,4 +486,78 @@ describe("displays commands", () => {
|
|
|
486
486
|
|
|
487
487
|
expect(fetchMock).not.toHaveBeenCalled();
|
|
488
488
|
});
|
|
489
|
+
|
|
490
|
+
it("creates a display folder with just a name", async () => {
|
|
491
|
+
const configPath = createApiConfig();
|
|
492
|
+
const output = createStdoutBuffer();
|
|
493
|
+
const fetchMock = createFetchMock();
|
|
494
|
+
fetchMock.mockResolvedValueOnce(
|
|
495
|
+
jsonResponse({ id: "folder-1", name: "Executive Dashboards", isRoot: false, displayCount: 0 }),
|
|
496
|
+
);
|
|
497
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
498
|
+
|
|
499
|
+
await program.parseAsync([
|
|
500
|
+
"node",
|
|
501
|
+
"zinkee",
|
|
502
|
+
"--json",
|
|
503
|
+
"displays",
|
|
504
|
+
"folders",
|
|
505
|
+
"create",
|
|
506
|
+
"--name",
|
|
507
|
+
"Executive Dashboards",
|
|
508
|
+
]);
|
|
509
|
+
|
|
510
|
+
expect(fetchMock.mock.calls[0]?.[0]).toBe("https://api.zinkee.test/api/v2/display-folders");
|
|
511
|
+
const init = fetchMock.mock.calls[0]?.[1];
|
|
512
|
+
expect(init?.method).toBe("POST");
|
|
513
|
+
expect(JSON.parse(String(init?.body))).toEqual({ name: "Executive Dashboards" });
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
it("moves a display into a folder resolved by name", async () => {
|
|
517
|
+
const configPath = createApiConfig();
|
|
518
|
+
const output = createStdoutBuffer();
|
|
519
|
+
const fetchMock = createFetchMock();
|
|
520
|
+
fetchMock.mockResolvedValueOnce(
|
|
521
|
+
jsonResponse([{ id: "folder-1", name: "Executive Dashboards", isRoot: false, displayCount: 2 }]),
|
|
522
|
+
);
|
|
523
|
+
fetchMock.mockResolvedValueOnce(
|
|
524
|
+
jsonResponse({ id: "folder-1", name: "Executive Dashboards", isRoot: false }),
|
|
525
|
+
);
|
|
526
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
527
|
+
|
|
528
|
+
await program.parseAsync([
|
|
529
|
+
"node",
|
|
530
|
+
"zinkee",
|
|
531
|
+
"--json",
|
|
532
|
+
"displays",
|
|
533
|
+
"folders",
|
|
534
|
+
"move",
|
|
535
|
+
"0f6f062f-1478-4df2-96a1-c495d037d260",
|
|
536
|
+
"--to",
|
|
537
|
+
"Executive Dashboards",
|
|
538
|
+
]);
|
|
539
|
+
|
|
540
|
+
expect(fetchMock.mock.calls[0]?.[0]).toBe("https://api.zinkee.test/api/v2/display-folders");
|
|
541
|
+
expect(fetchMock.mock.calls[1]?.[0]).toBe(
|
|
542
|
+
"https://api.zinkee.test/api/v2/display-folders/displays:move",
|
|
543
|
+
);
|
|
544
|
+
const moveInit = fetchMock.mock.calls[1]?.[1];
|
|
545
|
+
expect(moveInit?.method).toBe("POST");
|
|
546
|
+
expect(JSON.parse(String(moveInit?.body))).toEqual({
|
|
547
|
+
displayId: "0f6f062f-1478-4df2-96a1-c495d037d260",
|
|
548
|
+
targetFolderId: "folder-1",
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
it("blocks destructive display folder commands in read-only mode", async () => {
|
|
553
|
+
const configPath = createApiConfig();
|
|
554
|
+
const fetchMock = createFetchMock();
|
|
555
|
+
const program = buildProgram({ configPath });
|
|
556
|
+
|
|
557
|
+
await expect(
|
|
558
|
+
program.parseAsync(["node", "zinkee", "--read-only", "displays", "folders", "delete", "folder-1"]),
|
|
559
|
+
).rejects.toThrowError('Command "displays folders delete" is not allowed in read-only mode.');
|
|
560
|
+
|
|
561
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
562
|
+
});
|
|
489
563
|
});
|
package/src/commands/displays.ts
CHANGED
|
@@ -3,7 +3,12 @@ import type { Writable } from "node:stream";
|
|
|
3
3
|
|
|
4
4
|
import { Command, InvalidArgumentError } from "commander";
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
DisplaysApi,
|
|
8
|
+
type ApiDisplayFolder,
|
|
9
|
+
type DisplayResource,
|
|
10
|
+
type DisplaySummary,
|
|
11
|
+
} from "../api/displays.js";
|
|
7
12
|
import { ApiClient } from "../client.js";
|
|
8
13
|
import { defaultConfigPath, readRuntimeConfig } from "../config.js";
|
|
9
14
|
import {
|
|
@@ -308,6 +313,40 @@ function getDisplaySubresource(value: unknown): Record<string, unknown>[] {
|
|
|
308
313
|
return value.filter((entry): entry is Record<string, unknown> => Boolean(entry) && typeof entry === "object");
|
|
309
314
|
}
|
|
310
315
|
|
|
316
|
+
function displayFolderRows(folders: readonly ApiDisplayFolder[]): Record<string, unknown>[] {
|
|
317
|
+
return folders.map((folder) => ({
|
|
318
|
+
id: folder.id ?? "",
|
|
319
|
+
name: folder.name ?? "",
|
|
320
|
+
isRoot: folder.isRoot ?? "",
|
|
321
|
+
displayCount: folder.displayCount ?? "",
|
|
322
|
+
}));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function resolveDisplayFolderId(
|
|
326
|
+
api: DisplaysApi,
|
|
327
|
+
selector: NormalizedUuidOrSlug,
|
|
328
|
+
): Promise<string> {
|
|
329
|
+
if (selector.kind === "uuid") {
|
|
330
|
+
return selector.value;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const folders = await api.listFolders();
|
|
334
|
+
const matches = folders.filter((folder) => folder.name === selector.value);
|
|
335
|
+
|
|
336
|
+
if (matches.length === 1 && typeof matches[0]?.id === "string" && matches[0].id.length > 0) {
|
|
337
|
+
return matches[0].id;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (matches.length > 1) {
|
|
341
|
+
throw new CliError(
|
|
342
|
+
"invalid_cli_usage",
|
|
343
|
+
`Folder "${selector.value}" is ambiguous. Found ${matches.length} matches.`,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
throw new CliError("invalid_cli_usage", `Folder "${selector.value}" was not found.`);
|
|
348
|
+
}
|
|
349
|
+
|
|
311
350
|
function buildVariableBody(options: VariableMutationOptions): Record<string, unknown> {
|
|
312
351
|
const body = {
|
|
313
352
|
...readRawObject(options.raw, options.rawFile),
|
|
@@ -590,6 +629,109 @@ export function registerDisplaysCommands(
|
|
|
590
629
|
writeLine(stdout, `Deleted display "${display.value}".`);
|
|
591
630
|
});
|
|
592
631
|
|
|
632
|
+
const folders = displays
|
|
633
|
+
.command("folders")
|
|
634
|
+
.description("Manage display folders (flat: a folder only has a name, no order or properties)");
|
|
635
|
+
|
|
636
|
+
folders
|
|
637
|
+
.command("list")
|
|
638
|
+
.description("List display folders (non-root folders plus the root bucket, whose name is null)")
|
|
639
|
+
.action(async function action(this: Command) {
|
|
640
|
+
const { api, json } = createDisplaysApi(this, filePath);
|
|
641
|
+
const data = await api.listFolders();
|
|
642
|
+
if (json) {
|
|
643
|
+
emitJson(stdout, "displays folders list", data);
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
printTable(displayFolderRows(data), ["id", "name", "isRoot", "displayCount"], stdout);
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
folders
|
|
650
|
+
.command("create")
|
|
651
|
+
.description("Create a display folder")
|
|
652
|
+
.requiredOption("--name <name>", "folder name")
|
|
653
|
+
.action(async function action(this: Command, commandOptions: { name?: string }) {
|
|
654
|
+
const { api, json, readOnly } = createDisplaysApi(this, filePath);
|
|
655
|
+
ensureWritable("displays folders create", readOnly, "write");
|
|
656
|
+
if (commandOptions.name === undefined) {
|
|
657
|
+
throw new CliError("invalid_cli_usage", 'Provide folder name via "--name".');
|
|
658
|
+
}
|
|
659
|
+
const data = await api.createFolder({ name: commandOptions.name });
|
|
660
|
+
if (json) {
|
|
661
|
+
emitJson(stdout, "displays folders create", data);
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
writeLine(stdout, `Created display folder "${String(data.name ?? commandOptions.name)}".`);
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
folders
|
|
668
|
+
.command("update")
|
|
669
|
+
.description("Rename a display folder")
|
|
670
|
+
.argument("<folder>", "folder UUID or exact name", parseUuidOrSlugSelector)
|
|
671
|
+
.requiredOption("--name <name>", "folder name")
|
|
672
|
+
.action(async function action(
|
|
673
|
+
this: Command,
|
|
674
|
+
folder: NormalizedUuidOrSlug,
|
|
675
|
+
commandOptions: { name?: string },
|
|
676
|
+
) {
|
|
677
|
+
const { api, json, readOnly } = createDisplaysApi(this, filePath);
|
|
678
|
+
ensureWritable("displays folders update", readOnly, "write");
|
|
679
|
+
if (commandOptions.name === undefined) {
|
|
680
|
+
throw new CliError("invalid_cli_usage", 'Provide folder name via "--name".');
|
|
681
|
+
}
|
|
682
|
+
const folderId = await resolveDisplayFolderId(api, folder);
|
|
683
|
+
const data = await api.updateFolder({ kind: "uuid", value: folderId }, { name: commandOptions.name });
|
|
684
|
+
if (json) {
|
|
685
|
+
emitJson(stdout, "displays folders update", data);
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
writeLine(stdout, `Updated display folder "${String(data.name ?? folder.value)}".`);
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
folders
|
|
692
|
+
.command("delete")
|
|
693
|
+
.description("Delete a display folder (its displays move back to the root folder)")
|
|
694
|
+
.argument("<folder>", "folder UUID or exact name", parseUuidOrSlugSelector)
|
|
695
|
+
.action(async function action(this: Command, folder: NormalizedUuidOrSlug) {
|
|
696
|
+
const { api, json, readOnly } = createDisplaysApi(this, filePath);
|
|
697
|
+
ensureWritable("displays folders delete", readOnly, "destructive");
|
|
698
|
+
const folderId = await resolveDisplayFolderId(api, folder);
|
|
699
|
+
const data = await api.deleteFolder({ kind: "uuid", value: folderId });
|
|
700
|
+
if (json) {
|
|
701
|
+
emitJson(stdout, "displays folders delete", data);
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
writeLine(stdout, `Deleted display folder "${folder.value}".`);
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
folders
|
|
708
|
+
.command("move")
|
|
709
|
+
.description("Move a display into a folder")
|
|
710
|
+
.argument("<display>", "display UUID", parseUuidOrSlugSelector)
|
|
711
|
+
.requiredOption(
|
|
712
|
+
"--to <folder>",
|
|
713
|
+
"target folder UUID or exact name (use the root folder to move it out of any folder)",
|
|
714
|
+
parseUuidOrSlugSelector,
|
|
715
|
+
)
|
|
716
|
+
.action(async function action(
|
|
717
|
+
this: Command,
|
|
718
|
+
display: NormalizedUuidOrSlug,
|
|
719
|
+
commandOptions: { to?: NormalizedUuidOrSlug },
|
|
720
|
+
) {
|
|
721
|
+
const { api, json, readOnly } = createDisplaysApi(this, filePath);
|
|
722
|
+
ensureWritable("displays folders move", readOnly, "write");
|
|
723
|
+
if (!commandOptions.to) {
|
|
724
|
+
throw new CliError("invalid_cli_usage", 'Provide target folder via "--to".');
|
|
725
|
+
}
|
|
726
|
+
const targetFolderId = await resolveDisplayFolderId(api, commandOptions.to);
|
|
727
|
+
const data = await api.moveDisplay({ displayId: display.value, targetFolderId });
|
|
728
|
+
if (json) {
|
|
729
|
+
emitJson(stdout, "displays folders move", data);
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
writeLine(stdout, `Moved display "${display.value}" to folder "${targetFolderId}".`);
|
|
733
|
+
});
|
|
734
|
+
|
|
593
735
|
const layout = displays.command("layout").description("Manage display layout");
|
|
594
736
|
layout
|
|
595
737
|
.command("get")
|