zinkee 0.1.44 → 0.1.45

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.
Files changed (94) hide show
  1. package/README.md +4 -0
  2. package/dist/chunk-MHEVRMDX.js +1230 -0
  3. package/dist/chunk-MHEVRMDX.js.map +1 -0
  4. package/{src/utils/examples.ts → dist/examples-E6FFEVBE.js} +856 -1141
  5. package/dist/examples-E6FFEVBE.js.map +1 -0
  6. package/dist/index.js +1145 -8456
  7. package/dist/index.js.map +1 -1
  8. package/package.json +6 -3
  9. package/.github/workflows/npm-publish.yml +0 -77
  10. package/.github/workflows/pr-checks.yml +0 -36
  11. package/AGENTS.md +0 -110
  12. package/docs/cli-contract.md +0 -41
  13. package/docs/npm-release.md +0 -64
  14. package/docs/superpowers/plans/2026-03-24-zinkee-cli-implementation.md +0 -837
  15. package/docs/superpowers/plans/2026-03-25-cli-backend-error-contract.md +0 -503
  16. package/docs/superpowers/plans/2026-03-26-display-freeform-create.md +0 -389
  17. package/docs/superpowers/specs/2026-03-24-zinkee-cli-backend-blockers.md +0 -172
  18. package/docs/superpowers/specs/2026-03-24-zinkee-cli-design.md +0 -1576
  19. package/docs/superpowers/specs/2026-03-24-zinkee-cli-e2e-checklist.md +0 -215
  20. package/docs/superpowers/specs/2026-03-24-zinkee-cli-e2e-design.md +0 -492
  21. package/docs/superpowers/specs/2026-03-24-zinkee-cli-e2e-status.md +0 -307
  22. package/docs/superpowers/specs/2026-07-30-cli-pr-checks-design.md +0 -37
  23. package/src/api/automations.ts +0 -404
  24. package/src/api/comments.ts +0 -51
  25. package/src/api/displays.ts +0 -337
  26. package/src/api/document-templates.ts +0 -110
  27. package/src/api/files.ts +0 -50
  28. package/src/api/formulas.test.ts +0 -65
  29. package/src/api/formulas.ts +0 -67
  30. package/src/api/logs.ts +0 -34
  31. package/src/api/navigation.ts +0 -70
  32. package/src/api/records.ts +0 -184
  33. package/src/api/schemas.ts +0 -148
  34. package/src/api/teamspace.ts +0 -110
  35. package/src/cli-examples.ts +0 -130
  36. package/src/cli-runner.ts +0 -95
  37. package/src/client.test.ts +0 -189
  38. package/src/client.ts +0 -269
  39. package/src/command-registry.ts +0 -882
  40. package/src/commands/automations.test.ts +0 -1030
  41. package/src/commands/automations.ts +0 -2102
  42. package/src/commands/comments.test.ts +0 -214
  43. package/src/commands/comments.ts +0 -303
  44. package/src/commands/config.test.ts +0 -81
  45. package/src/commands/config.ts +0 -150
  46. package/src/commands/displays.test.ts +0 -1105
  47. package/src/commands/displays.ts +0 -1442
  48. package/src/commands/document-templates.test.ts +0 -569
  49. package/src/commands/document-templates.ts +0 -563
  50. package/src/commands/files.test.ts +0 -284
  51. package/src/commands/files.ts +0 -280
  52. package/src/commands/formulas.test.ts +0 -194
  53. package/src/commands/formulas.ts +0 -243
  54. package/src/commands/logs.test.ts +0 -123
  55. package/src/commands/logs.ts +0 -159
  56. package/src/commands/navigation.test.ts +0 -211
  57. package/src/commands/navigation.ts +0 -348
  58. package/src/commands/profiles.test.ts +0 -191
  59. package/src/commands/profiles.ts +0 -303
  60. package/src/commands/records.test.ts +0 -860
  61. package/src/commands/records.ts +0 -883
  62. package/src/commands/schemas.test.ts +0 -1252
  63. package/src/commands/schemas.ts +0 -890
  64. package/src/commands/teamspace.test.ts +0 -229
  65. package/src/commands/teamspace.ts +0 -546
  66. package/src/completion/engine.test.ts +0 -138
  67. package/src/completion/engine.ts +0 -168
  68. package/src/completion/install.test.ts +0 -179
  69. package/src/completion/install.ts +0 -260
  70. package/src/completion/runtime.ts +0 -150
  71. package/src/completion/scripts.ts +0 -91
  72. package/src/config.test.ts +0 -362
  73. package/src/config.ts +0 -294
  74. package/src/index.test.ts +0 -217
  75. package/src/index.ts +0 -8
  76. package/src/parsers/expressions.test.ts +0 -95
  77. package/src/parsers/expressions.ts +0 -128
  78. package/src/parsers/kv.test.ts +0 -35
  79. package/src/parsers/kv.ts +0 -49
  80. package/src/parsers/selectors.test.ts +0 -23
  81. package/src/parsers/selectors.ts +0 -29
  82. package/src/program.ts +0 -64
  83. package/src/runtime-context.ts +0 -103
  84. package/src/types.ts +0 -76
  85. package/src/utils/argv-rewrite.test.ts +0 -149
  86. package/src/utils/argv-rewrite.ts +0 -269
  87. package/src/utils/errors.test.ts +0 -85
  88. package/src/utils/errors.ts +0 -191
  89. package/src/utils/examples.test.ts +0 -374
  90. package/src/utils/output.test.ts +0 -65
  91. package/src/utils/output.ts +0 -154
  92. package/src/utils/schema-fields.ts +0 -1016
  93. package/tsconfig.json +0 -20
  94. package/tsup.config.ts +0 -13
