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,284 +0,0 @@
|
|
|
1
|
-
import { mkdtempSync, readFileSync, 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 { registerFilesCommands } from "./files.js";
|
|
9
|
-
|
|
10
|
-
function createTempConfig(contents: string): string {
|
|
11
|
-
const dir = mkdtempSync(join(tmpdir(), "zinkee-files-"));
|
|
12
|
-
const filePath = join(dir, "config.toml");
|
|
13
|
-
writeFileSync(filePath, contents, "utf8");
|
|
14
|
-
return filePath;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function createApiConfig(): string {
|
|
18
|
-
return createTempConfig(`
|
|
19
|
-
default_profile = "prod"
|
|
20
|
-
|
|
21
|
-
[profiles.prod]
|
|
22
|
-
api_key = "files-token-1234"
|
|
23
|
-
base_url = "https://api.zinkee.test"
|
|
24
|
-
`);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function createStdoutBuffer() {
|
|
28
|
-
const chunks: Buffer[] = [];
|
|
29
|
-
|
|
30
|
-
return {
|
|
31
|
-
stdout: {
|
|
32
|
-
write(chunk: string | Uint8Array) {
|
|
33
|
-
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
34
|
-
return true;
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
readText() {
|
|
38
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
39
|
-
},
|
|
40
|
-
readBytes() {
|
|
41
|
-
return Buffer.concat(chunks);
|
|
42
|
-
},
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function jsonResponse(body: unknown, status: number = 200): Response {
|
|
47
|
-
return new Response(JSON.stringify(body), {
|
|
48
|
-
status,
|
|
49
|
-
headers: {
|
|
50
|
-
"content-type": "application/json",
|
|
51
|
-
},
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function binaryResponse(bytes: number[], status: number = 200): Response {
|
|
56
|
-
return new Response(new Uint8Array(bytes), {
|
|
57
|
-
status,
|
|
58
|
-
headers: {
|
|
59
|
-
"content-type": "application/octet-stream",
|
|
60
|
-
},
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function createFetchMock() {
|
|
65
|
-
const fetchMock = vi.fn<typeof fetch>();
|
|
66
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
67
|
-
return fetchMock;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function createProgram(): Command {
|
|
71
|
-
return new Command()
|
|
72
|
-
.enablePositionalOptions()
|
|
73
|
-
.option("--profile <name>", "select a profile from config")
|
|
74
|
-
.option("--base-url <url>", "override the configured base URL")
|
|
75
|
-
.option("--api-key <token>", "override the configured API key")
|
|
76
|
-
.option("--read-only", "run in read-only mode")
|
|
77
|
-
.option("--json", "emit JSON output");
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
afterEach(() => {
|
|
81
|
-
vi.unstubAllGlobals();
|
|
82
|
-
vi.restoreAllMocks();
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
describe("files commands", () => {
|
|
86
|
-
it("registers the files command group", () => {
|
|
87
|
-
const program = createProgram();
|
|
88
|
-
registerFilesCommands(program);
|
|
89
|
-
|
|
90
|
-
expect(program.commands.some((command) => command.name() === "files")).toBe(true);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it("uploads a local file through multipart form data", async () => {
|
|
94
|
-
const configPath = createApiConfig();
|
|
95
|
-
const output = createStdoutBuffer();
|
|
96
|
-
const fetchMock = createFetchMock();
|
|
97
|
-
const filePath = join(mkdtempSync(join(tmpdir(), "zinkee-files-upload-")), "upload.txt");
|
|
98
|
-
writeFileSync(filePath, "hello world", "utf8");
|
|
99
|
-
fetchMock.mockResolvedValueOnce(
|
|
100
|
-
jsonResponse({
|
|
101
|
-
id: "file-1",
|
|
102
|
-
name: "upload.txt",
|
|
103
|
-
}),
|
|
104
|
-
);
|
|
105
|
-
const program = createProgram();
|
|
106
|
-
registerFilesCommands(program, { configPath, stdout: output.stdout });
|
|
107
|
-
|
|
108
|
-
await program.parseAsync([
|
|
109
|
-
"node",
|
|
110
|
-
"zinkee",
|
|
111
|
-
"--json",
|
|
112
|
-
"files",
|
|
113
|
-
"upload",
|
|
114
|
-
filePath,
|
|
115
|
-
]);
|
|
116
|
-
|
|
117
|
-
const init = fetchMock.mock.calls[0]?.[1];
|
|
118
|
-
const body = init?.body as FormData;
|
|
119
|
-
const file = body.get("file");
|
|
120
|
-
|
|
121
|
-
expect(fetchMock.mock.calls[0]?.[0]).toBe("https://api.zinkee.test/api/v2/files");
|
|
122
|
-
expect(init?.method).toBe("POST");
|
|
123
|
-
expect(file).not.toBeNull();
|
|
124
|
-
expect(file).toBeInstanceOf(File);
|
|
125
|
-
if (!(file instanceof File)) {
|
|
126
|
-
throw new Error("Expected the upload payload to be a File.");
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
expect(file.name).toBe("upload.txt");
|
|
130
|
-
expect(Buffer.from(await file.arrayBuffer()).toString("utf8")).toBe("hello world");
|
|
131
|
-
expect(JSON.parse(output.readText())).toEqual({
|
|
132
|
-
data: {
|
|
133
|
-
id: "file-1",
|
|
134
|
-
name: "upload.txt",
|
|
135
|
-
},
|
|
136
|
-
meta: {
|
|
137
|
-
command: "files upload",
|
|
138
|
-
configPath,
|
|
139
|
-
},
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it("downloads files to stdout when no output path is provided", async () => {
|
|
144
|
-
const configPath = createApiConfig();
|
|
145
|
-
const output = createStdoutBuffer();
|
|
146
|
-
const fetchMock = createFetchMock();
|
|
147
|
-
fetchMock.mockResolvedValueOnce(binaryResponse([104, 101, 108, 108, 111]));
|
|
148
|
-
const program = createProgram();
|
|
149
|
-
registerFilesCommands(program, { configPath, stdout: output.stdout });
|
|
150
|
-
|
|
151
|
-
await program.parseAsync(["node", "zinkee", "files", "download", "550E8400-E29B-41D4-A716-446655440001"]);
|
|
152
|
-
|
|
153
|
-
expect(fetchMock.mock.calls[0]?.[0]).toBe(
|
|
154
|
-
"https://api.zinkee.test/api/v2/files/550e8400-e29b-41d4-a716-446655440001",
|
|
155
|
-
);
|
|
156
|
-
expect(output.readBytes().equals(Buffer.from("hello"))).toBe(true);
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it("fails when json output is requested without --output for downloads", async () => {
|
|
160
|
-
const configPath = createApiConfig();
|
|
161
|
-
const fetchMock = createFetchMock();
|
|
162
|
-
const program = createProgram();
|
|
163
|
-
registerFilesCommands(program, { configPath });
|
|
164
|
-
|
|
165
|
-
await expect(
|
|
166
|
-
program.parseAsync([
|
|
167
|
-
"node",
|
|
168
|
-
"zinkee",
|
|
169
|
-
"--json",
|
|
170
|
-
"files",
|
|
171
|
-
"download",
|
|
172
|
-
"550E8400-E29B-41D4-A716-446655440001",
|
|
173
|
-
]),
|
|
174
|
-
).rejects.toThrowError('Provide "--output" when using "--json" with "files download".');
|
|
175
|
-
|
|
176
|
-
expect(fetchMock).not.toHaveBeenCalled();
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it("downloads to a file and emits structured metadata in json mode", async () => {
|
|
180
|
-
const configPath = createApiConfig();
|
|
181
|
-
const output = createStdoutBuffer();
|
|
182
|
-
const fetchMock = createFetchMock();
|
|
183
|
-
const tempDir = mkdtempSync(join(tmpdir(), "zinkee-files-download-"));
|
|
184
|
-
const outputPath = join(tempDir, "downloaded.bin");
|
|
185
|
-
fetchMock.mockResolvedValueOnce(binaryResponse([1, 2, 3]));
|
|
186
|
-
const program = createProgram();
|
|
187
|
-
registerFilesCommands(program, { configPath, stdout: output.stdout });
|
|
188
|
-
|
|
189
|
-
await program.parseAsync([
|
|
190
|
-
"node",
|
|
191
|
-
"zinkee",
|
|
192
|
-
"--json",
|
|
193
|
-
"files",
|
|
194
|
-
"download",
|
|
195
|
-
"550E8400-E29B-41D4-A716-446655440001",
|
|
196
|
-
"--output",
|
|
197
|
-
outputPath,
|
|
198
|
-
]);
|
|
199
|
-
|
|
200
|
-
expect(readFileSync(outputPath)).toEqual(Buffer.from([1, 2, 3]));
|
|
201
|
-
expect(JSON.parse(output.readText())).toEqual({
|
|
202
|
-
data: {
|
|
203
|
-
file: "550e8400-e29b-41d4-a716-446655440001",
|
|
204
|
-
output: outputPath,
|
|
205
|
-
bytes: 3,
|
|
206
|
-
},
|
|
207
|
-
meta: {
|
|
208
|
-
command: "files download",
|
|
209
|
-
configPath,
|
|
210
|
-
},
|
|
211
|
-
});
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it("blocks file deletion in read-only mode", async () => {
|
|
215
|
-
const configPath = createApiConfig();
|
|
216
|
-
const fetchMock = createFetchMock();
|
|
217
|
-
const program = createProgram();
|
|
218
|
-
registerFilesCommands(program, { configPath });
|
|
219
|
-
|
|
220
|
-
await expect(
|
|
221
|
-
program.parseAsync([
|
|
222
|
-
"node",
|
|
223
|
-
"zinkee",
|
|
224
|
-
"--read-only",
|
|
225
|
-
"files",
|
|
226
|
-
"delete",
|
|
227
|
-
"550E8400-E29B-41D4-A716-446655440001",
|
|
228
|
-
]),
|
|
229
|
-
).rejects.toThrowError('Command "files delete" is not allowed in read-only mode.');
|
|
230
|
-
|
|
231
|
-
expect(fetchMock).not.toHaveBeenCalled();
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
it("blocks file uploads in read-only mode", async () => {
|
|
235
|
-
const configPath = createApiConfig();
|
|
236
|
-
const fetchMock = createFetchMock();
|
|
237
|
-
const tempDir = mkdtempSync(join(tmpdir(), "zinkee-files-upload-ro-"));
|
|
238
|
-
const filePath = join(tempDir, "upload.txt");
|
|
239
|
-
writeFileSync(filePath, "hello world", "utf8");
|
|
240
|
-
const program = createProgram();
|
|
241
|
-
registerFilesCommands(program, { configPath });
|
|
242
|
-
|
|
243
|
-
await expect(
|
|
244
|
-
program.parseAsync([
|
|
245
|
-
"node",
|
|
246
|
-
"zinkee",
|
|
247
|
-
"--read-only",
|
|
248
|
-
"files",
|
|
249
|
-
"upload",
|
|
250
|
-
filePath,
|
|
251
|
-
]),
|
|
252
|
-
).rejects.toThrowError('Command "files upload" is not allowed in read-only mode.');
|
|
253
|
-
|
|
254
|
-
expect(fetchMock).not.toHaveBeenCalled();
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
it("requests file storage information", async () => {
|
|
258
|
-
const configPath = createApiConfig();
|
|
259
|
-
const output = createStdoutBuffer();
|
|
260
|
-
const fetchMock = createFetchMock();
|
|
261
|
-
fetchMock.mockResolvedValueOnce(
|
|
262
|
-
jsonResponse({
|
|
263
|
-
usedBytes: 10,
|
|
264
|
-
totalBytes: 100,
|
|
265
|
-
}),
|
|
266
|
-
);
|
|
267
|
-
const program = createProgram();
|
|
268
|
-
registerFilesCommands(program, { configPath, stdout: output.stdout });
|
|
269
|
-
|
|
270
|
-
await program.parseAsync(["node", "zinkee", "--json", "files", "storage"]);
|
|
271
|
-
|
|
272
|
-
expect(fetchMock.mock.calls[0]?.[0]).toBe("https://api.zinkee.test/api/v2/files/storage-info");
|
|
273
|
-
expect(JSON.parse(output.readText())).toEqual({
|
|
274
|
-
data: {
|
|
275
|
-
usedBytes: 10,
|
|
276
|
-
totalBytes: 100,
|
|
277
|
-
},
|
|
278
|
-
meta: {
|
|
279
|
-
command: "files storage",
|
|
280
|
-
configPath,
|
|
281
|
-
},
|
|
282
|
-
});
|
|
283
|
-
});
|
|
284
|
-
});
|
package/src/commands/files.ts
DELETED
|
@@ -1,280 +0,0 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { basename } from "node:path";
|
|
3
|
-
import type { Writable } from "node:stream";
|
|
4
|
-
|
|
5
|
-
import { Command, InvalidArgumentError } from "commander";
|
|
6
|
-
|
|
7
|
-
import { FilesApi } from "../api/files.js";
|
|
8
|
-
import { ApiClient } from "../client.js";
|
|
9
|
-
import { defaultConfigPath, readRuntimeConfig } from "../config.js";
|
|
10
|
-
import { normalizeUuidOrSlug, type NormalizedUuidOrSlug } from "../parsers/selectors.js";
|
|
11
|
-
import { resolveRuntimeContext } from "../runtime-context.js";
|
|
12
|
-
import type { JsonObject, RuntimeOverrides } from "../types.js";
|
|
13
|
-
import { CliError } from "../utils/errors.js";
|
|
14
|
-
import { buildJsonSuccess, printTable } from "../utils/output.js";
|
|
15
|
-
|
|
16
|
-
interface CommandIo {
|
|
17
|
-
write(chunk: string | Uint8Array): unknown;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface RegisterFilesCommandsOptions {
|
|
21
|
-
configPath?: string;
|
|
22
|
-
stdout?: CommandIo;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
interface GlobalOptions extends RuntimeOverrides {}
|
|
26
|
-
|
|
27
|
-
interface FilesDownloadCommandOptions {
|
|
28
|
-
output?: string;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function getCommandIo(stdout: CommandIo | undefined): CommandIo {
|
|
32
|
-
return stdout ?? (process.stdout as Writable);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function writeLine(stdout: CommandIo, value: string): void {
|
|
36
|
-
stdout.write(`${value}\n`);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function writeBinary(stdout: CommandIo, value: ArrayBuffer): void {
|
|
40
|
-
stdout.write(Buffer.from(value));
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function getGlobalOptions(command: Command): GlobalOptions {
|
|
44
|
-
return command.optsWithGlobals<GlobalOptions>();
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function emitJson(
|
|
48
|
-
stdout: CommandIo,
|
|
49
|
-
commandName: string,
|
|
50
|
-
data: unknown,
|
|
51
|
-
configPath: string,
|
|
52
|
-
meta: Record<string, unknown> = {},
|
|
53
|
-
): void {
|
|
54
|
-
writeLine(
|
|
55
|
-
stdout,
|
|
56
|
-
JSON.stringify(buildJsonSuccess(data, { command: commandName, configPath, ...meta })),
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function toConfigurationError(filePath: string, error: unknown): never {
|
|
61
|
-
if (error instanceof CliError) {
|
|
62
|
-
throw error;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (
|
|
66
|
-
typeof error === "object" &&
|
|
67
|
-
error !== null &&
|
|
68
|
-
"code" in error &&
|
|
69
|
-
error.code === "ENOENT"
|
|
70
|
-
) {
|
|
71
|
-
throw new CliError("configuration_error", `Config file not found at ${filePath}.`);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (error instanceof Error) {
|
|
75
|
-
throw new CliError("configuration_error", error.message);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
throw error;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function createFilesApi(
|
|
82
|
-
command: Command,
|
|
83
|
-
filePath: string,
|
|
84
|
-
): { api: FilesApi; json: boolean; readOnly: boolean } {
|
|
85
|
-
try {
|
|
86
|
-
const config = readRuntimeConfig(filePath);
|
|
87
|
-
const runtime = resolveRuntimeContext(config, getGlobalOptions(command));
|
|
88
|
-
|
|
89
|
-
if (!runtime.baseUrl) {
|
|
90
|
-
throw new CliError(
|
|
91
|
-
"configuration_error",
|
|
92
|
-
`Resolved profile "${runtime.profile}" is missing a base URL.`,
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return {
|
|
97
|
-
api: new FilesApi(
|
|
98
|
-
new ApiClient({
|
|
99
|
-
baseUrl: runtime.baseUrl,
|
|
100
|
-
apiKey: runtime.apiKey,
|
|
101
|
-
}),
|
|
102
|
-
),
|
|
103
|
-
json: runtime.json,
|
|
104
|
-
readOnly: runtime.readOnly,
|
|
105
|
-
};
|
|
106
|
-
} catch (error) {
|
|
107
|
-
toConfigurationError(filePath, error);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function ensureWritable(
|
|
112
|
-
commandName: string,
|
|
113
|
-
readOnly: boolean,
|
|
114
|
-
commandMode: "write" | "destructive",
|
|
115
|
-
): void {
|
|
116
|
-
if (readOnly) {
|
|
117
|
-
throw new CliError(
|
|
118
|
-
"command_not_allowed_in_read_only_mode",
|
|
119
|
-
`Command "${commandName}" is not allowed in read-only mode.`,
|
|
120
|
-
{
|
|
121
|
-
commandMode,
|
|
122
|
-
executionMode: "read-only",
|
|
123
|
-
},
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function parseUuidOrSlugSelector(input: string): NormalizedUuidOrSlug {
|
|
129
|
-
try {
|
|
130
|
-
return normalizeUuidOrSlug(input);
|
|
131
|
-
} catch (error) {
|
|
132
|
-
const message = error instanceof Error ? error.message : "Invalid selector.";
|
|
133
|
-
throw new InvalidArgumentError(message);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function isRecord(value: unknown): value is JsonObject {
|
|
138
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function readLocalFile(filePath: string): Buffer {
|
|
142
|
-
try {
|
|
143
|
-
return readFileSync(filePath);
|
|
144
|
-
} catch (error) {
|
|
145
|
-
const message = error instanceof Error ? error.message : "Unable to read file.";
|
|
146
|
-
throw new CliError("invalid_cli_usage", `Failed to read file at ${filePath}: ${message}`);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function writeDownloadedFile(outputPath: string, value: ArrayBuffer): void {
|
|
151
|
-
try {
|
|
152
|
-
writeFileSync(outputPath, Buffer.from(value));
|
|
153
|
-
} catch (error) {
|
|
154
|
-
const message = error instanceof Error ? error.message : "Unable to write file.";
|
|
155
|
-
throw new CliError(
|
|
156
|
-
"invalid_cli_usage",
|
|
157
|
-
`Failed to write downloaded file to ${outputPath}: ${message}`,
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export function registerFilesCommands(
|
|
163
|
-
program: Command,
|
|
164
|
-
options: RegisterFilesCommandsOptions = {},
|
|
165
|
-
): void {
|
|
166
|
-
const filePath = options.configPath ?? defaultConfigPath;
|
|
167
|
-
const stdout = getCommandIo(options.stdout);
|
|
168
|
-
|
|
169
|
-
const files = program.command("files").description("Manage files and storage");
|
|
170
|
-
|
|
171
|
-
files
|
|
172
|
-
.command("upload")
|
|
173
|
-
.description("Upload a local file")
|
|
174
|
-
.argument("<path>", "path to the local file")
|
|
175
|
-
.action(function action(this: Command, path: string) {
|
|
176
|
-
const { api, json, readOnly } = createFilesApi(this, filePath);
|
|
177
|
-
ensureWritable("files upload", readOnly, "write");
|
|
178
|
-
|
|
179
|
-
const contents = readLocalFile(path);
|
|
180
|
-
const formData = new FormData();
|
|
181
|
-
formData.append("file", new Blob([contents]), basename(path));
|
|
182
|
-
|
|
183
|
-
return api.uploadFile(formData).then((data) => {
|
|
184
|
-
if (json) {
|
|
185
|
-
emitJson(stdout, "files upload", data, filePath);
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
writeLine(stdout, `Uploaded file "${basename(path)}".`);
|
|
190
|
-
});
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
files
|
|
194
|
-
.command("download")
|
|
195
|
-
.description("Download a file")
|
|
196
|
-
.argument("<file>", "file identifier")
|
|
197
|
-
.option("--output <path>", "write the downloaded file to a path")
|
|
198
|
-
.action(function action(
|
|
199
|
-
this: Command,
|
|
200
|
-
file: string,
|
|
201
|
-
commandOptions: FilesDownloadCommandOptions,
|
|
202
|
-
) {
|
|
203
|
-
const { api, json } = createFilesApi(this, filePath);
|
|
204
|
-
|
|
205
|
-
if (json && !commandOptions.output) {
|
|
206
|
-
throw new CliError(
|
|
207
|
-
"invalid_cli_usage",
|
|
208
|
-
'Provide "--output" when using "--json" with "files download".',
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const fileSelector = parseUuidOrSlugSelector(file);
|
|
213
|
-
|
|
214
|
-
return api.downloadFile(fileSelector).then((data) => {
|
|
215
|
-
if (commandOptions.output) {
|
|
216
|
-
writeDownloadedFile(commandOptions.output, data);
|
|
217
|
-
|
|
218
|
-
if (json) {
|
|
219
|
-
emitJson(
|
|
220
|
-
stdout,
|
|
221
|
-
"files download",
|
|
222
|
-
{
|
|
223
|
-
file: fileSelector.value,
|
|
224
|
-
output: commandOptions.output,
|
|
225
|
-
bytes: data.byteLength,
|
|
226
|
-
},
|
|
227
|
-
filePath,
|
|
228
|
-
);
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
writeLine(stdout, `Downloaded file to "${commandOptions.output}".`);
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
writeBinary(stdout, data);
|
|
237
|
-
});
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
files
|
|
241
|
-
.command("delete")
|
|
242
|
-
.description("Delete a file")
|
|
243
|
-
.argument("<file>", "file identifier")
|
|
244
|
-
.action(function action(this: Command, file: string) {
|
|
245
|
-
const { api, json, readOnly } = createFilesApi(this, filePath);
|
|
246
|
-
ensureWritable("files delete", readOnly, "destructive");
|
|
247
|
-
|
|
248
|
-
const fileSelector = parseUuidOrSlugSelector(file);
|
|
249
|
-
|
|
250
|
-
return api.deleteFile(fileSelector).then((data) => {
|
|
251
|
-
if (json) {
|
|
252
|
-
emitJson(stdout, "files delete", data, filePath);
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
writeLine(stdout, `Deleted file "${fileSelector.value}".`);
|
|
257
|
-
});
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
files
|
|
261
|
-
.command("storage")
|
|
262
|
-
.description("Show storage usage information")
|
|
263
|
-
.action(function action(this: Command) {
|
|
264
|
-
const { api, json } = createFilesApi(this, filePath);
|
|
265
|
-
|
|
266
|
-
return api.getStorageInfo().then((data) => {
|
|
267
|
-
if (json) {
|
|
268
|
-
emitJson(stdout, "files storage", data, filePath);
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
if (isRecord(data)) {
|
|
273
|
-
printTable([data], Object.keys(data), stdout);
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
writeLine(stdout, String(data));
|
|
278
|
-
});
|
|
279
|
-
});
|
|
280
|
-
}
|