zinkee 0.1.39 → 0.1.41
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/.github/workflows/npm-publish.yml +77 -0
- package/.github/workflows/pr-checks.yml +36 -0
- package/AGENTS.md +4 -3
- package/dist/index.js +89 -24
- package/dist/index.js.map +1 -1
- package/docs/npm-release.md +34 -65
- package/docs/superpowers/specs/2026-07-30-cli-pr-checks-design.md +37 -0
- package/package.json +5 -1
- package/src/api/automations.ts +1 -0
- package/src/api/displays.ts +1 -0
- package/src/api/schemas.ts +1 -0
- package/src/commands/automations.test.ts +140 -0
- package/src/commands/automations.ts +51 -16
- package/src/commands/displays.ts +7 -2
- package/src/commands/schemas.ts +3 -2
- package/src/utils/examples.test.ts +2 -0
- package/src/utils/examples.ts +24 -4
- package/src/utils/output.test.ts +27 -1
- package/src/utils/output.ts +30 -0
- package/.claude/settings.local.json +0 -70
package/docs/npm-release.md
CHANGED
|
@@ -1,95 +1,64 @@
|
|
|
1
1
|
# Publish to npm
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The `Publish to npm` GitHub Actions workflow publishes new CLI versions when
|
|
4
|
+
they reach `main`. It uses npm trusted publishing with GitHub OIDC, so the
|
|
5
|
+
repository does not need an `NPM_TOKEN`.
|
|
4
6
|
|
|
5
|
-
##
|
|
7
|
+
## One-time npm configuration
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
- You have npm publish permissions for the `zinkee` package.
|
|
9
|
-
- You are logged in to npm:
|
|
9
|
+
Configure the `zinkee` package on npmjs.com with this trusted publisher:
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
npm login
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
## 1. Verify the package
|
|
22
|
-
|
|
23
|
-
Run the local checks before changing the package version:
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
npm install
|
|
27
|
-
npm run build
|
|
28
|
-
npm run typecheck
|
|
29
|
-
npm test
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
Optionally inspect what would be published:
|
|
33
|
-
|
|
34
|
-
```bash
|
|
35
|
-
npm pack --dry-run
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
## 2. Bump the patch version
|
|
39
|
-
|
|
40
|
-
For a normal patch release, use:
|
|
11
|
+
- Provider: GitHub Actions
|
|
12
|
+
- Organization: `Zinkee-Engineering`
|
|
13
|
+
- Repository: `cli`
|
|
14
|
+
- Workflow filename: `npm-publish.yml`
|
|
15
|
+
- Environment: empty
|
|
16
|
+
- Allowed action: `npm publish`
|
|
41
17
|
|
|
42
|
-
|
|
43
|
-
npm version patch
|
|
44
|
-
```
|
|
18
|
+
Enter only the workflow filename, not `.github/workflows/npm-publish.yml`.
|
|
45
19
|
|
|
46
|
-
|
|
20
|
+
## Release flow
|
|
47
21
|
|
|
48
|
-
|
|
22
|
+
Every pull request that ships a user-visible change must update the package
|
|
23
|
+
version. For a patch release, run:
|
|
49
24
|
|
|
50
25
|
```bash
|
|
51
|
-
npm version
|
|
52
|
-
npm version major
|
|
26
|
+
npm version patch --no-git-tag-version
|
|
53
27
|
```
|
|
54
28
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
Build again after the version bump:
|
|
29
|
+
This updates `package.json` and `package-lock.json` without creating a commit or
|
|
30
|
+
tag. Include both files in the feature pull request and run:
|
|
58
31
|
|
|
59
32
|
```bash
|
|
33
|
+
npm ci
|
|
34
|
+
npm test
|
|
35
|
+
npm run typecheck
|
|
60
36
|
npm run build
|
|
37
|
+
npm pack --dry-run
|
|
61
38
|
```
|
|
62
39
|
|
|
63
|
-
|
|
40
|
+
After the pull request is merged, `.github/workflows/npm-publish.yml`:
|
|
64
41
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
If npm requires explicit public access, use:
|
|
42
|
+
1. Checks whether the package version already exists on npm.
|
|
43
|
+
2. Skips the publish successfully when the version already exists.
|
|
44
|
+
3. Runs the full quality gate for a new version.
|
|
45
|
+
4. Publishes the package with `npm publish --access public`.
|
|
70
46
|
|
|
71
|
-
|
|
72
|
-
npm publish --access public
|
|
73
|
-
```
|
|
47
|
+
## Verification
|
|
74
48
|
|
|
75
|
-
|
|
49
|
+
Check the workflow run and the published version:
|
|
76
50
|
|
|
77
51
|
```bash
|
|
78
|
-
|
|
79
|
-
|
|
52
|
+
gh run list --workflow npm-publish.yml
|
|
53
|
+
npm view zinkee version
|
|
80
54
|
```
|
|
81
55
|
|
|
82
|
-
|
|
56
|
+
Optionally install the published version and exercise the CLI:
|
|
83
57
|
|
|
84
58
|
```bash
|
|
85
|
-
npm view zinkee version
|
|
86
59
|
npm install -g zinkee@latest
|
|
87
60
|
zinkee --help
|
|
88
61
|
```
|
|
89
62
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
- Do not manually edit the version unless there is a specific reason; prefer `npm version patch`.
|
|
93
|
-
- If the publish fails after `npm version patch`, fix the issue and run `npm publish` again. Do not create another version bump unless the failed version was already published.
|
|
94
|
-
- Keep stdout-sensitive CLI changes verified with `--json` before publishing.
|
|
95
|
-
|
|
63
|
+
Do not create another version bump after a failed publish unless npm confirms
|
|
64
|
+
that the failed version was already published.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# CLI pull request checks
|
|
2
|
+
|
|
3
|
+
## Context
|
|
4
|
+
|
|
5
|
+
The CLI repository has no GitHub Actions workflow that validates pull
|
|
6
|
+
requests. Its documented local quality gate consists of tests, TypeScript type
|
|
7
|
+
checking, and a production build.
|
|
8
|
+
|
|
9
|
+
## Decision
|
|
10
|
+
|
|
11
|
+
Add `.github/workflows/pr-checks.yml` and run it for pull requests targeting
|
|
12
|
+
`main`. The workflow will grant only `contents: read` permission and use one
|
|
13
|
+
Ubuntu job so dependencies are installed once.
|
|
14
|
+
|
|
15
|
+
The job will:
|
|
16
|
+
|
|
17
|
+
1. Check out the repository.
|
|
18
|
+
2. Set up Node.js 24 with npm dependency caching.
|
|
19
|
+
3. Install the locked dependency graph with `npm ci`.
|
|
20
|
+
4. Run `npm test`.
|
|
21
|
+
5. Run `npm run typecheck`.
|
|
22
|
+
6. Run `npm run build`.
|
|
23
|
+
|
|
24
|
+
Each quality check will be a separate step so GitHub identifies the failing
|
|
25
|
+
gate directly. A failure in any step will fail the job and prevent subsequent
|
|
26
|
+
steps from running.
|
|
27
|
+
|
|
28
|
+
## Scope
|
|
29
|
+
|
|
30
|
+
The workflow will not publish packages, deploy artifacts, access secrets,
|
|
31
|
+
change versions, run on pushes, or test a Node.js version matrix.
|
|
32
|
+
|
|
33
|
+
## Verification
|
|
34
|
+
|
|
35
|
+
Validate the workflow YAML and inspect its trigger, permissions, Node version,
|
|
36
|
+
dependency installation, and commands. Run the same three quality checks
|
|
37
|
+
locally from the CLI repository before completion.
|
package/package.json
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zinkee",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.41",
|
|
4
4
|
"description": "CLI for Zinkee API v2",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/Zinkee-Engineering/cli.git"
|
|
8
|
+
},
|
|
5
9
|
"type": "module",
|
|
6
10
|
"bin": {
|
|
7
11
|
"zinkee": "dist/index.js"
|
package/src/api/automations.ts
CHANGED
package/src/api/displays.ts
CHANGED
|
@@ -16,6 +16,7 @@ export interface ApiDisplay {
|
|
|
16
16
|
widgets?: Array<Record<string, unknown>>;
|
|
17
17
|
navigationTargets?: Array<Record<string, unknown>>;
|
|
18
18
|
crossWidgetFilter?: Array<Record<string, unknown>> | null;
|
|
19
|
+
folder?: { id?: string; name?: string | null; isRoot?: boolean };
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
export interface ApiDisplayTab {
|
package/src/api/schemas.ts
CHANGED
|
@@ -611,6 +611,146 @@ describe("automations commands", () => {
|
|
|
611
611
|
});
|
|
612
612
|
});
|
|
613
613
|
|
|
614
|
+
it("accepts the field:comparator literal form for no-value comparators", async () => {
|
|
615
|
+
const configPath = createApiConfig();
|
|
616
|
+
const output = createStdoutBuffer();
|
|
617
|
+
const fetchMock = createFetchMock();
|
|
618
|
+
fetchMock.mockResolvedValueOnce(jsonResponse({ ok: true }));
|
|
619
|
+
const program = buildAutomationsProgram({ configPath, stdout: output.stdout });
|
|
620
|
+
|
|
621
|
+
await program.parseAsync([
|
|
622
|
+
"node",
|
|
623
|
+
"zinkee",
|
|
624
|
+
"--json",
|
|
625
|
+
"automations",
|
|
626
|
+
"trigger",
|
|
627
|
+
"set",
|
|
628
|
+
"automation-1",
|
|
629
|
+
"--trigger-type",
|
|
630
|
+
"record_changed",
|
|
631
|
+
"--trigger-schema",
|
|
632
|
+
"schema-1",
|
|
633
|
+
"--trigger-field",
|
|
634
|
+
"field-1",
|
|
635
|
+
"--condition",
|
|
636
|
+
"field-1:chg",
|
|
637
|
+
]);
|
|
638
|
+
|
|
639
|
+
expect(JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body))).toEqual({
|
|
640
|
+
type: "record_changed",
|
|
641
|
+
config: {
|
|
642
|
+
schemaId: "schema-1",
|
|
643
|
+
fieldIds: ["field-1"],
|
|
644
|
+
conditions: [
|
|
645
|
+
{
|
|
646
|
+
field: "field-1",
|
|
647
|
+
comparator: "chg",
|
|
648
|
+
values: [],
|
|
649
|
+
},
|
|
650
|
+
],
|
|
651
|
+
},
|
|
652
|
+
});
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
it("accepts empty condition-json values for no-value comparators", async () => {
|
|
656
|
+
const configPath = createApiConfig();
|
|
657
|
+
const output = createStdoutBuffer();
|
|
658
|
+
const fetchMock = createFetchMock();
|
|
659
|
+
fetchMock.mockResolvedValueOnce(jsonResponse({ ok: true }));
|
|
660
|
+
const program = buildAutomationsProgram({ configPath, stdout: output.stdout });
|
|
661
|
+
|
|
662
|
+
await program.parseAsync([
|
|
663
|
+
"node",
|
|
664
|
+
"zinkee",
|
|
665
|
+
"--json",
|
|
666
|
+
"automations",
|
|
667
|
+
"trigger",
|
|
668
|
+
"set",
|
|
669
|
+
"automation-1",
|
|
670
|
+
"--trigger-type",
|
|
671
|
+
"record_changed",
|
|
672
|
+
"--trigger-schema",
|
|
673
|
+
"schema-1",
|
|
674
|
+
"--trigger-field",
|
|
675
|
+
"field-1",
|
|
676
|
+
"--condition-json",
|
|
677
|
+
'{"field":"field-1","comparator":"ie","values":[]}',
|
|
678
|
+
]);
|
|
679
|
+
|
|
680
|
+
expect(JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body))).toEqual({
|
|
681
|
+
type: "record_changed",
|
|
682
|
+
config: {
|
|
683
|
+
schemaId: "schema-1",
|
|
684
|
+
fieldIds: ["field-1"],
|
|
685
|
+
conditions: [
|
|
686
|
+
{
|
|
687
|
+
field: "field-1",
|
|
688
|
+
comparator: "ie",
|
|
689
|
+
values: [],
|
|
690
|
+
},
|
|
691
|
+
],
|
|
692
|
+
},
|
|
693
|
+
});
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
it("rejects the field:comparator literal form for comparators that need a value", async () => {
|
|
697
|
+
const configPath = createApiConfig();
|
|
698
|
+
const output = createStdoutBuffer();
|
|
699
|
+
const fetchMock = createFetchMock();
|
|
700
|
+
const program = buildAutomationsProgram({ configPath, stdout: output.stdout });
|
|
701
|
+
|
|
702
|
+
await expect(
|
|
703
|
+
program.parseAsync([
|
|
704
|
+
"node",
|
|
705
|
+
"zinkee",
|
|
706
|
+
"--json",
|
|
707
|
+
"automations",
|
|
708
|
+
"trigger",
|
|
709
|
+
"set",
|
|
710
|
+
"automation-1",
|
|
711
|
+
"--trigger-type",
|
|
712
|
+
"record_changed",
|
|
713
|
+
"--trigger-schema",
|
|
714
|
+
"schema-1",
|
|
715
|
+
"--trigger-field",
|
|
716
|
+
"field-1",
|
|
717
|
+
"--condition",
|
|
718
|
+
"field-1:eq",
|
|
719
|
+
]),
|
|
720
|
+
).rejects.toThrowError(/field:comparator for comparators without value/);
|
|
721
|
+
|
|
722
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
it("rejects empty condition-json values for comparators that need a value", async () => {
|
|
726
|
+
const configPath = createApiConfig();
|
|
727
|
+
const output = createStdoutBuffer();
|
|
728
|
+
const fetchMock = createFetchMock();
|
|
729
|
+
const program = buildAutomationsProgram({ configPath, stdout: output.stdout });
|
|
730
|
+
|
|
731
|
+
await expect(
|
|
732
|
+
program.parseAsync([
|
|
733
|
+
"node",
|
|
734
|
+
"zinkee",
|
|
735
|
+
"--json",
|
|
736
|
+
"automations",
|
|
737
|
+
"trigger",
|
|
738
|
+
"set",
|
|
739
|
+
"automation-1",
|
|
740
|
+
"--trigger-type",
|
|
741
|
+
"record_changed",
|
|
742
|
+
"--trigger-schema",
|
|
743
|
+
"schema-1",
|
|
744
|
+
"--trigger-field",
|
|
745
|
+
"field-1",
|
|
746
|
+
"--condition-json",
|
|
747
|
+
'{"field":"field-1","comparator":"eq","values":[]}',
|
|
748
|
+
]),
|
|
749
|
+
).rejects.toThrowError(/non-empty "values" array/);
|
|
750
|
+
|
|
751
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
752
|
+
});
|
|
753
|
+
|
|
614
754
|
it("rejects source-based trigger conditions", async () => {
|
|
615
755
|
const configPath = createApiConfig();
|
|
616
756
|
const output = createStdoutBuffer();
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
import { resolveRuntimeContext } from "../runtime-context.js";
|
|
30
30
|
import type { JsonObject, JsonValue, RuntimeOverrides } from "../types.js";
|
|
31
31
|
import { CliError } from "../utils/errors.js";
|
|
32
|
-
import { buildJsonSuccess, printTable } from "../utils/output.js";
|
|
32
|
+
import { buildJsonSuccess, folderColumns, FOLDER_TABLE_KEYS, printTable } from "../utils/output.js";
|
|
33
33
|
|
|
34
34
|
interface CommandIo {
|
|
35
35
|
write(chunk: string): unknown;
|
|
@@ -450,15 +450,47 @@ function readJsonObjectOption(
|
|
|
450
450
|
) as JsonObject;
|
|
451
451
|
}
|
|
452
452
|
|
|
453
|
+
const NO_VALUE_COMPARATORS = new Set([
|
|
454
|
+
"chg",
|
|
455
|
+
"changes",
|
|
456
|
+
"ie",
|
|
457
|
+
"is_empty",
|
|
458
|
+
"ine",
|
|
459
|
+
"is_not_empty",
|
|
460
|
+
]);
|
|
461
|
+
|
|
462
|
+
function isNoValueComparator(comparator: string): boolean {
|
|
463
|
+
return NO_VALUE_COMPARATORS.has(comparator.toLowerCase());
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function conditionFormatError(input: string): CliError {
|
|
467
|
+
return new CliError(
|
|
468
|
+
"invalid_cli_usage",
|
|
469
|
+
`Expected condition in the form field:comparator:value, or field:comparator for comparators without value (chg, ie, ine), received "${input}".`,
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
|
|
453
473
|
function parseCondition(input: string): ConditionSpec {
|
|
454
474
|
const firstSeparator = input.indexOf(":");
|
|
455
475
|
const secondSeparator = input.indexOf(":", firstSeparator + 1);
|
|
456
476
|
|
|
457
|
-
if (firstSeparator <= 0
|
|
458
|
-
throw
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
477
|
+
if (firstSeparator <= 0) {
|
|
478
|
+
throw conditionFormatError(input);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (secondSeparator === -1) {
|
|
482
|
+
const field = input.slice(0, firstSeparator).trim();
|
|
483
|
+
const comparator = input.slice(firstSeparator + 1).trim();
|
|
484
|
+
|
|
485
|
+
if (!field || !comparator || !isNoValueComparator(comparator)) {
|
|
486
|
+
throw conditionFormatError(input);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return { field, comparator, values: [] };
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (secondSeparator <= firstSeparator + 1) {
|
|
493
|
+
throw conditionFormatError(input);
|
|
462
494
|
}
|
|
463
495
|
|
|
464
496
|
const field = input.slice(0, firstSeparator).trim();
|
|
@@ -466,10 +498,7 @@ function parseCondition(input: string): ConditionSpec {
|
|
|
466
498
|
const value = input.slice(secondSeparator + 1).trim();
|
|
467
499
|
|
|
468
500
|
if (!field || !comparator || !value) {
|
|
469
|
-
throw
|
|
470
|
-
"invalid_cli_usage",
|
|
471
|
-
`Expected condition in the form field:comparator:value, received "${input}".`,
|
|
472
|
-
);
|
|
501
|
+
throw conditionFormatError(input);
|
|
473
502
|
}
|
|
474
503
|
|
|
475
504
|
return {
|
|
@@ -683,10 +712,11 @@ function normalizeAutomationConditions(
|
|
|
683
712
|
const comparator = typeof condition.comparator === "string" ? condition.comparator.trim() : "";
|
|
684
713
|
const values = condition.values;
|
|
685
714
|
|
|
686
|
-
|
|
715
|
+
const allowsEmptyValues = isNoValueComparator(comparator);
|
|
716
|
+
if (!field || !comparator || !Array.isArray(values) || (values.length === 0 && !allowsEmptyValues)) {
|
|
687
717
|
throw new CliError(
|
|
688
718
|
"invalid_cli_usage",
|
|
689
|
-
`Expected ${label} ${index + 1} to contain "field", "comparator", and a non-empty "values" array.`,
|
|
719
|
+
`Expected ${label} ${index + 1} to contain "field", "comparator", and a non-empty "values" array (comparators without value, e.g. chg/ie/ine, accept an empty array).`,
|
|
690
720
|
);
|
|
691
721
|
}
|
|
692
722
|
|
|
@@ -800,12 +830,12 @@ function automationRows(automations: readonly ApiAutomation[]): Record<string, u
|
|
|
800
830
|
id: automation.id ?? "",
|
|
801
831
|
name: automation.name ?? "",
|
|
802
832
|
status: automation.status ?? "",
|
|
803
|
-
folderId: automation.folderId ?? "",
|
|
804
833
|
triggerType:
|
|
805
834
|
isJsonObject(automation.trigger) && typeof automation.trigger.type === "string"
|
|
806
835
|
? automation.trigger.type
|
|
807
836
|
: "",
|
|
808
837
|
actionCount: Array.isArray(automation.actions) ? automation.actions.length : "",
|
|
838
|
+
...folderColumns(automation.folder),
|
|
809
839
|
}));
|
|
810
840
|
}
|
|
811
841
|
|
|
@@ -1329,7 +1359,7 @@ function addTriggerPayloadOptions(command: Command): Command {
|
|
|
1329
1359
|
)
|
|
1330
1360
|
.option(
|
|
1331
1361
|
"--condition <field:comparator:value>",
|
|
1332
|
-
"trigger condition",
|
|
1362
|
+
"trigger condition (use field:comparator for comparators without value: chg, ie, ine)",
|
|
1333
1363
|
collectValues,
|
|
1334
1364
|
[],
|
|
1335
1365
|
)
|
|
@@ -1368,7 +1398,12 @@ function addActionPayloadOptions(command: Command): Command {
|
|
|
1368
1398
|
.option("--target-schema <uuid>", "target schema id")
|
|
1369
1399
|
.option("--map <field=value>", "field mapping", collectValues, [])
|
|
1370
1400
|
.option("--map-json <field=json>", "JSON field mapping", collectValues, [])
|
|
1371
|
-
.option(
|
|
1401
|
+
.option(
|
|
1402
|
+
"--condition <field:comparator:value>",
|
|
1403
|
+
"action condition (use field:comparator for comparators without value: ie, ine)",
|
|
1404
|
+
collectValues,
|
|
1405
|
+
[],
|
|
1406
|
+
)
|
|
1372
1407
|
.option("--condition-json <json>", "action condition as JSON", collectValues, [])
|
|
1373
1408
|
.option("--use-trigger-record", "use the triggering record")
|
|
1374
1409
|
.option("--method <method>", "HTTP method")
|
|
@@ -1569,7 +1604,7 @@ export function registerAutomationsCommands(
|
|
|
1569
1604
|
|
|
1570
1605
|
printTable(
|
|
1571
1606
|
automationRows(data),
|
|
1572
|
-
["id", "name", "status", "
|
|
1607
|
+
["id", "name", "status", "triggerType", "actionCount", ...FOLDER_TABLE_KEYS],
|
|
1573
1608
|
stdout,
|
|
1574
1609
|
);
|
|
1575
1610
|
});
|
package/src/commands/displays.ts
CHANGED
|
@@ -20,7 +20,7 @@ import { normalizeUuidOrSlug, type NormalizedUuidOrSlug } from "../parsers/selec
|
|
|
20
20
|
import { resolveRuntimeContext } from "../runtime-context.js";
|
|
21
21
|
import type { RuntimeOverrides } from "../types.js";
|
|
22
22
|
import { CliError } from "../utils/errors.js";
|
|
23
|
-
import { buildJsonSuccess, printTable } from "../utils/output.js";
|
|
23
|
+
import { buildJsonSuccess, folderColumns, FOLDER_TABLE_KEYS, printTable } from "../utils/output.js";
|
|
24
24
|
|
|
25
25
|
const DATE_DYNAMIC_OPTIONS =
|
|
26
26
|
"today, thisWeek, thisMonth, thisQuarter, thisSemester, thisYear, next7Days, next30Days, next6Months, next12Months, last12Months";
|
|
@@ -628,6 +628,7 @@ function displayRows(displays: readonly DisplaySummary[]): Record<string, unknow
|
|
|
628
628
|
layoutTemplate: display.layoutTemplate ?? "",
|
|
629
629
|
variableCount: display.variableCount ?? "",
|
|
630
630
|
widgetCount: display.widgetCount ?? "",
|
|
631
|
+
...folderColumns(display.folder),
|
|
631
632
|
}));
|
|
632
633
|
}
|
|
633
634
|
|
|
@@ -953,7 +954,11 @@ export function registerDisplaysCommands(
|
|
|
953
954
|
emitJson(stdout, "displays list", data);
|
|
954
955
|
return;
|
|
955
956
|
}
|
|
956
|
-
printTable(
|
|
957
|
+
printTable(
|
|
958
|
+
displayRows(data),
|
|
959
|
+
["id", "name", "layoutTemplate", "variableCount", "widgetCount", ...FOLDER_TABLE_KEYS],
|
|
960
|
+
stdout,
|
|
961
|
+
);
|
|
957
962
|
});
|
|
958
963
|
|
|
959
964
|
displays
|
package/src/commands/schemas.ts
CHANGED
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
normalizeSchemaFieldUpdatePayload,
|
|
32
32
|
normalizeSchemaFieldType,
|
|
33
33
|
} from "../utils/schema-fields.js";
|
|
34
|
-
import { buildJsonSuccess, printTable } from "../utils/output.js";
|
|
34
|
+
import { buildJsonSuccess, folderColumns, FOLDER_TABLE_KEYS, printTable } from "../utils/output.js";
|
|
35
35
|
|
|
36
36
|
interface CommandIo {
|
|
37
37
|
write(chunk: string): unknown;
|
|
@@ -284,8 +284,9 @@ function emitSchemasTable(stdout: CommandIo, schemas: readonly ApiSchema[]): voi
|
|
|
284
284
|
slug: schema.slug ?? "",
|
|
285
285
|
auditable: schema.auditable ?? "",
|
|
286
286
|
commentable: schema.commentable ?? "",
|
|
287
|
+
...folderColumns(schema.folder),
|
|
287
288
|
})),
|
|
288
|
-
["id", "name", "slug", "auditable", "commentable"],
|
|
289
|
+
["id", "name", "slug", "auditable", "commentable", ...FOLDER_TABLE_KEYS],
|
|
289
290
|
stdout,
|
|
290
291
|
);
|
|
291
292
|
}
|
|
@@ -274,6 +274,8 @@ describe("getCommandExamples", () => {
|
|
|
274
274
|
expect(examples[0]?.response).toContain('"layoutTemplate": "widget_row_tabs"');
|
|
275
275
|
expect(examples[0]?.response).toContain('"layoutTemplate": "freeform"');
|
|
276
276
|
expect(examples[0]?.response).toContain('"layoutTemplate": "grid"');
|
|
277
|
+
expect(examples[0]?.response).toContain('"folder"');
|
|
278
|
+
expect(examples[0]?.response).toContain('"isRoot": true');
|
|
277
279
|
});
|
|
278
280
|
|
|
279
281
|
it("attaches a response body to schemas create and teamspace resources publish", () => {
|
package/src/utils/examples.ts
CHANGED
|
@@ -2909,21 +2909,36 @@ const specificExamples: Record<string, readonly CommandExample[]> = {
|
|
|
2909
2909
|
"name": "Executive Revenue",
|
|
2910
2910
|
"layoutTemplate": "widget_row_tabs",
|
|
2911
2911
|
"variableCount": 3,
|
|
2912
|
-
"widgetCount": 6
|
|
2912
|
+
"widgetCount": 6,
|
|
2913
|
+
"folder": {
|
|
2914
|
+
"id": "b1a2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
|
|
2915
|
+
"name": "📊 Executive",
|
|
2916
|
+
"isRoot": false
|
|
2917
|
+
}
|
|
2913
2918
|
},
|
|
2914
2919
|
{
|
|
2915
2920
|
"id": "6b7c8d9e-0f12-3456-789a-bcdef0123456",
|
|
2916
2921
|
"name": "Sales Pipeline",
|
|
2917
2922
|
"layoutTemplate": "freeform",
|
|
2918
2923
|
"variableCount": 1,
|
|
2919
|
-
"widgetCount": 3
|
|
2924
|
+
"widgetCount": 3,
|
|
2925
|
+
"folder": {
|
|
2926
|
+
"id": "a0b1c2d3-e4f5-4061-8273-8495a6b7c8d9",
|
|
2927
|
+
"name": null,
|
|
2928
|
+
"isRoot": true
|
|
2929
|
+
}
|
|
2920
2930
|
},
|
|
2921
2931
|
{
|
|
2922
2932
|
"id": "ef012345-6789-4abc-8def-0123456789ab",
|
|
2923
2933
|
"name": "Customer Detail",
|
|
2924
2934
|
"layoutTemplate": "grid",
|
|
2925
2935
|
"variableCount": 0,
|
|
2926
|
-
"widgetCount": 2
|
|
2936
|
+
"widgetCount": 2,
|
|
2937
|
+
"folder": {
|
|
2938
|
+
"id": "b1a2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
|
|
2939
|
+
"name": "📊 Executive",
|
|
2940
|
+
"isRoot": false
|
|
2941
|
+
}
|
|
2927
2942
|
}
|
|
2928
2943
|
]`,
|
|
2929
2944
|
},
|
|
@@ -3056,7 +3071,12 @@ const specificExamples: Record<string, readonly CommandExample[]> = {
|
|
|
3056
3071
|
"targetVariableId": "aaaaaaaa-1111-4111-a111-111111111111",
|
|
3057
3072
|
"label": "Open customer"
|
|
3058
3073
|
}
|
|
3059
|
-
]
|
|
3074
|
+
],
|
|
3075
|
+
"folder": {
|
|
3076
|
+
"id": "b1a2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
|
|
3077
|
+
"name": "📊 Executive",
|
|
3078
|
+
"isRoot": false
|
|
3079
|
+
}
|
|
3060
3080
|
}`,
|
|
3061
3081
|
},
|
|
3062
3082
|
]),
|
package/src/utils/output.test.ts
CHANGED
|
@@ -2,7 +2,33 @@ import { describe, expect, it } from "vitest";
|
|
|
2
2
|
|
|
3
3
|
import { getCommandMeta } from "../command-registry.js";
|
|
4
4
|
|
|
5
|
-
import { buildJsonError, buildJsonSuccess } from "./output.js";
|
|
5
|
+
import { buildJsonError, buildJsonSuccess, folderColumns } from "./output.js";
|
|
6
|
+
|
|
7
|
+
describe("folderColumns", () => {
|
|
8
|
+
it("flattens a named folder into id/name/isRoot columns", () => {
|
|
9
|
+
expect(folderColumns({ id: "f1", name: "Finance", isRoot: false })).toEqual({
|
|
10
|
+
folderId: "f1",
|
|
11
|
+
folderName: "Finance",
|
|
12
|
+
folderIsRoot: "false",
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("renders the root folder (null name) as (root)", () => {
|
|
17
|
+
expect(folderColumns({ id: "root-1", name: null, isRoot: true })).toEqual({
|
|
18
|
+
folderId: "root-1",
|
|
19
|
+
folderName: "(root)",
|
|
20
|
+
folderIsRoot: "true",
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("leaves every column empty when there is no folder", () => {
|
|
25
|
+
expect(folderColumns(undefined)).toEqual({
|
|
26
|
+
folderId: "",
|
|
27
|
+
folderName: "",
|
|
28
|
+
folderIsRoot: "",
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
});
|
|
6
32
|
|
|
7
33
|
describe("buildJsonSuccess", () => {
|
|
8
34
|
it("wraps data in the standard success envelope", () => {
|
package/src/utils/output.ts
CHANGED
|
@@ -59,6 +59,36 @@ export function buildTableColumns(keys: readonly string[]): readonly TableColumn
|
|
|
59
59
|
return keys.map((key) => ({ key, label: key }));
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
export interface FolderLocation {
|
|
63
|
+
id?: string;
|
|
64
|
+
name?: string | null;
|
|
65
|
+
isRoot?: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Column keys, in order, produced by {@link folderColumns}. Append them to a table's key list. */
|
|
69
|
+
export const FOLDER_TABLE_KEYS = ["folderId", "folderName", "folderIsRoot"] as const;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Flattens the API `folder` object (`{ id, name, isRoot }`) into the folderId/folderName/folderIsRoot table columns.
|
|
73
|
+
* The root folder arrives with a null name, so folderName renders it as "(root)". A missing folder (e.g. a workspace
|
|
74
|
+
* without a default group) leaves every column empty.
|
|
75
|
+
*/
|
|
76
|
+
export function folderColumns(folder: FolderLocation | null | undefined): {
|
|
77
|
+
folderId: string;
|
|
78
|
+
folderName: string;
|
|
79
|
+
folderIsRoot: string;
|
|
80
|
+
} {
|
|
81
|
+
if (!folder) {
|
|
82
|
+
return { folderId: "", folderName: "", folderIsRoot: "" };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
folderId: folder.id ?? "",
|
|
87
|
+
folderName: folder.name ?? (folder.isRoot === true ? "(root)" : ""),
|
|
88
|
+
folderIsRoot: folder.isRoot === undefined ? "" : String(folder.isRoot),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
62
92
|
export function buildTableRows(
|
|
63
93
|
rows: readonly Record<string, unknown>[],
|
|
64
94
|
columns: readonly TableColumn[],
|