@@ -1,1030 +0,0 @@
1
- import { mkdtempSync, writeFileSync } from "node:fs";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
-
5
- import { Command } from "commander";
6
- import { afterEach, describe, expect, it, vi } from "vitest";
7
-
8
- import { registerAutomationsCommands } from "./automations.js";
9
-
10
- function createTempConfig(contents: string): string {
11
- const dir = mkdtempSync(join(tmpdir(), "zinkee-automations-"));
12
- const filePath = join(dir, "config.toml");
13
- writeFileSync(filePath, contents, "utf8");
14
- return filePath;
15
- }
16
-
17
- function createApiConfig(): string {
18
- return createTempConfig(`
19
- default_profile = "prod"
20
-
21
- [profiles.prod]
22
- api_key = "automations-token-1234"
23
- base_url = "https://api.zinkee.test"
24
- `);
25
- }
26
-
27
- function createStdoutBuffer() {
28
- let output = "";
29
-
30
- return {
31
- stdout: {
32
- write(chunk: string) {
33
- output += chunk;
34
- return true;
35
- },
36
- },
37
- read() {
38
- return output;
39
- },
40
- };
41
- }
42
-
43
- function jsonResponse(body: unknown, status: number = 200): Response {
44
- return new Response(JSON.stringify(body), {
45
- status,
46
- headers: {
47
- "content-type": "application/json",
48
- },
49
- });
50
- }
51
-
52
- function createFetchMock() {
53
- const fetchMock = vi.fn<typeof fetch>();
54
- vi.stubGlobal("fetch", fetchMock);
55
- return fetchMock;
56
- }
57
-
58
- function buildAutomationsProgram(options: { configPath?: string; stdout?: { write(chunk: string): unknown } } = {}): Command {
59
- const program = new Command();
60
-
61
- program
62
- .name("zinkee")
63
- .description("CLI for Zinkee API v2")
64
- .version("0.1.0")
65
- .enablePositionalOptions()
66
- .showHelpAfterError()
67
- .option("--profile <name>", "select a profile from config")
68
- .option("--base-url <url>", "override the configured base URL")
69
- .option("--api-key <token>", "override the configured API key")
70
- .option("--read-only", "run in read-only mode")
71
- .option("--json", "emit JSON output");
72
-
73
- registerAutomationsCommands(program, options);
74
-
75
- return program;
76
- }
77
-
78
- afterEach(() => {
79
- vi.unstubAllGlobals();
80
- vi.restoreAllMocks();
81
- });
82
-
83
- describe("automations commands", () => {
84
- it("registers the automations command group", () => {
85
- const program = buildAutomationsProgram();
86
-
87
- expect(program.commands.some((command) => command.name() === "automations")).toBe(true);
88
- });
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
-
122
- it("sets trigger payloads through the generic trigger flags", async () => {
123
- const configPath = createApiConfig();
124
- const output = createStdoutBuffer();
125
- const fetchMock = createFetchMock();
126
- fetchMock.mockResolvedValueOnce(
127
- jsonResponse({
128
- ok: true,
129
- }),
130
- );
131
- const program = buildAutomationsProgram({ configPath, stdout: output.stdout });
132
-
133
- await program.parseAsync([
134
- "node",
135
- "zinkee",
136
- "--json",
137
- "automations",
138
- "trigger",
139
- "set",
140
- "automation-1",
141
- "--trigger-type",
142
- "record_changed",
143
- "--trigger-schema",
144
- "schema-1",
145
- "--trigger-field",
146
- "field-1",
147
- "--trigger-field",
148
- "field-2",
149
- "--condition",
150
- "status:eq:open",
151
- ]);
152
-
153
- const init = fetchMock.mock.calls[0]?.[1];
154
-
155
- expect(fetchMock.mock.calls[0]?.[0]).toBe(
156
- "https://api.zinkee.test/api/v2/automations/automation-1/trigger",
157
- );
158
- expect(init?.method).toBe("PUT");
159
- expect(JSON.parse(String(init?.body))).toEqual({
160
- type: "record_changed",
161
- config: {
162
- schemaId: "schema-1",
163
- fieldIds: ["field-1", "field-2"],
164
- conditions: [
165
- {
166
- field: "status",
167
- comparator: "eq",
168
- values: [
169
- {
170
- rawValue: "open",
171
- source: null,
172
- },
173
- ],
174
- },
175
- ],
176
- },
177
- });
178
- expect(JSON.parse(output.read())).toEqual({
179
- data: {
180
- ok: true,
181
- },
182
- meta: {
183
- command: "automations trigger set",
184
- configPath,
185
- },
186
- });
187
- });
188
-
189
- it("adds action payloads through the generic --type path", async () => {
190
- const configPath = createApiConfig();
191
- const output = createStdoutBuffer();
192
- const fetchMock = createFetchMock();
193
- fetchMock.mockResolvedValueOnce(
194
- jsonResponse({
195
- id: "action-1",
196
- }),
197
- );
198
- const program = buildAutomationsProgram({ configPath, stdout: output.stdout });
199
-
200
- await program.parseAsync([
201
- "node",
202
- "zinkee",
203
- "--json",
204
- "automations",
205
- "actions",
206
- "add",
207
- "automation-1",
208
- "--type",
209
- "http_request",
210
- "--name",
211
- "Notify",
212
- "--method",
213
- "post",
214
- "--url",
215
- "https://example.com/hook",
216
- "--header",
217
- "authorization=Bearer token",
218
- "--body",
219
- '{"ok":true}',
220
- "--content-type",
221
- "application/json",
222
- "--timeout-ms",
223
- "5000",
224
- "--var",
225
- "env=prod",
226
- "--connection",
227
- "connection-1",
228
- ]);
229
-
230
- const init = fetchMock.mock.calls[0]?.[1];
231
-
232
- expect(fetchMock.mock.calls[0]?.[0]).toBe(
233
- "https://api.zinkee.test/api/v2/automations/automation-1/actions",
234
- );
235
- expect(init?.method).toBe("POST");
236
- expect(JSON.parse(String(init?.body))).toEqual({
237
- type: "http_request",
238
- name: "Notify",
239
- config: {
240
- method: "POST",
241
- url: "https://example.com/hook",
242
- headers: {
243
- authorization: "Bearer token",
244
- },
245
- body: {
246
- ok: true,
247
- },
248
- contentType: "application/json",
249
- timeoutMs: 5000,
250
- variables: {
251
- env: "prod",
252
- },
253
- connectionId: "connection-1",
254
- },
255
- });
256
- });
257
-
258
- it("maps record action field mappings to the backend fieldMapping contract", async () => {
259
- const configPath = createApiConfig();
260
- const output = createStdoutBuffer();
261
- const fetchMock = createFetchMock();
262
- fetchMock.mockResolvedValueOnce(jsonResponse({ id: "action-1" }));
263
- const program = buildAutomationsProgram({ configPath, stdout: output.stdout });
264
-
265
- await program.parseAsync([
266
- "node",
267
- "zinkee",
268
- "--json",
269
- "automations",
270
- "actions",
271
- "add",
272
- "automation-1",
273
- "--type",
274
- "create_record",
275
- "--name",
276
- "Create follow-up",
277
- "--target-schema",
278
- "schema-1",
279
- "--map",
280
- "status=pending",
281
- "--map-json",
282
- 'meta={"source":"automation"}',
283
- ]);
284
-
285
- expect(JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body))).toEqual({
286
- type: "create_record",
287
- name: "Create follow-up",
288
- config: {
289
- targetSchemaId: "schema-1",
290
- fieldMapping: {
291
- status: {
292
- rawValue: "pending",
293
- source: null,
294
- },
295
- meta: {
296
- rawValue: {
297
- source: "automation",
298
- },
299
- source: null,
300
- },
301
- },
302
- },
303
- });
304
- });
305
-
306
- it("maps plugin arguments to the backend pluginArgs contract", async () => {
307
- const configPath = createApiConfig();
308
- const output = createStdoutBuffer();
309
- const fetchMock = createFetchMock();
310
- fetchMock.mockResolvedValueOnce(jsonResponse({ id: "action-1" }));
311
- const program = buildAutomationsProgram({ configPath, stdout: output.stdout });
312
-
313
- await program.parseAsync([
314
- "node",
315
- "zinkee",
316
- "--json",
317
- "automations",
318
- "actions",
319
- "add",
320
- "automation-1",
321
- "--type",
322
- "execute_plugin",
323
- "--name",
324
- "Run plugin",
325
- "--plugin",
326
- "demo-plugin",
327
- "--arg",
328
- "mode=safe",
329
- "--arg-json",
330
- 'payload={"dryRun":true}',
331
- ]);
332
-
333
- expect(JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body))).toEqual({
334
- type: "execute_plugin",
335
- name: "Run plugin",
336
- config: {
337
- pluginId: "demo-plugin",
338
- pluginArgs: {
339
- mode: "safe",
340
- payload: {
341
- dryRun: true,
342
- },
343
- },
344
- },
345
- });
346
- });
347
-
348
- it("preserves source-based field mappings for record actions", async () => {
349
- const configPath = createApiConfig();
350
- const output = createStdoutBuffer();
351
- const fetchMock = createFetchMock();
352
- fetchMock.mockResolvedValueOnce(jsonResponse({ id: "action-1" }));
353
- const program = buildAutomationsProgram({ configPath, stdout: output.stdout });
354
-
355
- await program.parseAsync([
356
- "node",
357
- "zinkee",
358
- "--json",
359
- "automations",
360
- "actions",
361
- "add",
362
- "automation-1",
363
- "--type",
364
- "create_record",
365
- "--name",
366
- "Create from trigger",
367
- "--target-schema",
368
- "schema-1",
369
- "--map-json",
370
- 'owner={"rawValue":null,"source":{"type":"TRIGGER","id":"f1e1d000-0000-0000-0000-000000000005"}}',
371
- ]);
372
-
373
- expect(JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body))).toEqual({
374
- type: "create_record",
375
- name: "Create from trigger",
376
- config: {
377
- targetSchemaId: "schema-1",
378
- fieldMapping: {
379
- owner: {
380
- rawValue: null,
381
- source: {
382
- type: "TRIGGER",
383
- id: "f1e1d000-0000-0000-0000-000000000005",
384
- },
385
- },
386
- },
387
- },
388
- });
389
- });
390
-
391
- it("maps search-record conditions to structured values", async () => {
392
- const configPath = createApiConfig();
393
- const output = createStdoutBuffer();
394
- const fetchMock = createFetchMock();
395
- fetchMock.mockResolvedValueOnce(jsonResponse({ id: "action-1" }));
396
- const program = buildAutomationsProgram({ configPath, stdout: output.stdout });
397
-
398
- await program.parseAsync([
399
- "node",
400
- "zinkee",
401
- "--json",
402
- "automations",
403
- "actions",
404
- "add",
405
- "automation-1",
406
- "--type",
407
- "search_records",
408
- "--name",
409
- "Search related",
410
- "--target-schema",
411
- "schema-1",
412
- "--condition-json",
413
- '{"field":"status","comparator":"eq","values":[{"rawValue":null,"source":{"type":"TRIGGER","id":"f1e1d000-0000-0000-0000-000000000005"}}]}',
414
- ]);
415
-
416
- expect(JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body))).toEqual({
417
- type: "search_records",
418
- name: "Search related",
419
- config: {
420
- targetSchemaId: "schema-1",
421
- conditions: [
422
- {
423
- field: "status",
424
- comparator: "eq",
425
- values: [
426
- {
427
- rawValue: null,
428
- source: {
429
- type: "TRIGGER",
430
- id: "f1e1d000-0000-0000-0000-000000000005",
431
- },
432
- },
433
- ],
434
- },
435
- ],
436
- },
437
- });
438
- });
439
-
440
- it("omits conditions when update_record uses the trigger record", async () => {
441
- const configPath = createApiConfig();
442
- const output = createStdoutBuffer();
443
- const fetchMock = createFetchMock();
444
- fetchMock.mockResolvedValueOnce(jsonResponse({ id: "action-1" }));
445
- const program = buildAutomationsProgram({ configPath, stdout: output.stdout });
446
-
447
- await program.parseAsync([
448
- "node",
449
- "zinkee",
450
- "--json",
451
- "automations",
452
- "actions",
453
- "add",
454
- "automation-1",
455
- "--type",
456
- "update_record",
457
- "--name",
458
- "Update trigger record",
459
- "--target-schema",
460
- "schema-1",
461
- "--map",
462
- "status=done",
463
- "--condition",
464
- "status:eq:open",
465
- "--use-trigger-record",
466
- ]);
467
-
468
- expect(JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body))).toEqual({
469
- type: "update_record",
470
- name: "Update trigger record",
471
- config: {
472
- targetSchemaId: "schema-1",
473
- fieldMapping: {
474
- status: {
475
- rawValue: "done",
476
- source: null,
477
- },
478
- },
479
- useTriggerRecord: true,
480
- },
481
- });
482
- });
483
-
484
- it("builds send_message actions with wrapped fieldMapping (recipients/subject/body)", async () => {
485
- const configPath = createApiConfig();
486
- const output = createStdoutBuffer();
487
- const fetchMock = createFetchMock();
488
- fetchMock.mockResolvedValueOnce(jsonResponse({ id: "action-1" }));
489
- const program = buildAutomationsProgram({ configPath, stdout: output.stdout });
490
-
491
- await program.parseAsync([
492
- "node",
493
- "zinkee",
494
- "--json",
495
- "automations",
496
- "actions",
497
- "add",
498
- "automation-1",
499
- "--type",
500
- "send_message",
501
- "--name",
502
- "Notify customer",
503
- "--to",
504
- "customer@example.com",
505
- "--subject",
506
- "Your order status has changed",
507
- "--message",
508
- "Hello, your order is now processed.",
509
- ]);
510
-
511
- expect(JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body))).toEqual({
512
- type: "send_message",
513
- name: "Notify customer",
514
- config: {
515
- fieldMapping: {
516
- recipients: {
517
- rawValue: "customer@example.com",
518
- source: null,
519
- },
520
- subject: {
521
- rawValue: "Your order status has changed",
522
- source: null,
523
- },
524
- body: {
525
- rawValue: "Hello, your order is now processed.",
526
- source: null,
527
- },
528
- },
529
- },
530
- });
531
- });
532
-
533
- it("wraps send_message --map values in {rawValue, source} like other field mappings", async () => {
534
- const configPath = createApiConfig();
535
- const output = createStdoutBuffer();
536
- const fetchMock = createFetchMock();
537
- fetchMock.mockResolvedValueOnce(jsonResponse({ id: "action-1" }));
538
- const program = buildAutomationsProgram({ configPath, stdout: output.stdout });
539
-
540
- await program.parseAsync([
541
- "node",
542
- "zinkee",
543
- "--json",
544
- "automations",
545
- "actions",
546
- "add",
547
- "automation-1",
548
- "--type",
549
- "send_message",
550
- "--name",
551
- "Notify customer",
552
- "--to",
553
- "customer@example.com",
554
- "--map-json",
555
- 'metadata={"orderId":"ORD-1"}',
556
- ]);
557
-
558
- expect(JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body))).toEqual({
559
- type: "send_message",
560
- name: "Notify customer",
561
- config: {
562
- fieldMapping: {
563
- recipients: {
564
- rawValue: "customer@example.com",
565
- source: null,
566
- },
567
- metadata: {
568
- rawValue: { orderId: "ORD-1" },
569
- source: null,
570
- },
571
- },
572
- },
573
- });
574
- });
575
-
576
- it("builds generate_document actions with template, schema, and file-field ids", async () => {
577
- const configPath = createApiConfig();
578
- const output = createStdoutBuffer();
579
- const fetchMock = createFetchMock();
580
- fetchMock.mockResolvedValueOnce(jsonResponse({ id: "action-1" }));
581
- const program = buildAutomationsProgram({ configPath, stdout: output.stdout });
582
-
583
- await program.parseAsync([
584
- "node",
585
- "zinkee",
586
- "--json",
587
- "automations",
588
- "actions",
589
- "add",
590
- "automation-1",
591
- "--type",
592
- "generate_document",
593
- "--name",
594
- "Generate invoice PDF",
595
- "--document-template",
596
- "6c08673e-f8a6-44d2-8c91-d09eca911f8f",
597
- "--target-schema",
598
- "68d2c7be-4392-42de-b5ae-625ad79cfc3c",
599
- "--target-file-field",
600
- "c8edbde8-a361-4e9f-a39a-2e88971f21a7",
601
- ]);
602
-
603
- expect(JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body))).toEqual({
604
- type: "generate_document",
605
- name: "Generate invoice PDF",
606
- config: {
607
- documentTemplateId: "6c08673e-f8a6-44d2-8c91-d09eca911f8f",
608
- targetSchemaId: "68d2c7be-4392-42de-b5ae-625ad79cfc3c",
609
- targetFileFieldId: "c8edbde8-a361-4e9f-a39a-2e88971f21a7",
610
- },
611
- });
612
- });
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
-
754
- it("rejects source-based trigger conditions", async () => {
755
- const configPath = createApiConfig();
756
- const output = createStdoutBuffer();
757
- const fetchMock = createFetchMock();
758
- const program = buildAutomationsProgram({ configPath, stdout: output.stdout });
759
-
760
- await expect(
761
- program.parseAsync([
762
- "node",
763
- "zinkee",
764
- "--json",
765
- "automations",
766
- "trigger",
767
- "set",
768
- "automation-1",
769
- "--trigger-type",
770
- "record_changed",
771
- "--trigger-schema",
772
- "schema-1",
773
- "--trigger-field",
774
- "field-1",
775
- "--condition-json",
776
- '{"field":"status","comparator":"eq","values":[{"rawValue":null,"source":{"type":"TRIGGER","id":"f1e1d000-0000-0000-0000-000000000005"}}]}',
777
- ]),
778
- ).rejects.toThrowError('Expected trigger condition 1 value 1 to use only literal "rawValue" values.');
779
-
780
- expect(fetchMock).not.toHaveBeenCalled();
781
- });
782
-
783
- it("sets flow payloads from repeated entry and transition flags", async () => {
784
- const configPath = createApiConfig();
785
- const output = createStdoutBuffer();
786
- const fetchMock = createFetchMock();
787
- fetchMock.mockResolvedValueOnce(
788
- jsonResponse({
789
- id: "automation-1",
790
- }),
791
- );
792
- const program = buildAutomationsProgram({ configPath, stdout: output.stdout });
793
-
794
- await program.parseAsync([
795
- "node",
796
- "zinkee",
797
- "--json",
798
- "automations",
799
- "flow",
800
- "set",
801
- "automation-1",
802
- "--entry",
803
- "action-1",
804
- "--entry",
805
- "action-2",
806
- "--transition",
807
- "action-1:action-2",
808
- "--transition",
809
- "action-2:action-3",
810
- ]);
811
-
812
- const init = fetchMock.mock.calls[0]?.[1];
813
-
814
- expect(fetchMock.mock.calls[0]?.[0]).toBe(
815
- "https://api.zinkee.test/api/v2/automations/automation-1/flow",
816
- );
817
- expect(init?.method).toBe("PUT");
818
- expect(JSON.parse(String(init?.body))).toEqual({
819
- entryActionIds: ["action-1", "action-2"],
820
- transitions: [
821
- { fromActionId: "action-1", toActionId: "action-2" },
822
- { fromActionId: "action-2", toActionId: "action-3" },
823
- ],
824
- });
825
- });
826
-
827
- it("blocks write operations in read-only mode", async () => {
828
- const configPath = createApiConfig();
829
- const fetchMock = createFetchMock();
830
- const program = buildAutomationsProgram({ configPath });
831
-
832
- await expect(
833
- program.parseAsync([
834
- "node",
835
- "zinkee",
836
- "--read-only",
837
- "automations",
838
- "trigger",
839
- "set",
840
- "automation-1",
841
- "--trigger-type",
842
- "scheduled",
843
- "--cron",
844
- "0 * * * *",
845
- ]),
846
- ).rejects.toThrowError(
847
- 'Command "automations trigger set" is not allowed in read-only mode.',
848
- );
849
-
850
- expect(fetchMock).not.toHaveBeenCalled();
851
- });
852
-
853
- it("updates automations using the backend patch contract with set and unset", async () => {
854
- const configPath = createApiConfig();
855
- const output = createStdoutBuffer();
856
- const fetchMock = createFetchMock();
857
- fetchMock.mockResolvedValueOnce(
858
- jsonResponse({
859
- id: "automation-1",
860
- name: "Lead welcome",
861
- }),
862
- );
863
- const program = buildAutomationsProgram({ configPath, stdout: output.stdout });
864
-
865
- await program.parseAsync([
866
- "node",
867
- "zinkee",
868
- "--json",
869
- "automations",
870
- "update",
871
- "automation-1",
872
- "--name",
873
- "Lead welcome",
874
- "--raw",
875
- '{"description":"Updated automation","unset":["archivedAt"]}',
876
- ]);
877
-
878
- expect(fetchMock.mock.calls[0]?.[0]).toBe(
879
- "https://api.zinkee.test/api/v2/automations/automation-1",
880
- );
881
- expect(JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body))).toEqual({
882
- set: {
883
- description: "Updated automation",
884
- name: "Lead welcome",
885
- },
886
- unset: ["archivedAt"],
887
- });
888
- expect(JSON.parse(output.read())).toEqual({
889
- data: {
890
- id: "automation-1",
891
- name: "Lead welcome",
892
- },
893
- meta: {
894
- command: "automations update",
895
- configPath,
896
- },
897
- });
898
- });
899
-
900
- it("updates connections deterministically for nested config paths", async () => {
901
- const configPath = createApiConfig();
902
- const output = createStdoutBuffer();
903
- const fetchMock = createFetchMock();
904
- fetchMock.mockResolvedValueOnce(
905
- jsonResponse({
906
- id: "connection-1",
907
- name: "Primary",
908
- }),
909
- );
910
- const program = buildAutomationsProgram({ configPath, stdout: output.stdout });
911
-
912
- await program.parseAsync([
913
- "node",
914
- "zinkee",
915
- "--json",
916
- "automations",
917
- "connections",
918
- "update",
919
- "connection-1",
920
- "--set",
921
- "config.baseUrl=https://api.example.com",
922
- "--set",
923
- "config.auth.token=secret",
924
- "--set",
925
- "name=Primary",
926
- "--unset",
927
- "config.legacy",
928
- "--raw",
929
- '{"set":{"config":{"region":"eu"}}, "unset":["config.baseUrl","oldField"]}',
930
- ]);
931
-
932
- expect(fetchMock.mock.calls[0]?.[0]).toBe(
933
- "https://api.zinkee.test/api/v2/automation-connections/connection-1",
934
- );
935
- expect(JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body))).toEqual({
936
- set: {
937
- config: {
938
- region: "eu",
939
- baseUrl: "https://api.example.com",
940
- auth: {
941
- token: "secret",
942
- },
943
- },
944
- name: "Primary",
945
- },
946
- unset: ["oldField", "config.legacy"],
947
- });
948
- });
949
-
950
- it("uses backend webhook endpoint field names", async () => {
951
- const configPath = createApiConfig();
952
- const output = createStdoutBuffer();
953
- const fetchMock = createFetchMock();
954
- fetchMock.mockResolvedValueOnce(jsonResponse({ id: "webhook-1" }));
955
- const program = buildAutomationsProgram({ configPath, stdout: output.stdout });
956
-
957
- await program.parseAsync([
958
- "node",
959
- "zinkee",
960
- "--json",
961
- "automations",
962
- "webhook",
963
- "set",
964
- "automation-1",
965
- "--active",
966
- "--idempotency-key-path",
967
- "$.id",
968
- "--event-type-path",
969
- "$.type",
970
- "--allowed-event-type",
971
- "frontend.demo.created",
972
- "--field-mapping",
973
- '{"variable":"external_id","jsonPath":"$.data.id"}',
974
- ]);
975
-
976
- expect(JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body))).toEqual({
977
- active: true,
978
- idempotencyKeyJsonPath: "$.id",
979
- eventTypeJsonPath: "$.type",
980
- allowedEventTypes: ["frontend.demo.created"],
981
- fieldMappings: [
982
- {
983
- variable: "external_id",
984
- jsonPath: "$.data.id",
985
- },
986
- ],
987
- });
988
- });
989
-
990
- it("uses secret instead of secrets when creating connections", async () => {
991
- const configPath = createApiConfig();
992
- const output = createStdoutBuffer();
993
- const fetchMock = createFetchMock();
994
- fetchMock.mockResolvedValueOnce(jsonResponse({ id: "connection-1" }));
995
- const program = buildAutomationsProgram({ configPath, stdout: output.stdout });
996
-
997
- await program.parseAsync([
998
- "node",
999
- "zinkee",
1000
- "--json",
1001
- "automations",
1002
- "connections",
1003
- "create",
1004
- "--name",
1005
- "ERP API Key",
1006
- "--type",
1007
- "api_key",
1008
- "--scope",
1009
- "workspace",
1010
- "--config",
1011
- '{"headerName":"Authorization"}',
1012
- "--secret",
1013
- "apiKey=super-secret",
1014
- "--active",
1015
- ]);
1016
-
1017
- expect(JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body))).toEqual({
1018
- name: "ERP API Key",
1019
- type: "api_key",
1020
- scope: "workspace",
1021
- config: {
1022
- headerName: "Authorization",
1023
- },
1024
- secret: {
1025
- apiKey: "super-secret",
1026
- },
1027
- active: true,
1028
- });
1029
- });
1030
- });