@corvio/cli 0.1.0-beta.4 → 0.1.0-beta.6
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 +34 -0
- package/dist/cli.js +743 -37
- package/dist/core.js +306 -48
- package/package.json +1 -1
package/dist/core.js
CHANGED
|
@@ -32,6 +32,59 @@ function exitCodeForStatus(status) {
|
|
|
32
32
|
return EXIT.REMOTE;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
|
|
36
|
+
const MAX_REQUEST_TIMEOUT_MS = 10 * 60_000;
|
|
37
|
+
const RETRYABLE_HTTP_STATUSES = new Set([408, 429, 502, 503, 504]);
|
|
38
|
+
|
|
39
|
+
function normalizeRequestTimeout(value, fallback = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
40
|
+
const raw = value === undefined || value === null || value === "" ? fallback : Number(value);
|
|
41
|
+
if (!Number.isSafeInteger(raw) || raw < 1 || raw > MAX_REQUEST_TIMEOUT_MS) {
|
|
42
|
+
throw new CliError(
|
|
43
|
+
`Request timeout must be an integer between 1 and ${MAX_REQUEST_TIMEOUT_MS} milliseconds.`,
|
|
44
|
+
{ code: "invalid_request_timeout" },
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return raw;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function networkFailureDetails({
|
|
51
|
+
cause,
|
|
52
|
+
url,
|
|
53
|
+
method,
|
|
54
|
+
requestHeaders = {},
|
|
55
|
+
body = undefined,
|
|
56
|
+
retrySafe = false,
|
|
57
|
+
}) {
|
|
58
|
+
const nested = cause && typeof cause === "object" ? cause.cause : null;
|
|
59
|
+
const causeCode = String(nested?.code || cause?.code || "").trim() || null;
|
|
60
|
+
const syscall = String(nested?.syscall || cause?.syscall || "").trim() || null;
|
|
61
|
+
const normalizedMethod = String(method || "GET").toUpperCase();
|
|
62
|
+
const operationKey = String(requestHeaders["idempotency-key"] || requestHeaders["Idempotency-Key"] || "").trim();
|
|
63
|
+
const revision = body?.expected_content_revision;
|
|
64
|
+
const revisionGuarded = normalizedMethod === "PUT" && Number.isSafeInteger(revision) && revision >= 0;
|
|
65
|
+
const safeToRetry = retrySafe || ["GET", "HEAD"].includes(normalizedMethod) || Boolean(operationKey) || revisionGuarded;
|
|
66
|
+
const timedOut = cause?.name === "TimeoutError" || cause?.name === "AbortError";
|
|
67
|
+
return {
|
|
68
|
+
method: normalizedMethod,
|
|
69
|
+
origin: new URL(String(url)).origin,
|
|
70
|
+
response_received: false,
|
|
71
|
+
timed_out: timedOut,
|
|
72
|
+
safe_to_retry: safeToRetry,
|
|
73
|
+
retry_safety: operationKey
|
|
74
|
+
? "idempotency_key"
|
|
75
|
+
: revisionGuarded
|
|
76
|
+
? "revision_guard"
|
|
77
|
+
: retrySafe
|
|
78
|
+
? "operation_contract"
|
|
79
|
+
: safeToRetry
|
|
80
|
+
? "read_only"
|
|
81
|
+
: "unknown_commit_state",
|
|
82
|
+
...(operationKey ? { idempotency_key: operationKey } : {}),
|
|
83
|
+
...(causeCode ? { cause_code: causeCode } : {}),
|
|
84
|
+
...(syscall ? { syscall } : {}),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
35
88
|
export function parseArgv(argv) {
|
|
36
89
|
const options = Object.create(null);
|
|
37
90
|
const positionals = [];
|
|
@@ -42,19 +95,34 @@ export function parseArgv(argv) {
|
|
|
42
95
|
"allow-actions",
|
|
43
96
|
"private-link",
|
|
44
97
|
"force",
|
|
98
|
+
"once",
|
|
99
|
+
"include-revoked",
|
|
100
|
+
"start-new-chain",
|
|
45
101
|
"help",
|
|
46
102
|
"version",
|
|
47
103
|
]);
|
|
48
104
|
for (let index = 0; index < argv.length; index += 1) {
|
|
49
105
|
const value = argv[index];
|
|
106
|
+
if (value === "--") {
|
|
107
|
+
positionals.push(...argv.slice(index + 1));
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
50
110
|
if (!value.startsWith("--")) {
|
|
51
111
|
positionals.push(value);
|
|
52
112
|
continue;
|
|
53
113
|
}
|
|
54
114
|
const separator = value.indexOf("=");
|
|
55
115
|
const name = value.slice(2, separator === -1 ? undefined : separator);
|
|
116
|
+
if (!name) throw new CliError("Option name must not be empty.", { code: "invalid_option" });
|
|
117
|
+
if (Object.hasOwn(options, name)) {
|
|
118
|
+
throw new CliError(`Option --${name} was provided more than once.`, { code: "duplicate_option" });
|
|
119
|
+
}
|
|
56
120
|
if (booleanOptions.has(name)) {
|
|
57
|
-
|
|
121
|
+
const booleanValue = separator === -1 ? "true" : value.slice(separator + 1);
|
|
122
|
+
if (booleanValue !== "true" && booleanValue !== "false") {
|
|
123
|
+
throw new CliError(`Option --${name} must be true or false.`, { code: "invalid_boolean_option" });
|
|
124
|
+
}
|
|
125
|
+
options[name] = booleanValue === "true";
|
|
58
126
|
continue;
|
|
59
127
|
}
|
|
60
128
|
if (separator !== -1) {
|
|
@@ -71,6 +139,21 @@ export function parseArgv(argv) {
|
|
|
71
139
|
return { options, positionals };
|
|
72
140
|
}
|
|
73
141
|
|
|
142
|
+
export function assertKnownOptions(options, allowed) {
|
|
143
|
+
const allowedSet = new Set(allowed);
|
|
144
|
+
for (const name of Object.keys(options)) {
|
|
145
|
+
if (!allowedSet.has(name)) {
|
|
146
|
+
throw new CliError(`Unknown option --${name}.`, { code: "unknown_option" });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function assertPositionalCount(positionals, maximum) {
|
|
152
|
+
if (positionals.length > maximum) {
|
|
153
|
+
throw new CliError(`Unexpected argument: ${positionals[maximum]}`, { code: "unexpected_argument" });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
74
157
|
export function option(options, name, { required = false, fallback = undefined } = {}) {
|
|
75
158
|
const value = options[name] ?? fallback;
|
|
76
159
|
if (required && (value === undefined || String(value).trim() === "")) {
|
|
@@ -85,28 +168,43 @@ export function listOption(options, name, fallback = []) {
|
|
|
85
168
|
return String(value).split(",").map((item) => item.trim()).filter(Boolean);
|
|
86
169
|
}
|
|
87
170
|
|
|
88
|
-
export function numberOption(options, name, fallback) {
|
|
171
|
+
export function numberOption(options, name, fallback, { minimum = 1, maximum = Number.MAX_SAFE_INTEGER } = {}) {
|
|
89
172
|
const value = options[name];
|
|
90
173
|
if (value === undefined) return fallback;
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
174
|
+
const normalized = String(value).trim();
|
|
175
|
+
const parsed = /^\d+$/.test(normalized) ? Number(normalized) : Number.NaN;
|
|
176
|
+
if (!Number.isSafeInteger(parsed) || parsed < minimum || parsed > maximum) {
|
|
177
|
+
throw new CliError(
|
|
178
|
+
`Option --${name} must be an integer between ${minimum} and ${maximum}.`,
|
|
179
|
+
{ code: "invalid_number_option" },
|
|
180
|
+
);
|
|
94
181
|
}
|
|
95
182
|
return parsed;
|
|
96
183
|
}
|
|
97
184
|
|
|
98
185
|
export function normalizeApiBase(value, fallback) {
|
|
99
186
|
const raw = String(value || fallback).trim().replace(/\/+$/, "");
|
|
100
|
-
|
|
187
|
+
let parsed;
|
|
188
|
+
try {
|
|
189
|
+
parsed = new URL(raw);
|
|
190
|
+
} catch {
|
|
191
|
+
throw new CliError("Corvio API base must be a valid URL.", { code: "invalid_api_base" });
|
|
192
|
+
}
|
|
193
|
+
const localhost = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "[::1]";
|
|
194
|
+
if (parsed.username || parsed.password || (parsed.protocol !== "https:" && !(parsed.protocol === "http:" && localhost))) {
|
|
101
195
|
throw new CliError("Corvio API base must use HTTPS (localhost is allowed for development).", {
|
|
102
196
|
code: "insecure_api_base",
|
|
103
197
|
});
|
|
104
198
|
}
|
|
105
|
-
|
|
199
|
+
if (parsed.search || parsed.hash) {
|
|
200
|
+
throw new CliError("Corvio API base must not include a query string or fragment.", { code: "invalid_api_base" });
|
|
201
|
+
}
|
|
202
|
+
const normalized = parsed.toString().replace(/\/+$/, "");
|
|
203
|
+
return normalized.endsWith("/v1") ? normalized : `${normalized}/v1`;
|
|
106
204
|
}
|
|
107
205
|
|
|
108
206
|
function parseSemver(value) {
|
|
109
|
-
const match = String(value || "").trim().match(/^(\d
|
|
207
|
+
const match = String(value || "").trim().match(/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/);
|
|
110
208
|
if (!match) return null;
|
|
111
209
|
return {
|
|
112
210
|
core: match.slice(1, 4).map((item) => Number.parseInt(item, 10)),
|
|
@@ -187,15 +285,31 @@ export async function resolveNpmUpdateStatus({
|
|
|
187
285
|
}
|
|
188
286
|
|
|
189
287
|
export class ApiClient {
|
|
190
|
-
constructor({
|
|
288
|
+
constructor({
|
|
289
|
+
baseUrl,
|
|
290
|
+
credential = null,
|
|
291
|
+
workspaceId = null,
|
|
292
|
+
userAgent,
|
|
293
|
+
fetchImpl = globalThis.fetch,
|
|
294
|
+
timeoutMs = process.env.CORVIO_REQUEST_TIMEOUT_MS,
|
|
295
|
+
}) {
|
|
191
296
|
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
192
297
|
this.credential = credential;
|
|
193
298
|
this.workspaceId = String(workspaceId || "").trim() || null;
|
|
194
299
|
this.userAgent = userAgent;
|
|
195
300
|
this.fetchImpl = fetchImpl;
|
|
301
|
+
this.timeoutMs = normalizeRequestTimeout(timeoutMs);
|
|
196
302
|
}
|
|
197
303
|
|
|
198
|
-
async request(path, {
|
|
304
|
+
async request(path, {
|
|
305
|
+
method = "GET",
|
|
306
|
+
body,
|
|
307
|
+
query,
|
|
308
|
+
headers = {},
|
|
309
|
+
auth = true,
|
|
310
|
+
retrySafe = false,
|
|
311
|
+
timeoutMs = this.timeoutMs,
|
|
312
|
+
} = {}) {
|
|
199
313
|
const url = new URL(`${this.baseUrl}${path.startsWith("/") ? path : `/${path}`}`);
|
|
200
314
|
for (const [key, value] of Object.entries(query || {})) {
|
|
201
315
|
if (value !== undefined && value !== null && value !== "") url.searchParams.set(key, String(value));
|
|
@@ -216,18 +330,56 @@ export class ApiClient {
|
|
|
216
330
|
requestHeaders["X-Corvio-Workspace-Context-Contract"] = "required-v1";
|
|
217
331
|
}
|
|
218
332
|
}
|
|
333
|
+
const normalizedTimeout = normalizeRequestTimeout(timeoutMs, this.timeoutMs);
|
|
219
334
|
let response;
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
335
|
+
for (let attempt = 1; attempt <= 2; attempt += 1) {
|
|
336
|
+
try {
|
|
337
|
+
response = await this.fetchImpl(url, {
|
|
338
|
+
method,
|
|
339
|
+
headers: requestHeaders,
|
|
340
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
341
|
+
signal: AbortSignal.timeout(normalizedTimeout),
|
|
342
|
+
});
|
|
343
|
+
const safety = networkFailureDetails({
|
|
344
|
+
cause: null,
|
|
345
|
+
url,
|
|
346
|
+
method,
|
|
347
|
+
requestHeaders,
|
|
348
|
+
body,
|
|
349
|
+
retrySafe,
|
|
350
|
+
});
|
|
351
|
+
if (attempt === 1 && safety.safe_to_retry && RETRYABLE_HTTP_STATUSES.has(response.status)) {
|
|
352
|
+
await response.body?.cancel().catch(() => {});
|
|
353
|
+
const retryAfterValue = response.headers.get("retry-after");
|
|
354
|
+
const retryAfterSeconds = retryAfterValue === null ? Number.NaN : Number(retryAfterValue);
|
|
355
|
+
const delayMs = Number.isFinite(retryAfterSeconds)
|
|
356
|
+
? Math.min(2_000, Math.max(0, retryAfterSeconds * 1000))
|
|
357
|
+
: 150;
|
|
358
|
+
await sleep(delayMs);
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
break;
|
|
362
|
+
} catch (cause) {
|
|
363
|
+
const details = networkFailureDetails({ cause, url, method, requestHeaders, body, retrySafe });
|
|
364
|
+
if (attempt === 1 && details.safe_to_retry) {
|
|
365
|
+
await sleep(150);
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
throw new CliError(
|
|
369
|
+
details.timed_out
|
|
370
|
+
? `Corvio did not respond within ${normalizedTimeout} ms.`
|
|
371
|
+
: `Unable to reach Corvio: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
372
|
+
{
|
|
373
|
+
code: details.timed_out ? "request_timeout" : "network_error",
|
|
374
|
+
exitCode: EXIT.REMOTE,
|
|
375
|
+
details: {
|
|
376
|
+
...details,
|
|
377
|
+
attempts: attempt,
|
|
378
|
+
automatic_retry_attempted: attempt > 1,
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
);
|
|
382
|
+
}
|
|
231
383
|
}
|
|
232
384
|
const text = await response.text();
|
|
233
385
|
let payload = null;
|
|
@@ -244,13 +396,18 @@ export class ApiClient {
|
|
|
244
396
|
code: String(envelope.code || `http_${response.status}`),
|
|
245
397
|
status: response.status,
|
|
246
398
|
exitCode: exitCodeForStatus(response.status),
|
|
247
|
-
details:
|
|
399
|
+
details: {
|
|
400
|
+
...(envelope.details || {}),
|
|
401
|
+
...(envelope.request_id || response.headers.get("x-request-id")
|
|
402
|
+
? { request_id: envelope.request_id || response.headers.get("x-request-id") }
|
|
403
|
+
: {}),
|
|
404
|
+
},
|
|
248
405
|
});
|
|
249
406
|
}
|
|
250
407
|
return payload;
|
|
251
408
|
}
|
|
252
409
|
|
|
253
|
-
async requestBytes(path, { query, headers = {}, auth = true } = {}) {
|
|
410
|
+
async requestBytes(path, { query, headers = {}, auth = true, timeoutMs = this.timeoutMs } = {}) {
|
|
254
411
|
const url = new URL(`${this.baseUrl}${path.startsWith("/") ? path : `/${path}`}`);
|
|
255
412
|
for (const [key, value] of Object.entries(query || {})) {
|
|
256
413
|
if (value !== undefined && value !== null && value !== "") url.searchParams.set(key, String(value));
|
|
@@ -270,14 +427,38 @@ export class ApiClient {
|
|
|
270
427
|
requestHeaders["X-Corvio-Workspace-Context-Contract"] = "required-v1";
|
|
271
428
|
}
|
|
272
429
|
}
|
|
430
|
+
const normalizedTimeout = normalizeRequestTimeout(timeoutMs, this.timeoutMs);
|
|
273
431
|
let response;
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
432
|
+
for (let attempt = 1; attempt <= 2; attempt += 1) {
|
|
433
|
+
try {
|
|
434
|
+
response = await this.fetchImpl(url, {
|
|
435
|
+
method: "GET",
|
|
436
|
+
headers: requestHeaders,
|
|
437
|
+
signal: AbortSignal.timeout(normalizedTimeout),
|
|
438
|
+
});
|
|
439
|
+
if (attempt === 1 && RETRYABLE_HTTP_STATUSES.has(response.status)) {
|
|
440
|
+
await response.body?.cancel().catch(() => {});
|
|
441
|
+
await sleep(150);
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
break;
|
|
445
|
+
} catch (cause) {
|
|
446
|
+
const details = networkFailureDetails({ cause, url, method: "GET", requestHeaders });
|
|
447
|
+
if (attempt === 1) {
|
|
448
|
+
await sleep(150);
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
throw new CliError(
|
|
452
|
+
details.timed_out
|
|
453
|
+
? `Corvio did not respond within ${normalizedTimeout} ms.`
|
|
454
|
+
: `Unable to reach Corvio: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
455
|
+
{
|
|
456
|
+
code: details.timed_out ? "request_timeout" : "network_error",
|
|
457
|
+
exitCode: EXIT.REMOTE,
|
|
458
|
+
details: { ...details, attempts: attempt, automatic_retry_attempted: true },
|
|
459
|
+
},
|
|
460
|
+
);
|
|
461
|
+
}
|
|
281
462
|
}
|
|
282
463
|
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
283
464
|
if (!response.ok) {
|
|
@@ -293,25 +474,57 @@ export class ApiClient {
|
|
|
293
474
|
code: String(envelope.code || `http_${response.status}`),
|
|
294
475
|
status: response.status,
|
|
295
476
|
exitCode: exitCodeForStatus(response.status),
|
|
296
|
-
details:
|
|
477
|
+
details: {
|
|
478
|
+
...(envelope.details || {}),
|
|
479
|
+
...(envelope.request_id || response.headers.get("x-request-id")
|
|
480
|
+
? { request_id: envelope.request_id || response.headers.get("x-request-id") }
|
|
481
|
+
: {}),
|
|
482
|
+
},
|
|
297
483
|
});
|
|
298
484
|
}
|
|
299
485
|
return bytes;
|
|
300
486
|
}
|
|
301
487
|
|
|
302
|
-
async putUrl(url, content, { headers = {} } = {}) {
|
|
488
|
+
async putUrl(url, content, { headers = {}, timeoutMs = this.timeoutMs } = {}) {
|
|
489
|
+
const normalizedTimeout = normalizeRequestTimeout(timeoutMs, this.timeoutMs);
|
|
303
490
|
let response;
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
491
|
+
for (let attempt = 1; attempt <= 2; attempt += 1) {
|
|
492
|
+
try {
|
|
493
|
+
response = await this.fetchImpl(String(url), {
|
|
494
|
+
method: "PUT",
|
|
495
|
+
headers,
|
|
496
|
+
body: content,
|
|
497
|
+
signal: AbortSignal.timeout(normalizedTimeout),
|
|
498
|
+
});
|
|
499
|
+
if (attempt === 1 && RETRYABLE_HTTP_STATUSES.has(response.status)) {
|
|
500
|
+
await response.body?.cancel().catch(() => {});
|
|
501
|
+
await sleep(150);
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
break;
|
|
505
|
+
} catch (cause) {
|
|
506
|
+
const details = networkFailureDetails({
|
|
507
|
+
cause,
|
|
508
|
+
url,
|
|
509
|
+
method: "PUT",
|
|
510
|
+
requestHeaders: headers,
|
|
511
|
+
retrySafe: true,
|
|
512
|
+
});
|
|
513
|
+
if (attempt === 1) {
|
|
514
|
+
await sleep(150);
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
throw new CliError(
|
|
518
|
+
details.timed_out
|
|
519
|
+
? `The file upload did not respond within ${normalizedTimeout} ms.`
|
|
520
|
+
: `Unable to upload the file: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
521
|
+
{
|
|
522
|
+
code: details.timed_out ? "upload_timeout" : "upload_network_error",
|
|
523
|
+
exitCode: EXIT.REMOTE,
|
|
524
|
+
details: { ...details, attempts: attempt, automatic_retry_attempted: true },
|
|
525
|
+
},
|
|
526
|
+
);
|
|
527
|
+
}
|
|
315
528
|
}
|
|
316
529
|
if (!response.ok) {
|
|
317
530
|
const message = (await response.text()).slice(0, 500);
|
|
@@ -375,6 +588,23 @@ export async function readWorkspaceCredential(env = process.env) {
|
|
|
375
588
|
return { key, source: "device_link", profile: profileName, metadata };
|
|
376
589
|
}
|
|
377
590
|
|
|
591
|
+
export function readAgentCredential(env = process.env) {
|
|
592
|
+
const key = String(env.CORVIO_AGENT_API_KEY || "").trim();
|
|
593
|
+
if (!key) {
|
|
594
|
+
throw new CliError(
|
|
595
|
+
"No Agent credential found. Set CORVIO_AGENT_API_KEY to the cvg_ secret returned when the Agent was created.",
|
|
596
|
+
{ code: "agent_auth_required", exitCode: EXIT.AUTH },
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
if (!key.startsWith("cvg_")) {
|
|
600
|
+
throw new CliError("CORVIO_AGENT_API_KEY must be an Agent cvg_ key.", {
|
|
601
|
+
code: "invalid_agent_key",
|
|
602
|
+
exitCode: EXIT.AUTH,
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
return { key, source: "environment", profile: null, metadata: null };
|
|
606
|
+
}
|
|
607
|
+
|
|
378
608
|
export async function storeWorkspaceCredential(payload, env = process.env) {
|
|
379
609
|
const path = workspaceCredentialPath(env);
|
|
380
610
|
const root = dirname(path);
|
|
@@ -534,7 +764,21 @@ export function writeError(error, { json = false, stderr = process.stderr } = {}
|
|
|
534
764
|
...(error instanceof CliError && error.details ? { details: error.details } : {}),
|
|
535
765
|
},
|
|
536
766
|
};
|
|
537
|
-
|
|
767
|
+
if (json) {
|
|
768
|
+
stderr.write(`${JSON.stringify(safe)}\n`);
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
stderr.write(`Error: ${safe.error.message}\n`);
|
|
772
|
+
if (safe.error.details?.request_id) stderr.write(`Request ID: ${safe.error.details.request_id}\n`);
|
|
773
|
+
if (safe.error.details?.safe_to_retry && safe.error.details?.idempotency_key) {
|
|
774
|
+
stderr.write(
|
|
775
|
+
`Safe retry: rerun the command with --idempotency-key ${safe.error.details.idempotency_key}\n`,
|
|
776
|
+
);
|
|
777
|
+
} else if (safe.error.details?.retry_safety === "revision_guard") {
|
|
778
|
+
stderr.write("Safe retry: rerun with the same --expected-revision; Corvio will reject an already-committed replacement.\n");
|
|
779
|
+
} else if (safe.error.details?.retry_safety === "unknown_commit_state") {
|
|
780
|
+
stderr.write("Retry state is unknown; read the remote object before repeating this mutation.\n");
|
|
781
|
+
}
|
|
538
782
|
}
|
|
539
783
|
|
|
540
784
|
export async function runMain(main, argv = process.argv.slice(2)) {
|
|
@@ -551,8 +795,8 @@ export async function runMain(main, argv = process.argv.slice(2)) {
|
|
|
551
795
|
|
|
552
796
|
export async function openBrowser(url) {
|
|
553
797
|
const target = String(url);
|
|
554
|
-
const command = platform() === "darwin" ? "open" : platform() === "win32" ? "
|
|
555
|
-
const args = platform() === "win32" ? ["
|
|
798
|
+
const command = platform() === "darwin" ? "open" : platform() === "win32" ? "rundll32.exe" : "xdg-open";
|
|
799
|
+
const args = platform() === "win32" ? ["url.dll,FileProtocolHandler", target] : [target];
|
|
556
800
|
return new Promise((resolvePromise) => {
|
|
557
801
|
let settled = false;
|
|
558
802
|
const finish = (opened) => {
|
|
@@ -584,14 +828,28 @@ export async function readTextFile(path) {
|
|
|
584
828
|
export async function writeTextFile(path, content) {
|
|
585
829
|
const target = resolve(String(path));
|
|
586
830
|
await mkdir(dirname(target), { recursive: true });
|
|
587
|
-
|
|
831
|
+
const temporary = `${target}.${randomUUID()}.tmp`;
|
|
832
|
+
try {
|
|
833
|
+
await writeFile(temporary, content, { encoding: "utf8", flag: "wx" });
|
|
834
|
+
await rename(temporary, target);
|
|
835
|
+
} catch (error) {
|
|
836
|
+
await rm(temporary, { force: true }).catch(() => {});
|
|
837
|
+
throw error;
|
|
838
|
+
}
|
|
588
839
|
return target;
|
|
589
840
|
}
|
|
590
841
|
|
|
591
842
|
export async function writeBinaryFile(path, content) {
|
|
592
843
|
const target = resolve(String(path));
|
|
593
844
|
await mkdir(dirname(target), { recursive: true });
|
|
594
|
-
|
|
845
|
+
const temporary = `${target}.${randomUUID()}.tmp`;
|
|
846
|
+
try {
|
|
847
|
+
await writeFile(temporary, content, { flag: "wx" });
|
|
848
|
+
await rename(temporary, target);
|
|
849
|
+
} catch (error) {
|
|
850
|
+
await rm(temporary, { force: true }).catch(() => {});
|
|
851
|
+
throw error;
|
|
852
|
+
}
|
|
595
853
|
return target;
|
|
596
854
|
}
|
|
597
855
|
|