zinkee 0.1.44 → 0.1.46

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-67U5IRBS.js} +866 -1141
  5. package/dist/examples-67U5IRBS.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
package/src/config.ts DELETED
@@ -1,294 +0,0 @@
1
- import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { dirname, join } from "node:path";
4
-
5
- import { parseTOML, stringifyTOML } from "confbox/toml";
6
-
7
- import type { AppConfig, ProfileConfig, RuntimeOverrides, ResolvedProfile } from "./types.js";
8
-
9
- export const defaultConfigPath = join(homedir(), ".config", "zinkee", "config.toml");
10
- export const redactedSecretMask = "********";
11
- export const ENV_API_KEY = "ZINKEE_API_KEY";
12
- export const ENV_BASE_URL = "ZINKEE_API_BASE_URL";
13
- export const ENV_PROFILE = "ZINKEE_PROFILE";
14
- export const DEFAULT_PRODUCTION_BASE_URL = "https://api-z2.zinkee.com";
15
-
16
- export class ProfileResolutionError extends Error {
17
- override name = "ProfileResolutionError";
18
-
19
- constructor(message: string) {
20
- super(message);
21
- }
22
- }
23
-
24
- function isRecord(value: unknown): value is Record<string, unknown> {
25
- return typeof value === "object" && value !== null && !Array.isArray(value);
26
- }
27
-
28
- function toStringValue(value: unknown): string | undefined {
29
- return typeof value === "string" ? value : undefined;
30
- }
31
-
32
- function toNonEmptyString(value: string | undefined): string | undefined {
33
- const normalized = value?.trim();
34
- return normalized ? normalized : undefined;
35
- }
36
-
37
- function parseProfileConfig(value: unknown): ProfileConfig {
38
- const profile: ProfileConfig = {};
39
-
40
- if (!isRecord(value)) {
41
- return profile;
42
- }
43
-
44
- for (const [key, rawValue] of Object.entries(value)) {
45
- if (key === "api_key") {
46
- profile.apiKey = toStringValue(rawValue);
47
- continue;
48
- }
49
-
50
- if (key === "base_url") {
51
- profile.baseUrl = toStringValue(rawValue);
52
- continue;
53
- }
54
-
55
- profile[key] = rawValue;
56
- }
57
-
58
- return profile;
59
- }
60
-
61
- export function parseConfig(toml: string): AppConfig {
62
- const parsed = parseTOML<unknown>(toml);
63
- const config: AppConfig = { profiles: {} };
64
-
65
- if (!isRecord(parsed)) {
66
- return config;
67
- }
68
-
69
- for (const [key, value] of Object.entries(parsed)) {
70
- if (key === "default_profile") {
71
- config.defaultProfile = toStringValue(value);
72
- continue;
73
- }
74
-
75
- if (key === "profiles") {
76
- if (!isRecord(value)) {
77
- continue;
78
- }
79
-
80
- for (const [profileName, profileValue] of Object.entries(value)) {
81
- config.profiles[profileName] = parseProfileConfig(profileValue);
82
- }
83
-
84
- continue;
85
- }
86
-
87
- config[key] = value;
88
- }
89
-
90
- return config;
91
- }
92
-
93
- export function readConfig(filePath: string = defaultConfigPath): AppConfig {
94
- return parseConfig(readFileSync(filePath, "utf8"));
95
- }
96
-
97
- export function readConfigIfExists(filePath: string = defaultConfigPath): AppConfig | undefined {
98
- try {
99
- return readConfig(filePath);
100
- } catch (error) {
101
- if (
102
- typeof error === "object" &&
103
- error !== null &&
104
- "code" in error &&
105
- error.code === "ENOENT"
106
- ) {
107
- return undefined;
108
- }
109
-
110
- throw error;
111
- }
112
- }
113
-
114
- export function readRuntimeConfig(filePath: string = defaultConfigPath): AppConfig {
115
- return readConfigIfExists(filePath) ?? { profiles: {} };
116
- }
117
-
118
- function serializeProfileConfig(profile: ProfileConfig): Record<string, unknown> {
119
- const serialized: Record<string, unknown> = {};
120
-
121
- for (const [key, value] of Object.entries(profile)) {
122
- if (value === undefined) {
123
- continue;
124
- }
125
-
126
- if (key === "apiKey") {
127
- serialized.api_key = value;
128
- continue;
129
- }
130
-
131
- if (key === "baseUrl") {
132
- serialized.base_url = value;
133
- continue;
134
- }
135
-
136
- serialized[key] = value;
137
- }
138
-
139
- return serialized;
140
- }
141
-
142
- export function serializeConfig(config: AppConfig): string {
143
- const serialized: Record<string, unknown> = {};
144
-
145
- if (config.defaultProfile !== undefined) {
146
- serialized.default_profile = config.defaultProfile;
147
- }
148
-
149
- for (const [key, value] of Object.entries(config)) {
150
- if (key === "defaultProfile" || key === "profiles" || value === undefined) {
151
- continue;
152
- }
153
-
154
- serialized[key] = value;
155
- }
156
-
157
- const serializedProfiles = Object.fromEntries(
158
- Object.entries(config.profiles).map(([name, profile]) => [name, serializeProfileConfig(profile)]),
159
- );
160
-
161
- if (Object.keys(serializedProfiles).length > 0) {
162
- serialized.profiles = serializedProfiles;
163
- }
164
-
165
- return stringifyTOML(serialized).trimEnd().concat("\n");
166
- }
167
-
168
- export function writeConfig(config: AppConfig, filePath: string = defaultConfigPath): void {
169
- mkdirSync(dirname(filePath), { recursive: true });
170
- writeFileSync(filePath, serializeConfig(config), "utf8");
171
- }
172
-
173
- export function setProfileConfig(
174
- config: AppConfig,
175
- profileName: string,
176
- updates: Partial<ProfileConfig>,
177
- ): AppConfig {
178
- return {
179
- ...config,
180
- profiles: {
181
- ...config.profiles,
182
- [profileName]: {
183
- ...config.profiles[profileName],
184
- ...updates,
185
- },
186
- },
187
- };
188
- }
189
-
190
- export function removeProfileConfig(config: AppConfig, profileName: string): AppConfig {
191
- const nextProfiles = { ...config.profiles };
192
- delete nextProfiles[profileName];
193
-
194
- return {
195
- ...config,
196
- defaultProfile:
197
- config.defaultProfile === profileName
198
- ? undefined
199
- : config.defaultProfile,
200
- profiles: nextProfiles,
201
- };
202
- }
203
-
204
- export function setDefaultProfile(config: AppConfig, profileName: string): AppConfig {
205
- return {
206
- ...config,
207
- defaultProfile: profileName,
208
- };
209
- }
210
-
211
- export function redactSecret(value: string | undefined): string | undefined {
212
- if (value === undefined) {
213
- return undefined;
214
- }
215
-
216
- if (value.length <= 4) {
217
- return redactedSecretMask;
218
- }
219
-
220
- return `${redactedSecretMask}${value.slice(-4)}`;
221
- }
222
-
223
- export function redactProfile(profile: ProfileConfig): ProfileConfig {
224
- return {
225
- ...profile,
226
- apiKey: redactSecret(profile.apiKey),
227
- };
228
- }
229
-
230
- export function redactConfig(config: AppConfig): AppConfig {
231
- return {
232
- ...config,
233
- profiles: Object.fromEntries(
234
- Object.entries(config.profiles).map(([name, profile]) => [name, redactProfile(profile)]),
235
- ),
236
- };
237
- }
238
-
239
- export function resolveProfile(
240
- config: AppConfig,
241
- overrides: Pick<RuntimeOverrides, "profile" | "baseUrl" | "apiKey"> = {},
242
- ): ResolvedProfile {
243
- const profileName = overrides.profile ?? config.defaultProfile;
244
- if (profileName) {
245
- const profile = config.profiles[profileName];
246
-
247
- if (!profile) {
248
- const label = overrides.profile ? "Profile" : "Default profile";
249
- throw new ProfileResolutionError(`${label} "${profileName}" not found in config.`);
250
- }
251
-
252
- return {
253
- ...profile,
254
- name: profileName,
255
- baseUrl: overrides.baseUrl ?? profile.baseUrl,
256
- apiKey: overrides.apiKey ?? profile.apiKey,
257
- };
258
- }
259
-
260
- throw new ProfileResolutionError("No profile could be resolved from config.");
261
- }
262
-
263
- export function validateBaseUrl(value: string, source: string = "Base URL"): string {
264
- let parsed: URL;
265
-
266
- try {
267
- parsed = new URL(value);
268
- } catch {
269
- throw new ProfileResolutionError(`${source} must be an absolute http(s) URL.`);
270
- }
271
-
272
- if (!["http:", "https:"].includes(parsed.protocol) || !parsed.host) {
273
- throw new ProfileResolutionError(`${source} must be an absolute http(s) URL.`);
274
- }
275
-
276
- return value.replace(/\/+$/, "");
277
- }
278
-
279
- export function readEnvApiKey(env: NodeJS.ProcessEnv = process.env): string | undefined {
280
- return toNonEmptyString(env[ENV_API_KEY]);
281
- }
282
-
283
- export function readEnvBaseUrl(env: NodeJS.ProcessEnv = process.env): string | undefined {
284
- const baseUrl = toNonEmptyString(env[ENV_BASE_URL]);
285
- if (!baseUrl) {
286
- return undefined;
287
- }
288
-
289
- return validateBaseUrl(baseUrl, `Environment variable ${ENV_BASE_URL}`);
290
- }
291
-
292
- export function readEnvProfile(env: NodeJS.ProcessEnv = process.env): string | undefined {
293
- return toNonEmptyString(env[ENV_PROFILE]);
294
- }
package/src/index.test.ts DELETED
@@ -1,217 +0,0 @@
1
- import { mkdtempSync, rmSync, symlinkSync } from "node:fs";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
- import { fileURLToPath, pathToFileURL } from "node:url";
5
-
6
- import { afterEach, describe, expect, it } from "vitest";
7
-
8
- import { extractExampleRequest, showExamples } from "./cli-examples.js";
9
- import { isExecutedAsEntry } from "./cli-runner.js";
10
- import { buildProgram } from "./program.js";
11
-
12
- const tempDirs: string[] = [];
13
-
14
- describe("project bootstrap", () => {
15
- afterEach(() => {
16
- while (tempDirs.length > 0) {
17
- rmSync(tempDirs.pop()!, { force: true, recursive: true });
18
- }
19
- });
20
-
21
- it("loads the CLI entry module", async () => {
22
- const mod = await import("./index.js");
23
-
24
- expect(mod).toBeTruthy();
25
- expect(mod.buildProgram).toBeTypeOf("function");
26
- });
27
-
28
- it("registers the global --example option", () => {
29
- const program = buildProgram();
30
-
31
- expect(program.options.some((option) => option.long === "--example")).toBe(true);
32
- });
33
-
34
- it("extracts example requests before commander parsing", () => {
35
- expect(
36
- extractExampleRequest(["node", "zinkee", "--json", "--example", "records", "query"]),
37
- ).toEqual({
38
- commandName: "records query",
39
- filters: {},
40
- json: true,
41
- });
42
- });
43
-
44
- it("extracts typed schema field examples without treating schema args as part of the command", () => {
45
- expect(
46
- extractExampleRequest([
47
- "node",
48
- "zinkee",
49
- "--profile",
50
- "local",
51
- "--example",
52
- "schemas",
53
- "fields",
54
- "create",
55
- "contacts",
56
- "--type",
57
- "option",
58
- ]),
59
- ).toEqual({
60
- commandName: "schemas fields create",
61
- filters: {
62
- type: "option",
63
- },
64
- json: false,
65
- });
66
- });
67
-
68
- it("extracts example requests when --example appears at the end of argv", () => {
69
- expect(
70
- extractExampleRequest(["node", "zinkee", "schemas", "list", "--example"]),
71
- ).toEqual({
72
- commandName: "schemas list",
73
- filters: {},
74
- json: false,
75
- });
76
- });
77
-
78
- it("extracts example filters when --example appears after the filter flags", () => {
79
- expect(
80
- extractExampleRequest([
81
- "node",
82
- "zinkee",
83
- "schemas",
84
- "fields",
85
- "create",
86
- "contacts",
87
- "--type",
88
- "option",
89
- "--example",
90
- ]),
91
- ).toEqual({
92
- commandName: "schemas fields create",
93
- filters: { type: "option" },
94
- json: false,
95
- });
96
- });
97
-
98
- it("extracts example requests regardless of --example position", () => {
99
- expect(
100
- extractExampleRequest([
101
- "node",
102
- "zinkee",
103
- "schemas",
104
- "list",
105
- "--profile",
106
- "staging",
107
- "--example",
108
- "--json",
109
- ]),
110
- ).toEqual({
111
- commandName: "schemas list",
112
- filters: {},
113
- json: true,
114
- });
115
- });
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
-
143
- it("shows field examples filtered by type", async () => {
144
- let output = "";
145
-
146
- await showExamples(
147
- "schemas fields create",
148
- false,
149
- {
150
- write(chunk: string) {
151
- output += chunk;
152
- return true;
153
- },
154
- },
155
- { type: "option" },
156
- );
157
-
158
- expect(output).toContain('zinkee --json schemas fields create contacts --raw');
159
- expect(output).toContain('"type":"OptionField"');
160
- expect(output).not.toContain('"type":"TextField"');
161
- expect(output).toContain("Option field (select dropdown)");
162
- });
163
-
164
- it("shows every backend number variant, including currency formatting", async () => {
165
- let output = "";
166
-
167
- await showExamples(
168
- "schemas fields create",
169
- false,
170
- {
171
- write(chunk: string) {
172
- output += chunk;
173
- return true;
174
- },
175
- },
176
- { type: "number" },
177
- );
178
-
179
- expect(output).toContain('"slug":"quantity"');
180
- expect(output).toContain('"slug":"amount-eur"');
181
- expect(output).toContain('"slug":"amount-usd"');
182
- expect(output).toContain('"defaultValue":"0"');
183
- expect(output).toContain('"symbolEnabled":true');
184
- expect(output).toContain('"symbolPosition":"before"');
185
- expect(output).toContain('"symbolPosition":"right"');
186
- expect(output).toContain('"format":"percentage"');
187
- expect(output).toContain('"durationFormat":"HH:MM"');
188
- });
189
-
190
- it("shows automation examples with descriptions and equivalent payloads", async () => {
191
- let output = "";
192
-
193
- await showExamples("automations create", false, {
194
- write(chunk: string) {
195
- output += chunk;
196
- return true;
197
- },
198
- });
199
-
200
- expect(output).toContain("Create a scheduled automation");
201
- expect(output).toContain("Equivalent JSON payload:");
202
- expect(output).toContain('"type": "scheduled"');
203
- expect(output).toContain('"cronExpression": "0 15 10 * * 1"');
204
- });
205
-
206
- it("treats symlinked executables as the CLI entrypoint", () => {
207
- const tempDir = mkdtempSync(join(tmpdir(), "zinkee-entry-"));
208
- const realEntry = join(tempDir, "real-entry.js");
209
- const linkedEntry = join(tempDir, "zinkee");
210
- tempDirs.push(tempDir);
211
-
212
- symlinkSync(fileURLToPath(import.meta.url), realEntry);
213
- symlinkSync(realEntry, linkedEntry);
214
-
215
- expect(isExecutedAsEntry(linkedEntry, pathToFileURL(realEntry).href)).toBe(true);
216
- });
217
- });
package/src/index.ts DELETED
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { runCli, isExecutedAsEntry } from "./cli-runner.js";
4
- export { buildProgram, type BuildProgramOptions } from "./program.js";
5
-
6
- if (isExecutedAsEntry(process.argv[1], import.meta.url)) {
7
- await runCli(process.argv);
8
- }
@@ -1,95 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- import { parseExpression } from "./expressions.js";
4
-
5
- describe("parseExpression", () => {
6
- describe("scalar comparators", () => {
7
- it.each([
8
- ["eq", "status eq active", "status", "active"],
9
- ["neq", "status neq archived", "status", "archived"],
10
- ["gt", "amount gt 1000", "amount", "1000"],
11
- ["gte", "amount gte 500", "amount", "500"],
12
- ["lt", "amount lt 100", "amount", "100"],
13
- ["lte", "due-date lte 2026-12-31T23:59:59Z", "due-date", "2026-12-31T23:59:59Z"],
14
- ["contains", "contact-name contains Lovelace", "contact-name", "Lovelace"],
15
- ])("parses %s expressions", (comparator, input, field, value) => {
16
- expect(parseExpression(input)).toEqual({
17
- field,
18
- comparator,
19
- value,
20
- raw: input,
21
- });
22
- });
23
-
24
- it("preserves spaces inside contains values", () => {
25
- expect(parseExpression("email contains @acme.com")).toEqual({
26
- field: "email",
27
- comparator: "contains",
28
- value: "@acme.com",
29
- raw: "email contains @acme.com",
30
- });
31
- });
32
- });
33
-
34
- describe("unary comparators", () => {
35
- it.each([
36
- ["is_empty", "due-date is_empty", "due-date"],
37
- ["is_not_empty", "contact-email is_not_empty", "contact-email"],
38
- ])("parses %s expressions without a value", (comparator, input, field) => {
39
- expect(parseExpression(input)).toEqual({
40
- field,
41
- comparator,
42
- value: null,
43
- raw: input,
44
- });
45
- });
46
- });
47
-
48
- describe("list comparators", () => {
49
- it("parses in expressions with a comma-separated value", () => {
50
- expect(parseExpression("ownerId in a,b,c")).toEqual({
51
- field: "ownerId",
52
- comparator: "in",
53
- value: ["a", "b", "c"],
54
- raw: "ownerId in a,b,c",
55
- });
56
- });
57
- });
58
-
59
- describe("delimiters", () => {
60
- it("parses colon-delimited expressions", () => {
61
- expect(parseExpression("amount:gt:1000")).toEqual({
62
- field: "amount",
63
- comparator: "gt",
64
- value: "1000",
65
- raw: "amount:gt:1000",
66
- });
67
- });
68
- });
69
-
70
- describe("errors", () => {
71
- it("rejects unknown comparators", () => {
72
- expect(() => parseExpression("status ne archived")).toThrow(
73
- 'Unsupported comparator "ne" in expression "status ne archived".',
74
- );
75
- });
76
-
77
- it("rejects scalar comparators without a value", () => {
78
- expect(() => parseExpression("status eq")).toThrow(
79
- 'Comparator "eq" requires a value.',
80
- );
81
- });
82
-
83
- it("rejects unary comparators that carry a value", () => {
84
- expect(() => parseExpression("due-date is_empty 2026-01-01")).toThrow(
85
- 'Comparator "is_empty" does not accept a value.',
86
- );
87
- });
88
-
89
- it("rejects in expressions with no values", () => {
90
- expect(() => parseExpression("ownerId in")).toThrow(
91
- 'Comparator "in" requires at least one value.',
92
- );
93
- });
94
- });
95
- });