zinkee 0.1.34 → 0.1.36

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.34",
3
+ "version": "0.1.36",
4
4
  "description": "CLI for Zinkee API v2",
5
5
  "type": "module",
6
6
  "bin": {
@@ -56,7 +56,28 @@ export function extractExampleRequest(
56
56
  const cmdPath = findCommandPath(operands, commandRegistry);
57
57
 
58
58
  if (!cmdPath) {
59
- throw new CliError("invalid_cli_usage", 'Provide a command after "--example".');
59
+ const typed = operands.map((operand) => operand.token).join(" ");
60
+
61
+ if (typed.length === 0) {
62
+ throw new CliError("invalid_cli_usage", 'Provide a command after "--example".');
63
+ }
64
+
65
+ // The token(s) didn't resolve to a runnable command. If they form the prefix
66
+ // of one or more registered commands, the user named a command group (e.g.
67
+ // "schemas") — list its subcommands instead of the generic error.
68
+ const subcommands = Object.keys(commandRegistry).filter((name) =>
69
+ name.startsWith(`${typed} `),
70
+ );
71
+
72
+ if (subcommands.length > 0) {
73
+ const list = subcommands.map((name) => ` zinkee --example ${name}`).join("\n");
74
+ throw new CliError(
75
+ "invalid_cli_usage",
76
+ `"${typed}" is a command group, not a runnable command. Available subcommands:\n${list}`,
77
+ );
78
+ }
79
+
80
+ throw new CliError("invalid_cli_usage", `Unknown command "${typed}".`);
60
81
  }
61
82
 
62
83
  const remainingTokens = userArgs.slice(cmdPath.lastArgvIndex + 1);
@@ -750,7 +750,12 @@ export function registerRecordsCommands(
750
750
  .command("create")
751
751
  .description("Create one or more records")
752
752
  .argument("<schema>", "schema uuid or slug", parseUuidOrSlugSelector)
753
- .option("--set <field=value>", "set a string field", collectKeyValueAssignments, [])
753
+ .option(
754
+ "--set <field=value>",
755
+ "set a string field (reference fields: a record id, 'recordId::label', or an exact display value)",
756
+ collectKeyValueAssignments,
757
+ [],
758
+ )
754
759
  .option("--set-json <field=json>", "set a structured field", collectJsonAssignments, [])
755
760
  .option("--raw <json>", "raw JSON object payload")
756
761
  .option("--raw-file <path>", "path to a raw JSON object payload")
@@ -790,7 +795,12 @@ export function registerRecordsCommands(
790
795
  .description("Update a record")
791
796
  .argument("<schema>", "schema uuid or slug", parseUuidOrSlugSelector)
792
797
  .argument("<record>", "record uuid or slug", parseUuidOrSlugSelector)
793
- .option("--set <field=value>", "set a string field", collectKeyValueAssignments, [])
798
+ .option(
799
+ "--set <field=value>",
800
+ "set a string field (reference fields: a record id, 'recordId::label', or an exact display value)",
801
+ collectKeyValueAssignments,
802
+ [],
803
+ )
794
804
  .option("--set-json <field=json>", "set a structured field", collectJsonAssignments, [])
795
805
  .option("--unset <field>", "unset a field", collectStringValues, [])
796
806
  .option("--raw <json>", "raw JSON object of fields to set")
package/src/index.test.ts CHANGED
@@ -114,6 +114,32 @@ describe("project bootstrap", () => {
114
114
  });
115
115
  });
116
116
 
117
+ it("rejects a command group by listing all its subcommands", () => {
118
+ let message = "";
119
+ try {
120
+ extractExampleRequest(["node", "zinkee", "--example", "schemas"]);
121
+ } catch (error) {
122
+ message = (error as Error).message;
123
+ }
124
+
125
+ expect(message).toContain('"schemas" is a command group');
126
+ expect(message).toContain("zinkee --example schemas list");
127
+ expect(message).toContain("zinkee --example schemas get");
128
+ expect(message).toContain("zinkee --example schemas fields create");
129
+ });
130
+
131
+ it("reports an unknown command after --example", () => {
132
+ expect(() =>
133
+ extractExampleRequest(["node", "zinkee", "--example", "frobnicate"]),
134
+ ).toThrowError('Unknown command "frobnicate".');
135
+ });
136
+
137
+ it("still asks for a command when --example has no operand", () => {
138
+ expect(() => extractExampleRequest(["node", "zinkee", "--example"])).toThrowError(
139
+ 'Provide a command after "--example".',
140
+ );
141
+ });
142
+
117
143
  it("shows field examples filtered by type", async () => {
118
144
  let output = "";
119
145
 
@@ -177,13 +177,15 @@ describe("getCommandExamples", () => {
177
177
  }
178
178
  });
179
179
 
180
- it("offers both a minimal and a multi-type records create example", () => {
180
+ it("offers minimal, multi-type, and reference records create examples", () => {
181
181
  const examples = getCommandExamples("records create");
182
182
 
183
- expect(examples).toHaveLength(2);
183
+ expect(examples).toHaveLength(3);
184
184
  expect(examples[0]?.raw).toContain('"contact-name"');
185
185
  expect(examples[1]?.raw).toContain('"customer-ref"');
186
186
  expect(examples[1]?.raw).toContain('"owner"');
187
+ expect(examples[2]?.raw).toContain('"customer-ref"');
188
+ expect(examples[2]?.response).toContain("data_field_reference_not_found");
187
189
  });
188
190
 
189
191
  it("provides full/minimal/folder variants for schemas create", () => {
@@ -332,6 +334,22 @@ describe("getCommandExamples", () => {
332
334
  expect(getCommandExamples("config validate")[0]?.response).toContain('"valid": true');
333
335
  });
334
336
 
337
+ it("wraps example responses in the { data, meta } JSON envelope", () => {
338
+ const listExample = getCommandExamples("schemas list")[0];
339
+ const parsed = JSON.parse(listExample?.response ?? "{}");
340
+
341
+ expect(parsed).toHaveProperty("data");
342
+ expect(parsed).toHaveProperty("meta");
343
+ expect(parsed.meta).toMatchObject({ command: "schemas list" });
344
+ expect(Array.isArray(parsed.data)).toBe(true);
345
+
346
+ // get/detail commands wrap a single object under data, not an array
347
+ const getExample = getCommandExamples("schemas get")[0];
348
+ const parsedGet = JSON.parse(getExample?.response ?? "{}");
349
+ expect(Array.isArray(parsedGet.data)).toBe(false);
350
+ expect(parsedGet.data).toHaveProperty("fields");
351
+ });
352
+
335
353
  it.each(Object.keys(commandRegistry))(
336
354
  "returns at least one example for %s",
337
355
  (commandName) => {
@@ -2,6 +2,7 @@ import {
2
2
  getSchemaFieldTypeDefinitions,
3
3
  resolveSchemaFieldType,
4
4
  } from "./schema-fields.js";
5
+ import { buildJsonSuccess } from "./output.js";
5
6
 
6
7
  export interface CommandExample {
7
8
  command: string;
@@ -698,7 +699,7 @@ const specificExamples: Record<string, readonly CommandExample[]> = {
698
699
  },
699
700
  {
700
701
  description:
701
- "Create many records from a JSON array file (one object per record, slug -> value). Best-effort: valid records are created and invalid ones are reported per index in 'errors'. ReferenceField values must match the target's current targetDisplayField.",
702
+ "Create many records from a JSON array file (one object per record, slug -> value). Best-effort: valid records are created and invalid ones are reported per index in 'errors'. ReferenceField values accept the target record id (a bare UUID, or the compound 'recordId::anything' whose label is ignored) or an exact display value; the server stores the canonical 'recordId::displayValue'.",
702
703
  command: `zinkee --json records create contacts --records-file ./records.json`,
703
704
  raw: `{
704
705
  "records": [
@@ -729,6 +730,31 @@ const specificExamples: Record<string, readonly CommandExample[]> = {
729
730
  "action": "Use a valid field slug from the schema and retry."
730
731
  }
731
732
  ]
733
+ }`,
734
+ },
735
+ {
736
+ description:
737
+ "Set a ReferenceField by the target record id (a bare UUID, or the compound 'recordId::anything' whose label is ignored) or by an exact display value. The server resolves and stores the canonical 'recordId::displayValue'. Unknown references are rejected per index with reason 'data_field_reference_not_found'.",
738
+ command:
739
+ `zinkee --json records create deals --set deal-name="Acme renewal" --set customer-ref=00000000-0000-0000-0000-000000000000`,
740
+ raw: `{
741
+ "records": [
742
+ {
743
+ "deal-name": "Acme renewal",
744
+ "customer-ref": "00000000-0000-0000-0000-000000000000"
745
+ }
746
+ ]
747
+ }`,
748
+ response: `{
749
+ "created": [],
750
+ "errors": [
751
+ {
752
+ "index": 0,
753
+ "reason": "data_field_reference_not_found",
754
+ "message": "Referenced record was not found in the target schema.",
755
+ "action": "Use an existing record id or an existing display value for this reference field."
756
+ }
757
+ ]
732
758
  }`,
733
759
  },
734
760
  ]),
@@ -773,6 +799,24 @@ const specificExamples: Record<string, readonly CommandExample[]> = {
773
799
  'zinkee --json records update contacts 550e8400-e29b-41d4-a716-446655440000 --unset contact-email --unset due-date',
774
800
  raw: `{
775
801
  "unset": ["contact-email", "due-date"]
802
+ }`,
803
+ },
804
+ {
805
+ description:
806
+ "Set a ReferenceField on an existing record. Pass the target record id (a bare UUID, or the compound 'recordId::anything' whose label is ignored) or an exact display value; the response shows the canonical 'recordId::displayValue' the server stored. If the reference doesn't exist the whole update is rejected with reason 'data_field_reference_not_found'.",
807
+ command:
808
+ 'zinkee --json records update deals 550e8400-e29b-41d4-a716-446655440000 --set customer-ref=8ad602a1-f6e3-43f6-9ab2-f1c5afc99621',
809
+ raw: `{
810
+ "set": {
811
+ "customer-ref": "8ad602a1-f6e3-43f6-9ab2-f1c5afc99621"
812
+ }
813
+ }`,
814
+ response: `{
815
+ "recordId": "550e8400-e29b-41d4-a716-446655440000",
816
+ "values": {
817
+ "deal-name": "Acme renewal",
818
+ "customer-ref": "8ad602a1-f6e3-43f6-9ab2-f1c5afc99621::Acme Corp"
819
+ }
776
820
  }`,
777
821
  },
778
822
  ]),
@@ -2200,7 +2244,7 @@ const specificExamples: Record<string, readonly CommandExample[]> = {
2200
2244
  "required": false,
2201
2245
  "system": false,
2202
2246
  "config": {
2203
- "formulaText": "date_format(d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f90, \"AAAA-MM\")",
2247
+ "formulaText": "date_format(d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f90, \\"AAAA-MM\\")",
2204
2248
  "uiFormatKind": "date",
2205
2249
  "status": "ACTIVE",
2206
2250
  "defaultValue": null,
@@ -4293,11 +4337,37 @@ export function getCommandExamples(
4293
4337
  commandName: string,
4294
4338
  filters: { type?: string } = {},
4295
4339
  ): readonly CommandExample[] {
4296
- if (commandName === "schemas fields create") {
4297
- return buildSchemaFieldCreateExamples(filters.type);
4340
+ const examples =
4341
+ commandName === "schemas fields create"
4342
+ ? buildSchemaFieldCreateExamples(filters.type)
4343
+ : (specificExamples[commandName] ?? fallbackExamples(commandName));
4344
+
4345
+ return examples.map((example) => withResponseEnvelope(example, commandName));
4346
+ }
4347
+
4348
+ // The CLI's --json output wraps every payload in a { data, meta } envelope (see
4349
+ // buildJsonSuccess in ./output). Example "response" bodies are authored as the
4350
+ // bare data payload for readability, so we wrap them here to match what the
4351
+ // command actually prints. Non-JSON responses (if any) are left untouched.
4352
+ function withResponseEnvelope(
4353
+ example: CommandExample,
4354
+ commandName: string,
4355
+ ): CommandExample {
4356
+ if (example.response === undefined) {
4357
+ return example;
4298
4358
  }
4299
4359
 
4300
- return specificExamples[commandName] ?? fallbackExamples(commandName);
4360
+ let payload: unknown;
4361
+ try {
4362
+ payload = JSON.parse(example.response);
4363
+ } catch {
4364
+ return example;
4365
+ }
4366
+
4367
+ return {
4368
+ ...example,
4369
+ response: JSON.stringify(buildJsonSuccess(payload, { command: commandName }), null, 2),
4370
+ };
4301
4371
  }
4302
4372
 
4303
4373
  export function getResolvedCommandExamples(
@@ -1,70 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(npm test *)",
5
- "Bash(npm run *)",
6
- "Bash(pnpm typecheck *)",
7
- "Bash(pnpm test *)",
8
- "Bash(npx tsc *)",
9
- "Read(//home/guillermo/dev/z2-backend/api/src/main/java/com/zinkee/api/domain/model/v2/log/**)",
10
- "Read(//home/guillermo/dev/z2-backend/**)",
11
- "Bash(grep *)",
12
- "Bash(npx vitest *)",
13
- "Bash(npx tsx *)",
14
- "Bash(node dist/index.js --help)",
15
- "Bash(gh repo *)",
16
- "WebFetch(domain:github.com)",
17
- "Bash(git config *)",
18
- "Read(//home/guillermo/.config/**)",
19
- "Read(//home/guillermo/**)",
20
- "Bash(curl *)",
21
- "Bash(node dist/index.js --show-completion zsh)",
22
- "Bash(node *)",
23
- "Bash(_ZINKEE_COMPLETE=zsh_complete COMP_WORDS=\"zinkee\" COMP_CWORD=1 node *)",
24
- "Bash(_ZINKEE_COMPLETE=zsh_complete COMP_WORDS=\"zinkee schemas\" COMP_CWORD=2 node *)",
25
- "Bash(_ZINKEE_COMPLETE=zsh_complete COMP_WORDS=\"zinkee schemas fields\" COMP_CWORD=3 node dist/index.js)",
26
- "Bash(_ZINKEE_COMPLETE=zsh_complete COMP_WORDS=\"zinkee schemas fields cr\" COMP_CWORD=3 node dist/index.js)",
27
- "Bash(_ZINKEE_COMPLETE=zsh_complete COMP_WORDS=\"zinkee schemas list --j\" COMP_CWORD=3 node dist/index.js)",
28
- "Bash(rm -rf /tmp/zinkee-install)",
29
- "Bash(mkdir -p /tmp/zinkee-install)",
30
- "Bash(HOME=/tmp/zinkee-install SHELL=/bin/zsh node dist/index.js --install-completion)",
31
- "Read(//tmp/**)",
32
- "Read(//tmp/zinkee-install/**)",
33
- "Bash(rm -rf /tmp/zinkee-install-bash)",
34
- "Bash(mkdir -p /tmp/zinkee-install-bash)",
35
- "Bash(HOME=/tmp/zinkee-install-bash SHELL=/bin/bash node dist/index.js --install-completion)",
36
- "Bash(rm -rf /tmp/zinkee-install-fish)",
37
- "Bash(mkdir -p /tmp/zinkee-install-fish)",
38
- "Bash(_ZINKEE_COMPLETE=zsh_complete node dist/index.js)",
39
- "Bash(echo \"exit: $?\")",
40
- "Bash(_ZINKEE_COMPLETE=zsh_complete COMP_WORDS=\"zinkee schemas\" COMP_CWORD=abc node dist/index.js)",
41
- "Bash(_ZINKEE_COMPLETE=zsh_complete COMP_WORDS=\"zinkee config show --foo --bar baz quux ?\" COMP_CWORD=10 node dist/index.js)",
42
- "Bash(git -C /home/guillermo/dev/cli status)",
43
- "Bash(git -C /home/guillermo/dev/cli branch --show-current)",
44
- "Bash(git -C /home/guillermo/dev/cli diff --stat)",
45
- "Bash(git fetch *)",
46
- "Bash(pnpm exec *)",
47
- "Bash(echo \"=== exit $? \\(grep\\) ===\")",
48
- "Bash(grep -rn \"resourceType\" src/)",
49
- "Bash(grep -rn \"automation-folders/\\\\${\\\\|/automations:move\\\\|{folderId}/automations\" src/)",
50
- "Bash(grep -rn \"navigation resources move.*--type\\\\|resourceType\" docs/ README.md)",
51
- "Bash(zinkee --version)",
52
- "Bash(zinkee --json profiles list)",
53
- "Bash(zinkee displays *)",
54
- "Bash(chmod +x /tmp/audit_search.sh)",
55
- "Bash(/tmp/audit_search.sh)",
56
- "Bash(head -60 CLAUDE.md)",
57
- "Bash(head -40 AGENTS.md)",
58
- "Bash(xargs cat)",
59
- "Bash(./gradlew :api:test --tests \"com.zinkee.api.service.v2.data.DataV2TranslatorTest\" --tests \"com.zinkee.api.service.v2.data.DataV2ServiceTest\" --tests \"com.zinkee.api.domain.model.v2.data.DataV2PatchRecordRequestTest\")",
60
- "Bash(./gradlew :api:test --tests \"com.zinkee.api.service.v2.data.DataV2ServiceTest\" --tests \"com.zinkee.api.domain.model.v2.data.DataV2PatchRecordRequestTest\")",
61
- "Bash(./gradlew :api:spotlessApply)",
62
- "Bash(./gradlew :api:test :api:spotbugsMain spotlessCheck)",
63
- "Bash(./gradlew :api:spotbugsMain)",
64
- "Bash(./gradlew spotlessCheck)",
65
- "Bash(git --no-pager diff --stat)",
66
- "Bash(git --no-pager diff -- src/commands/records.ts src/api/records.ts)",
67
- "Bash(echo \"===EXIT: $?===\")"
68
- ]
69
- }
70
- }