zinkee 0.1.24 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zinkee",
3
- "version": "0.1.24",
3
+ "version": "0.1.25",
4
4
  "description": "CLI for Zinkee API v2",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- `/api/v2/automation-folders/${segment(input.targetFolderId)}/automations:move`,
183
+ "/api/v2/automation-folders/automations:move",
184
184
  jsonOptions(input, "POST"),
185
185
  );
186
186
  }
@@ -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
  }
@@ -16,7 +16,6 @@ export interface NavigationFolderMutation {
16
16
 
17
17
  export interface NavigationMoveResourceInput {
18
18
  resourceId: string;
19
- resourceType: string;
20
19
  targetFolderId: string;
21
20
  }
22
21
 
@@ -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",
@@ -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.command("folders").description("Manage automation folders");
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("--to <folder>", "target folder UUID or exact name", parseUuidOrSlugSelector)
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
  });
@@ -3,7 +3,12 @@ import type { Writable } from "node:stream";
3
3
 
4
4
  import { Command, InvalidArgumentError } from "commander";
5
5
 
6
- import { DisplaysApi, type DisplayResource, type DisplaySummary } from "../api/displays.js";
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")
@@ -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 resource type and resolved folder id", async () => {
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: "DISPLAY",
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>", "folder order")
255
- .option("--props <json>", "folder additionalProperties JSON")
256
- .option("--props-file <path>", "folder additionalProperties JSON file")
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>", "folder order")
276
- .option("--props <json>", "folder additionalProperties JSON")
277
- .option("--props-file <path>", "folder additionalProperties JSON file")
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("Move a resource into a folder")
323
- .argument("<resource>", "resource UUID")
324
- .requiredOption("--type <type>", "resource type")
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: { type: string; to: string },
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
 
