zinkee 0.1.0

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 (61) hide show
  1. package/AGENTS.md +46 -0
  2. package/README.md +395 -0
  3. package/dist/index.d.ts +13 -0
  4. package/dist/index.js +7142 -0
  5. package/dist/index.js.map +1 -0
  6. package/package.json +32 -0
  7. package/src/api/automations.ts +403 -0
  8. package/src/api/comments.ts +51 -0
  9. package/src/api/displays.ts +276 -0
  10. package/src/api/files.ts +50 -0
  11. package/src/api/navigation.ts +71 -0
  12. package/src/api/records.ts +167 -0
  13. package/src/api/schemas.ts +130 -0
  14. package/src/api/teamspace.ts +110 -0
  15. package/src/cli-examples.ts +175 -0
  16. package/src/cli-runner.ts +73 -0
  17. package/src/client.test.ts +128 -0
  18. package/src/client.ts +243 -0
  19. package/src/command-registry.ts +749 -0
  20. package/src/commands/automations.test.ts +728 -0
  21. package/src/commands/automations.ts +2013 -0
  22. package/src/commands/comments.test.ts +214 -0
  23. package/src/commands/comments.ts +303 -0
  24. package/src/commands/config.test.ts +81 -0
  25. package/src/commands/config.ts +150 -0
  26. package/src/commands/displays.test.ts +266 -0
  27. package/src/commands/displays.ts +755 -0
  28. package/src/commands/files.test.ts +284 -0
  29. package/src/commands/files.ts +280 -0
  30. package/src/commands/navigation.test.ts +214 -0
  31. package/src/commands/navigation.ts +348 -0
  32. package/src/commands/profiles.test.ts +82 -0
  33. package/src/commands/profiles.ts +281 -0
  34. package/src/commands/records.test.ts +367 -0
  35. package/src/commands/records.ts +726 -0
  36. package/src/commands/schemas.test.ts +748 -0
  37. package/src/commands/schemas.ts +746 -0
  38. package/src/commands/teamspace.test.ts +229 -0
  39. package/src/commands/teamspace.ts +540 -0
  40. package/src/config.test.ts +165 -0
  41. package/src/config.ts +248 -0
  42. package/src/index.test.ts +140 -0
  43. package/src/index.ts +8 -0
  44. package/src/parsers/expressions.test.ts +50 -0
  45. package/src/parsers/expressions.ts +111 -0
  46. package/src/parsers/kv.test.ts +35 -0
  47. package/src/parsers/kv.ts +49 -0
  48. package/src/parsers/selectors.test.ts +23 -0
  49. package/src/parsers/selectors.ts +29 -0
  50. package/src/program.ts +56 -0
  51. package/src/runtime-context.ts +18 -0
  52. package/src/types.ts +71 -0
  53. package/src/utils/errors.test.ts +48 -0
  54. package/src/utils/errors.ts +183 -0
  55. package/src/utils/examples.test.ts +67 -0
  56. package/src/utils/examples.ts +855 -0
  57. package/src/utils/output.test.ts +39 -0
  58. package/src/utils/output.ts +124 -0
  59. package/src/utils/schema-fields.ts +529 -0
  60. package/tsconfig.json +20 -0
  61. package/tsup.config.ts +13 -0
