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.
- package/AGENTS.md +46 -0
- package/README.md +395 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +7142 -0
- package/dist/index.js.map +1 -0
- package/package.json +32 -0
- package/src/api/automations.ts +403 -0
- package/src/api/comments.ts +51 -0
- package/src/api/displays.ts +276 -0
- package/src/api/files.ts +50 -0
- package/src/api/navigation.ts +71 -0
- package/src/api/records.ts +167 -0
- package/src/api/schemas.ts +130 -0
- package/src/api/teamspace.ts +110 -0
- package/src/cli-examples.ts +175 -0
- package/src/cli-runner.ts +73 -0
- package/src/client.test.ts +128 -0
- package/src/client.ts +243 -0
- package/src/command-registry.ts +749 -0
- package/src/commands/automations.test.ts +728 -0
- package/src/commands/automations.ts +2013 -0
- package/src/commands/comments.test.ts +214 -0
- package/src/commands/comments.ts +303 -0
- package/src/commands/config.test.ts +81 -0
- package/src/commands/config.ts +150 -0
- package/src/commands/displays.test.ts +266 -0
- package/src/commands/displays.ts +755 -0
- package/src/commands/files.test.ts +284 -0
- package/src/commands/files.ts +280 -0
- package/src/commands/navigation.test.ts +214 -0
- package/src/commands/navigation.ts +348 -0
- package/src/commands/profiles.test.ts +82 -0
- package/src/commands/profiles.ts +281 -0
- package/src/commands/records.test.ts +367 -0
- package/src/commands/records.ts +726 -0
- package/src/commands/schemas.test.ts +748 -0
- package/src/commands/schemas.ts +746 -0
- package/src/commands/teamspace.test.ts +229 -0
- package/src/commands/teamspace.ts +540 -0
- package/src/config.test.ts +165 -0
- package/src/config.ts +248 -0
- package/src/index.test.ts +140 -0
- package/src/index.ts +8 -0
- package/src/parsers/expressions.test.ts +50 -0
- package/src/parsers/expressions.ts +111 -0
- package/src/parsers/kv.test.ts +35 -0
- package/src/parsers/kv.ts +49 -0
- package/src/parsers/selectors.test.ts +23 -0
- package/src/parsers/selectors.ts +29 -0
- package/src/program.ts +56 -0
- package/src/runtime-context.ts +18 -0
- package/src/types.ts +71 -0
- package/src/utils/errors.test.ts +48 -0
- package/src/utils/errors.ts +183 -0
- package/src/utils/examples.test.ts +67 -0
- package/src/utils/examples.ts +855 -0
- package/src/utils/output.test.ts +39 -0
- package/src/utils/output.ts +124 -0
- package/src/utils/schema-fields.ts +529 -0
- package/tsconfig.json +20 -0
- package/tsup.config.ts +13 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { Writable } from "node:stream";
|
|
2
|
+
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
defaultConfigPath,
|
|
7
|
+
readConfig,
|
|
8
|
+
redactConfig,
|
|
9
|
+
resolveProfile,
|
|
10
|
+
} from "../config.js";
|
|
11
|
+
import { CliError } from "../utils/errors.js";
|
|
12
|
+
import { buildJsonSuccess, printTable } from "../utils/output.js";
|
|
13
|
+
|
|
14
|
+
interface CommandIo {
|
|
15
|
+
write(chunk: string): unknown;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface RegisterConfigCommandsOptions {
|
|
19
|
+
configPath?: string;
|
|
20
|
+
stdout?: CommandIo;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface GlobalOptions {
|
|
24
|
+
json?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getCommandIo(stdout: CommandIo | undefined): CommandIo {
|
|
28
|
+
return stdout ?? (process.stdout as Writable);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function writeLine(stdout: CommandIo, value: string): void {
|
|
32
|
+
stdout.write(`${value}\n`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getGlobalOptions(command: Command): GlobalOptions {
|
|
36
|
+
return command.optsWithGlobals<GlobalOptions>();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function emitJson(
|
|
40
|
+
stdout: CommandIo,
|
|
41
|
+
commandName: string,
|
|
42
|
+
data: unknown,
|
|
43
|
+
configPath: string,
|
|
44
|
+
): void {
|
|
45
|
+
writeLine(
|
|
46
|
+
stdout,
|
|
47
|
+
JSON.stringify(buildJsonSuccess(data, { command: commandName, configPath })),
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function toConfigurationError(filePath: string, error: unknown): never {
|
|
52
|
+
if (error instanceof CliError) {
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (
|
|
57
|
+
typeof error === "object" &&
|
|
58
|
+
error !== null &&
|
|
59
|
+
"code" in error &&
|
|
60
|
+
error.code === "ENOENT"
|
|
61
|
+
) {
|
|
62
|
+
throw new CliError("configuration_error", `Config file not found at ${filePath}.`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (error instanceof Error) {
|
|
66
|
+
throw new CliError("configuration_error", error.message);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function registerConfigCommands(
|
|
73
|
+
program: Command,
|
|
74
|
+
options: RegisterConfigCommandsOptions = {},
|
|
75
|
+
): void {
|
|
76
|
+
const filePath = options.configPath ?? defaultConfigPath;
|
|
77
|
+
const stdout = getCommandIo(options.stdout);
|
|
78
|
+
const config = program.command("config").description("Inspect local CLI config");
|
|
79
|
+
|
|
80
|
+
config
|
|
81
|
+
.command("path")
|
|
82
|
+
.description("Show the effective config file path")
|
|
83
|
+
.action(function action(this: Command) {
|
|
84
|
+
const data = { path: filePath };
|
|
85
|
+
|
|
86
|
+
if (getGlobalOptions(this).json) {
|
|
87
|
+
emitJson(stdout, "config path", data, filePath);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
writeLine(stdout, filePath);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
config
|
|
95
|
+
.command("show")
|
|
96
|
+
.description("Show the local config with secrets redacted")
|
|
97
|
+
.action(function action(this: Command) {
|
|
98
|
+
try {
|
|
99
|
+
const data = redactConfig(readConfig(filePath));
|
|
100
|
+
|
|
101
|
+
if (getGlobalOptions(this).json) {
|
|
102
|
+
emitJson(stdout, "config show", data, filePath);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
writeLine(stdout, `Default profile: ${data.defaultProfile ?? "(none)"}`);
|
|
107
|
+
printTable(
|
|
108
|
+
Object.entries(data.profiles).map(([name, profile]) => ({
|
|
109
|
+
name,
|
|
110
|
+
default: data.defaultProfile === name ? "yes" : "",
|
|
111
|
+
baseUrl:
|
|
112
|
+
typeof profile.baseUrl === "string"
|
|
113
|
+
? profile.baseUrl
|
|
114
|
+
: "",
|
|
115
|
+
apiKey:
|
|
116
|
+
typeof profile.apiKey === "string"
|
|
117
|
+
? profile.apiKey
|
|
118
|
+
: "",
|
|
119
|
+
})),
|
|
120
|
+
["name", "default", "baseUrl", "apiKey"],
|
|
121
|
+
stdout,
|
|
122
|
+
);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
toConfigurationError(filePath, error);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
config
|
|
129
|
+
.command("validate")
|
|
130
|
+
.description("Validate that the local config can resolve a profile")
|
|
131
|
+
.action(function action(this: Command) {
|
|
132
|
+
try {
|
|
133
|
+
const parsed = readConfig(filePath);
|
|
134
|
+
const profile = resolveProfile(parsed);
|
|
135
|
+
const data = {
|
|
136
|
+
valid: true,
|
|
137
|
+
profile: profile.name,
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
if (getGlobalOptions(this).json) {
|
|
141
|
+
emitJson(stdout, "config validate", data, filePath);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
writeLine(stdout, `Config is valid. Resolved profile "${profile.name}".`);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
toConfigurationError(filePath, error);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
|
|
7
|
+
import { buildProgram } from "../index.js";
|
|
8
|
+
|
|
9
|
+
function createTempConfig(contents: string): string {
|
|
10
|
+
const dir = mkdtempSync(join(tmpdir(), "zinkee-displays-"));
|
|
11
|
+
const filePath = join(dir, "config.toml");
|
|
12
|
+
writeFileSync(filePath, contents, "utf8");
|
|
13
|
+
return filePath;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function createApiConfig(): string {
|
|
17
|
+
return createTempConfig(`
|
|
18
|
+
default_profile = "prod"
|
|
19
|
+
|
|
20
|
+
[profiles.prod]
|
|
21
|
+
api_key = "displays-token-1234"
|
|
22
|
+
base_url = "https://api.zinkee.test"
|
|
23
|
+
`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function createStdoutBuffer() {
|
|
27
|
+
let output = "";
|
|
28
|
+
return {
|
|
29
|
+
stdout: {
|
|
30
|
+
write(chunk: string) {
|
|
31
|
+
output += chunk;
|
|
32
|
+
return true;
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
read() {
|
|
36
|
+
return output;
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function jsonResponse(body: unknown, status: number = 200): Response {
|
|
42
|
+
return new Response(JSON.stringify(body), {
|
|
43
|
+
status,
|
|
44
|
+
headers: { "content-type": "application/json" },
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function createFetchMock() {
|
|
49
|
+
const fetchMock = vi.fn<typeof fetch>();
|
|
50
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
51
|
+
return fetchMock;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
vi.unstubAllGlobals();
|
|
56
|
+
vi.restoreAllMocks();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("displays commands", () => {
|
|
60
|
+
it("registers the displays command group", () => {
|
|
61
|
+
const program = buildProgram();
|
|
62
|
+
expect(program.commands.some((command) => command.name() === "displays")).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("creates displays with the expected payload", async () => {
|
|
66
|
+
const configPath = createApiConfig();
|
|
67
|
+
const output = createStdoutBuffer();
|
|
68
|
+
const fetchMock = createFetchMock();
|
|
69
|
+
fetchMock.mockResolvedValueOnce(jsonResponse({ displayId: "display-1" }));
|
|
70
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
71
|
+
|
|
72
|
+
await program.parseAsync([
|
|
73
|
+
"node",
|
|
74
|
+
"zinkee",
|
|
75
|
+
"--json",
|
|
76
|
+
"displays",
|
|
77
|
+
"create",
|
|
78
|
+
"--name",
|
|
79
|
+
"Pipeline",
|
|
80
|
+
"--template",
|
|
81
|
+
"widget_row_tabs",
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
const init = fetchMock.mock.calls[0]?.[1];
|
|
85
|
+
expect(fetchMock.mock.calls[0]?.[0]).toBe("https://api.zinkee.test/api/v2/displays");
|
|
86
|
+
expect(init?.method).toBe("POST");
|
|
87
|
+
expect(JSON.parse(String(init?.body))).toEqual({
|
|
88
|
+
name: "Pipeline",
|
|
89
|
+
layout: { template: "widget_row_tabs" },
|
|
90
|
+
});
|
|
91
|
+
expect(JSON.parse(output.read())).toEqual({
|
|
92
|
+
data: { displayId: "display-1" },
|
|
93
|
+
meta: { command: "displays create" },
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("creates freeform displays with nested freeform layouts", async () => {
|
|
98
|
+
const configPath = createApiConfig();
|
|
99
|
+
const output = createStdoutBuffer();
|
|
100
|
+
const fetchMock = createFetchMock();
|
|
101
|
+
fetchMock.mockResolvedValueOnce(jsonResponse({ displayId: "display-2" }));
|
|
102
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
103
|
+
|
|
104
|
+
await program.parseAsync([
|
|
105
|
+
"node",
|
|
106
|
+
"zinkee",
|
|
107
|
+
"--json",
|
|
108
|
+
"displays",
|
|
109
|
+
"create",
|
|
110
|
+
"--name",
|
|
111
|
+
"Control Proyecto Freeform",
|
|
112
|
+
"--template",
|
|
113
|
+
"freeform",
|
|
114
|
+
"--freeform-layouts",
|
|
115
|
+
'{"lg":[{"i":"widget-1","x":0,"y":0,"w":6,"h":5}]}',
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
const init = fetchMock.mock.calls[0]?.[1];
|
|
119
|
+
expect(JSON.parse(String(init?.body))).toEqual({
|
|
120
|
+
name: "Control Proyecto Freeform",
|
|
121
|
+
layout: {
|
|
122
|
+
template: "freeform",
|
|
123
|
+
freeformLayouts: {
|
|
124
|
+
lg: [{ i: "widget-1", x: 0, y: 0, w: 6, h: 5 }],
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
expect(JSON.parse(output.read())).toEqual({
|
|
129
|
+
data: { displayId: "display-2" },
|
|
130
|
+
meta: { command: "displays create" },
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("creates widgets through the generic --type path", async () => {
|
|
135
|
+
const configPath = createApiConfig();
|
|
136
|
+
const output = createStdoutBuffer();
|
|
137
|
+
const fetchMock = createFetchMock();
|
|
138
|
+
fetchMock.mockResolvedValueOnce(jsonResponse({ displayId: "display-1", widgetId: "widget-1" }));
|
|
139
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
140
|
+
|
|
141
|
+
await program.parseAsync([
|
|
142
|
+
"node",
|
|
143
|
+
"zinkee",
|
|
144
|
+
"--json",
|
|
145
|
+
"displays",
|
|
146
|
+
"widgets",
|
|
147
|
+
"create",
|
|
148
|
+
"display-1",
|
|
149
|
+
"--type",
|
|
150
|
+
"chart",
|
|
151
|
+
"--name",
|
|
152
|
+
"Revenue",
|
|
153
|
+
"--chart-type",
|
|
154
|
+
"bar",
|
|
155
|
+
]);
|
|
156
|
+
|
|
157
|
+
const init = fetchMock.mock.calls[0]?.[1];
|
|
158
|
+
expect(fetchMock.mock.calls[0]?.[0]).toBe("https://api.zinkee.test/api/v2/displays/display-1/widgets");
|
|
159
|
+
expect(JSON.parse(String(init?.body))).toMatchObject({
|
|
160
|
+
type: "CHART",
|
|
161
|
+
presentation: { name: "Revenue" },
|
|
162
|
+
chart: { chartType: "bar" },
|
|
163
|
+
});
|
|
164
|
+
expect(JSON.parse(output.read())).toEqual({
|
|
165
|
+
data: { displayId: "display-1", widgetId: "widget-1" },
|
|
166
|
+
meta: { command: "displays widgets create" },
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("deep merges chart flags with chart data from --raw", async () => {
|
|
171
|
+
const configPath = createApiConfig();
|
|
172
|
+
const output = createStdoutBuffer();
|
|
173
|
+
const fetchMock = createFetchMock();
|
|
174
|
+
fetchMock.mockResolvedValueOnce(jsonResponse({ displayId: "display-1", widgetId: "widget-2" }));
|
|
175
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
176
|
+
|
|
177
|
+
await program.parseAsync([
|
|
178
|
+
"node",
|
|
179
|
+
"zinkee",
|
|
180
|
+
"--json",
|
|
181
|
+
"displays",
|
|
182
|
+
"widgets",
|
|
183
|
+
"create",
|
|
184
|
+
"display-1",
|
|
185
|
+
"--type",
|
|
186
|
+
"chart",
|
|
187
|
+
"--name",
|
|
188
|
+
"Horas",
|
|
189
|
+
"--origin-resource",
|
|
190
|
+
"schema-1",
|
|
191
|
+
"--chart-type",
|
|
192
|
+
"bars",
|
|
193
|
+
"--x-axis-field",
|
|
194
|
+
"rh-empleado",
|
|
195
|
+
"--raw",
|
|
196
|
+
'{"chart":{"yAxisSeries":[{"typeCalc":"SUM","fieldCalc":"rh-horas","axisType":"PRIMARY"}],"legend":{"position":"bottom"}}}',
|
|
197
|
+
]);
|
|
198
|
+
|
|
199
|
+
const init = fetchMock.mock.calls[0]?.[1];
|
|
200
|
+
expect(JSON.parse(String(init?.body))).toMatchObject({
|
|
201
|
+
type: "CHART",
|
|
202
|
+
presentation: { name: "Horas" },
|
|
203
|
+
source: { originResourceId: "schema-1" },
|
|
204
|
+
chart: {
|
|
205
|
+
chartType: "bars",
|
|
206
|
+
xAxisFieldId: "rh-empleado",
|
|
207
|
+
yAxisSeries: [{ typeCalc: "SUM", fieldCalc: "rh-horas", axisType: "PRIMARY" }],
|
|
208
|
+
legend: { position: "bottom" },
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
expect(JSON.parse(output.read())).toEqual({
|
|
212
|
+
data: { displayId: "display-1", widgetId: "widget-2" },
|
|
213
|
+
meta: { command: "displays widgets create" },
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("preserves raw chart properties when --y-axis-serie is used", async () => {
|
|
218
|
+
const configPath = createApiConfig();
|
|
219
|
+
const output = createStdoutBuffer();
|
|
220
|
+
const fetchMock = createFetchMock();
|
|
221
|
+
fetchMock.mockResolvedValueOnce(jsonResponse({ displayId: "display-1", widgetId: "widget-3" }));
|
|
222
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
223
|
+
|
|
224
|
+
await program.parseAsync([
|
|
225
|
+
"node",
|
|
226
|
+
"zinkee",
|
|
227
|
+
"--json",
|
|
228
|
+
"displays",
|
|
229
|
+
"widgets",
|
|
230
|
+
"create",
|
|
231
|
+
"display-1",
|
|
232
|
+
"--type",
|
|
233
|
+
"chart",
|
|
234
|
+
"--raw",
|
|
235
|
+
'{"chart":{"chartType":"bars","xAxisFieldId":"rh-empleado"}}',
|
|
236
|
+
"--y-axis-serie",
|
|
237
|
+
'{"typeCalc":"SUM","fieldCalc":"rh-horas","axisType":"PRIMARY"}',
|
|
238
|
+
]);
|
|
239
|
+
|
|
240
|
+
const init = fetchMock.mock.calls[0]?.[1];
|
|
241
|
+
expect(JSON.parse(String(init?.body))).toMatchObject({
|
|
242
|
+
type: "CHART",
|
|
243
|
+
chart: {
|
|
244
|
+
chartType: "bars",
|
|
245
|
+
xAxisFieldId: "rh-empleado",
|
|
246
|
+
yAxisSeries: [{ typeCalc: "SUM", fieldCalc: "rh-horas", axisType: "PRIMARY" }],
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
expect(JSON.parse(output.read())).toEqual({
|
|
250
|
+
data: { displayId: "display-1", widgetId: "widget-3" },
|
|
251
|
+
meta: { command: "displays widgets create" },
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("blocks destructive display commands in read-only mode", async () => {
|
|
256
|
+
const configPath = createApiConfig();
|
|
257
|
+
const fetchMock = createFetchMock();
|
|
258
|
+
const program = buildProgram({ configPath });
|
|
259
|
+
|
|
260
|
+
await expect(
|
|
261
|
+
program.parseAsync(["node", "zinkee", "--read-only", "displays", "delete", "display-1"]),
|
|
262
|
+
).rejects.toThrowError('Command "displays delete" is not allowed in read-only mode.');
|
|
263
|
+
|
|
264
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
265
|
+
});
|
|
266
|
+
});
|