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.
- 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-67U5IRBS.js} +866 -1141
- package/dist/examples-67U5IRBS.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,214 +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-comments-"));
|
|
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 = "comments-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("comments commands", () => {
|
|
63
|
-
it("registers the comments command group", () => {
|
|
64
|
-
const program = buildProgram();
|
|
65
|
-
|
|
66
|
-
expect(program.commands.some((command) => command.name() === "comments")).toBe(true);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("lists comments for a record", async () => {
|
|
70
|
-
const configPath = createApiConfig();
|
|
71
|
-
const output = createStdoutBuffer();
|
|
72
|
-
const fetchMock = createFetchMock();
|
|
73
|
-
fetchMock.mockResolvedValueOnce(
|
|
74
|
-
jsonResponse([
|
|
75
|
-
{
|
|
76
|
-
id: "comment-1",
|
|
77
|
-
text: "Hello",
|
|
78
|
-
},
|
|
79
|
-
]),
|
|
80
|
-
);
|
|
81
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
82
|
-
|
|
83
|
-
await program.parseAsync([
|
|
84
|
-
"node",
|
|
85
|
-
"zinkee",
|
|
86
|
-
"--json",
|
|
87
|
-
"comments",
|
|
88
|
-
"list",
|
|
89
|
-
"contacts",
|
|
90
|
-
"550E8400-E29B-41D4-A716-446655440001",
|
|
91
|
-
]);
|
|
92
|
-
|
|
93
|
-
expect(fetchMock.mock.calls[0]?.[0]).toBe(
|
|
94
|
-
"https://api.zinkee.test/api/v2/schemas/contacts/records/550e8400-e29b-41d4-a716-446655440001/comments",
|
|
95
|
-
);
|
|
96
|
-
expect(JSON.parse(output.read())).toEqual({
|
|
97
|
-
data: [
|
|
98
|
-
{
|
|
99
|
-
id: "comment-1",
|
|
100
|
-
text: "Hello",
|
|
101
|
-
},
|
|
102
|
-
],
|
|
103
|
-
meta: {
|
|
104
|
-
command: "comments list",
|
|
105
|
-
},
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it("creates comments with text-first input", async () => {
|
|
110
|
-
const configPath = createApiConfig();
|
|
111
|
-
const output = createStdoutBuffer();
|
|
112
|
-
const fetchMock = createFetchMock();
|
|
113
|
-
fetchMock.mockResolvedValueOnce(
|
|
114
|
-
jsonResponse({
|
|
115
|
-
id: "comment-1",
|
|
116
|
-
text: "Hello world",
|
|
117
|
-
}),
|
|
118
|
-
);
|
|
119
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
120
|
-
|
|
121
|
-
await program.parseAsync([
|
|
122
|
-
"node",
|
|
123
|
-
"zinkee",
|
|
124
|
-
"--json",
|
|
125
|
-
"comments",
|
|
126
|
-
"create",
|
|
127
|
-
"contacts",
|
|
128
|
-
"550E8400-E29B-41D4-A716-446655440001",
|
|
129
|
-
"--text",
|
|
130
|
-
"Hello world",
|
|
131
|
-
]);
|
|
132
|
-
|
|
133
|
-
const init = fetchMock.mock.calls[0]?.[1];
|
|
134
|
-
|
|
135
|
-
expect(fetchMock.mock.calls[0]?.[0]).toBe(
|
|
136
|
-
"https://api.zinkee.test/api/v2/schemas/contacts/records/550e8400-e29b-41d4-a716-446655440001/comments",
|
|
137
|
-
);
|
|
138
|
-
expect(init?.method).toBe("POST");
|
|
139
|
-
expect(JSON.parse(String(init?.body))).toEqual({
|
|
140
|
-
content: [
|
|
141
|
-
{
|
|
142
|
-
type: "TEXT",
|
|
143
|
-
value: "Hello world",
|
|
144
|
-
},
|
|
145
|
-
],
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it("accepts raw-file comment payloads without requiring text", async () => {
|
|
150
|
-
const configPath = createApiConfig();
|
|
151
|
-
const output = createStdoutBuffer();
|
|
152
|
-
const fetchMock = createFetchMock();
|
|
153
|
-
const rawFile = join(
|
|
154
|
-
mkdtempSync(join(tmpdir(), "zinkee-comment-create-")),
|
|
155
|
-
"comment.json",
|
|
156
|
-
);
|
|
157
|
-
writeFileSync(
|
|
158
|
-
rawFile,
|
|
159
|
-
JSON.stringify({
|
|
160
|
-
kind: "rich",
|
|
161
|
-
blocks: [{ type: "paragraph", text: "Hello" }],
|
|
162
|
-
}),
|
|
163
|
-
"utf8",
|
|
164
|
-
);
|
|
165
|
-
fetchMock.mockResolvedValueOnce(
|
|
166
|
-
jsonResponse({
|
|
167
|
-
id: "comment-2",
|
|
168
|
-
}),
|
|
169
|
-
);
|
|
170
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
171
|
-
|
|
172
|
-
await program.parseAsync([
|
|
173
|
-
"node",
|
|
174
|
-
"zinkee",
|
|
175
|
-
"--json",
|
|
176
|
-
"comments",
|
|
177
|
-
"create",
|
|
178
|
-
"contacts",
|
|
179
|
-
"record-1",
|
|
180
|
-
"--raw-file",
|
|
181
|
-
rawFile,
|
|
182
|
-
]);
|
|
183
|
-
|
|
184
|
-
const init = fetchMock.mock.calls[0]?.[1];
|
|
185
|
-
|
|
186
|
-
expect(JSON.parse(String(init?.body))).toEqual({
|
|
187
|
-
kind: "rich",
|
|
188
|
-
blocks: [{ type: "paragraph", text: "Hello" }],
|
|
189
|
-
});
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it("blocks comment creation in read-only mode", async () => {
|
|
193
|
-
const configPath = createApiConfig();
|
|
194
|
-
const output = createStdoutBuffer();
|
|
195
|
-
const fetchMock = createFetchMock();
|
|
196
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
197
|
-
|
|
198
|
-
await expect(
|
|
199
|
-
program.parseAsync([
|
|
200
|
-
"node",
|
|
201
|
-
"zinkee",
|
|
202
|
-
"--read-only",
|
|
203
|
-
"comments",
|
|
204
|
-
"create",
|
|
205
|
-
"contacts",
|
|
206
|
-
"record-1",
|
|
207
|
-
"--text",
|
|
208
|
-
"Hello world",
|
|
209
|
-
]),
|
|
210
|
-
).rejects.toThrowError('Command "comments create" is not allowed in read-only mode.');
|
|
211
|
-
|
|
212
|
-
expect(fetchMock).not.toHaveBeenCalled();
|
|
213
|
-
});
|
|
214
|
-
});
|
package/src/commands/comments.ts
DELETED
|
@@ -1,303 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import type { Writable } from "node:stream";
|
|
3
|
-
|
|
4
|
-
import { Command, InvalidArgumentError } from "commander";
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
CommentsApi,
|
|
8
|
-
type CreateCommentInput,
|
|
9
|
-
} from "../api/comments.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 { JsonObject, RuntimeOverrides } from "../types.js";
|
|
18
|
-
import { CliError } from "../utils/errors.js";
|
|
19
|
-
import { buildJsonSuccess, printTable } from "../utils/output.js";
|
|
20
|
-
|
|
21
|
-
interface CommandIo {
|
|
22
|
-
write(chunk: string): unknown;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface RegisterCommentsCommandsOptions {
|
|
26
|
-
configPath?: string;
|
|
27
|
-
stdout?: CommandIo;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
interface GlobalOptions extends RuntimeOverrides {}
|
|
31
|
-
|
|
32
|
-
interface CommentCreateCommandOptions {
|
|
33
|
-
text?: string;
|
|
34
|
-
raw?: string;
|
|
35
|
-
rawFile?: string;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function getCommandIo(stdout: CommandIo | undefined): CommandIo {
|
|
39
|
-
return stdout ?? (process.stdout as Writable);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function writeLine(stdout: CommandIo, value: string): void {
|
|
43
|
-
stdout.write(`${value}\n`);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function getGlobalOptions(command: Command): GlobalOptions {
|
|
47
|
-
return command.optsWithGlobals<GlobalOptions>();
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function emitJson(
|
|
51
|
-
stdout: CommandIo,
|
|
52
|
-
commandName: string,
|
|
53
|
-
data: unknown,
|
|
54
|
-
..._extra: unknown[]
|
|
55
|
-
): void {
|
|
56
|
-
writeLine(
|
|
57
|
-
stdout,
|
|
58
|
-
JSON.stringify(buildJsonSuccess(data, { command: commandName })),
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function toConfigurationError(filePath: string, error: unknown): never {
|
|
63
|
-
if (error instanceof CliError) {
|
|
64
|
-
throw error;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (
|
|
68
|
-
typeof error === "object" &&
|
|
69
|
-
error !== null &&
|
|
70
|
-
"code" in error &&
|
|
71
|
-
error.code === "ENOENT"
|
|
72
|
-
) {
|
|
73
|
-
throw new CliError("configuration_error", `Config file not found at ${filePath}.`);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (error instanceof Error) {
|
|
77
|
-
throw new CliError("configuration_error", error.message);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
throw error;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function createCommentsApi(
|
|
84
|
-
command: Command,
|
|
85
|
-
filePath: string,
|
|
86
|
-
): { api: CommentsApi; json: boolean; readOnly: boolean } {
|
|
87
|
-
try {
|
|
88
|
-
const config = readRuntimeConfig(filePath);
|
|
89
|
-
const runtime = resolveRuntimeContext(config, getGlobalOptions(command));
|
|
90
|
-
|
|
91
|
-
if (!runtime.baseUrl) {
|
|
92
|
-
throw new CliError(
|
|
93
|
-
"configuration_error",
|
|
94
|
-
`Resolved profile "${runtime.profile}" is missing a base URL.`,
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return {
|
|
99
|
-
api: new CommentsApi(
|
|
100
|
-
new ApiClient({
|
|
101
|
-
baseUrl: runtime.baseUrl,
|
|
102
|
-
apiKey: runtime.apiKey,
|
|
103
|
-
}),
|
|
104
|
-
),
|
|
105
|
-
json: runtime.json,
|
|
106
|
-
readOnly: runtime.readOnly,
|
|
107
|
-
};
|
|
108
|
-
} catch (error) {
|
|
109
|
-
toConfigurationError(filePath, error);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function ensureWritable(
|
|
114
|
-
commandName: string,
|
|
115
|
-
readOnly: boolean,
|
|
116
|
-
commandMode: "write" | "destructive",
|
|
117
|
-
): void {
|
|
118
|
-
if (readOnly) {
|
|
119
|
-
throw new CliError(
|
|
120
|
-
"command_not_allowed_in_read_only_mode",
|
|
121
|
-
`Command "${commandName}" is not allowed in read-only mode.`,
|
|
122
|
-
{
|
|
123
|
-
commandMode,
|
|
124
|
-
executionMode: "read-only",
|
|
125
|
-
},
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function parseUuidOrSlugSelector(input: string): NormalizedUuidOrSlug {
|
|
131
|
-
try {
|
|
132
|
-
return normalizeUuidOrSlug(input);
|
|
133
|
-
} catch (error) {
|
|
134
|
-
const message = error instanceof Error ? error.message : "Invalid selector.";
|
|
135
|
-
throw new InvalidArgumentError(message);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function isJsonObject(value: unknown): value is JsonObject {
|
|
140
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function parseRawJsonObject(input: string, sourceLabel: string): JsonObject {
|
|
144
|
-
let parsed: unknown;
|
|
145
|
-
|
|
146
|
-
try {
|
|
147
|
-
parsed = JSON.parse(input);
|
|
148
|
-
} catch (error) {
|
|
149
|
-
const message = error instanceof Error ? error.message : "Invalid JSON.";
|
|
150
|
-
throw new CliError(
|
|
151
|
-
"invalid_cli_usage",
|
|
152
|
-
`Failed to parse JSON from ${sourceLabel}: ${message}`,
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (!isJsonObject(parsed)) {
|
|
157
|
-
throw new CliError(
|
|
158
|
-
"invalid_cli_usage",
|
|
159
|
-
`Expected ${sourceLabel} to contain a JSON object.`,
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return parsed;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function readRawObject(
|
|
167
|
-
raw: string | undefined,
|
|
168
|
-
rawFile: string | undefined,
|
|
169
|
-
): JsonObject {
|
|
170
|
-
if (raw && rawFile) {
|
|
171
|
-
throw new CliError(
|
|
172
|
-
"invalid_cli_usage",
|
|
173
|
-
'Provide either "--raw" or "--raw-file", not both.',
|
|
174
|
-
);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
if (raw) {
|
|
178
|
-
return parseRawJsonObject(raw, "--raw");
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
if (rawFile) {
|
|
182
|
-
let contents: string;
|
|
183
|
-
|
|
184
|
-
try {
|
|
185
|
-
contents = readFileSync(rawFile, "utf8");
|
|
186
|
-
} catch (error) {
|
|
187
|
-
const message = error instanceof Error ? error.message : "Unable to read file.";
|
|
188
|
-
throw new CliError(
|
|
189
|
-
"invalid_cli_usage",
|
|
190
|
-
`Failed to read raw payload from ${rawFile}: ${message}`,
|
|
191
|
-
);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return parseRawJsonObject(contents, `--raw-file ${rawFile}`);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
return {};
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function isEmptyObject(value: object): boolean {
|
|
201
|
-
return Object.keys(value).length === 0;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function buildCommentCreateInput(options: CommentCreateCommandOptions): CreateCommentInput {
|
|
205
|
-
const input: CreateCommentInput = {
|
|
206
|
-
...readRawObject(options.raw, options.rawFile),
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
if (options.text !== undefined) {
|
|
210
|
-
input.content = [
|
|
211
|
-
{
|
|
212
|
-
type: "TEXT",
|
|
213
|
-
value: options.text,
|
|
214
|
-
},
|
|
215
|
-
];
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
if (isEmptyObject(input)) {
|
|
219
|
-
throw new CliError(
|
|
220
|
-
"invalid_cli_usage",
|
|
221
|
-
'Provide comment content via "--text", "--raw", or "--raw-file".',
|
|
222
|
-
);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
return input;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function toTableRows(data: unknown): Record<string, unknown>[] {
|
|
229
|
-
if (Array.isArray(data)) {
|
|
230
|
-
return data.map((entry) => (isJsonObject(entry) ? entry : { value: entry }));
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
if (isJsonObject(data)) {
|
|
234
|
-
return [data];
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
return [{ value: data }];
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function emitTable(stdout: CommandIo, data: unknown): void {
|
|
241
|
-
printTable(toTableRows(data), undefined, stdout);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
export function registerCommentsCommands(
|
|
245
|
-
program: Command,
|
|
246
|
-
options: RegisterCommentsCommandsOptions = {},
|
|
247
|
-
): void {
|
|
248
|
-
const filePath = options.configPath ?? defaultConfigPath;
|
|
249
|
-
const stdout = getCommandIo(options.stdout);
|
|
250
|
-
const comments = program.command("comments").description("Manage record comments");
|
|
251
|
-
|
|
252
|
-
comments
|
|
253
|
-
.command("list")
|
|
254
|
-
.description("List comments for a record")
|
|
255
|
-
.argument("<schema>", "schema uuid or slug", parseUuidOrSlugSelector)
|
|
256
|
-
.argument("<record>", "record uuid or slug", parseUuidOrSlugSelector)
|
|
257
|
-
.action(async function action(
|
|
258
|
-
this: Command,
|
|
259
|
-
schema: NormalizedUuidOrSlug,
|
|
260
|
-
record: NormalizedUuidOrSlug,
|
|
261
|
-
) {
|
|
262
|
-
const { api, json } = createCommentsApi(this, filePath);
|
|
263
|
-
const data = await api.listComments(schema, record.value);
|
|
264
|
-
|
|
265
|
-
if (json) {
|
|
266
|
-
emitJson(stdout, "comments list", data, filePath);
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
emitTable(stdout, data);
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
comments
|
|
274
|
-
.command("create")
|
|
275
|
-
.description("Create a comment on a record")
|
|
276
|
-
.argument("<schema>", "schema uuid or slug", parseUuidOrSlugSelector)
|
|
277
|
-
.argument("<record>", "record uuid or slug", parseUuidOrSlugSelector)
|
|
278
|
-
.option("--text <text>", "comment text")
|
|
279
|
-
.option("--raw <json>", "raw JSON object payload")
|
|
280
|
-
.option("--raw-file <path>", "path to a raw JSON object payload")
|
|
281
|
-
.action(async function action(
|
|
282
|
-
this: Command,
|
|
283
|
-
schema: NormalizedUuidOrSlug,
|
|
284
|
-
record: NormalizedUuidOrSlug,
|
|
285
|
-
commandOptions: CommentCreateCommandOptions,
|
|
286
|
-
) {
|
|
287
|
-
const { api, json, readOnly } = createCommentsApi(this, filePath);
|
|
288
|
-
ensureWritable("comments create", readOnly, "write");
|
|
289
|
-
|
|
290
|
-
const data = await api.createComment(
|
|
291
|
-
schema,
|
|
292
|
-
record.value,
|
|
293
|
-
buildCommentCreateInput(commandOptions),
|
|
294
|
-
);
|
|
295
|
-
|
|
296
|
-
if (json) {
|
|
297
|
-
emitJson(stdout, "comments create", data, filePath);
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
writeLine(stdout, `Created comment "${String(data.id ?? "unknown")}".`);
|
|
302
|
-
});
|
|
303
|
-
}
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
|
|
5
|
-
import { describe, expect, it } from "vitest";
|
|
6
|
-
|
|
7
|
-
import { buildProgram } from "../index.js";
|
|
8
|
-
|
|
9
|
-
function createTempConfig(contents: string): string {
|
|
10
|
-
const dir = mkdtempSync(join(tmpdir(), "zinkee-config-"));
|
|
11
|
-
const filePath = join(dir, "config.toml");
|
|
12
|
-
writeFileSync(filePath, contents, "utf8");
|
|
13
|
-
return filePath;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function createStdoutBuffer() {
|
|
17
|
-
let output = "";
|
|
18
|
-
|
|
19
|
-
return {
|
|
20
|
-
stdout: {
|
|
21
|
-
write(chunk: string) {
|
|
22
|
-
output += chunk;
|
|
23
|
-
return true;
|
|
24
|
-
},
|
|
25
|
-
},
|
|
26
|
-
read() {
|
|
27
|
-
return output;
|
|
28
|
-
},
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
describe("config commands", () => {
|
|
33
|
-
it("registers the config command group", () => {
|
|
34
|
-
const program = buildProgram();
|
|
35
|
-
|
|
36
|
-
expect(program.commands.some((command) => command.name() === "config")).toBe(true);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("redacts secrets in config show output", async () => {
|
|
40
|
-
const configPath = createTempConfig(`
|
|
41
|
-
default_profile = "prod"
|
|
42
|
-
|
|
43
|
-
[profiles.prod]
|
|
44
|
-
api_key = "super-secret-1234"
|
|
45
|
-
base_url = "https://api.zinkee.test"
|
|
46
|
-
`);
|
|
47
|
-
const output = createStdoutBuffer();
|
|
48
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
49
|
-
|
|
50
|
-
await program.parseAsync(["node", "zinkee", "config", "show"]);
|
|
51
|
-
|
|
52
|
-
expect(output.read()).toContain("Default profile: prod");
|
|
53
|
-
expect(output.read()).toContain("apiKey");
|
|
54
|
-
expect(output.read()).toContain("********1234");
|
|
55
|
-
expect(output.read()).not.toContain("super-secret-1234");
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("returns a JSON envelope for config validate", async () => {
|
|
59
|
-
const configPath = createTempConfig(`
|
|
60
|
-
default_profile = "prod"
|
|
61
|
-
|
|
62
|
-
[profiles.prod]
|
|
63
|
-
api_key = "token-1234"
|
|
64
|
-
`);
|
|
65
|
-
const output = createStdoutBuffer();
|
|
66
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
67
|
-
|
|
68
|
-
await program.parseAsync(["node", "zinkee", "--json", "config", "validate"]);
|
|
69
|
-
|
|
70
|
-
expect(JSON.parse(output.read())).toEqual({
|
|
71
|
-
data: {
|
|
72
|
-
valid: true,
|
|
73
|
-
profile: "prod",
|
|
74
|
-
},
|
|
75
|
-
meta: {
|
|
76
|
-
command: "config validate",
|
|
77
|
-
configPath,
|
|
78
|
-
},
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
});
|