@tryarcanist/cli 0.1.317 → 0.1.319
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 +18 -4
- package/dist/index.js +379 -139
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ Requires Node.js 22 or newer.
|
|
|
13
13
|
## Quick start
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
arcanist auth login #
|
|
16
|
+
arcanist auth login # approve login in your browser
|
|
17
17
|
arcanist sessions list --json
|
|
18
18
|
```
|
|
19
19
|
|
|
@@ -25,14 +25,14 @@ arcanist sessions events "$SESSION_ID" --follow --json
|
|
|
25
25
|
|
|
26
26
|
## Authentication
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
Start browser authorization from the CLI:
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
31
|
arcanist auth login
|
|
32
|
-
#
|
|
32
|
+
# approve the request in the browser
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
Token config is stored at `~/.arcanist/config.json`. Multiple active tokens per user are supported; revoke tokens independently when a device or workflow no longer needs access.
|
|
35
|
+
Token config is stored at `~/.arcanist/config.json`. Existing automation can still provide a token through `ARCANIST_TOKEN` or `--token-stdin`. Multiple active tokens per user are supported; revoke tokens independently when a device or workflow no longer needs access.
|
|
36
36
|
|
|
37
37
|
Use a read-scoped token for inspection and debugging; use a write-scoped token only to create sessions, send prompts, or stop runs.
|
|
38
38
|
|
|
@@ -132,6 +132,18 @@ arcanist environments retry owner/repo --wait --json
|
|
|
132
132
|
|
|
133
133
|
Agents should follow `repository.nextAction` and request a Zeus review only when it is `review`. The command waits through internal handoffs until setup is ready or there is a real action for the user or Arcanist.
|
|
134
134
|
|
|
135
|
+
### `arcanist setup`
|
|
136
|
+
|
|
137
|
+
Connect Arcanist and prepare one or more repository environments. The command starts browser authentication when needed, lists only repositories where GitHub reports write access, and queues at most 20 builds.
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
arcanist setup
|
|
141
|
+
arcanist setup acme/web
|
|
142
|
+
arcanist setup acme/web acme/api --json
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
With no repository arguments, the command returns the eligible repositories and `selection_required`. Rerun with the repositories to prepare. Builds continue after the command exits. The command prints an `arcanist environments status owner/repo --wait` command for each build so agents can track progress or leave it running.
|
|
146
|
+
|
|
135
147
|
### `arcanist auth login`
|
|
136
148
|
|
|
137
149
|
```bash
|
|
@@ -140,6 +152,8 @@ printf "arc_..." | arcanist auth login --token-stdin
|
|
|
140
152
|
arcanist auth login --api-url https://api.tryarcanist.com
|
|
141
153
|
```
|
|
142
154
|
|
|
155
|
+
The default flow prints a browser authorization URL and waits for approval. `--json` writes the action immediately as JSONL on stderr and writes one final result to stdout. `--token-stdin` and `ARCANIST_TOKEN` remain available for automation that already has a token.
|
|
156
|
+
|
|
143
157
|
### `arcanist auth whoami`
|
|
144
158
|
|
|
145
159
|
```bash
|
package/dist/index.js
CHANGED
|
@@ -7370,7 +7370,7 @@ import { Command } from "commander";
|
|
|
7370
7370
|
// package.json
|
|
7371
7371
|
var package_default = {
|
|
7372
7372
|
name: "@tryarcanist/cli",
|
|
7373
|
-
version: "0.1.
|
|
7373
|
+
version: "0.1.319",
|
|
7374
7374
|
description: "CLI for Arcanist - create and manage coding agent sessions",
|
|
7375
7375
|
type: "module",
|
|
7376
7376
|
bin: {
|
|
@@ -7620,7 +7620,7 @@ function timeoutError(timeoutMs) {
|
|
|
7620
7620
|
hint: `Increase ${HTTP_TIMEOUT_ENV} for slow links, or check the API connection.`
|
|
7621
7621
|
});
|
|
7622
7622
|
}
|
|
7623
|
-
async function apiRequest(
|
|
7623
|
+
async function apiRequest(connection, path, init, read) {
|
|
7624
7624
|
const timeoutMs = parseHttpTimeoutMs();
|
|
7625
7625
|
const controller = new AbortController();
|
|
7626
7626
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -7628,8 +7628,8 @@ async function apiRequest(config2, path, init, read) {
|
|
|
7628
7628
|
const headers = new Headers(init?.headers);
|
|
7629
7629
|
headers.set("Content-Type", "application/json");
|
|
7630
7630
|
headers.set("User-Agent", CLI_USER_AGENT);
|
|
7631
|
-
headers.set("Authorization", `Bearer ${
|
|
7632
|
-
const res = await fetch(`${normalizeBaseUrl(
|
|
7631
|
+
if (connection.token) headers.set("Authorization", `Bearer ${connection.token}`);
|
|
7632
|
+
const res = await fetch(`${normalizeBaseUrl(connection.apiUrl)}${path}`, {
|
|
7633
7633
|
...init,
|
|
7634
7634
|
headers,
|
|
7635
7635
|
signal: controller.signal
|
|
@@ -7659,6 +7659,9 @@ function resourceNounForPath(path) {
|
|
|
7659
7659
|
async function apiFetch(config2, path, init) {
|
|
7660
7660
|
return apiRequest(config2, path, init, (res) => res.json());
|
|
7661
7661
|
}
|
|
7662
|
+
async function publicApiFetch(apiUrl, path, init) {
|
|
7663
|
+
return apiRequest({ apiUrl }, path, init, (res) => res.json());
|
|
7664
|
+
}
|
|
7662
7665
|
async function apiFetchText(config2, path, init) {
|
|
7663
7666
|
return apiRequest(config2, path, init, (res) => res.text());
|
|
7664
7667
|
}
|
|
@@ -7865,9 +7868,6 @@ function isLoopbackHost(parsed) {
|
|
|
7865
7868
|
// src/runtime.ts
|
|
7866
7869
|
import { randomUUID } from "crypto";
|
|
7867
7870
|
import { createInterface } from "readline/promises";
|
|
7868
|
-
var ASCII_FIRST_PRINTABLE = 32;
|
|
7869
|
-
var ASCII_DELETE = 127;
|
|
7870
|
-
var ANSI_CONTROL_SEQUENCE = /\u001b\[[0-9:;<=>?]*[ -/]*[@-~]/g;
|
|
7871
7871
|
function getRuntimeOptions(command, options = {}) {
|
|
7872
7872
|
const globals = command?.optsWithGlobals?.();
|
|
7873
7873
|
const merged = { ...globals, ...options };
|
|
@@ -7915,70 +7915,6 @@ async function confirmOrThrow(message) {
|
|
|
7915
7915
|
rl.close();
|
|
7916
7916
|
}
|
|
7917
7917
|
}
|
|
7918
|
-
async function readHiddenPrompt(prompt) {
|
|
7919
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
7920
|
-
throw new CliError("user", "No interactive terminal available. Re-run with --token-stdin or set ARCANIST_TOKEN.");
|
|
7921
|
-
}
|
|
7922
|
-
return new Promise((resolve3, reject) => {
|
|
7923
|
-
const stdin = process.stdin;
|
|
7924
|
-
const inputChars = [];
|
|
7925
|
-
let ansiCarry = "";
|
|
7926
|
-
let settled = false;
|
|
7927
|
-
const cleanup = () => {
|
|
7928
|
-
stdin.removeListener("data", onData);
|
|
7929
|
-
stdin.removeListener("error", onError);
|
|
7930
|
-
if (stdin.isTTY) stdin.setRawMode(false);
|
|
7931
|
-
stdin.pause();
|
|
7932
|
-
};
|
|
7933
|
-
const finish = () => {
|
|
7934
|
-
if (settled) return;
|
|
7935
|
-
settled = true;
|
|
7936
|
-
cleanup();
|
|
7937
|
-
process.stdout.write("\n");
|
|
7938
|
-
resolve3(inputChars.join(""));
|
|
7939
|
-
};
|
|
7940
|
-
const fail = (error51) => {
|
|
7941
|
-
if (settled) return;
|
|
7942
|
-
settled = true;
|
|
7943
|
-
cleanup();
|
|
7944
|
-
process.stdout.write("\n");
|
|
7945
|
-
reject(error51);
|
|
7946
|
-
};
|
|
7947
|
-
const onData = (chunk) => {
|
|
7948
|
-
const normalized = normalizePromptChunk(chunk, ansiCarry);
|
|
7949
|
-
ansiCarry = normalized.carry;
|
|
7950
|
-
const text = normalized.text;
|
|
7951
|
-
for (const char of text) {
|
|
7952
|
-
if (char === "\n" || char === "\r") {
|
|
7953
|
-
finish();
|
|
7954
|
-
return;
|
|
7955
|
-
}
|
|
7956
|
-
if (char === "") {
|
|
7957
|
-
fail(new CliError("user", "Interrupted.", { exitCode: EXIT_CODE_INTERRUPTED }));
|
|
7958
|
-
return;
|
|
7959
|
-
}
|
|
7960
|
-
if (char === "\x7F" || char === "\b") {
|
|
7961
|
-
if (inputChars.length > 0) {
|
|
7962
|
-
inputChars.pop();
|
|
7963
|
-
process.stdout.write("\b \b");
|
|
7964
|
-
}
|
|
7965
|
-
continue;
|
|
7966
|
-
}
|
|
7967
|
-
if (isControlCharacter(char)) continue;
|
|
7968
|
-
inputChars.push(char);
|
|
7969
|
-
process.stdout.write("*");
|
|
7970
|
-
}
|
|
7971
|
-
};
|
|
7972
|
-
const onError = (error51) => {
|
|
7973
|
-
fail(new CliError("user", `Failed to read input: ${error51.message}`));
|
|
7974
|
-
};
|
|
7975
|
-
process.stdout.write(prompt);
|
|
7976
|
-
stdin.resume();
|
|
7977
|
-
if (stdin.isTTY) stdin.setRawMode(true);
|
|
7978
|
-
stdin.on("data", onData);
|
|
7979
|
-
stdin.on("error", onError);
|
|
7980
|
-
});
|
|
7981
|
-
}
|
|
7982
7918
|
function applyColorEnvironment(options) {
|
|
7983
7919
|
if (options.noColor === true || !process.stdout.isTTY) {
|
|
7984
7920
|
process.env.NO_COLOR = "1";
|
|
@@ -7998,47 +7934,6 @@ function assertArcanistSessionMutationAllowed(subcommand) {
|
|
|
7998
7934
|
}
|
|
7999
7935
|
);
|
|
8000
7936
|
}
|
|
8001
|
-
function normalizePromptChunk(chunk, carry) {
|
|
8002
|
-
const raw = carry + (Buffer.isBuffer(chunk) ? chunk.toString("utf8") : chunk);
|
|
8003
|
-
let text = "";
|
|
8004
|
-
let index = 0;
|
|
8005
|
-
while (index < raw.length) {
|
|
8006
|
-
const char = raw[index];
|
|
8007
|
-
if (char !== "\x1B") {
|
|
8008
|
-
text += char;
|
|
8009
|
-
index += 1;
|
|
8010
|
-
continue;
|
|
8011
|
-
}
|
|
8012
|
-
const sequence = matchAnsiControlSequence(raw, index);
|
|
8013
|
-
if (sequence.kind === "incomplete") {
|
|
8014
|
-
return { text, carry: raw.slice(index) };
|
|
8015
|
-
}
|
|
8016
|
-
if (sequence.kind === "complete") {
|
|
8017
|
-
index = sequence.nextIndex;
|
|
8018
|
-
continue;
|
|
8019
|
-
}
|
|
8020
|
-
index += 1;
|
|
8021
|
-
}
|
|
8022
|
-
return { text, carry: "" };
|
|
8023
|
-
}
|
|
8024
|
-
function matchAnsiControlSequence(text, startIndex) {
|
|
8025
|
-
if (startIndex + 1 >= text.length) {
|
|
8026
|
-
return { kind: "incomplete", nextIndex: startIndex };
|
|
8027
|
-
}
|
|
8028
|
-
if (text[startIndex + 1] !== "[") {
|
|
8029
|
-
return { kind: "none", nextIndex: startIndex + 1 };
|
|
8030
|
-
}
|
|
8031
|
-
ANSI_CONTROL_SEQUENCE.lastIndex = startIndex;
|
|
8032
|
-
const match = ANSI_CONTROL_SEQUENCE.exec(text);
|
|
8033
|
-
if (match && match.index === startIndex) {
|
|
8034
|
-
return { kind: "complete", nextIndex: startIndex + match[0].length };
|
|
8035
|
-
}
|
|
8036
|
-
return { kind: "incomplete", nextIndex: startIndex };
|
|
8037
|
-
}
|
|
8038
|
-
function isControlCharacter(char) {
|
|
8039
|
-
const code = char.charCodeAt(0);
|
|
8040
|
-
return code < ASCII_FIRST_PRINTABLE || code === ASCII_DELETE;
|
|
8041
|
-
}
|
|
8042
7937
|
|
|
8043
7938
|
// src/utils/terminal-text.ts
|
|
8044
7939
|
function sanitizeTerminalText(value) {
|
|
@@ -22729,6 +22624,7 @@ var INTEGRATION_REGISTRY = {
|
|
|
22729
22624
|
};
|
|
22730
22625
|
|
|
22731
22626
|
// ../../shared/constants/onboarding.ts
|
|
22627
|
+
var MAX_ENVIRONMENT_SETUP_REPOSITORIES = 20;
|
|
22732
22628
|
var CREDENTIAL_VALIDATION_STATUS = {
|
|
22733
22629
|
VALIDATED: "validated",
|
|
22734
22630
|
SAVED_UNVERIFIED: "saved_unverified",
|
|
@@ -23293,6 +23189,16 @@ async function waitForSetup(config2, fullName, pollIntervalMs) {
|
|
|
23293
23189
|
}
|
|
23294
23190
|
});
|
|
23295
23191
|
}
|
|
23192
|
+
async function prepareEnvironments(config2, options) {
|
|
23193
|
+
const response = await apiFetch(config2, "/api/settings/repositories/onboarding", {
|
|
23194
|
+
method: "POST",
|
|
23195
|
+
body: JSON.stringify({
|
|
23196
|
+
repositories: options.repositories.map(({ owner, repo }) => ({ owner, name: repo })),
|
|
23197
|
+
...options.workspace ? { workspace: true } : {}
|
|
23198
|
+
})
|
|
23199
|
+
});
|
|
23200
|
+
return response.repositories;
|
|
23201
|
+
}
|
|
23296
23202
|
async function environmentStatusCommand(repoArg, options = {}, command) {
|
|
23297
23203
|
const pollIntervalMs = options.wait ? parsePollInterval(options.pollInterval, { defaultMs: SETUP_POLL_INTERVAL_MS, maxMs: null }) : null;
|
|
23298
23204
|
const repo = requestedRepo(repoArg);
|
|
@@ -23305,11 +23211,7 @@ async function environmentPrepareCommand(repoArg, options = {}, command) {
|
|
|
23305
23211
|
const pollIntervalMs = options.wait ? parsePollInterval(options.pollInterval, { defaultMs: SETUP_POLL_INTERVAL_MS, maxMs: null }) : null;
|
|
23306
23212
|
const repo = requestedRepo(repoArg);
|
|
23307
23213
|
const { config: config2 } = resolveBusinessContext(command, options);
|
|
23308
|
-
const
|
|
23309
|
-
method: "POST",
|
|
23310
|
-
body: JSON.stringify({ repositories: [{ owner: repo.owner, name: repo.repo }] })
|
|
23311
|
-
});
|
|
23312
|
-
const result = response.repositories[0];
|
|
23214
|
+
const [result] = await prepareEnvironments(config2, { repositories: [repo] });
|
|
23313
23215
|
if (!result) throw new CliError("server", "Environment setup returned no repository result.");
|
|
23314
23216
|
if (result.status === "forbidden") {
|
|
23315
23217
|
throw new CliError("auth", `You must have write access to ${repo.fullName} to prepare its environment.`);
|
|
@@ -23357,15 +23259,18 @@ async function hadesCommand(repo, options = {}, command) {
|
|
|
23357
23259
|
}
|
|
23358
23260
|
|
|
23359
23261
|
// src/commands/login.ts
|
|
23262
|
+
var DEVICE_AUTH_START_PATH = "/api/auth/cli/device";
|
|
23263
|
+
var DEVICE_AUTH_TOKEN_PATH = "/api/auth/cli/device/token";
|
|
23360
23264
|
async function loginCommand(options, command) {
|
|
23361
23265
|
const runtime = getRuntimeOptions(command, options);
|
|
23362
|
-
|
|
23266
|
+
const apiUrl = resolveLoginApiUrl(runtime.apiUrl);
|
|
23267
|
+
let token = null;
|
|
23363
23268
|
if (options.tokenStdin) {
|
|
23364
23269
|
token = await readStdinTrimmed();
|
|
23365
23270
|
} else if (runtime.token) {
|
|
23366
23271
|
token = runtime.token;
|
|
23367
23272
|
} else {
|
|
23368
|
-
token = await
|
|
23273
|
+
token = await loginWithDeviceAuthorization(apiUrl, runtime);
|
|
23369
23274
|
}
|
|
23370
23275
|
if (!token) {
|
|
23371
23276
|
throw new CliError("user", "No token provided.");
|
|
@@ -23373,25 +23278,55 @@ async function loginCommand(options, command) {
|
|
|
23373
23278
|
if (!token.startsWith("arc_")) {
|
|
23374
23279
|
throw new CliError("user", "Invalid token format. Token must start with 'arc_'.");
|
|
23375
23280
|
}
|
|
23376
|
-
const apiUrl = resolveLoginApiUrl(runtime.apiUrl);
|
|
23377
|
-
saveConfig({ apiUrl, token });
|
|
23378
|
-
if (runtime.json) {
|
|
23379
|
-
writeJson({ ok: true, apiUrl });
|
|
23380
|
-
} else if (!runtime.quiet) {
|
|
23381
|
-
console.log(`Logged in. API: ${apiUrl}`);
|
|
23382
|
-
}
|
|
23383
23281
|
try {
|
|
23384
|
-
await apiFetch({ apiUrl, token }, "/api/
|
|
23385
|
-
|
|
23282
|
+
await apiFetch({ apiUrl, token }, "/api/auth/whoami");
|
|
23283
|
+
const config2 = { apiUrl, token };
|
|
23284
|
+
saveConfig(config2);
|
|
23285
|
+
if (options.emitResult === false) return config2;
|
|
23286
|
+
if (runtime.json) writeJson({ ok: true, apiUrl });
|
|
23287
|
+
else if (!runtime.quiet) {
|
|
23288
|
+
console.log(`Logged in. API: ${apiUrl}`);
|
|
23289
|
+
console.log("Token verified.");
|
|
23290
|
+
}
|
|
23291
|
+
return config2;
|
|
23386
23292
|
} catch (err) {
|
|
23387
|
-
if (
|
|
23388
|
-
|
|
23389
|
-
console.warn("Warning: Token could not be verified (401). It may be invalid or expired.");
|
|
23390
|
-
} else {
|
|
23391
|
-
console.warn("Warning: Could not reach API to verify token.");
|
|
23392
|
-
}
|
|
23293
|
+
if (err instanceof ApiError && (err.status === 401 || err.status === 403)) {
|
|
23294
|
+
throw new CliError("auth", "Token could not be verified. It may be invalid or expired.");
|
|
23393
23295
|
}
|
|
23296
|
+
throw err;
|
|
23297
|
+
}
|
|
23298
|
+
}
|
|
23299
|
+
async function loginWithDeviceAuthorization(apiUrl, runtime) {
|
|
23300
|
+
const authorization = await publicApiFetch(apiUrl, DEVICE_AUTH_START_PATH, {
|
|
23301
|
+
method: "POST",
|
|
23302
|
+
body: "{}"
|
|
23303
|
+
});
|
|
23304
|
+
emitAction(runtime, { url: authorization.verificationUrl, userCode: authorization.userCode });
|
|
23305
|
+
if (!runtime.json && !runtime.quiet) console.error("Waiting for approval...");
|
|
23306
|
+
const deadline = Date.now() + authorization.expiresIn * 1e3;
|
|
23307
|
+
const pollIntervalMs = Math.max(1, authorization.pollInterval) * 1e3;
|
|
23308
|
+
while (Date.now() < deadline) {
|
|
23309
|
+
const response = await publicApiFetch(apiUrl, DEVICE_AUTH_TOKEN_PATH, {
|
|
23310
|
+
method: "POST",
|
|
23311
|
+
body: JSON.stringify({ deviceCode: authorization.deviceCode })
|
|
23312
|
+
});
|
|
23313
|
+
if ("accessToken" in response) return response.accessToken;
|
|
23314
|
+
await sleep(pollIntervalMs);
|
|
23315
|
+
}
|
|
23316
|
+
throw new CliError("auth", "CLI authorization expired.", { hint: "Run `arcanist auth login` again." });
|
|
23317
|
+
}
|
|
23318
|
+
function emitAction(runtime, action) {
|
|
23319
|
+
if (runtime.json) {
|
|
23320
|
+
process.stderr.write(
|
|
23321
|
+
`${JSON.stringify({ type: "action_required", action: { kind: "authorize_cli", ...action } })}
|
|
23322
|
+
`
|
|
23323
|
+
);
|
|
23324
|
+
return;
|
|
23394
23325
|
}
|
|
23326
|
+
process.stderr.write(`Authorize Arcanist in your browser:
|
|
23327
|
+
${action.url}
|
|
23328
|
+
Code: ${action.userCode}
|
|
23329
|
+
`);
|
|
23395
23330
|
}
|
|
23396
23331
|
|
|
23397
23332
|
// ../../shared/agent/agent-runtime-backend.ts
|
|
@@ -23962,7 +23897,6 @@ async function reviewCommand(prUrl, options = {}, command) {
|
|
|
23962
23897
|
method: "POST",
|
|
23963
23898
|
body: JSON.stringify({
|
|
23964
23899
|
prUrl,
|
|
23965
|
-
...options.focus ? { focus: options.focus } : {},
|
|
23966
23900
|
// Send an explicitly passed model even when empty (e.g. --model "$VAR" with an
|
|
23967
23901
|
// unset VAR) so the server rejects it as invalid instead of silently routing.
|
|
23968
23902
|
...options.model !== void 0 ? { model: options.model } : {},
|
|
@@ -26995,6 +26929,296 @@ async function usageCommand(sessionId, options, command) {
|
|
|
26995
26929
|
if (usage.totalCostUsd !== void 0) console.log(`Cost: ${String(usage.totalCostUsd)}`);
|
|
26996
26930
|
}
|
|
26997
26931
|
|
|
26932
|
+
// ../../shared/utils/shell.ts
|
|
26933
|
+
var SHELL_ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
26934
|
+
|
|
26935
|
+
// ../../shared/types/sandbox.ts
|
|
26936
|
+
var APP_RUNTIME_ENTRY_TYPE = {
|
|
26937
|
+
Compose: "compose",
|
|
26938
|
+
Dockerfile: "dockerfile",
|
|
26939
|
+
Command: "command"
|
|
26940
|
+
};
|
|
26941
|
+
|
|
26942
|
+
// ../../shared/types/repo-runtime.ts
|
|
26943
|
+
var ENVIRONMENT_REPOS_MAX = 8;
|
|
26944
|
+
var SANDBOX_PATH_PATTERN = /^\/workspace\/(?!\.+$)(?!.*\.\.)[A-Za-z0-9._-]+$/;
|
|
26945
|
+
var EnvironmentRepoSchema = external_exports.object({
|
|
26946
|
+
repoOwner: external_exports.string().trim().min(1, "repoOwner is required"),
|
|
26947
|
+
repoName: external_exports.string().trim().min(1, "repoName is required"),
|
|
26948
|
+
sandboxPath: external_exports.string().regex(SANDBOX_PATH_PATTERN, "sandboxPath must be a single path segment under /workspace").optional()
|
|
26949
|
+
}).strict();
|
|
26950
|
+
function environmentRepoSandboxPath(repo) {
|
|
26951
|
+
return repo.sandboxPath ?? `/workspace/${repo.repoName}`;
|
|
26952
|
+
}
|
|
26953
|
+
var EnvironmentReposSchema = external_exports.array(EnvironmentRepoSchema).min(1, "at least one repo is required").max(ENVIRONMENT_REPOS_MAX, `at most ${ENVIRONMENT_REPOS_MAX} repos are allowed`).superRefine((repos2, ctx) => {
|
|
26954
|
+
if (repos2.length === 0) return;
|
|
26955
|
+
const primaryOwner = repos2[0].repoOwner.toLowerCase();
|
|
26956
|
+
if (repos2[0].sandboxPath !== void 0) {
|
|
26957
|
+
ctx.addIssue({
|
|
26958
|
+
code: "custom",
|
|
26959
|
+
path: [0, "sandboxPath"],
|
|
26960
|
+
message: `the primary repo is always cloned to /workspace/${repos2[0].repoName}`
|
|
26961
|
+
});
|
|
26962
|
+
}
|
|
26963
|
+
const seenRepos = /* @__PURE__ */ new Set();
|
|
26964
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
26965
|
+
repos2.forEach((repo, index) => {
|
|
26966
|
+
if (repo.repoOwner.toLowerCase() !== primaryOwner) {
|
|
26967
|
+
ctx.addIssue({
|
|
26968
|
+
code: "custom",
|
|
26969
|
+
path: [index, "repoOwner"],
|
|
26970
|
+
message: "all repos must share the primary repo's owner"
|
|
26971
|
+
});
|
|
26972
|
+
}
|
|
26973
|
+
const repoKey = `${repo.repoOwner}/${repo.repoName}`.toLowerCase();
|
|
26974
|
+
if (seenRepos.has(repoKey)) {
|
|
26975
|
+
ctx.addIssue({ code: "custom", path: [index], message: `duplicate repo ${repo.repoOwner}/${repo.repoName}` });
|
|
26976
|
+
}
|
|
26977
|
+
seenRepos.add(repoKey);
|
|
26978
|
+
const path = environmentRepoSandboxPath(repo);
|
|
26979
|
+
if (!SANDBOX_PATH_PATTERN.test(path)) {
|
|
26980
|
+
ctx.addIssue({
|
|
26981
|
+
code: "custom",
|
|
26982
|
+
path: [index],
|
|
26983
|
+
message: `repo name ${repo.repoName} needs an explicit sandboxPath`
|
|
26984
|
+
});
|
|
26985
|
+
return;
|
|
26986
|
+
}
|
|
26987
|
+
if (seenPaths.has(path)) {
|
|
26988
|
+
ctx.addIssue({ code: "custom", path: [index], message: `sandbox path ${path} is already taken` });
|
|
26989
|
+
}
|
|
26990
|
+
seenPaths.add(path);
|
|
26991
|
+
});
|
|
26992
|
+
});
|
|
26993
|
+
var CredentialDeclarationSchema = external_exports.object({
|
|
26994
|
+
name: external_exports.string(),
|
|
26995
|
+
envVar: external_exports.string(),
|
|
26996
|
+
source: external_exports.enum(["business_openai_key", "business_anthropic_key"]).optional()
|
|
26997
|
+
});
|
|
26998
|
+
var StringRecordSchema = external_exports.record(
|
|
26999
|
+
external_exports.string().regex(SHELL_ENV_KEY_PATTERN, "must be a POSIX-style environment variable name"),
|
|
27000
|
+
external_exports.string()
|
|
27001
|
+
);
|
|
27002
|
+
var AppRuntimeProfileConfigSchema = external_exports.object({
|
|
27003
|
+
cwd: external_exports.string().optional(),
|
|
27004
|
+
kind: external_exports.string().optional(),
|
|
27005
|
+
runner: external_exports.string().optional(),
|
|
27006
|
+
entry: external_exports.object({
|
|
27007
|
+
type: external_exports.enum([
|
|
27008
|
+
APP_RUNTIME_ENTRY_TYPE.Compose,
|
|
27009
|
+
APP_RUNTIME_ENTRY_TYPE.Dockerfile,
|
|
27010
|
+
APP_RUNTIME_ENTRY_TYPE.Command
|
|
27011
|
+
]),
|
|
27012
|
+
files: external_exports.array(external_exports.string()).optional(),
|
|
27013
|
+
service: external_exports.string().optional(),
|
|
27014
|
+
profiles: external_exports.array(external_exports.string()).optional(),
|
|
27015
|
+
context: external_exports.string().optional(),
|
|
27016
|
+
dockerfile: external_exports.string().optional(),
|
|
27017
|
+
command: external_exports.string().optional()
|
|
27018
|
+
}).strict().optional(),
|
|
27019
|
+
url: external_exports.object({ hostPort: external_exports.number().int().optional(), path: external_exports.string().optional() }).strict().optional(),
|
|
27020
|
+
portMapping: external_exports.object({ containerPort: external_exports.number().int().optional() }).strict().optional(),
|
|
27021
|
+
additionalPorts: external_exports.array(
|
|
27022
|
+
external_exports.object({
|
|
27023
|
+
service: external_exports.string().optional(),
|
|
27024
|
+
hostPort: external_exports.number().int().optional(),
|
|
27025
|
+
containerPort: external_exports.number().int().optional()
|
|
27026
|
+
}).strict()
|
|
27027
|
+
).optional(),
|
|
27028
|
+
runtimeVariables: StringRecordSchema.optional(),
|
|
27029
|
+
composeEnv: StringRecordSchema.optional(),
|
|
27030
|
+
generatedComposeEnv: external_exports.record(
|
|
27031
|
+
external_exports.string(),
|
|
27032
|
+
external_exports.union([
|
|
27033
|
+
external_exports.object({ type: external_exports.literal("alphanumeric"), length: external_exports.number().int() }).strict(),
|
|
27034
|
+
external_exports.object({ type: external_exports.literal("hex"), bytes: external_exports.number().int() }).strict()
|
|
27035
|
+
])
|
|
27036
|
+
).optional(),
|
|
27037
|
+
env: StringRecordSchema.optional(),
|
|
27038
|
+
ready: external_exports.object({
|
|
27039
|
+
path: external_exports.string().optional(),
|
|
27040
|
+
nonEmptyPath: external_exports.string().optional(),
|
|
27041
|
+
timeoutSeconds: external_exports.number().int().optional()
|
|
27042
|
+
}).strict().optional(),
|
|
27043
|
+
open: external_exports.object({ path: external_exports.string().optional() }).strict().optional(),
|
|
27044
|
+
auth: external_exports.object({
|
|
27045
|
+
command: external_exports.string().optional(),
|
|
27046
|
+
validatePath: external_exports.string().optional(),
|
|
27047
|
+
browserStateEnvVar: external_exports.string().optional(),
|
|
27048
|
+
credentials: external_exports.array(CredentialDeclarationSchema).optional()
|
|
27049
|
+
}).strict().optional(),
|
|
27050
|
+
e2e: external_exports.object({
|
|
27051
|
+
testCommand: external_exports.string(),
|
|
27052
|
+
seedCommand: external_exports.string().optional(),
|
|
27053
|
+
resetCommand: external_exports.string().optional(),
|
|
27054
|
+
credentials: external_exports.array(CredentialDeclarationSchema).optional()
|
|
27055
|
+
}).strict().optional()
|
|
27056
|
+
}).strict();
|
|
27057
|
+
var EnvironmentProvisioningConfigSchema = external_exports.object({
|
|
27058
|
+
sandbox: external_exports.object({ appRuntime: AppRuntimeProfileConfigSchema.optional() }).strict().optional()
|
|
27059
|
+
}).strict();
|
|
27060
|
+
|
|
27061
|
+
// src/commands/setup.ts
|
|
27062
|
+
var INSTALL_POLL_INTERVAL_MS = 5e3;
|
|
27063
|
+
var INSTALL_WAIT_TIMEOUT_MS = 20 * 60 * 1e3;
|
|
27064
|
+
var ONBOARDING_REPOS_PATH = "/api/onboarding/repos";
|
|
27065
|
+
async function setupCommand(repoArgs = [], options = {}, command) {
|
|
27066
|
+
if (repoArgs.length > MAX_ENVIRONMENT_SETUP_REPOSITORIES) {
|
|
27067
|
+
throw new CliError("user", `At most ${MAX_ENVIRONMENT_SETUP_REPOSITORIES} repositories can be prepared at once.`);
|
|
27068
|
+
}
|
|
27069
|
+
if (options.workspace && repoArgs.length === 1) {
|
|
27070
|
+
throw new CliError("user", "A multi-repository workspace requires at least two repositories.");
|
|
27071
|
+
}
|
|
27072
|
+
if (options.workspace && repoArgs.length > ENVIRONMENT_REPOS_MAX) {
|
|
27073
|
+
throw new CliError("user", `A workspace supports at most ${ENVIRONMENT_REPOS_MAX} repositories.`);
|
|
27074
|
+
}
|
|
27075
|
+
const runtime = getRuntimeOptions(command, options);
|
|
27076
|
+
let config2 = loadConfig(runtime);
|
|
27077
|
+
if (!config2) config2 = await loginCommand({ ...runtime, emitResult: false }, command);
|
|
27078
|
+
const requested = repoArgs.map((value) => {
|
|
27079
|
+
const repo = parseRepoArg(value);
|
|
27080
|
+
return `${repo.owner}/${repo.repo}`;
|
|
27081
|
+
});
|
|
27082
|
+
const inventory = await ensureGitHubRepositories({ config: config2, requested, runtime });
|
|
27083
|
+
if (!inventory) return;
|
|
27084
|
+
const eligible = eligibleRepositoryNames(inventory.repos);
|
|
27085
|
+
const selected = selectRepositories({ eligible, requested, runtime });
|
|
27086
|
+
if (!selected) return;
|
|
27087
|
+
const repositories = await prepareEnvironments(config2, {
|
|
27088
|
+
repositories: selected.map((value) => parseRepoArg(value)),
|
|
27089
|
+
workspace: options.workspace
|
|
27090
|
+
});
|
|
27091
|
+
const rejected = repositories.filter((result) => result.status === "forbidden" || result.status === "failed");
|
|
27092
|
+
const accepted = repositories.filter((result) => result.status !== "forbidden" && result.status !== "failed");
|
|
27093
|
+
if (accepted.length === 0) {
|
|
27094
|
+
throw new CliError(
|
|
27095
|
+
rejected.some((result) => result.status === "forbidden") ? "auth" : "server",
|
|
27096
|
+
`Environment setup was rejected for: ${rejected.map((result) => result.fullName).join(", ")}.`
|
|
27097
|
+
);
|
|
27098
|
+
}
|
|
27099
|
+
const queued = accepted.filter((result) => result.status !== "already_ready");
|
|
27100
|
+
const tracking = trackingCommands(queued.map((result) => result.fullName));
|
|
27101
|
+
const payload = {
|
|
27102
|
+
status: rejected.length > 0 ? "partial" : queued.length > 0 ? "queued" : "ready",
|
|
27103
|
+
repositories,
|
|
27104
|
+
workspace: options.workspace === true,
|
|
27105
|
+
tracking
|
|
27106
|
+
};
|
|
27107
|
+
emit(void 0, runtime, payload, () => {
|
|
27108
|
+
console.log(
|
|
27109
|
+
queued.length > 0 ? `Environment preparation started for ${queued.length} ${queued.length === 1 ? "repository" : "repositories"}.` : "The selected environments are ready."
|
|
27110
|
+
);
|
|
27111
|
+
if (rejected.length > 0) {
|
|
27112
|
+
console.log(`Could not start: ${rejected.map((result) => sanitizeTerminalText(result.fullName)).join(", ")}.`);
|
|
27113
|
+
}
|
|
27114
|
+
for (const line of tracking) console.log(line);
|
|
27115
|
+
});
|
|
27116
|
+
}
|
|
27117
|
+
function eligibleRepositoryNames(repos2) {
|
|
27118
|
+
return repos2.filter((repo) => {
|
|
27119
|
+
return typeof repo.fullName === "string" && repo.canReview === true;
|
|
27120
|
+
}).map((repo) => repo.fullName).sort((a, b) => a.localeCompare(b));
|
|
27121
|
+
}
|
|
27122
|
+
function selectRepositories(options) {
|
|
27123
|
+
const { eligible, requested, runtime } = options;
|
|
27124
|
+
if (requested.length === 0) {
|
|
27125
|
+
emit(
|
|
27126
|
+
void 0,
|
|
27127
|
+
runtime,
|
|
27128
|
+
{
|
|
27129
|
+
status: "selection_required",
|
|
27130
|
+
repositories: eligible,
|
|
27131
|
+
next: "Rerun with one or more owner/repo arguments."
|
|
27132
|
+
},
|
|
27133
|
+
() => {
|
|
27134
|
+
console.log("Repositories available to prepare:");
|
|
27135
|
+
for (const repository of eligible) console.log(` ${sanitizeTerminalText(repository)}`);
|
|
27136
|
+
console.log("Rerun with one or more owner/repo arguments.");
|
|
27137
|
+
}
|
|
27138
|
+
);
|
|
27139
|
+
return null;
|
|
27140
|
+
}
|
|
27141
|
+
const eligibleNames = new Map(eligible.map((repository) => [repository.toLowerCase(), repository]));
|
|
27142
|
+
const selected = [
|
|
27143
|
+
...new Map(
|
|
27144
|
+
requested.map((repository) => [
|
|
27145
|
+
repository.toLowerCase(),
|
|
27146
|
+
eligibleNames.get(repository.toLowerCase()) ?? repository
|
|
27147
|
+
])
|
|
27148
|
+
).values()
|
|
27149
|
+
];
|
|
27150
|
+
const unavailable = selected.filter((repository) => !eligibleNames.has(repository.toLowerCase()));
|
|
27151
|
+
if (unavailable.length > 0) {
|
|
27152
|
+
throw new CliError("auth", `Repositories are unavailable or lack write access: ${unavailable.join(", ")}.`);
|
|
27153
|
+
}
|
|
27154
|
+
return selected;
|
|
27155
|
+
}
|
|
27156
|
+
async function ensureGitHubRepositories(options) {
|
|
27157
|
+
const { config: config2, requested, runtime } = options;
|
|
27158
|
+
let inventory = await apiFetch(config2, `${ONBOARDING_REPOS_PATH}?refresh=1`);
|
|
27159
|
+
const readOnly = new Set(
|
|
27160
|
+
inventory.repos.filter((repo) => repo.canReview !== true && typeof repo.fullName === "string").map((repo) => String(repo.fullName).toLowerCase())
|
|
27161
|
+
);
|
|
27162
|
+
const inaccessible = requested.filter((repository) => readOnly.has(repository.toLowerCase()));
|
|
27163
|
+
if (inaccessible.length > 0) {
|
|
27164
|
+
throw new CliError("auth", `Repositories lack write access: ${inaccessible.join(", ")}.`);
|
|
27165
|
+
}
|
|
27166
|
+
if (inventorySatisfiesRequest(inventory, requested)) return inventory;
|
|
27167
|
+
if (inventory.ssoOrgs.length > 0) return emitSsoRequirement({ apiUrl: config2.apiUrl, inventory, runtime });
|
|
27168
|
+
const install = await apiFetch(config2, "/api/github/install-url");
|
|
27169
|
+
emitSetupAction(runtime, { kind: "install_github", url: install.url });
|
|
27170
|
+
if (!runtime.json && !runtime.quiet) console.error("Waiting for GitHub repository access...");
|
|
27171
|
+
const deadline = Date.now() + INSTALL_WAIT_TIMEOUT_MS;
|
|
27172
|
+
while (Date.now() < deadline) {
|
|
27173
|
+
await sleep(INSTALL_POLL_INTERVAL_MS);
|
|
27174
|
+
inventory = await apiFetch(config2, ONBOARDING_REPOS_PATH);
|
|
27175
|
+
if (inventorySatisfiesRequest(inventory, requested)) return inventory;
|
|
27176
|
+
if (inventory.ssoOrgs.length > 0) return emitSsoRequirement({ apiUrl: config2.apiUrl, inventory, runtime });
|
|
27177
|
+
}
|
|
27178
|
+
throw new CliError("server", "Timed out waiting for GitHub repository access.", {
|
|
27179
|
+
hint: `Finish repository selection at ${install.url}, then rerun \`arcanist setup\`.`
|
|
27180
|
+
});
|
|
27181
|
+
}
|
|
27182
|
+
function inventorySatisfiesRequest(inventory, requested) {
|
|
27183
|
+
const eligible = new Set(eligibleRepositoryNames(inventory.repos).map((repository) => repository.toLowerCase()));
|
|
27184
|
+
if (requested.length > 0) return requested.every((repository) => eligible.has(repository.toLowerCase()));
|
|
27185
|
+
return inventory.complete === true && eligible.size > 0;
|
|
27186
|
+
}
|
|
27187
|
+
function emitSsoRequirement(options) {
|
|
27188
|
+
const { apiUrl, inventory, runtime } = options;
|
|
27189
|
+
for (const org of inventory.ssoOrgs) {
|
|
27190
|
+
if (typeof org.authorizeUrl !== "string") continue;
|
|
27191
|
+
const url2 = org.authorizeUrl.startsWith("/") ? `${apiUrl}${org.authorizeUrl}` : org.authorizeUrl;
|
|
27192
|
+
emitSetupAction(runtime, {
|
|
27193
|
+
kind: "authorize_github_sso",
|
|
27194
|
+
url: url2,
|
|
27195
|
+
organization: org.login ?? org.orgId
|
|
27196
|
+
});
|
|
27197
|
+
}
|
|
27198
|
+
emit(
|
|
27199
|
+
void 0,
|
|
27200
|
+
runtime,
|
|
27201
|
+
{
|
|
27202
|
+
status: "action_required",
|
|
27203
|
+
action: "authorize_github_sso",
|
|
27204
|
+
repositories: eligibleRepositoryNames(inventory.repos),
|
|
27205
|
+
ssoOrgs: inventory.ssoOrgs,
|
|
27206
|
+
next: "Authorize GitHub SSO, then rerun arcanist setup."
|
|
27207
|
+
},
|
|
27208
|
+
() => console.log("Authorize GitHub SSO, then rerun arcanist setup.")
|
|
27209
|
+
);
|
|
27210
|
+
return null;
|
|
27211
|
+
}
|
|
27212
|
+
function emitSetupAction(runtime, action) {
|
|
27213
|
+
if (runtime.json) process.stderr.write(`${JSON.stringify({ type: "action_required", action })}
|
|
27214
|
+
`);
|
|
27215
|
+
else process.stderr.write(`Action required: ${action.url}
|
|
27216
|
+
`);
|
|
27217
|
+
}
|
|
27218
|
+
function trackingCommands(repositories) {
|
|
27219
|
+
return repositories.map((repository) => `Track later: arcanist environments status ${repository} --wait`);
|
|
27220
|
+
}
|
|
27221
|
+
|
|
26998
27222
|
// src/commands/stop.ts
|
|
26999
27223
|
async function stopCommand(sessionId, options = {}, command) {
|
|
27000
27224
|
const { config: config2 } = resolveBusinessContext(command, options);
|
|
@@ -27239,7 +27463,7 @@ program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
|
27239
27463
|
applyColorEnvironment(getRuntimeOptions(actionCommand));
|
|
27240
27464
|
});
|
|
27241
27465
|
var auth = program.command("auth").description("Authentication commands");
|
|
27242
|
-
auth.command("login").description("Authenticate
|
|
27466
|
+
auth.command("login").description("Authenticate through your browser").option("--token-stdin", "Read token from stdin instead of interactive prompt").option("--api-url <url>", "Set custom API URL").addHelpText(
|
|
27243
27467
|
"after",
|
|
27244
27468
|
`
|
|
27245
27469
|
Examples:
|
|
@@ -27247,7 +27471,9 @@ Examples:
|
|
|
27247
27471
|
printf "arc_..." | arcanist auth login --token-stdin
|
|
27248
27472
|
ARCANIST_TOKEN=arc_... arcanist auth whoami --json
|
|
27249
27473
|
`
|
|
27250
|
-
).action((options, command) =>
|
|
27474
|
+
).action(async (options, command) => {
|
|
27475
|
+
await loginCommand(options, command);
|
|
27476
|
+
});
|
|
27251
27477
|
auth.command("whoami").description("Print the authenticated user and token scope").addHelpText(
|
|
27252
27478
|
"after",
|
|
27253
27479
|
`
|
|
@@ -27256,6 +27482,21 @@ Examples:
|
|
|
27256
27482
|
ARCANIST_TOKEN=arc_... arcanist auth whoami --json
|
|
27257
27483
|
`
|
|
27258
27484
|
).action((options, command) => whoamiCommand(options, command));
|
|
27485
|
+
program.command("setup").description("Connect GitHub repositories and prepare their Zeus environments").argument("[repos...]", "Repositories in owner/name form").option("--workspace", "Prepare the repositories as one multi-repository workspace").addHelpText(
|
|
27486
|
+
"after",
|
|
27487
|
+
`
|
|
27488
|
+
Examples:
|
|
27489
|
+
arcanist setup
|
|
27490
|
+
arcanist setup acme/web
|
|
27491
|
+
arcanist setup acme/web acme/api --json
|
|
27492
|
+
arcanist setup acme/web acme/api --workspace
|
|
27493
|
+
|
|
27494
|
+
With no repositories, the command returns the eligible repositories. Rerun with the
|
|
27495
|
+
repositories to prepare. By default each repository gets its own environment; use
|
|
27496
|
+
--workspace to prepare them together. Builds continue after the command exits; use
|
|
27497
|
+
the emitted environment status commands to track them.
|
|
27498
|
+
`
|
|
27499
|
+
).action((repos2, options, command) => setupCommand(repos2, options, command));
|
|
27259
27500
|
var codex = program.command("codex").description("Codex subscription (bring-your-own-subscription) commands");
|
|
27260
27501
|
codex.command("login").description("Authenticate a Codex/ChatGPT subscription and store it for your Codex sessions").option("--codex-path <path>", "Path to the codex executable (default: codex on PATH, or ARCANIST_CODEX_BIN)").addHelpText(
|
|
27261
27502
|
"after",
|
|
@@ -27273,12 +27514,11 @@ codex.command("use <state>").description("Turn using your saved Codex subscripti
|
|
|
27273
27514
|
codex.command("status").description("Show Codex subscription availability and whether auth is saved").action((options, command) => codexStatusCommand(options, command));
|
|
27274
27515
|
codex.command("logout").description("Deactivate the selector and remove the stored Codex subscription auth for your user").action((options, command) => codexLogoutCommand(options, command));
|
|
27275
27516
|
var sessions = program.command("sessions").description("Session commands");
|
|
27276
|
-
program.command("review <pr-url>").description("Run a Zeus review for a GitHub pull request").option("--focus <text>", "
|
|
27517
|
+
program.command("review <pr-url>").description("Run a Zeus review for a GitHub pull request").option("--focus <text>", "Ignored. Zeus always reviews the full pull request").option("--model <model>", "Pin the reviewer model (Arcanist members only); defaults to the Codex reviewer").option("--sandbox-runtime <runtime>", "Force the sandbox runtime (Arcanist members only): modal or e2b").option("--wait", "Wait for the review and print its verdict and findings").option("--poll-interval <ms>", "Polling interval in milliseconds while waiting (default 10000)").addHelpText(
|
|
27277
27518
|
"after",
|
|
27278
27519
|
`
|
|
27279
27520
|
Examples:
|
|
27280
27521
|
arcanist review https://github.com/org/repo/pull/123 --wait --json
|
|
27281
|
-
arcanist review https://github.com/org/repo/pull/123 --focus "Check auth" --wait
|
|
27282
27522
|
arcanist review https://github.com/org/repo/pull/123 --model gpt-5.5 --wait
|
|
27283
27523
|
arcanist review https://github.com/org/repo/pull/123 --sandbox-runtime modal --wait
|
|
27284
27524
|
|