@corvio/admin-cli 0.1.0-beta.2 → 0.1.0-beta.3
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 +5 -5
- package/dist/cli.js +155 -20
- package/dist/core.js +112 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# Corvio Platform Administrator CLI
|
|
2
2
|
|
|
3
|
-
Official read-only CLI for bounded Corvio platform investigations. It is separate from the Workspace CLI and accepts only a short-lived `cva_` credential.
|
|
3
|
+
Official read-only CLI for bounded Corvio platform investigations. It is separate from the Workspace CLI and accepts only a short-lived `cva_` credential, supplied by environment or administrator device link.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npm install --global @corvio/admin-cli
|
|
7
|
-
|
|
8
|
-
corvio-admin auth status
|
|
6
|
+
npm install --global @corvio/admin-cli@beta
|
|
7
|
+
corvio-admin auth login
|
|
8
|
+
corvio-admin auth status --json
|
|
9
9
|
corvio-admin catalog --json
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
Every data query requires a purpose and returns an auditable receipt. This package contains no ordinary Workspace commands. See [the administrator CLI guide](https://corvio.ai/developers/admin-cli).
|
|
12
|
+
For Agents and CI, inject `CORVIO_ADMIN_API_KEY='cva_...'`; it takes precedence over the stored device-link credential. Every data query requires a purpose and returns an auditable receipt. This package contains no ordinary Workspace commands. See [the administrator CLI guide](https://corvio.ai/developers/admin-cli).
|
package/dist/cli.js
CHANGED
|
@@ -4,25 +4,31 @@ import {
|
|
|
4
4
|
ApiClient,
|
|
5
5
|
CliError,
|
|
6
6
|
EXIT,
|
|
7
|
+
clearAdminCredential,
|
|
7
8
|
listOption,
|
|
8
9
|
normalizeApiBase,
|
|
9
10
|
numberOption,
|
|
11
|
+
openBrowser,
|
|
10
12
|
option,
|
|
11
13
|
readAdminCredential,
|
|
12
14
|
readTextFile,
|
|
13
15
|
runMain,
|
|
16
|
+
sleep,
|
|
17
|
+
storeAdminCredential,
|
|
18
|
+
writeBinaryFile,
|
|
14
19
|
writeResult,
|
|
15
20
|
} from "./core.js";
|
|
16
21
|
|
|
17
|
-
const VERSION = "0.1.0-beta.
|
|
22
|
+
const VERSION = "0.1.0-beta.3";
|
|
18
23
|
const DEFAULT_API_BASE = "https://api.corvio.ai/v1";
|
|
19
24
|
const USER_AGENT = `corvio-admin-cli/${VERSION}`;
|
|
20
25
|
|
|
21
26
|
const HELP = `Corvio Platform Administrator CLI ${VERSION}
|
|
22
27
|
|
|
23
28
|
Usage:
|
|
24
|
-
corvio-admin auth status
|
|
29
|
+
corvio-admin auth login|status|logout
|
|
25
30
|
corvio-admin capabilities
|
|
31
|
+
corvio-admin skill download [--output <path>]
|
|
26
32
|
corvio-admin catalog
|
|
27
33
|
corvio-admin query|data query --file <request.json>
|
|
28
34
|
corvio-admin workspaces list --purpose <reason>
|
|
@@ -33,24 +39,34 @@ Usage:
|
|
|
33
39
|
corvio-admin receipts get <id>
|
|
34
40
|
|
|
35
41
|
Authentication:
|
|
36
|
-
|
|
42
|
+
CORVIO_ADMIN_API_KEY=cva_... takes precedence over the stored device-link login.
|
|
43
|
+
Run 'corvio-admin auth login' when no environment key is configured.
|
|
37
44
|
|
|
38
45
|
This package contains no Workspace ask, search, docs, files, sync, or share commands.
|
|
39
46
|
`;
|
|
40
47
|
|
|
41
|
-
function baseUrl(options) {
|
|
42
|
-
return normalizeApiBase(
|
|
48
|
+
function baseUrl(options, credential = null) {
|
|
49
|
+
return normalizeApiBase(
|
|
50
|
+
options["api-base"]
|
|
51
|
+
|| process.env.CORVIO_ADMIN_API_BASE
|
|
52
|
+
|| process.env.CORVIO_API_BASE
|
|
53
|
+
|| credential?.metadata?.api_base,
|
|
54
|
+
DEFAULT_API_BASE,
|
|
55
|
+
);
|
|
43
56
|
}
|
|
44
57
|
|
|
45
|
-
function client(options) {
|
|
46
|
-
const credential = readAdminCredential();
|
|
58
|
+
async function client(options) {
|
|
59
|
+
const credential = await readAdminCredential();
|
|
47
60
|
if (!credential) {
|
|
48
|
-
throw new CliError("Set CORVIO_ADMIN_API_KEY
|
|
61
|
+
throw new CliError("Set CORVIO_ADMIN_API_KEY or run `corvio-admin auth login`.", {
|
|
49
62
|
code: "admin_auth_required",
|
|
50
63
|
exitCode: EXIT.AUTH,
|
|
51
64
|
});
|
|
52
65
|
}
|
|
53
|
-
return
|
|
66
|
+
return {
|
|
67
|
+
api: new ApiClient({ baseUrl: baseUrl(options, credential), credential: credential.key, userAgent: USER_AGENT }),
|
|
68
|
+
credential,
|
|
69
|
+
};
|
|
54
70
|
}
|
|
55
71
|
|
|
56
72
|
function listQuery(options) {
|
|
@@ -63,8 +79,85 @@ function listQuery(options) {
|
|
|
63
79
|
};
|
|
64
80
|
}
|
|
65
81
|
|
|
82
|
+
async function authLogin(options) {
|
|
83
|
+
const current = await readAdminCredential();
|
|
84
|
+
if (current && !options.force) {
|
|
85
|
+
const api = new ApiClient({ baseUrl: baseUrl(options, current), credential: current.key, userAgent: USER_AGENT });
|
|
86
|
+
const identity = await api.request("/me");
|
|
87
|
+
writeResult({ authenticated: true, source: current.source, identity }, { json: options.json });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const requestedScopes = listOption(options, "scopes", [
|
|
91
|
+
"admin_data:read",
|
|
92
|
+
"admin_support:read",
|
|
93
|
+
"admin_costs:read",
|
|
94
|
+
]);
|
|
95
|
+
if (requestedScopes.includes("admin_content:read") && !options.force) {
|
|
96
|
+
throw new CliError(
|
|
97
|
+
"Requesting admin_content:read requires --force and should be limited to a specific Run investigation.",
|
|
98
|
+
{ code: "admin_content_scope_confirmation_required" },
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
const api = new ApiClient({ baseUrl: baseUrl(options), userAgent: USER_AGENT });
|
|
102
|
+
const authorization = await api.request("/admin/auth/device-authorizations", {
|
|
103
|
+
method: "POST",
|
|
104
|
+
auth: false,
|
|
105
|
+
body: {
|
|
106
|
+
client_name: "Corvio Platform Administrator CLI",
|
|
107
|
+
cli_version: VERSION,
|
|
108
|
+
scopes: requestedScopes,
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
process.stderr.write(`Open ${authorization.verification_uri_complete}\nCode: ${authorization.user_code}\n`);
|
|
112
|
+
if (!options["no-input"]) await openBrowser(authorization.verification_uri_complete);
|
|
113
|
+
|
|
114
|
+
const expiresAt = Date.now() + Number(authorization.expires_in || 600) * 1000;
|
|
115
|
+
let intervalSeconds = Math.max(5, Number(authorization.interval || 5));
|
|
116
|
+
let token;
|
|
117
|
+
while (Date.now() < expiresAt) {
|
|
118
|
+
await sleep(intervalSeconds * 1000);
|
|
119
|
+
try {
|
|
120
|
+
token = await api.request("/admin/auth/device-token", {
|
|
121
|
+
method: "POST",
|
|
122
|
+
auth: false,
|
|
123
|
+
body: { device_code: authorization.device_code },
|
|
124
|
+
});
|
|
125
|
+
break;
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (!(error instanceof CliError)) throw error;
|
|
128
|
+
if (error.code === "authorization_pending") continue;
|
|
129
|
+
if (error.code === "slow_down") {
|
|
130
|
+
intervalSeconds = Math.min(30, intervalSeconds + 5);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (!token?.secret || token.audience !== "platform_admin") {
|
|
137
|
+
throw new CliError("Administrator device authorization expired; run `corvio-admin auth login` again.", {
|
|
138
|
+
code: "expired_token",
|
|
139
|
+
exitCode: EXIT.AUTH,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
await storeAdminCredential({ ...token, api_base: baseUrl(options) });
|
|
143
|
+
const authenticatedApi = new ApiClient({ baseUrl: baseUrl(options), credential: token.secret, userAgent: USER_AGENT });
|
|
144
|
+
const identity = await authenticatedApi.request("/me");
|
|
145
|
+
writeResult({
|
|
146
|
+
authenticated: true,
|
|
147
|
+
source: "device_link",
|
|
148
|
+
audience: identity.api_key?.audience,
|
|
149
|
+
scopes: identity.api_key?.scopes || [],
|
|
150
|
+
expires_at: token.expires_at,
|
|
151
|
+
}, { json: options.json });
|
|
152
|
+
}
|
|
153
|
+
|
|
66
154
|
async function authStatus(options) {
|
|
67
|
-
const
|
|
155
|
+
const credential = await readAdminCredential();
|
|
156
|
+
if (!credential) {
|
|
157
|
+
writeResult({ authenticated: false, sources: ["CORVIO_ADMIN_API_KEY", "device_link"] }, { json: options.json });
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const api = new ApiClient({ baseUrl: baseUrl(options, credential), credential: credential.key, userAgent: USER_AGENT });
|
|
68
161
|
const identity = await api.request("/me");
|
|
69
162
|
if (identity.api_key?.audience !== "platform_admin") {
|
|
70
163
|
throw new CliError("The configured key is not a platform administrator credential.", {
|
|
@@ -72,17 +165,36 @@ async function authStatus(options) {
|
|
|
72
165
|
exitCode: EXIT.AUTH,
|
|
73
166
|
});
|
|
74
167
|
}
|
|
75
|
-
writeResult({ authenticated: true, source:
|
|
168
|
+
writeResult({ authenticated: true, source: credential.source, identity }, { json: options.json });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function authLogout(options) {
|
|
172
|
+
const credential = await readAdminCredential();
|
|
173
|
+
if (!credential) {
|
|
174
|
+
writeResult({ authenticated: false, changed: false }, { json: options.json });
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (credential.source === "environment") {
|
|
178
|
+
throw new CliError("CORVIO_ADMIN_API_KEY is managed by the parent environment and cannot be removed by the CLI.", {
|
|
179
|
+
code: "environment_credential_managed_externally",
|
|
180
|
+
exitCode: EXIT.AUTH,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
const api = new ApiClient({ baseUrl: baseUrl(options, credential), credential: credential.key, userAgent: USER_AGENT });
|
|
184
|
+
await api.request("/admin/auth/credentials/current:revoke", { method: "POST" });
|
|
185
|
+
await clearAdminCredential();
|
|
186
|
+
writeResult({ authenticated: false, revoked: true }, { json: options.json });
|
|
76
187
|
}
|
|
77
188
|
|
|
78
189
|
async function capabilities(options) {
|
|
79
|
-
const api = client(options);
|
|
190
|
+
const { api, credential } = await client(options);
|
|
80
191
|
const [index, identity] = await Promise.all([api.request("/admin"), api.request("/me")]);
|
|
81
|
-
writeResult({ cli_version: VERSION, api: index, principal: identity }, { json: options.json });
|
|
192
|
+
writeResult({ cli_version: VERSION, auth_source: credential.source, api: index, principal: identity }, { json: options.json });
|
|
82
193
|
}
|
|
83
194
|
|
|
84
195
|
async function catalog(options) {
|
|
85
|
-
|
|
196
|
+
const { api } = await client(options);
|
|
197
|
+
writeResult(await api.request("/admin/data/catalog"), { json: options.json });
|
|
86
198
|
}
|
|
87
199
|
|
|
88
200
|
async function query(options) {
|
|
@@ -103,19 +215,39 @@ async function query(options) {
|
|
|
103
215
|
cursor: options.cursor || null,
|
|
104
216
|
};
|
|
105
217
|
}
|
|
106
|
-
|
|
218
|
+
const { api } = await client(options);
|
|
219
|
+
writeResult(await api.request("/admin/data/query", { method: "POST", body }), { json: options.json });
|
|
107
220
|
}
|
|
108
221
|
|
|
109
222
|
async function listResource(options, resource) {
|
|
110
|
-
|
|
223
|
+
const { api } = await client(options);
|
|
224
|
+
writeResult(await api.request(`/admin/${resource}`, { query: listQuery(options) }), { json: options.json });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function downloadSkill(options) {
|
|
228
|
+
const { api } = await client(options);
|
|
229
|
+
const output = String(options.output || "corvio-admin-data.zip");
|
|
230
|
+
const bytes = await api.requestBytes("/admin/skill/corvio-admin-data.zip");
|
|
231
|
+
const path = await writeBinaryFile(output, bytes);
|
|
232
|
+
writeResult({
|
|
233
|
+
downloaded: true,
|
|
234
|
+
output: path,
|
|
235
|
+
install: `npx skills add ${path}`,
|
|
236
|
+
}, { json: options.json });
|
|
111
237
|
}
|
|
112
238
|
|
|
113
239
|
async function main({ options, positionals }) {
|
|
114
240
|
const [command, action, id] = positionals;
|
|
115
241
|
if (options.version || command === "version") return writeResult(VERSION, { json: options.json });
|
|
116
242
|
if (options.help || !command || command === "help") return writeResult(HELP.trimEnd(), { json: false });
|
|
117
|
-
if (command === "auth"
|
|
243
|
+
if (command === "auth") {
|
|
244
|
+
if (action === "login") return authLogin(options);
|
|
245
|
+
if (action === "status") return authStatus(options);
|
|
246
|
+
if (action === "logout") return authLogout(options);
|
|
247
|
+
throw new CliError("auth action must be login, status, or logout.");
|
|
248
|
+
}
|
|
118
249
|
if (command === "capabilities") return capabilities(options);
|
|
250
|
+
if (command === "skill" && action === "download") return downloadSkill(options);
|
|
119
251
|
if (command === "catalog") return catalog(options);
|
|
120
252
|
if (command === "query") return query(options);
|
|
121
253
|
if (command === "data" && action === "query") return query(options);
|
|
@@ -126,15 +258,18 @@ async function main({ options, positionals }) {
|
|
|
126
258
|
if (command === "runs" && action === "content") {
|
|
127
259
|
const runId = id || option(options, "id", { required: true });
|
|
128
260
|
const purpose = option(options, "purpose", { required: true });
|
|
129
|
-
|
|
261
|
+
const { api } = await client(options);
|
|
262
|
+
return writeResult(await api.request(`/admin/runs/${runId}/content`, { query: { purpose } }), { json: options.json });
|
|
130
263
|
}
|
|
131
264
|
if (command === "usage" && action === "events") return listResource(options, "usage/events");
|
|
132
265
|
if (command === "usage" && action === "summary") {
|
|
133
|
-
|
|
266
|
+
const { api } = await client(options);
|
|
267
|
+
return writeResult(await api.request("/admin/usage/summary", { query: listQuery(options) }), { json: options.json });
|
|
134
268
|
}
|
|
135
269
|
if (command === "receipts" && action === "get") {
|
|
136
270
|
const receiptId = id || option(options, "id", { required: true });
|
|
137
|
-
|
|
271
|
+
const { api } = await client(options);
|
|
272
|
+
return writeResult(await api.request(`/admin/data/query-receipts/${receiptId}`), { json: options.json });
|
|
138
273
|
}
|
|
139
274
|
throw new CliError(`Unknown administrator command: ${positionals.join(" ")}`);
|
|
140
275
|
}
|
package/dist/core.js
CHANGED
|
@@ -163,6 +163,51 @@ export class ApiClient {
|
|
|
163
163
|
}
|
|
164
164
|
return payload;
|
|
165
165
|
}
|
|
166
|
+
|
|
167
|
+
async requestBytes(path, { query, headers = {}, auth = true } = {}) {
|
|
168
|
+
const url = new URL(`${this.baseUrl}${path.startsWith("/") ? path : `/${path}`}`);
|
|
169
|
+
for (const [key, value] of Object.entries(query || {})) {
|
|
170
|
+
if (value !== undefined && value !== null && value !== "") url.searchParams.set(key, String(value));
|
|
171
|
+
}
|
|
172
|
+
const requestHeaders = {
|
|
173
|
+
accept: "application/zip, application/octet-stream",
|
|
174
|
+
"user-agent": this.userAgent,
|
|
175
|
+
...headers,
|
|
176
|
+
};
|
|
177
|
+
if (auth) {
|
|
178
|
+
if (!this.credential) {
|
|
179
|
+
throw new CliError("Corvio authentication is required.", { code: "auth_required", exitCode: EXIT.AUTH });
|
|
180
|
+
}
|
|
181
|
+
requestHeaders.authorization = `Bearer ${this.credential}`;
|
|
182
|
+
}
|
|
183
|
+
let response;
|
|
184
|
+
try {
|
|
185
|
+
response = await this.fetchImpl(url, { method: "GET", headers: requestHeaders });
|
|
186
|
+
} catch (cause) {
|
|
187
|
+
throw new CliError(`Unable to reach Corvio: ${cause instanceof Error ? cause.message : String(cause)}`, {
|
|
188
|
+
code: "network_error",
|
|
189
|
+
exitCode: EXIT.REMOTE,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
193
|
+
if (!response.ok) {
|
|
194
|
+
const text = new TextDecoder().decode(bytes);
|
|
195
|
+
let payload = {};
|
|
196
|
+
try {
|
|
197
|
+
payload = JSON.parse(text);
|
|
198
|
+
} catch {
|
|
199
|
+
payload = { message: text.slice(0, 500) };
|
|
200
|
+
}
|
|
201
|
+
const envelope = payload?.error || payload?.detail || payload || {};
|
|
202
|
+
throw new CliError(String(envelope.message || envelope.code || `Corvio request failed (${response.status}).`), {
|
|
203
|
+
code: String(envelope.code || `http_${response.status}`),
|
|
204
|
+
status: response.status,
|
|
205
|
+
exitCode: exitCodeForStatus(response.status),
|
|
206
|
+
details: envelope.details || null,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
return bytes;
|
|
210
|
+
}
|
|
166
211
|
}
|
|
167
212
|
|
|
168
213
|
function configRoot(env = process.env) {
|
|
@@ -175,6 +220,10 @@ export function workspaceCredentialPath(env = process.env) {
|
|
|
175
220
|
return join(configRoot(env), "credentials.json");
|
|
176
221
|
}
|
|
177
222
|
|
|
223
|
+
export function adminCredentialPath(env = process.env) {
|
|
224
|
+
return join(configRoot(env), "admin-credentials.json");
|
|
225
|
+
}
|
|
226
|
+
|
|
178
227
|
export async function readWorkspaceCredential(env = process.env) {
|
|
179
228
|
const environmentKey = String(env.CORVIO_API_KEY || "").trim();
|
|
180
229
|
if (environmentKey) {
|
|
@@ -241,16 +290,66 @@ export async function clearWorkspaceCredential(env = process.env) {
|
|
|
241
290
|
return path;
|
|
242
291
|
}
|
|
243
292
|
|
|
244
|
-
export function readAdminCredential(env = process.env) {
|
|
293
|
+
export async function readAdminCredential(env = process.env) {
|
|
245
294
|
const key = String(env.CORVIO_ADMIN_API_KEY || "").trim();
|
|
246
|
-
if (
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
295
|
+
if (key) {
|
|
296
|
+
if (!key.startsWith("cva_")) {
|
|
297
|
+
throw new CliError("CORVIO_ADMIN_API_KEY must be a platform administrator cva_ key.", {
|
|
298
|
+
code: "invalid_admin_key",
|
|
299
|
+
exitCode: EXIT.AUTH,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
return { key, source: "environment", metadata: null };
|
|
303
|
+
}
|
|
304
|
+
const path = adminCredentialPath(env);
|
|
305
|
+
let parsed;
|
|
306
|
+
try {
|
|
307
|
+
parsed = JSON.parse(await readFile(path, "utf8"));
|
|
308
|
+
} catch (cause) {
|
|
309
|
+
if (cause?.code === "ENOENT") return null;
|
|
310
|
+
throw new CliError("Stored Corvio administrator credentials are unreadable.", {
|
|
311
|
+
code: "admin_credential_store_invalid",
|
|
312
|
+
exitCode: EXIT.AUTH,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
const storedKey = String(parsed.api_key || "").trim();
|
|
316
|
+
if (!storedKey.startsWith("cva_")) {
|
|
317
|
+
throw new CliError("Stored Corvio administrator credentials are invalid; run `corvio-admin auth login` again.", {
|
|
318
|
+
code: "admin_credential_store_invalid",
|
|
250
319
|
exitCode: EXIT.AUTH,
|
|
251
320
|
});
|
|
252
321
|
}
|
|
253
|
-
return { key, source: "
|
|
322
|
+
return { key: storedKey, source: "device_link", metadata: parsed };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export async function storeAdminCredential(payload, env = process.env) {
|
|
326
|
+
const path = adminCredentialPath(env);
|
|
327
|
+
const root = dirname(path);
|
|
328
|
+
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
329
|
+
await chmod(root, 0o700).catch(() => {});
|
|
330
|
+
const document = {
|
|
331
|
+
version: 1,
|
|
332
|
+
api_key: payload.secret,
|
|
333
|
+
key_id: payload.key_id,
|
|
334
|
+
audience: payload.audience,
|
|
335
|
+
api_base: payload.api_base,
|
|
336
|
+
scopes: payload.scopes || [],
|
|
337
|
+
expires_at: payload.expires_at,
|
|
338
|
+
created_at: payload.created_at || new Date().toISOString(),
|
|
339
|
+
source: "device_link",
|
|
340
|
+
};
|
|
341
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
342
|
+
await writeFile(temporary, `${JSON.stringify(document, null, 2)}\n`, { mode: 0o600, flag: "wx" });
|
|
343
|
+
await chmod(temporary, 0o600);
|
|
344
|
+
await rename(temporary, path);
|
|
345
|
+
await chmod(path, 0o600);
|
|
346
|
+
return { path };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export async function clearAdminCredential(env = process.env) {
|
|
350
|
+
const path = adminCredentialPath(env);
|
|
351
|
+
await rm(path, { force: true });
|
|
352
|
+
return path;
|
|
254
353
|
}
|
|
255
354
|
|
|
256
355
|
export function writeResult(value, { json = false, stdout = process.stdout } = {}) {
|
|
@@ -313,6 +412,13 @@ export async function writeTextFile(path, content) {
|
|
|
313
412
|
return target;
|
|
314
413
|
}
|
|
315
414
|
|
|
415
|
+
export async function writeBinaryFile(path, content) {
|
|
416
|
+
const target = resolve(String(path));
|
|
417
|
+
await mkdir(dirname(target), { recursive: true });
|
|
418
|
+
await writeFile(target, content);
|
|
419
|
+
return target;
|
|
420
|
+
}
|
|
421
|
+
|
|
316
422
|
export function requireConfirmation(options, action) {
|
|
317
423
|
if (!options.yes) {
|
|
318
424
|
throw new CliError(`${action} requires --yes.`, { code: "confirmation_required" });
|