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
|
@@ -1,194 +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 { registerFormulasCommands } from "./formulas.js";
|
|
9
|
-
|
|
10
|
-
function createConfig(): string {
|
|
11
|
-
const dir = mkdtempSync(join(tmpdir(), "zinkee-formulas-"));
|
|
12
|
-
const filePath = join(dir, "config.toml");
|
|
13
|
-
writeFileSync(
|
|
14
|
-
filePath,
|
|
15
|
-
`default_profile = "test"\n\n[profiles.test]\napi_key = "token"\nbase_url = "https://api.zinkee.test"\n`,
|
|
16
|
-
"utf8",
|
|
17
|
-
);
|
|
18
|
-
return filePath;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function createStdoutBuffer() {
|
|
22
|
-
let output = "";
|
|
23
|
-
return {
|
|
24
|
-
stdout: {
|
|
25
|
-
write(chunk: string) {
|
|
26
|
-
output += chunk;
|
|
27
|
-
return true;
|
|
28
|
-
},
|
|
29
|
-
},
|
|
30
|
-
read: () => output,
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function buildProgram(options: { configPath: string; stdout: { write(chunk: string): unknown } }): Command {
|
|
35
|
-
const program = new Command();
|
|
36
|
-
program
|
|
37
|
-
.name("zinkee")
|
|
38
|
-
.enablePositionalOptions()
|
|
39
|
-
.option("--read-only")
|
|
40
|
-
.option("--json");
|
|
41
|
-
registerFormulasCommands(program, options);
|
|
42
|
-
return program;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function queuedResponse(scope: "FIELD" | "SCHEMA" | "WORKSPACE", eventsQueued = 1): Response {
|
|
46
|
-
return new Response(
|
|
47
|
-
JSON.stringify({
|
|
48
|
-
requestId: "request-1",
|
|
49
|
-
scope,
|
|
50
|
-
formulaCount: eventsQueued,
|
|
51
|
-
skippedFormulaCount: 0,
|
|
52
|
-
eventsQueued,
|
|
53
|
-
status: "QUEUED",
|
|
54
|
-
}),
|
|
55
|
-
{ status: 202, headers: { "content-type": "application/json" } },
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
afterEach(() => {
|
|
60
|
-
vi.unstubAllGlobals();
|
|
61
|
-
vi.restoreAllMocks();
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
describe("formulas commands", () => {
|
|
65
|
-
it("registers the formulas command group", () => {
|
|
66
|
-
const output = createStdoutBuffer();
|
|
67
|
-
const program = buildProgram({ configPath: createConfig(), stdout: output.stdout });
|
|
68
|
-
|
|
69
|
-
expect(program.commands.some((command) => command.name() === "formulas")).toBe(true);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("queues one field by slug and emits the JSON envelope", async () => {
|
|
73
|
-
const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(queuedResponse("FIELD"));
|
|
74
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
75
|
-
const configPath = createConfig();
|
|
76
|
-
const output = createStdoutBuffer();
|
|
77
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
78
|
-
|
|
79
|
-
await program.parseAsync([
|
|
80
|
-
"node",
|
|
81
|
-
"zinkee",
|
|
82
|
-
"--json",
|
|
83
|
-
"formulas",
|
|
84
|
-
"recompute",
|
|
85
|
-
"field",
|
|
86
|
-
"invoices",
|
|
87
|
-
"total",
|
|
88
|
-
]);
|
|
89
|
-
|
|
90
|
-
const [, init] = fetchMock.mock.calls[0]!;
|
|
91
|
-
expect(JSON.parse(String(init?.body))).toEqual({
|
|
92
|
-
scope: "FIELD",
|
|
93
|
-
schema: "invoices",
|
|
94
|
-
field: "total",
|
|
95
|
-
});
|
|
96
|
-
expect(JSON.parse(output.read())).toEqual({
|
|
97
|
-
data: {
|
|
98
|
-
requestId: "request-1",
|
|
99
|
-
scope: "FIELD",
|
|
100
|
-
formulaCount: 1,
|
|
101
|
-
skippedFormulaCount: 0,
|
|
102
|
-
eventsQueued: 1,
|
|
103
|
-
status: "QUEUED",
|
|
104
|
-
},
|
|
105
|
-
meta: { command: "formulas recompute field", configPath },
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it.each([
|
|
110
|
-
["schema", ["invoices"]],
|
|
111
|
-
["workspace", []],
|
|
112
|
-
])("requires --yes before the %s request", async (scope, args) => {
|
|
113
|
-
const fetchMock = vi.fn<typeof fetch>();
|
|
114
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
115
|
-
const output = createStdoutBuffer();
|
|
116
|
-
const program = buildProgram({ configPath: createConfig(), stdout: output.stdout });
|
|
117
|
-
|
|
118
|
-
await expect(
|
|
119
|
-
program.parseAsync([
|
|
120
|
-
"node",
|
|
121
|
-
"zinkee",
|
|
122
|
-
"formulas",
|
|
123
|
-
"recompute",
|
|
124
|
-
scope,
|
|
125
|
-
...args,
|
|
126
|
-
]),
|
|
127
|
-
).rejects.toThrow("requires --yes");
|
|
128
|
-
expect(fetchMock).not.toHaveBeenCalled();
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it("queues every schema formula after confirmation", async () => {
|
|
132
|
-
const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(queuedResponse("SCHEMA", 3));
|
|
133
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
134
|
-
const output = createStdoutBuffer();
|
|
135
|
-
const program = buildProgram({ configPath: createConfig(), stdout: output.stdout });
|
|
136
|
-
|
|
137
|
-
await program.parseAsync([
|
|
138
|
-
"node",
|
|
139
|
-
"zinkee",
|
|
140
|
-
"formulas",
|
|
141
|
-
"recompute",
|
|
142
|
-
"schema",
|
|
143
|
-
"invoices",
|
|
144
|
-
"--yes",
|
|
145
|
-
]);
|
|
146
|
-
|
|
147
|
-
expect(output.read()).toContain("Queued 3 formula recalculation event(s)");
|
|
148
|
-
expect(output.read()).not.toContain("completed");
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it("queues the workspace payload after confirmation", async () => {
|
|
152
|
-
const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(queuedResponse("WORKSPACE", 5));
|
|
153
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
154
|
-
const output = createStdoutBuffer();
|
|
155
|
-
const program = buildProgram({ configPath: createConfig(), stdout: output.stdout });
|
|
156
|
-
|
|
157
|
-
await program.parseAsync([
|
|
158
|
-
"node",
|
|
159
|
-
"zinkee",
|
|
160
|
-
"--json",
|
|
161
|
-
"formulas",
|
|
162
|
-
"recompute",
|
|
163
|
-
"workspace",
|
|
164
|
-
"--yes",
|
|
165
|
-
]);
|
|
166
|
-
|
|
167
|
-
const [, init] = fetchMock.mock.calls[0]!;
|
|
168
|
-
expect(JSON.parse(String(init?.body))).toEqual({ scope: "WORKSPACE" });
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it.each([
|
|
172
|
-
["field", ["invoices", "total"]],
|
|
173
|
-
["schema", ["invoices", "--yes"]],
|
|
174
|
-
["workspace", ["--yes"]],
|
|
175
|
-
])("blocks %s in read-only mode before any network request", async (scope, args) => {
|
|
176
|
-
const fetchMock = vi.fn<typeof fetch>();
|
|
177
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
178
|
-
const output = createStdoutBuffer();
|
|
179
|
-
const program = buildProgram({ configPath: createConfig(), stdout: output.stdout });
|
|
180
|
-
|
|
181
|
-
await expect(
|
|
182
|
-
program.parseAsync([
|
|
183
|
-
"node",
|
|
184
|
-
"zinkee",
|
|
185
|
-
"--read-only",
|
|
186
|
-
"formulas",
|
|
187
|
-
"recompute",
|
|
188
|
-
scope,
|
|
189
|
-
...args,
|
|
190
|
-
]),
|
|
191
|
-
).rejects.toThrow("not allowed in read-only mode");
|
|
192
|
-
expect(fetchMock).not.toHaveBeenCalled();
|
|
193
|
-
});
|
|
194
|
-
});
|
package/src/commands/formulas.ts
DELETED
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
import type { Writable } from "node:stream";
|
|
2
|
-
|
|
3
|
-
import { Command, InvalidArgumentError } from "commander";
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
FormulasApi,
|
|
7
|
-
type FormulaRecomputeInput,
|
|
8
|
-
type FormulaRecomputeResponse,
|
|
9
|
-
} from "../api/formulas.js";
|
|
10
|
-
import { ApiClient } from "../client.js";
|
|
11
|
-
import { defaultConfigPath, readRuntimeConfig } from "../config.js";
|
|
12
|
-
import {
|
|
13
|
-
normalizeUuidOrSlug,
|
|
14
|
-
type NormalizedUuidOrSlug,
|
|
15
|
-
} from "../parsers/selectors.js";
|
|
16
|
-
import { resolveRuntimeContext } from "../runtime-context.js";
|
|
17
|
-
import type { RuntimeOverrides } from "../types.js";
|
|
18
|
-
import { CliError } from "../utils/errors.js";
|
|
19
|
-
import { buildJsonSuccess } from "../utils/output.js";
|
|
20
|
-
|
|
21
|
-
interface CommandIo {
|
|
22
|
-
write(chunk: string): unknown;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface RegisterFormulasCommandsOptions {
|
|
26
|
-
configPath?: string;
|
|
27
|
-
stdout?: CommandIo;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
interface GlobalOptions extends RuntimeOverrides {}
|
|
31
|
-
|
|
32
|
-
interface ConfirmedCommandOptions {
|
|
33
|
-
yes?: boolean;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function getCommandIo(stdout: CommandIo | undefined): CommandIo {
|
|
37
|
-
return stdout ?? (process.stdout as Writable);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function writeLine(stdout: CommandIo, value: string): void {
|
|
41
|
-
stdout.write(`${value}\n`);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function getGlobalOptions(command: Command): GlobalOptions {
|
|
45
|
-
return command.optsWithGlobals<GlobalOptions>();
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function toConfigurationError(filePath: string, error: unknown): never {
|
|
49
|
-
if (error instanceof CliError) {
|
|
50
|
-
throw error;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (
|
|
54
|
-
typeof error === "object" &&
|
|
55
|
-
error !== null &&
|
|
56
|
-
"code" in error &&
|
|
57
|
-
error.code === "ENOENT"
|
|
58
|
-
) {
|
|
59
|
-
throw new CliError("configuration_error", `Config file not found at ${filePath}.`);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (error instanceof Error) {
|
|
63
|
-
throw new CliError("configuration_error", error.message);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
throw error;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function createFormulasApi(
|
|
70
|
-
command: Command,
|
|
71
|
-
filePath: string,
|
|
72
|
-
): { api: FormulasApi; json: boolean; readOnly: boolean } {
|
|
73
|
-
try {
|
|
74
|
-
const config = readRuntimeConfig(filePath);
|
|
75
|
-
const runtime = resolveRuntimeContext(config, getGlobalOptions(command));
|
|
76
|
-
|
|
77
|
-
if (!runtime.baseUrl) {
|
|
78
|
-
throw new CliError(
|
|
79
|
-
"configuration_error",
|
|
80
|
-
`Resolved profile "${runtime.profile}" is missing a base URL.`,
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return {
|
|
85
|
-
api: new FormulasApi(
|
|
86
|
-
new ApiClient({
|
|
87
|
-
baseUrl: runtime.baseUrl,
|
|
88
|
-
apiKey: runtime.apiKey,
|
|
89
|
-
}),
|
|
90
|
-
),
|
|
91
|
-
json: runtime.json,
|
|
92
|
-
readOnly: runtime.readOnly,
|
|
93
|
-
};
|
|
94
|
-
} catch (error) {
|
|
95
|
-
toConfigurationError(filePath, error);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function ensureWritable(commandName: string, readOnly: boolean): void {
|
|
100
|
-
if (readOnly) {
|
|
101
|
-
throw new CliError(
|
|
102
|
-
"command_not_allowed_in_read_only_mode",
|
|
103
|
-
`Command "${commandName}" is not allowed in read-only mode.`,
|
|
104
|
-
{
|
|
105
|
-
commandMode: "write",
|
|
106
|
-
executionMode: "read-only",
|
|
107
|
-
},
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function ensureConfirmed(commandName: string, confirmed: boolean | undefined): void {
|
|
113
|
-
if (!confirmed) {
|
|
114
|
-
throw new CliError(
|
|
115
|
-
"invalid_cli_usage",
|
|
116
|
-
`Command "${commandName}" requires --yes because it may enqueue a large recalculation.`,
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function parseUuidOrSlugSelector(input: string): NormalizedUuidOrSlug {
|
|
122
|
-
try {
|
|
123
|
-
return normalizeUuidOrSlug(input);
|
|
124
|
-
} catch (error) {
|
|
125
|
-
const message = error instanceof Error ? error.message : "Invalid selector.";
|
|
126
|
-
throw new InvalidArgumentError(message);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function emitResult(
|
|
131
|
-
stdout: CommandIo,
|
|
132
|
-
commandName: string,
|
|
133
|
-
result: FormulaRecomputeResponse,
|
|
134
|
-
configPath: string,
|
|
135
|
-
json: boolean,
|
|
136
|
-
): void {
|
|
137
|
-
if (json) {
|
|
138
|
-
writeLine(
|
|
139
|
-
stdout,
|
|
140
|
-
JSON.stringify(buildJsonSuccess(result, { command: commandName, configPath })),
|
|
141
|
-
);
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
writeLine(
|
|
146
|
-
stdout,
|
|
147
|
-
`Queued ${result.eventsQueued} formula recalculation event(s) for ${result.scope.toLowerCase()} scope (request ${result.requestId}).`,
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
async function recompute(
|
|
152
|
-
command: Command,
|
|
153
|
-
commandName: string,
|
|
154
|
-
input: FormulaRecomputeInput,
|
|
155
|
-
options: RegisterFormulasCommandsOptions,
|
|
156
|
-
requireConfirmation: boolean,
|
|
157
|
-
confirmed?: boolean,
|
|
158
|
-
): Promise<void> {
|
|
159
|
-
const configPath = options.configPath ?? defaultConfigPath;
|
|
160
|
-
const stdout = getCommandIo(options.stdout);
|
|
161
|
-
const { api, json, readOnly } = createFormulasApi(command, configPath);
|
|
162
|
-
|
|
163
|
-
ensureWritable(commandName, readOnly);
|
|
164
|
-
if (requireConfirmation) {
|
|
165
|
-
ensureConfirmed(commandName, confirmed);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const result = await api.recompute(input);
|
|
169
|
-
emitResult(stdout, commandName, result, configPath, json);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
export function registerFormulasCommands(
|
|
173
|
-
program: Command,
|
|
174
|
-
options: RegisterFormulasCommandsOptions = {},
|
|
175
|
-
): void {
|
|
176
|
-
const formulas = program
|
|
177
|
-
.command("formulas")
|
|
178
|
-
.description("Recalculate formula values");
|
|
179
|
-
const recomputeCommand = formulas
|
|
180
|
-
.command("recompute")
|
|
181
|
-
.description("Queue a full formula recalculation");
|
|
182
|
-
|
|
183
|
-
recomputeCommand
|
|
184
|
-
.command("field")
|
|
185
|
-
.description("Queue recalculation of one formula column")
|
|
186
|
-
.argument("<schema>", "schema UUID or slug", parseUuidOrSlugSelector)
|
|
187
|
-
.argument("<field>", "formula field UUID or slug", parseUuidOrSlugSelector)
|
|
188
|
-
.action(
|
|
189
|
-
async function action(
|
|
190
|
-
this: Command,
|
|
191
|
-
schema: NormalizedUuidOrSlug,
|
|
192
|
-
field: NormalizedUuidOrSlug,
|
|
193
|
-
) {
|
|
194
|
-
await recompute(
|
|
195
|
-
this,
|
|
196
|
-
"formulas recompute field",
|
|
197
|
-
{ scope: "FIELD", schema, field },
|
|
198
|
-
options,
|
|
199
|
-
false,
|
|
200
|
-
);
|
|
201
|
-
},
|
|
202
|
-
);
|
|
203
|
-
|
|
204
|
-
recomputeCommand
|
|
205
|
-
.command("schema")
|
|
206
|
-
.description("Queue recalculation of all active formulas in one schema")
|
|
207
|
-
.argument("<schema>", "schema UUID or slug", parseUuidOrSlugSelector)
|
|
208
|
-
.option("--yes", "confirm the potentially expensive recalculation")
|
|
209
|
-
.action(
|
|
210
|
-
async function action(
|
|
211
|
-
this: Command,
|
|
212
|
-
schema: NormalizedUuidOrSlug,
|
|
213
|
-
commandOptions: ConfirmedCommandOptions,
|
|
214
|
-
) {
|
|
215
|
-
await recompute(
|
|
216
|
-
this,
|
|
217
|
-
"formulas recompute schema",
|
|
218
|
-
{ scope: "SCHEMA", schema },
|
|
219
|
-
options,
|
|
220
|
-
true,
|
|
221
|
-
commandOptions.yes,
|
|
222
|
-
);
|
|
223
|
-
},
|
|
224
|
-
);
|
|
225
|
-
|
|
226
|
-
recomputeCommand
|
|
227
|
-
.command("workspace")
|
|
228
|
-
.description("Queue recalculation of all active formulas in the workspace")
|
|
229
|
-
.option("--yes", "confirm the potentially expensive recalculation")
|
|
230
|
-
.action(async function action(
|
|
231
|
-
this: Command,
|
|
232
|
-
commandOptions: ConfirmedCommandOptions,
|
|
233
|
-
) {
|
|
234
|
-
await recompute(
|
|
235
|
-
this,
|
|
236
|
-
"formulas recompute workspace",
|
|
237
|
-
{ scope: "WORKSPACE" },
|
|
238
|
-
options,
|
|
239
|
-
true,
|
|
240
|
-
commandOptions.yes,
|
|
241
|
-
);
|
|
242
|
-
});
|
|
243
|
-
}
|
|
@@ -1,123 +0,0 @@
|
|
|
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-logs-"));
|
|
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 = "logs-token-1234"
|
|
22
|
-
base_url = "https://api.zinkee.test"
|
|
23
|
-
`);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function createStdoutBuffer() {
|
|
27
|
-
let output = "";
|
|
28
|
-
|
|
29
|
-
return {
|
|
30
|
-
stdout: {
|
|
31
|
-
write(chunk: string) {
|
|
32
|
-
output += chunk;
|
|
33
|
-
return true;
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
read() {
|
|
37
|
-
return output;
|
|
38
|
-
},
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function jsonResponse(body: unknown, status: number = 200): Response {
|
|
43
|
-
return new Response(JSON.stringify(body), {
|
|
44
|
-
status,
|
|
45
|
-
headers: {
|
|
46
|
-
"content-type": "application/json",
|
|
47
|
-
},
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function createFetchMock() {
|
|
52
|
-
const fetchMock = vi.fn<typeof fetch>();
|
|
53
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
54
|
-
return fetchMock;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
afterEach(() => {
|
|
58
|
-
vi.unstubAllGlobals();
|
|
59
|
-
vi.restoreAllMocks();
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
describe("logs commands", () => {
|
|
63
|
-
it("registers the logs command group", () => {
|
|
64
|
-
const program = buildProgram();
|
|
65
|
-
|
|
66
|
-
expect(program.commands.some((command) => command.name() === "logs")).toBe(true);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("lists logs for a record and lowercases the record uuid in the path", async () => {
|
|
70
|
-
const configPath = createApiConfig();
|
|
71
|
-
const output = createStdoutBuffer();
|
|
72
|
-
const fetchMock = createFetchMock();
|
|
73
|
-
fetchMock.mockResolvedValueOnce(
|
|
74
|
-
jsonResponse([
|
|
75
|
-
{
|
|
76
|
-
recordId: "550e8400-e29b-41d4-a716-446655440001",
|
|
77
|
-
timestamp: "2026-04-22T10:00:00Z",
|
|
78
|
-
changeField: "",
|
|
79
|
-
change: {
|
|
80
|
-
beforeValue: null,
|
|
81
|
-
afterValue: null,
|
|
82
|
-
author: "developer@zinkee.com",
|
|
83
|
-
operation: "CREATE",
|
|
84
|
-
},
|
|
85
|
-
},
|
|
86
|
-
]),
|
|
87
|
-
);
|
|
88
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
89
|
-
|
|
90
|
-
await program.parseAsync([
|
|
91
|
-
"node",
|
|
92
|
-
"zinkee",
|
|
93
|
-
"--json",
|
|
94
|
-
"logs",
|
|
95
|
-
"list",
|
|
96
|
-
"contacts",
|
|
97
|
-
"550E8400-E29B-41D4-A716-446655440001",
|
|
98
|
-
]);
|
|
99
|
-
|
|
100
|
-
expect(fetchMock.mock.calls[0]?.[0]).toBe(
|
|
101
|
-
"https://api.zinkee.test/api/v2/schemas/contacts/records/550e8400-e29b-41d4-a716-446655440001/logs",
|
|
102
|
-
);
|
|
103
|
-
expect(fetchMock.mock.calls[0]?.[1]?.method ?? "GET").toBe("GET");
|
|
104
|
-
expect(JSON.parse(output.read())).toEqual({
|
|
105
|
-
data: [
|
|
106
|
-
{
|
|
107
|
-
recordId: "550e8400-e29b-41d4-a716-446655440001",
|
|
108
|
-
timestamp: "2026-04-22T10:00:00Z",
|
|
109
|
-
changeField: "",
|
|
110
|
-
change: {
|
|
111
|
-
beforeValue: null,
|
|
112
|
-
afterValue: null,
|
|
113
|
-
author: "developer@zinkee.com",
|
|
114
|
-
operation: "CREATE",
|
|
115
|
-
},
|
|
116
|
-
},
|
|
117
|
-
],
|
|
118
|
-
meta: {
|
|
119
|
-
command: "logs list",
|
|
120
|
-
},
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
});
|
package/src/commands/logs.ts
DELETED
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
import type { Writable } from "node:stream";
|
|
2
|
-
|
|
3
|
-
import { Command, InvalidArgumentError } from "commander";
|
|
4
|
-
|
|
5
|
-
import { LogsApi } from "../api/logs.js";
|
|
6
|
-
import { ApiClient } from "../client.js";
|
|
7
|
-
import { defaultConfigPath, readRuntimeConfig } from "../config.js";
|
|
8
|
-
import {
|
|
9
|
-
normalizeUuidOrSlug,
|
|
10
|
-
type NormalizedUuidOrSlug,
|
|
11
|
-
} from "../parsers/selectors.js";
|
|
12
|
-
import { resolveRuntimeContext } from "../runtime-context.js";
|
|
13
|
-
import type { JsonObject, RuntimeOverrides } from "../types.js";
|
|
14
|
-
import { CliError } from "../utils/errors.js";
|
|
15
|
-
import { buildJsonSuccess, printTable } from "../utils/output.js";
|
|
16
|
-
|
|
17
|
-
interface CommandIo {
|
|
18
|
-
write(chunk: string): unknown;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface RegisterLogsCommandsOptions {
|
|
22
|
-
configPath?: string;
|
|
23
|
-
stdout?: CommandIo;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
interface GlobalOptions extends RuntimeOverrides {}
|
|
27
|
-
|
|
28
|
-
function getCommandIo(stdout: CommandIo | undefined): CommandIo {
|
|
29
|
-
return stdout ?? (process.stdout as Writable);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function writeLine(stdout: CommandIo, value: string): void {
|
|
33
|
-
stdout.write(`${value}\n`);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function getGlobalOptions(command: Command): GlobalOptions {
|
|
37
|
-
return command.optsWithGlobals<GlobalOptions>();
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function emitJson(
|
|
41
|
-
stdout: CommandIo,
|
|
42
|
-
commandName: string,
|
|
43
|
-
data: unknown,
|
|
44
|
-
..._extra: unknown[]
|
|
45
|
-
): void {
|
|
46
|
-
writeLine(
|
|
47
|
-
stdout,
|
|
48
|
-
JSON.stringify(buildJsonSuccess(data, { command: commandName })),
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function toConfigurationError(filePath: string, error: unknown): never {
|
|
53
|
-
if (error instanceof CliError) {
|
|
54
|
-
throw error;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (
|
|
58
|
-
typeof error === "object" &&
|
|
59
|
-
error !== null &&
|
|
60
|
-
"code" in error &&
|
|
61
|
-
error.code === "ENOENT"
|
|
62
|
-
) {
|
|
63
|
-
throw new CliError("configuration_error", `Config file not found at ${filePath}.`);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (error instanceof Error) {
|
|
67
|
-
throw new CliError("configuration_error", error.message);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
throw error;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function createLogsApi(
|
|
74
|
-
command: Command,
|
|
75
|
-
filePath: string,
|
|
76
|
-
): { api: LogsApi; json: boolean } {
|
|
77
|
-
try {
|
|
78
|
-
const config = readRuntimeConfig(filePath);
|
|
79
|
-
const runtime = resolveRuntimeContext(config, getGlobalOptions(command));
|
|
80
|
-
|
|
81
|
-
if (!runtime.baseUrl) {
|
|
82
|
-
throw new CliError(
|
|
83
|
-
"configuration_error",
|
|
84
|
-
`Resolved profile "${runtime.profile}" is missing a base URL.`,
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return {
|
|
89
|
-
api: new LogsApi(
|
|
90
|
-
new ApiClient({
|
|
91
|
-
baseUrl: runtime.baseUrl,
|
|
92
|
-
apiKey: runtime.apiKey,
|
|
93
|
-
}),
|
|
94
|
-
),
|
|
95
|
-
json: runtime.json,
|
|
96
|
-
};
|
|
97
|
-
} catch (error) {
|
|
98
|
-
toConfigurationError(filePath, error);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function parseUuidOrSlugSelector(input: string): NormalizedUuidOrSlug {
|
|
103
|
-
try {
|
|
104
|
-
return normalizeUuidOrSlug(input);
|
|
105
|
-
} catch (error) {
|
|
106
|
-
const message = error instanceof Error ? error.message : "Invalid selector.";
|
|
107
|
-
throw new InvalidArgumentError(message);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function isJsonObject(value: unknown): value is JsonObject {
|
|
112
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function toTableRows(data: unknown): Record<string, unknown>[] {
|
|
116
|
-
if (Array.isArray(data)) {
|
|
117
|
-
return data.map((entry) => (isJsonObject(entry) ? entry : { value: entry }));
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (isJsonObject(data)) {
|
|
121
|
-
return [data];
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return [{ value: data }];
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function emitTable(stdout: CommandIo, data: unknown): void {
|
|
128
|
-
printTable(toTableRows(data), undefined, stdout);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export function registerLogsCommands(
|
|
132
|
-
program: Command,
|
|
133
|
-
options: RegisterLogsCommandsOptions = {},
|
|
134
|
-
): void {
|
|
135
|
-
const filePath = options.configPath ?? defaultConfigPath;
|
|
136
|
-
const stdout = getCommandIo(options.stdout);
|
|
137
|
-
const logs = program.command("logs").description("Inspect record change logs");
|
|
138
|
-
|
|
139
|
-
logs
|
|
140
|
-
.command("list")
|
|
141
|
-
.description("List change logs for a record")
|
|
142
|
-
.argument("<schema>", "schema uuid or slug", parseUuidOrSlugSelector)
|
|
143
|
-
.argument("<record>", "record uuid or slug", parseUuidOrSlugSelector)
|
|
144
|
-
.action(async function action(
|
|
145
|
-
this: Command,
|
|
146
|
-
schema: NormalizedUuidOrSlug,
|
|
147
|
-
record: NormalizedUuidOrSlug,
|
|
148
|
-
) {
|
|
149
|
-
const { api, json } = createLogsApi(this, filePath);
|
|
150
|
-
const data = await api.listLogs(schema, record.value);
|
|
151
|
-
|
|
152
|
-
if (json) {
|
|
153
|
-
emitJson(stdout, "logs list", data, filePath);
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
emitTable(stdout, data);
|
|
158
|
-
});
|
|
159
|
-
}
|