@tryarcanist/cli 0.1.167 → 0.1.169
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 +14 -2
- package/dist/index.js +172 -60
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -53,9 +53,12 @@ Supported environment variables:
|
|
|
53
53
|
```bash
|
|
54
54
|
export ARCANIST_TOKEN=arc_...
|
|
55
55
|
export ARCANIST_API_URL=https://api.tryarcanist.com
|
|
56
|
+
export ARCANIST_HTTP_TIMEOUT_MS=30000
|
|
56
57
|
```
|
|
57
58
|
|
|
58
59
|
`--token` and `--api-url` are available for one-off invocations. Prefer `ARCANIST_TOKEN` over `--token` for persistent use; CLI flags can appear in shell history and process lists.
|
|
60
|
+
Each HTTP request times out after `ARCANIST_HTTP_TIMEOUT_MS` milliseconds, default `30000`.
|
|
61
|
+
The value must be between `1000` and `600000`; invalid values fail closed instead of falling back silently.
|
|
59
62
|
|
|
60
63
|
Non-local API URLs must use HTTPS and must not embed credentials.
|
|
61
64
|
|
|
@@ -89,10 +92,18 @@ With `--json`, successful command output is machine-readable and newline-termina
|
|
|
89
92
|
|
|
90
93
|
```json
|
|
91
94
|
{
|
|
92
|
-
"error": {
|
|
95
|
+
"error": {
|
|
96
|
+
"code": "auth",
|
|
97
|
+
"message": "Not logged in.",
|
|
98
|
+
"hint": "Run `arcanist auth login` or set `ARCANIST_TOKEN`.",
|
|
99
|
+
"data": { "serverCode": "token_revoked" }
|
|
100
|
+
}
|
|
93
101
|
}
|
|
94
102
|
```
|
|
95
103
|
|
|
104
|
+
`error.data` is omitted when there is no structured recovery data.
|
|
105
|
+
Stable fields are `serverCode` for server-provided error codes and `sessionId`/`sessionUrl` when `sessions create` created the session but failed to enqueue the prompt.
|
|
106
|
+
|
|
96
107
|
Exit codes:
|
|
97
108
|
|
|
98
109
|
```text
|
|
@@ -105,7 +116,7 @@ Exit codes:
|
|
|
105
116
|
130 interrupted
|
|
106
117
|
```
|
|
107
118
|
|
|
108
|
-
Mutation commands send an `Idempotency-Key` header. The CLI does not auto-retry mutation endpoints; retry explicitly with the same `--idempotency-key` when a request may have already reached the server. `--idempotency-key` is available on `sessions create` and `
|
|
119
|
+
Mutation commands send an `Idempotency-Key` header. The CLI does not auto-retry mutation endpoints; retry explicitly with the same `--idempotency-key` when a request may have already reached the server. `--idempotency-key` is available on `sessions create`, `sessions send`, `tokens create`, and `sandbox build`. `tokens create` and `sandbox build` use auto-random keys by default, so only an explicit key makes cross-invocation retries safe.
|
|
109
120
|
|
|
110
121
|
## Commands
|
|
111
122
|
|
|
@@ -349,6 +360,7 @@ Creates a CLI token and prints the plaintext token exactly once.
|
|
|
349
360
|
```bash
|
|
350
361
|
arcanist tokens create --scope read
|
|
351
362
|
arcanist tokens create --scope read --expires-in-days 30 --json
|
|
363
|
+
arcanist tokens create --scope read --idempotency-key 1f0e6f1a-...
|
|
352
364
|
```
|
|
353
365
|
|
|
354
366
|
Read-scoped tokens cannot create write-scoped tokens; the server enforces that and returns 403.
|
package/dist/index.js
CHANGED
|
@@ -24,6 +24,7 @@ var CliError = class extends Error {
|
|
|
24
24
|
exitCode;
|
|
25
25
|
hint;
|
|
26
26
|
requestId;
|
|
27
|
+
data;
|
|
27
28
|
constructor(code, message, options = {}) {
|
|
28
29
|
super(message);
|
|
29
30
|
this.name = "CliError";
|
|
@@ -31,17 +32,20 @@ var CliError = class extends Error {
|
|
|
31
32
|
this.exitCode = options.exitCode ?? exitCodeForErrorCode(code);
|
|
32
33
|
this.hint = options.hint;
|
|
33
34
|
this.requestId = options.requestId;
|
|
35
|
+
this.data = options.data;
|
|
34
36
|
}
|
|
35
37
|
};
|
|
36
38
|
var ApiError = class extends CliError {
|
|
37
39
|
status;
|
|
38
40
|
body;
|
|
39
|
-
constructor(status, body, requestId) {
|
|
41
|
+
constructor(status, body, requestId, resourceNoun) {
|
|
40
42
|
const code = codeForHttpStatus(status);
|
|
41
|
-
|
|
43
|
+
const parsed = parseApiErrorBody(body);
|
|
44
|
+
super(code, messageForApiError(status, body, parsed), {
|
|
42
45
|
exitCode: exitCodeForHttpStatus(status),
|
|
43
|
-
hint: hintForHttpStatus(status),
|
|
44
|
-
requestId
|
|
46
|
+
hint: hintForHttpStatus(status, resourceNoun),
|
|
47
|
+
requestId,
|
|
48
|
+
data: parsed?.serverCode ? { serverCode: parsed.serverCode } : void 0
|
|
45
49
|
});
|
|
46
50
|
this.name = "ApiError";
|
|
47
51
|
this.status = status;
|
|
@@ -73,9 +77,9 @@ function codeForHttpStatus(status) {
|
|
|
73
77
|
function exitCodeForHttpStatus(status) {
|
|
74
78
|
return exitCodeForErrorCode(codeForHttpStatus(status));
|
|
75
79
|
}
|
|
76
|
-
function messageForApiError(status, body) {
|
|
77
|
-
|
|
78
|
-
if (parsed) return parsed;
|
|
80
|
+
function messageForApiError(status, body, parsed = parseApiErrorBody(body)) {
|
|
81
|
+
if (parsed?.message) return parsed.message;
|
|
82
|
+
if (parsed?.serverCode) return parsed.serverCode;
|
|
79
83
|
return body ? `API error ${status}: ${body}` : `API error ${status}`;
|
|
80
84
|
}
|
|
81
85
|
function parseApiErrorBody(body) {
|
|
@@ -83,14 +87,36 @@ function parseApiErrorBody(body) {
|
|
|
83
87
|
const parsed = JSON.parse(body);
|
|
84
88
|
if (!parsed || typeof parsed !== "object") return null;
|
|
85
89
|
const error = parsed.error;
|
|
86
|
-
|
|
90
|
+
if (typeof error === "string" && error.length > 0) {
|
|
91
|
+
const message2 = parsed.message;
|
|
92
|
+
const code = parsed.code;
|
|
93
|
+
return {
|
|
94
|
+
serverCode: typeof code === "string" && code ? code : error,
|
|
95
|
+
...typeof message2 === "string" && message2 ? { message: message2 } : { message: error }
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
if (error && typeof error === "object") {
|
|
99
|
+
const code = error.code;
|
|
100
|
+
const message2 = error.message;
|
|
101
|
+
return {
|
|
102
|
+
...typeof message2 === "string" && message2 ? { message: message2 } : {},
|
|
103
|
+
...typeof code === "string" && code ? { serverCode: code } : {}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const message = parsed.message;
|
|
107
|
+
return typeof message === "string" && message.length > 0 ? { message } : null;
|
|
87
108
|
} catch {
|
|
88
109
|
return null;
|
|
89
110
|
}
|
|
90
111
|
}
|
|
91
|
-
function hintForHttpStatus(status) {
|
|
112
|
+
function hintForHttpStatus(status, resourceNoun) {
|
|
92
113
|
if (status === 401 || status === 403) return "Run `arcanist auth login` or set `ARCANIST_TOKEN`.";
|
|
93
|
-
if (status === 404)
|
|
114
|
+
if (status === 404) {
|
|
115
|
+
if (resourceNoun === "token") return "List tokens with `arcanist tokens list`.";
|
|
116
|
+
if (resourceNoun === "sandbox") return "Check sandbox state with `arcanist sandbox status`.";
|
|
117
|
+
if (resourceNoun === "repo") return "Check accessible repositories with `arcanist repos list`.";
|
|
118
|
+
return "List sessions with `arcanist sessions list`.";
|
|
119
|
+
}
|
|
94
120
|
if (status === 409) return "Check the current resource state and retry the command when it is ready.";
|
|
95
121
|
if (status >= 500) return "Retry later or check the control-plane logs.";
|
|
96
122
|
return void 0;
|
|
@@ -106,7 +132,8 @@ function formatJsonError(err) {
|
|
|
106
132
|
code: err.code,
|
|
107
133
|
message: err.message,
|
|
108
134
|
...err.hint ? { hint: err.hint } : {},
|
|
109
|
-
...err.requestId ? { requestId: err.requestId } : {}
|
|
135
|
+
...err.requestId ? { requestId: err.requestId } : {},
|
|
136
|
+
...err.data ? { data: err.data } : {}
|
|
110
137
|
}
|
|
111
138
|
});
|
|
112
139
|
}
|
|
@@ -115,33 +142,72 @@ function formatJsonError(err) {
|
|
|
115
142
|
var require2 = createRequire(import.meta.url);
|
|
116
143
|
var { version } = require2("../package.json");
|
|
117
144
|
var CLI_USER_AGENT = `arcanist-cli/${version}`;
|
|
118
|
-
|
|
119
|
-
|
|
145
|
+
var DEFAULT_HTTP_TIMEOUT_MS = 3e4;
|
|
146
|
+
var MIN_HTTP_TIMEOUT_MS = 1e3;
|
|
147
|
+
var MAX_HTTP_TIMEOUT_MS = 6e5;
|
|
148
|
+
var HTTP_TIMEOUT_ENV = "ARCANIST_HTTP_TIMEOUT_MS";
|
|
149
|
+
function parseHttpTimeoutMs(env = process.env) {
|
|
150
|
+
const raw = env[HTTP_TIMEOUT_ENV];
|
|
151
|
+
if (!raw) return DEFAULT_HTTP_TIMEOUT_MS;
|
|
152
|
+
if (!/^\d+$/.test(raw)) {
|
|
153
|
+
throw new CliError("user", `${HTTP_TIMEOUT_ENV} must be a positive integer between 1000 and 600000.`);
|
|
154
|
+
}
|
|
155
|
+
const value = Number(raw);
|
|
156
|
+
if (value < MIN_HTTP_TIMEOUT_MS || value > MAX_HTTP_TIMEOUT_MS) {
|
|
157
|
+
throw new CliError("user", `${HTTP_TIMEOUT_ENV} must be between 1000 and 600000 milliseconds.`);
|
|
158
|
+
}
|
|
159
|
+
return value;
|
|
160
|
+
}
|
|
161
|
+
function isAbortError(err) {
|
|
162
|
+
return err instanceof DOMException && err.name === "AbortError";
|
|
163
|
+
}
|
|
164
|
+
function timeoutError(timeoutMs) {
|
|
165
|
+
return new CliError("server", `Network timeout after ${timeoutMs}ms.`, {
|
|
166
|
+
hint: `Increase ${HTTP_TIMEOUT_ENV} for slow links, or check the API connection.`
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
async function apiRequest(config, path, init, read) {
|
|
170
|
+
const timeoutMs = parseHttpTimeoutMs();
|
|
171
|
+
const controller = new AbortController();
|
|
172
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
120
173
|
try {
|
|
121
174
|
const headers = new Headers(init?.headers);
|
|
122
175
|
headers.set("Content-Type", "application/json");
|
|
123
176
|
headers.set("User-Agent", CLI_USER_AGENT);
|
|
124
177
|
headers.set("Authorization", `Bearer ${config.token}`);
|
|
125
|
-
res = await fetch(`${normalizeBaseUrl(config.apiUrl)}${path}`, {
|
|
178
|
+
const res = await fetch(`${normalizeBaseUrl(config.apiUrl)}${path}`, {
|
|
126
179
|
...init,
|
|
127
|
-
headers
|
|
180
|
+
headers,
|
|
181
|
+
signal: controller.signal
|
|
128
182
|
});
|
|
183
|
+
if (!res.ok) {
|
|
184
|
+
const body = await res.text().catch((err) => {
|
|
185
|
+
if (isAbortError(err)) throw timeoutError(timeoutMs);
|
|
186
|
+
return "";
|
|
187
|
+
});
|
|
188
|
+
throw new ApiError(res.status, body, res.headers.get("x-request-id") ?? void 0, resourceNounForPath(path));
|
|
189
|
+
}
|
|
190
|
+
return await read(res);
|
|
129
191
|
} catch (err) {
|
|
192
|
+
if (err instanceof CliError) throw err;
|
|
193
|
+
if (isAbortError(err)) throw timeoutError(timeoutMs);
|
|
130
194
|
throw new CliError("server", `Network error: ${stringifyError(err)}`);
|
|
195
|
+
} finally {
|
|
196
|
+
clearTimeout(timeout);
|
|
131
197
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
return
|
|
198
|
+
}
|
|
199
|
+
function resourceNounForPath(path) {
|
|
200
|
+
if (path.startsWith("/api/cli-tokens")) return "token";
|
|
201
|
+
if (path.startsWith("/api/repos")) return "repo";
|
|
202
|
+
if (path.startsWith("/api/sandbox")) return "sandbox";
|
|
203
|
+
if (path.startsWith("/api/sessions")) return "session";
|
|
204
|
+
return void 0;
|
|
137
205
|
}
|
|
138
206
|
async function apiFetch(config, path, init) {
|
|
139
|
-
|
|
140
|
-
return res.json();
|
|
207
|
+
return apiRequest(config, path, init, (res) => res.json());
|
|
141
208
|
}
|
|
142
209
|
async function apiFetchText(config, path, init) {
|
|
143
|
-
|
|
144
|
-
return res.text();
|
|
210
|
+
return apiRequest(config, path, init, (res) => res.text());
|
|
145
211
|
}
|
|
146
212
|
async function resolveBusinessId(config, options) {
|
|
147
213
|
if (options.business && options.business.trim()) return options.business.trim();
|
|
@@ -1126,8 +1192,9 @@ function isTerminalPhase(phase, _sessionKind) {
|
|
|
1126
1192
|
}
|
|
1127
1193
|
|
|
1128
1194
|
// src/constants/watch.ts
|
|
1129
|
-
var MIN_WATCH_POLL_INTERVAL_MS =
|
|
1195
|
+
var MIN_WATCH_POLL_INTERVAL_MS = 250;
|
|
1130
1196
|
var DEFAULT_WATCH_POLL_INTERVAL_MS = 1e3;
|
|
1197
|
+
var MAX_WATCH_POLL_INTERVAL_MS = 6e4;
|
|
1131
1198
|
var WATCH_REPLAY_PAGE_SIZE = 200;
|
|
1132
1199
|
function isWatchTerminal(phase) {
|
|
1133
1200
|
return isTerminalPhase(phase);
|
|
@@ -2702,7 +2769,14 @@ function parsePollInterval(raw) {
|
|
|
2702
2769
|
if (!/^\d+$/.test(raw)) {
|
|
2703
2770
|
throw new CliError("user", "Polling interval must be a non-negative integer.");
|
|
2704
2771
|
}
|
|
2705
|
-
|
|
2772
|
+
const value = Number(raw);
|
|
2773
|
+
if (value < MIN_WATCH_POLL_INTERVAL_MS || value > MAX_WATCH_POLL_INTERVAL_MS) {
|
|
2774
|
+
throw new CliError(
|
|
2775
|
+
"user",
|
|
2776
|
+
`Polling interval must be between ${MIN_WATCH_POLL_INTERVAL_MS} and ${MAX_WATCH_POLL_INTERVAL_MS} milliseconds.`
|
|
2777
|
+
);
|
|
2778
|
+
}
|
|
2779
|
+
return value;
|
|
2706
2780
|
}
|
|
2707
2781
|
function parseNonNegativeInteger(raw, name, defaultValue) {
|
|
2708
2782
|
if (!raw) return defaultValue;
|
|
@@ -2762,7 +2836,6 @@ async function watchCommand(sessionId, options, command) {
|
|
|
2762
2836
|
const runtime = getRuntimeOptions(command, options);
|
|
2763
2837
|
const config = requireConfig(runtime);
|
|
2764
2838
|
const pollIntervalMs = parsePollInterval(options.pollInterval);
|
|
2765
|
-
const effectivePollIntervalMs = Math.max(pollIntervalMs, MIN_WATCH_POLL_INTERVAL_MS);
|
|
2766
2839
|
const initialAfterSequence = parseNonNegativeInteger(options.afterSequence, "--after-sequence", 0);
|
|
2767
2840
|
const pageSize = parseNonNegativeInteger(options.limit, "--limit", WATCH_REPLAY_PAGE_SIZE);
|
|
2768
2841
|
if (pageSize === 0) {
|
|
@@ -2845,7 +2918,7 @@ async function watchCommand(sessionId, options, command) {
|
|
|
2845
2918
|
}
|
|
2846
2919
|
break;
|
|
2847
2920
|
}
|
|
2848
|
-
await sleep(
|
|
2921
|
+
await sleep(pollIntervalMs);
|
|
2849
2922
|
}
|
|
2850
2923
|
} finally {
|
|
2851
2924
|
if (textOpen) process.stdout.write("\n");
|
|
@@ -3020,7 +3093,12 @@ async function createCommand(repoUrl, promptArg, options, command) {
|
|
|
3020
3093
|
{
|
|
3021
3094
|
exitCode: err instanceof CliError ? err.exitCode : void 0,
|
|
3022
3095
|
hint: `Retry with: arcanist sessions send ${sessionId} --prompt-stdin`,
|
|
3023
|
-
requestId: err instanceof CliError ? err.requestId : void 0
|
|
3096
|
+
requestId: err instanceof CliError ? err.requestId : void 0,
|
|
3097
|
+
data: {
|
|
3098
|
+
...err instanceof CliError && err.data ? err.data : {},
|
|
3099
|
+
sessionId,
|
|
3100
|
+
...sessionData.sessionUrl ? { sessionUrl: sessionData.sessionUrl } : {}
|
|
3101
|
+
}
|
|
3024
3102
|
}
|
|
3025
3103
|
);
|
|
3026
3104
|
}
|
|
@@ -3232,16 +3310,16 @@ async function loginCommand(options, command) {
|
|
|
3232
3310
|
console.log(`Logged in. API: ${apiUrl}`);
|
|
3233
3311
|
}
|
|
3234
3312
|
try {
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
if (
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3313
|
+
await apiFetch({ apiUrl, token }, "/api/cli-tokens");
|
|
3314
|
+
if (!runtime.json && !runtime.quiet) console.log("Token verified.");
|
|
3315
|
+
} catch (err) {
|
|
3316
|
+
if (!runtime.json && !runtime.quiet) {
|
|
3317
|
+
if (err instanceof ApiError && (err.status === 401 || err.status === 403)) {
|
|
3318
|
+
console.warn("Warning: Token could not be verified (401). It may be invalid or expired.");
|
|
3319
|
+
} else {
|
|
3320
|
+
console.warn("Warning: Could not reach API to verify token.");
|
|
3321
|
+
}
|
|
3242
3322
|
}
|
|
3243
|
-
} catch {
|
|
3244
|
-
if (!runtime.json && !runtime.quiet) console.warn("Warning: Could not reach API to verify token.");
|
|
3245
3323
|
}
|
|
3246
3324
|
}
|
|
3247
3325
|
|
|
@@ -4235,20 +4313,34 @@ async function sandboxBuildCommand(sourceRepoArg, options = {}, command) {
|
|
|
4235
4313
|
const targetRepo = options.targetRepo ? parseRepoArg2(options.targetRepo, currentRepo) : null;
|
|
4236
4314
|
const businessId = await resolveBusinessId(config, options);
|
|
4237
4315
|
const ref = options.ref ?? currentRef();
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4316
|
+
let payload;
|
|
4317
|
+
try {
|
|
4318
|
+
payload = await apiFetch(
|
|
4319
|
+
config,
|
|
4320
|
+
`/api/businesses/${encodeURIComponent(businessId)}/repos/${encodeURIComponent(sourceRepo.owner)}/${encodeURIComponent(
|
|
4321
|
+
sourceRepo.repo
|
|
4322
|
+
)}/sandbox-layer/build-requests`,
|
|
4323
|
+
{
|
|
4324
|
+
method: "POST",
|
|
4325
|
+
headers: { "Idempotency-Key": options.idempotencyKey ?? randomIdempotencyKey() },
|
|
4326
|
+
body: JSON.stringify({
|
|
4327
|
+
ref,
|
|
4328
|
+
manifestPath,
|
|
4329
|
+
...targetRepo ? { targetRepo: { owner: targetRepo.owner, name: targetRepo.repo } } : {}
|
|
4330
|
+
})
|
|
4331
|
+
}
|
|
4332
|
+
);
|
|
4333
|
+
} catch (err) {
|
|
4334
|
+
if (err instanceof ApiError && err.status === 409 && err.data?.serverCode === "duplicate_request") {
|
|
4335
|
+
throw new CliError(err.code, err.message, {
|
|
4336
|
+
exitCode: err.exitCode,
|
|
4337
|
+
requestId: err.requestId,
|
|
4338
|
+
data: err.data,
|
|
4339
|
+
hint: "Use the same --idempotency-key only for retrying the original sandbox build request."
|
|
4340
|
+
});
|
|
4250
4341
|
}
|
|
4251
|
-
|
|
4342
|
+
throw err;
|
|
4343
|
+
}
|
|
4252
4344
|
emit(command, options, payload, (buildPayload) => printBuildRequest(buildPayload.buildRequest));
|
|
4253
4345
|
if (!options.wait && !options.follow) return;
|
|
4254
4346
|
const json = isJson(command, options);
|
|
@@ -4416,6 +4508,9 @@ async function sandboxAssignRepoCommand(targetRepoArg, sourceRepoArg, options =
|
|
|
4416
4508
|
});
|
|
4417
4509
|
}
|
|
4418
4510
|
async function sandboxUnassignDefaultCommand(options = {}, command) {
|
|
4511
|
+
if (isJson(command, options) && options.yes !== true) {
|
|
4512
|
+
throw new CliError("user", "`sandbox unassign default --json` requires --yes.");
|
|
4513
|
+
}
|
|
4419
4514
|
const { config } = resolveBusinessContext(command, options);
|
|
4420
4515
|
const businessId = await resolveBusinessId(config, options);
|
|
4421
4516
|
if (!options.yes) await confirmOrThrow("Clear the business default sandbox layer source?");
|
|
@@ -4427,6 +4522,9 @@ async function sandboxUnassignDefaultCommand(options = {}, command) {
|
|
|
4427
4522
|
emit(command, options, payload, () => console.log("Cleared default sandbox layer source."));
|
|
4428
4523
|
}
|
|
4429
4524
|
async function sandboxUnassignRepoCommand(targetRepoArg, options = {}, command) {
|
|
4525
|
+
if (isJson(command, options) && options.yes !== true) {
|
|
4526
|
+
throw new CliError("user", "`sandbox unassign repo --json` requires --yes.");
|
|
4527
|
+
}
|
|
4430
4528
|
const { config } = resolveBusinessContext(command, options);
|
|
4431
4529
|
const businessId = await resolveBusinessId(config, options);
|
|
4432
4530
|
const targetRepo = parseRepoArg2(targetRepoArg, currentRepo);
|
|
@@ -4712,13 +4810,27 @@ async function createTokenCommand(options, command) {
|
|
|
4712
4810
|
if (expiresInDays !== void 0 && (!Number.isInteger(expiresInDays) || expiresInDays <= 0)) {
|
|
4713
4811
|
throw new CliError("user", "expiresInDays must be a positive integer.");
|
|
4714
4812
|
}
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4813
|
+
let payload;
|
|
4814
|
+
try {
|
|
4815
|
+
payload = await apiFetch(config, "/api/cli-tokens", {
|
|
4816
|
+
method: "POST",
|
|
4817
|
+
headers: { "Idempotency-Key": options.idempotencyKey ?? randomIdempotencyKey() },
|
|
4818
|
+
body: JSON.stringify({
|
|
4819
|
+
scope: options.scope ?? "read",
|
|
4820
|
+
...expiresInDays !== void 0 ? { expiresInDays } : {}
|
|
4821
|
+
})
|
|
4822
|
+
});
|
|
4823
|
+
} catch (err) {
|
|
4824
|
+
if (err instanceof ApiError && err.status === 409 && err.data?.serverCode === "duplicate_request") {
|
|
4825
|
+
throw new CliError(err.code, err.message, {
|
|
4826
|
+
exitCode: err.exitCode,
|
|
4827
|
+
requestId: err.requestId,
|
|
4828
|
+
data: err.data,
|
|
4829
|
+
hint: "Use a new --idempotency-key for a new token, or retry the original command only if the first result was lost."
|
|
4830
|
+
});
|
|
4831
|
+
}
|
|
4832
|
+
throw err;
|
|
4833
|
+
}
|
|
4722
4834
|
emit(command, options, payload, (tokenPayload) => {
|
|
4723
4835
|
console.log(`Token: ${String(tokenPayload.token)}`);
|
|
4724
4836
|
console.log(`ID: ${String(tokenPayload.id)}`);
|
|
@@ -4984,7 +5096,7 @@ Examples:
|
|
|
4984
5096
|
var sandbox = program.command("sandbox").description("Sandbox layer commands");
|
|
4985
5097
|
sandbox.command("init").description("Create sandbox layer source files").action((options, command) => sandboxInitCommand(options, command));
|
|
4986
5098
|
sandbox.command("validate").description("Validate local sandbox layer source files").option("--manifest <path>", "Manifest path", ".arcanist/sandbox.yaml").action((options, command) => sandboxValidateCommand(options, command));
|
|
4987
|
-
sandbox.command("build").description("Build a repo-sourced sandbox layer").argument("[source-repo]", "Source repository owner/name; defaults to current git remote").option("--manifest <path>", "Manifest path", ".arcanist/sandbox.yaml").option("--ref <ref>", "Git ref to build").option("--target-repo <repo>", "Target repository owner/name for resource-profile coverage").option("--business <id>", "Business ID; defaults to authenticated user's business").option("--wait", "Wait for the build to finish").option("--follow", "Follow logs while waiting").option("--poll-interval <ms>", "Polling interval in milliseconds").action((sourceRepo, options, command) => sandboxBuildCommand(sourceRepo, options, command));
|
|
5099
|
+
sandbox.command("build").description("Build a repo-sourced sandbox layer").argument("[source-repo]", "Source repository owner/name; defaults to current git remote").option("--manifest <path>", "Manifest path", ".arcanist/sandbox.yaml").option("--ref <ref>", "Git ref to build").option("--target-repo <repo>", "Target repository owner/name for resource-profile coverage").option("--business <id>", "Business ID; defaults to authenticated user's business").option("--wait", "Wait for the build to finish").option("--follow", "Follow logs while waiting").option("--poll-interval <ms>", "Polling interval in milliseconds").option("--idempotency-key <uuid>", "Request idempotency key for safe manual retries").action((sourceRepo, options, command) => sandboxBuildCommand(sourceRepo, options, command));
|
|
4988
5100
|
sandbox.command("status").description("Show sandbox layer status").argument("[repo]", "Repository owner/name; defaults to current git remote").option("--business <id>", "Business ID; defaults to authenticated user's business").action((repo, options, command) => sandboxStatusCommand(repo, options, command));
|
|
4989
5101
|
sandbox.command("history").description("Show sandbox layer build history").argument("[source-repo]", "Source repository owner/name; defaults to current git remote").option("--business <id>", "Business ID; defaults to authenticated user's business").option("--target-repo <repo>", "Target repository owner/name for resource-profile coverage").option("--status <status>", "Filter by build status").option("--limit <n>", "Maximum builds to return").action((sourceRepo, options, command) => sandboxHistoryCommand(sourceRepo, options, command));
|
|
4990
5102
|
sandbox.command("logs").description("Print sandbox layer build logs").argument("<build-id>", "Build ID").option("--business <id>", "Business ID; defaults to authenticated user's business").option("--follow", "Follow logs").option("--after-sequence <n>", "Return logs after this sequence").option("--limit <n>", "Maximum log chunks to return").option("--poll-interval <ms>", "Polling interval in milliseconds").action((buildId, options, command) => sandboxLogsCommand(buildId, options, command));
|
|
@@ -5006,7 +5118,7 @@ egress.command("sync").description("Apply the merged egress allowlist source fil
|
|
|
5006
5118
|
egress.command("validate").description("Validate a local egress allowlist source file").argument("[path]", "Path to validate", ".arcanist/egress-allowlist.txt").action((path, options, command) => egressValidateCommand(path, options, command));
|
|
5007
5119
|
var tokens = program.command("tokens").description("CLI token commands");
|
|
5008
5120
|
tokens.command("list").description("List CLI tokens").option("--limit <n>", "Maximum tokens to return").option("--cursor <cursor>", "Pagination cursor").action((options, command) => listTokensCommand(options, command));
|
|
5009
|
-
tokens.command("create").description("Create a CLI token and print it once").option("--scope <scope>", "Token scope: read or write", "read").option("--expires-in-days <days>", "Token expiry in days").addHelpText(
|
|
5121
|
+
tokens.command("create").description("Create a CLI token and print it once").option("--scope <scope>", "Token scope: read or write", "read").option("--expires-in-days <days>", "Token expiry in days").option("--idempotency-key <uuid>", "Request idempotency key for safe manual retries").addHelpText(
|
|
5010
5122
|
"after",
|
|
5011
5123
|
`
|
|
5012
5124
|
Examples:
|