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.
- package/README.md +4 -0
- package/dist/chunk-MHEVRMDX.js +1230 -0
- package/dist/chunk-MHEVRMDX.js.map +1 -0
- package/{src/utils/examples.ts → dist/examples-E6FFEVBE.js} +856 -1141
- package/dist/examples-E6FFEVBE.js.map +1 -0
- package/dist/index.js +1145 -8456
- package/dist/index.js.map +1 -1
- package/package.json +6 -3
- package/.github/workflows/npm-publish.yml +0 -77
- package/.github/workflows/pr-checks.yml +0 -36
- package/AGENTS.md +0 -110
- package/docs/cli-contract.md +0 -41
- package/docs/npm-release.md +0 -64
- package/docs/superpowers/plans/2026-03-24-zinkee-cli-implementation.md +0 -837
- package/docs/superpowers/plans/2026-03-25-cli-backend-error-contract.md +0 -503
- package/docs/superpowers/plans/2026-03-26-display-freeform-create.md +0 -389
- package/docs/superpowers/specs/2026-03-24-zinkee-cli-backend-blockers.md +0 -172
- package/docs/superpowers/specs/2026-03-24-zinkee-cli-design.md +0 -1576
- package/docs/superpowers/specs/2026-03-24-zinkee-cli-e2e-checklist.md +0 -215
- package/docs/superpowers/specs/2026-03-24-zinkee-cli-e2e-design.md +0 -492
- package/docs/superpowers/specs/2026-03-24-zinkee-cli-e2e-status.md +0 -307
- package/docs/superpowers/specs/2026-07-30-cli-pr-checks-design.md +0 -37
- package/src/api/automations.ts +0 -404
- package/src/api/comments.ts +0 -51
- package/src/api/displays.ts +0 -337
- package/src/api/document-templates.ts +0 -110
- package/src/api/files.ts +0 -50
- package/src/api/formulas.test.ts +0 -65
- package/src/api/formulas.ts +0 -67
- package/src/api/logs.ts +0 -34
- package/src/api/navigation.ts +0 -70
- package/src/api/records.ts +0 -184
- package/src/api/schemas.ts +0 -148
- package/src/api/teamspace.ts +0 -110
- package/src/cli-examples.ts +0 -130
- package/src/cli-runner.ts +0 -95
- package/src/client.test.ts +0 -189
- package/src/client.ts +0 -269
- package/src/command-registry.ts +0 -882
- package/src/commands/automations.test.ts +0 -1030
- package/src/commands/automations.ts +0 -2102
- package/src/commands/comments.test.ts +0 -214
- package/src/commands/comments.ts +0 -303
- package/src/commands/config.test.ts +0 -81
- package/src/commands/config.ts +0 -150
- package/src/commands/displays.test.ts +0 -1105
- package/src/commands/displays.ts +0 -1442
- package/src/commands/document-templates.test.ts +0 -569
- package/src/commands/document-templates.ts +0 -563
- package/src/commands/files.test.ts +0 -284
- package/src/commands/files.ts +0 -280
- package/src/commands/formulas.test.ts +0 -194
- package/src/commands/formulas.ts +0 -243
- package/src/commands/logs.test.ts +0 -123
- package/src/commands/logs.ts +0 -159
- package/src/commands/navigation.test.ts +0 -211
- package/src/commands/navigation.ts +0 -348
- package/src/commands/profiles.test.ts +0 -191
- package/src/commands/profiles.ts +0 -303
- package/src/commands/records.test.ts +0 -860
- package/src/commands/records.ts +0 -883
- package/src/commands/schemas.test.ts +0 -1252
- package/src/commands/schemas.ts +0 -890
- package/src/commands/teamspace.test.ts +0 -229
- package/src/commands/teamspace.ts +0 -546
- package/src/completion/engine.test.ts +0 -138
- package/src/completion/engine.ts +0 -168
- package/src/completion/install.test.ts +0 -179
- package/src/completion/install.ts +0 -260
- package/src/completion/runtime.ts +0 -150
- package/src/completion/scripts.ts +0 -91
- package/src/config.test.ts +0 -362
- package/src/config.ts +0 -294
- package/src/index.test.ts +0 -217
- package/src/index.ts +0 -8
- package/src/parsers/expressions.test.ts +0 -95
- package/src/parsers/expressions.ts +0 -128
- package/src/parsers/kv.test.ts +0 -35
- package/src/parsers/kv.ts +0 -49
- package/src/parsers/selectors.test.ts +0 -23
- package/src/parsers/selectors.ts +0 -29
- package/src/program.ts +0 -64
- package/src/runtime-context.ts +0 -103
- package/src/types.ts +0 -76
- package/src/utils/argv-rewrite.test.ts +0 -149
- package/src/utils/argv-rewrite.ts +0 -269
- package/src/utils/errors.test.ts +0 -85
- package/src/utils/errors.ts +0 -191
- package/src/utils/examples.test.ts +0 -374
- package/src/utils/output.test.ts +0 -65
- package/src/utils/output.ts +0 -154
- package/src/utils/schema-fields.ts +0 -1016
- package/tsconfig.json +0 -20
- 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,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
|
-
});
|