@tryarcanist/cli 0.1.318 → 0.1.320

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.
Files changed (3) hide show
  1. package/README.md +18 -4
  2. package/dist/index.js +378 -136
  3. 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 # paste a token from Settings > CLI
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
- Generate a CLI token in the Arcanist UI at **Settings > CLI**, then:
28
+ Start browser authorization from the CLI:
29
29
 
30
30
  ```bash
31
31
  arcanist auth login
32
- # paste your arc_... token when prompted; input is masked
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.318",
7373
+ version: "0.1.320",
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(config2, path, init, read) {
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 ${config2.token}`);
7632
- const res = await fetch(`${normalizeBaseUrl(config2.apiUrl)}${path}`, {
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 response = await apiFetch(config2, "/api/settings/repositories/onboarding", {
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
- let token;
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 readHiddenPrompt("Enter your CLI token: ");
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/cli-tokens");
23385
- if (!runtime.json && !runtime.quiet) console.log("Token verified.");
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 (!runtime.json && !runtime.quiet) {
23388
- if (err instanceof ApiError && (err.status === 401 || err.status === 403)) {
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
@@ -26994,6 +26929,296 @@ async function usageCommand(sessionId, options, command) {
26994
26929
  if (usage.totalCostUsd !== void 0) console.log(`Cost: ${String(usage.totalCostUsd)}`);
26995
26930
  }
26996
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
+
26997
27222
  // src/commands/stop.ts
26998
27223
  async function stopCommand(sessionId, options = {}, command) {
26999
27224
  const { config: config2 } = resolveBusinessContext(command, options);
@@ -27238,7 +27463,7 @@ program.hook("preAction", (_thisCommand, actionCommand) => {
27238
27463
  applyColorEnvironment(getRuntimeOptions(actionCommand));
27239
27464
  });
27240
27465
  var auth = program.command("auth").description("Authentication commands");
27241
- auth.command("login").description("Authenticate with a personal access token").option("--token-stdin", "Read token from stdin instead of interactive prompt").option("--api-url <url>", "Set custom API URL").addHelpText(
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(
27242
27467
  "after",
27243
27468
  `
27244
27469
  Examples:
@@ -27246,7 +27471,9 @@ Examples:
27246
27471
  printf "arc_..." | arcanist auth login --token-stdin
27247
27472
  ARCANIST_TOKEN=arc_... arcanist auth whoami --json
27248
27473
  `
27249
- ).action((options, command) => loginCommand(options, command));
27474
+ ).action(async (options, command) => {
27475
+ await loginCommand(options, command);
27476
+ });
27250
27477
  auth.command("whoami").description("Print the authenticated user and token scope").addHelpText(
27251
27478
  "after",
27252
27479
  `
@@ -27255,6 +27482,21 @@ Examples:
27255
27482
  ARCANIST_TOKEN=arc_... arcanist auth whoami --json
27256
27483
  `
27257
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));
27258
27500
  var codex = program.command("codex").description("Codex subscription (bring-your-own-subscription) commands");
27259
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(
27260
27502
  "after",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tryarcanist/cli",
3
- "version": "0.1.318",
3
+ "version": "0.1.320",
4
4
  "description": "CLI for Arcanist - create and manage coding agent sessions",
5
5
  "type": "module",
6
6
  "bin": {