package/src/program.ts ADDED
@@ -0,0 +1,56 @@
1
+ import { createRequire } from "node:module";
2
+
3
+ import { Command } from "commander";
4
+
5
+ import { registerConfigCommands } from "./commands/config.js";
6
+ import { registerAutomationsCommands } from "./commands/automations.js";
7
+ import { registerCommentsCommands } from "./commands/comments.js";
8
+ import { registerDisplaysCommands } from "./commands/displays.js";
9
+ import { registerFilesCommands } from "./commands/files.js";
10
+ import { registerNavigationCommands } from "./commands/navigation.js";
11
+ import { registerProfilesCommands } from "./commands/profiles.js";
12
+ import { registerRecordsCommands } from "./commands/records.js";
13
+ import { registerSchemasCommands } from "./commands/schemas.js";
14
+ import { registerTeamspaceCommands } from "./commands/teamspace.js";
15
+
16
+ const require = createRequire(import.meta.url);
17
+ const { version } = require("../package.json") as { version: string };
18
+
19
+ interface CommandIo {
20
+ write(chunk: string): unknown;
21
+ }
22
+
23
+ export interface BuildProgramOptions {
24
+ configPath?: string;
25
+ stdout?: CommandIo;
26
+ }
27
+
28
+ export function buildProgram(options: BuildProgramOptions = {}): Command {
29
+ const program = new Command();
30
+
31
+ program
32
+ .name("zinkee")
33
+ .description("CLI for Zinkee API v2")
34
+ .version(version)
35
+ .enablePositionalOptions()
36
+ .showHelpAfterError()
37
+ .option("--profile <name>", "select a profile from config")
38
+ .option("--base-url <url>", "override the configured base URL")
39
+ .option("--api-key <token>", "override the configured API key")
40
+ .option("--read-only", "run in read-only mode")
41
+ .option("--example", "show examples for a command")
42
+ .option("--json", "emit JSON output");
43
+
44
+ registerProfilesCommands(program, options);
45
+ registerAutomationsCommands(program, options);
46
+ registerConfigCommands(program, options);
47
+ registerDisplaysCommands(program, options);
48
+ registerFilesCommands(program, options);
49
+ registerNavigationCommands(program, options);
50
+ registerSchemasCommands(program, options);
51
+ registerRecordsCommands(program, options);
52
+ registerCommentsCommands(program, options);
53
+ registerTeamspaceCommands(program, options);
54
+
55
+ return program;
56
+ }
@@ -0,0 +1,18 @@
1
+ import type { AppConfig, RuntimeContext, RuntimeOverrides } from "./types.js";
2
+
3
+ import { resolveProfile } from "./config.js";
4
+
5
+ export function resolveRuntimeContext(
6
+ config: AppConfig,
7
+ overrides: RuntimeOverrides = {},
8
+ ): RuntimeContext {
9
+ const profile = resolveProfile(config, overrides);
10
+
11
+ return {
12
+ profile: profile.name,
13
+ baseUrl: overrides.baseUrl ?? profile.baseUrl,
14
+ apiKey: overrides.apiKey ?? profile.apiKey,
15
+ readOnly: overrides.readOnly ?? false,
16
+ json: overrides.json ?? false,
17
+ };
18
+ }
package/src/types.ts ADDED
@@ -0,0 +1,71 @@
1
+ export interface CliContext {
2
+ argv: string[];
3
+ }
4
+
5
+ export interface ProfileConfig {
6
+ apiKey?: string;
7
+ baseUrl?: string;
8
+ [key: string]: unknown;
9
+ }
10
+
11
+ export interface AppConfig {
12
+ defaultProfile?: string;
13
+ profiles: Record<string, ProfileConfig>;
14
+ [key: string]: unknown;
15
+ }
16
+
17
+ export interface ResolvedProfile extends ProfileConfig {
18
+ name: string;
19
+ }
20
+
21
+ export interface RuntimeOverrides {
22
+ profile?: string;
23
+ baseUrl?: string;
24
+ apiKey?: string;
25
+ readOnly?: boolean;
26
+ json?: boolean;
27
+ }
28
+
29
+ export interface RuntimeContext {
30
+ profile: string;
31
+ baseUrl?: string;
32
+ apiKey?: string;
33
+ readOnly: boolean;
34
+ json: boolean;
35
+ }
36
+
37
+ export type JsonScalar = string | number | boolean | null;
38
+
39
+ export interface JsonObject {
40
+ [key: string]: JsonValue;
41
+ }
42
+
43
+ export type JsonValue = JsonScalar | JsonObject | JsonValue[];
44
+
45
+ export interface JsonEnvelopeMeta {
46
+ [key: string]: unknown;
47
+ }
48
+
49
+ export interface JsonSuccessEnvelope<TData = JsonValue, TMeta extends JsonEnvelopeMeta = JsonEnvelopeMeta> {
50
+ data: TData;
51
+ meta: TMeta;
52
+ }
53
+
54
+ export interface JsonErrorEnvelope<TErr = JsonValue, TMeta extends JsonEnvelopeMeta = JsonEnvelopeMeta> {
55
+ error: TErr;
56
+ meta: TMeta;
57
+ }
58
+
59
+ export type OutputFormat = "json" | "table";
60
+
61
+ export type CommandAccessLevel = "read" | "write" | "destructive";
62
+
63
+ export type CommandOutputCapability = "table-json" | "json-only" | "binary";
64
+
65
+ export interface CommandMeta {
66
+ command: string;
67
+ accessLevel: CommandAccessLevel;
68
+ supportsExample: boolean;
69
+ supportsRaw: boolean;
70
+ output: CommandOutputCapability;
71
+ }
@@ -0,0 +1,48 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { ApiClientError } from "../client.js";
4
+
5
+ import { formatCliErrorJson } from "./errors.js";
6
+
7
+ describe("formatCliErrorJson", () => {
8
+ it("preserves backend reason, action, requestId, and status details", () => {
9
+ const error = new ApiClientError(
10
+ "http_error",
11
+ "Request failed with status 409 for PATCH https://api.example.com/api/v2/automations/1.",
12
+ {
13
+ url: "https://api.example.com/api/v2/automations/1",
14
+ method: "PATCH",
15
+ status: 409,
16
+ responseBody: {
17
+ error: {
18
+ reason: "schema_locked",
19
+ action: "retry_later",
20
+ message: "Schema is locked.",
21
+ },
22
+ meta: {
23
+ requestId: "req_123",
24
+ },
25
+ },
26
+ },
27
+ );
28
+
29
+ expect(JSON.parse(formatCliErrorJson(error, { exitCode: 1 }))).toEqual({
30
+ error: {
31
+ code: "unknown_error",
32
+ message: "Schema is locked.",
33
+ exitCode: 1,
34
+ details: {
35
+ reason: "schema_locked",
36
+ action: "retry_later",
37
+ requestId: "req_123",
38
+ status: 409,
39
+ },
40
+ },
41
+ meta: {
42
+ requestId: "req_123",
43
+ status: 409,
44
+ exitCode: 1,
45
+ },
46
+ });
47
+ });
48
+ });
@@ -0,0 +1,183 @@
1
+ import { ApiClientError } from "../client.js";
2
+ import type { JsonEnvelopeMeta, JsonErrorEnvelope } from "../types.js";
3
+
4
+ import { buildJsonError } from "./output.js";
5
+
6
+ export type CliErrorCode =
7
+ | "unknown_error"
8
+ | "invalid_cli_usage"
9
+ | "configuration_error"
10
+ | "command_not_allowed_in_read_only_mode";
11
+
12
+ export const cliExitCodeByErrorCode = {
13
+ unknown_error: 1,
14
+ invalid_cli_usage: 2,
15
+ configuration_error: 2,
16
+ command_not_allowed_in_read_only_mode: 7,
17
+ } as const satisfies Record<CliErrorCode, number>;
18
+
19
+ export interface CliErrorDetails {
20
+ [key: string]: unknown;
21
+ }
22
+
23
+ export interface CliErrorPayload {
24
+ code: CliErrorCode;
25
+ message: string;
26
+ exitCode: number;
27
+ details?: CliErrorDetails;
28
+ }
29
+
30
+ const errorCodeByName: Record<string, CliErrorCode> = {
31
+ ProfileResolutionError: "configuration_error",
32
+ };
33
+
34
+ export class CliError extends Error {
35
+ override name = "CliError";
36
+
37
+ readonly code: CliErrorCode;
38
+
39
+ readonly exitCode: number;
40
+
41
+ readonly details?: CliErrorDetails;
42
+
43
+ constructor(
44
+ code: CliErrorCode,
45
+ message: string,
46
+ details: CliErrorDetails = {},
47
+ exitCode: number = cliExitCodeByErrorCode[code],
48
+ ) {
49
+ super(message);
50
+ this.code = code;
51
+ this.exitCode = exitCode;
52
+
53
+ if (Object.keys(details).length > 0) {
54
+ this.details = details;
55
+ }
56
+ }
57
+
58
+ toJSON(): CliErrorPayload {
59
+ const payload: CliErrorPayload = {
60
+ code: this.code,
61
+ message: this.message,
62
+ exitCode: this.exitCode,
63
+ };
64
+
65
+ if (this.details) {
66
+ payload.details = this.details;
67
+ }
68
+
69
+ return payload;
70
+ }
71
+ }
72
+
73
+ export function isCliError(error: unknown): error is CliError {
74
+ return error instanceof CliError;
75
+ }
76
+
77
+ function isRecord(value: unknown): value is Record<string, unknown> {
78
+ return typeof value === "object" && value !== null && !Array.isArray(value);
79
+ }
80
+
81
+ function readString(value: unknown): string | undefined {
82
+ return typeof value === "string" && value.trim().length > 0 ? value : undefined;
83
+ }
84
+
85
+ function readNumber(value: unknown): number | undefined {
86
+ return typeof value === "number" ? value : undefined;
87
+ }
88
+
89
+ function extractApiErrorDetails(error: ApiClientError): {
90
+ message: string;
91
+ details: CliErrorDetails;
92
+ meta: JsonEnvelopeMeta;
93
+ } {
94
+ const details: CliErrorDetails = {};
95
+ const meta: JsonEnvelopeMeta = {};
96
+ const responseBody = isRecord(error.responseBody) ? error.responseBody : undefined;
97
+ const errorBody = responseBody?.error;
98
+ const backendError = isRecord(errorBody) ? errorBody : responseBody;
99
+ const backendMeta = isRecord(responseBody?.meta) ? responseBody.meta : undefined;
100
+ const reason =
101
+ readString(backendError?.reason) ??
102
+ readString(backendError?.code);
103
+ const action = readString(backendError?.action);
104
+ const requestId =
105
+ readString(backendMeta?.requestId) ??
106
+ readString(responseBody?.requestId) ??
107
+ readString(backendError?.requestId);
108
+ const status =
109
+ readNumber(backendError?.status) ??
110
+ readNumber(responseBody?.status) ??
111
+ error.status;
112
+ const backendMessage = readString(backendError?.message);
113
+
114
+ if (reason !== undefined) {
115
+ details.reason = reason;
116
+ }
117
+
118
+ if (action !== undefined) {
119
+ details.action = action;
120
+ }
121
+
122
+ if (requestId !== undefined) {
123
+ details.requestId = requestId;
124
+ meta.requestId = requestId;
125
+ }
126
+
127
+ if (status !== undefined) {
128
+ details.status = status;
129
+ meta.status = status;
130
+ }
131
+
132
+ return {
133
+ message: backendMessage ?? error.message,
134
+ details,
135
+ meta,
136
+ };
137
+ }
138
+
139
+ export function toCliError(error: unknown): CliError {
140
+ if (error instanceof CliError) {
141
+ return error;
142
+ }
143
+
144
+ if (error instanceof ApiClientError) {
145
+ const apiError = extractApiErrorDetails(error);
146
+
147
+ return new CliError("unknown_error", apiError.message, apiError.details);
148
+ }
149
+
150
+ if (error instanceof Error) {
151
+ return new CliError(errorCodeByName[error.name] ?? "unknown_error", error.message);
152
+ }
153
+
154
+ return new CliError("unknown_error", "Unknown error");
155
+ }
156
+
157
+ export function getExitCode(error: unknown): number {
158
+ return toCliError(error).exitCode;
159
+ }
160
+
161
+ export function buildJsonCliError<TMeta extends JsonEnvelopeMeta = JsonEnvelopeMeta>(
162
+ error: unknown,
163
+ meta: TMeta = {} as TMeta,
164
+ ): JsonErrorEnvelope<CliErrorPayload, TMeta> {
165
+ const cliError = toCliError(error);
166
+ const apiErrorMeta =
167
+ error instanceof ApiClientError
168
+ ? extractApiErrorDetails(error).meta
169
+ : {};
170
+
171
+ return buildJsonError(cliError.toJSON(), { ...apiErrorMeta, ...meta } as TMeta);
172
+ }
173
+
174
+ export function formatCliErrorText(error: unknown): string {
175
+ return toCliError(error).message;
176
+ }
177
+
178
+ export function formatCliErrorJson<TMeta extends JsonEnvelopeMeta = JsonEnvelopeMeta>(
179
+ error: unknown,
180
+ meta: TMeta = {} as TMeta,
181
+ ): string {
182
+ return JSON.stringify(buildJsonCliError(error, meta));
183
+ }
@@ -0,0 +1,67 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { getCommandExamples } from "./examples.js";
4
+
5
+ describe("getCommandExamples", () => {
6
+ it("returns registered examples for complex commands", () => {
7
+ const examples = getCommandExamples("records query");
8
+
9
+ expect(examples.length).toBeGreaterThan(0);
10
+ expect(examples[0]?.command).toContain("records query");
11
+ });
12
+
13
+ it("returns fallback examples for simple commands", () => {
14
+ const examples = getCommandExamples("schemas list");
15
+
16
+ expect(examples.length).toBeGreaterThan(0);
17
+ expect(examples[0]?.command).toContain("schemas list");
18
+ });
19
+
20
+ it("returns schema field examples filtered by member type", () => {
21
+ const examples = getCommandExamples("schemas fields create", { type: "member" });
22
+
23
+ expect(examples).toHaveLength(1);
24
+ expect(examples[0]?.command).toContain('"type":"member"');
25
+ expect(examples[0]?.description).toContain("member field");
26
+ });
27
+
28
+ it("returns both plain and currency-style examples for number fields", () => {
29
+ const examples = getCommandExamples("schemas fields create", { type: "number" });
30
+
31
+ expect(examples).toHaveLength(2);
32
+ expect(examples[0]?.command).toContain('"slug":"amount"');
33
+ expect(examples[1]?.command).toContain('"symbolEnabled":true');
34
+ expect(examples[1]?.command).toContain('"symbolValue":" €"');
35
+ expect(examples[1]?.description).toContain("Moneda");
36
+ });
37
+
38
+ it("returns canonical automation creation examples", () => {
39
+ const examples = getCommandExamples("automations create");
40
+
41
+ expect(examples.length).toBeGreaterThanOrEqual(3);
42
+ expect(examples[0]?.command).toContain("--trigger-type scheduled");
43
+ expect(examples.some((example) => example.command.includes("--trigger-type record_changed"))).toBe(
44
+ true,
45
+ );
46
+ });
47
+
48
+ it("returns automation trigger, webhook, and connection examples", () => {
49
+ expect(getCommandExamples("automations trigger set")).not.toHaveLength(0);
50
+ expect(getCommandExamples("automations webhook set")).not.toHaveLength(0);
51
+ expect(getCommandExamples("automations connections create")).not.toHaveLength(0);
52
+ expect(getCommandExamples("automations connections update")).not.toHaveLength(0);
53
+ });
54
+
55
+ it("includes raw payloads for key automation examples", () => {
56
+ const createExamples = getCommandExamples("automations create");
57
+ const actionExamples = getCommandExamples("automations actions add");
58
+ const webhookExamples = getCommandExamples("automations webhook set");
59
+
60
+ expect(createExamples.every((example) => typeof example.raw === "string" && example.raw.length > 0)).toBe(
61
+ true,
62
+ );
63
+ expect(actionExamples.some((example) => example.command.includes("--condition-json"))).toBe(true);
64
+ expect(actionExamples.some((example) => example.raw?.includes('"rawValue"'))).toBe(true);
65
+ expect(webhookExamples[0]?.raw).toContain("idempotencyKeyJsonPath");
66
+ });
67
+ });