@@ -385,7 +385,7 @@ export function registerTeamspaceCommands(
385
385
  folders
386
386
  .command("list")
387
387
  .description("List teamspace folders")
388
- .option("--include-resources", "include nested resources")
388
+ .option("--include-resources", "include the published resources inside each folder")
389
389
  .action(async function action(this: Command, commandOptions: FolderListCommandOptions) {
390
390
  const { api, json } = createTeamspaceApi(this, filePath);
391
391
  const data = await api.listFolders(Boolean(commandOptions.includeResources));
@@ -402,7 +402,7 @@ export function registerTeamspaceCommands(
402
402
  .command("get")
403
403
  .description("Get a teamspace folder by exact name or UUID")
404
404
  .argument("<folder>", "folder UUID or exact name", parseUuidOrSlugSelector)
405
- .option("--include-resources", "include nested resources")
405
+ .option("--include-resources", "include the published resources inside each folder")
406
406
  .action(async function action(
407
407
  this: Command,
408
408
  folder: NormalizedUuidOrSlug,
@@ -425,9 +425,9 @@ export function registerTeamspaceCommands(
425
425
  .command("create")
426
426
  .description("Create a teamspace folder")
427
427
  .requiredOption("--name <name>", "folder name")
428
- .option("--order <n>", "folder order", parseNonNegativeInteger)
429
- .option("--props <json>", "folder properties payload")
430
- .option("--props-file <path>", "path to a folder properties JSON payload")
428
+ .option("--order <n>", "display order (lower values appear first)", parseNonNegativeInteger)
429
+ .option("--props <json>", "additionalProperties JSON: free-form client metadata, e.g. {\"icon\":\"folder-users\",\"color\":\"gold\"}")
430
+ .option("--props-file <path>", "path to a file with the additionalProperties JSON")
431
431
  .action(async function action(
432
432
  this: Command,
433
433
  commandOptions: FolderCreateCommandOptions,
@@ -450,9 +450,9 @@ export function registerTeamspaceCommands(
450
450
  .description("Update a teamspace folder")
451
451
  .argument("<folder>", "folder UUID or exact name", parseUuidOrSlugSelector)
452
452
  .option("--name <name>", "folder name")
453
- .option("--order <n>", "folder order", parseNonNegativeInteger)
454
- .option("--props <json>", "folder properties payload")
455
- .option("--props-file <path>", "path to a folder properties JSON payload")
453
+ .option("--order <n>", "display order (lower values appear first)", parseNonNegativeInteger)
454
+ .option("--props <json>", "additionalProperties JSON: free-form client metadata, e.g. {\"icon\":\"folder-users\",\"color\":\"gold\"}")
455
+ .option("--props-file <path>", "path to a file with the additionalProperties JSON")
456
456
  .action(async function action(
457
457
  this: Command,
458
458
  folder: NormalizedUuidOrSlug,
@@ -495,9 +495,15 @@ export function registerTeamspaceCommands(
495
495
 
496
496
  resources
497
497
  .command("publish")
498
- .description("Publish a resource to a teamspace folder")
499
- .argument("<resource>", "resource UUID or exact name", parseUuidOrSlugSelector)
500
- .requiredOption("--to <folder>", "target folder", parseUuidOrSlugSelector)
498
+ .description(
499
+ "Publish a resource to a teamspace folder. Publishing an already-published resource moves it to the new folder.",
500
+ )
501
+ .argument("<resource>", "schema UUID, schema slug, or display UUID", parseUuidOrSlugSelector)
502
+ .requiredOption(
503
+ "--to <folder>",
504
+ "target folder UUID or exact name (root folder UUID is 00000000-0000-0000-0000-000000000000)",
505
+ parseUuidOrSlugSelector,
506
+ )
501
507
  .action(async function action(
502
508
  this: Command,
503
509
  resource: NormalizedUuidOrSlug,
@@ -522,8 +528,8 @@ export function registerTeamspaceCommands(
522
528
 
523
529
  resources
524
530
  .command("unpublish")
525
- .description("Unpublish a resource from teamspace")
526
- .argument("<resource>", "resource UUID or exact name", parseUuidOrSlugSelector)
531
+ .description("Unpublish a resource from teamspace (removes it from its shared folder)")
532
+ .argument("<resource>", "schema UUID, schema slug, or display UUID", parseUuidOrSlugSelector)
527
533
  .action(async function action(this: Command, resource: NormalizedUuidOrSlug) {
528
534
  const { api, json, readOnly } = createTeamspaceApi(this, filePath);
529
535
  ensureWritable("teamspace resources unpublish", readOnly, "write");
@@ -211,14 +211,23 @@ describe("getCommandExamples", () => {
211
211
  expect(rawPayloads.some((raw) => raw.includes('"dynamicOption": "thisMonth"'))).toBe(true);
212
212
  });
213
213
 
214
- it("covers SCHEMA, WORKFLOW and DISPLAY variants in navigation resources move", () => {
214
+ it("documents navigation resources move as schema-only with no resourceType", () => {
215
215
  const examples = getCommandExamples("navigation resources move");
216
216
  const rawPayloads = examples.map((example) => example.raw ?? "");
217
217
 
218
- expect(examples.length).toBeGreaterThanOrEqual(5);
219
- for (const resourceType of ["SCHEMA", "WORKFLOW", "DISPLAY", "SCHEDULE"]) {
220
- expect(rawPayloads.some((raw) => raw.includes(`"resourceType": "${resourceType}"`))).toBe(true);
218
+ expect(examples.length).toBeGreaterThanOrEqual(2);
219
+ // Navigation only moves schemas now, so the payload no longer carries a
220
+ // resourceType and the example commands must not pass --type.
221
+ for (const raw of rawPayloads) {
222
+ expect(raw).not.toContain("resourceType");
221
223
  }
224
+ for (const example of examples) {
225
+ expect(example.command).not.toContain("--type");
226
+ }
227
+ // Covers moving into a folder and back to the root folder.
228
+ expect(
229
+ rawPayloads.some((raw) => raw.includes('"targetFolderId": "00000000-0000-0000-0000-000000000000"')),
230
+ ).toBe(true);
222
231
  });
223
232
 
224
233
  it("covers slug and UUID variants in teamspace resources publish", () => {