@tryarcanist/cli 0.1.132 → 0.1.134
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 +15 -1
- package/dist/index.js +241 -57
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -131,7 +131,7 @@ arcanist sessions create your-org/your-repo "retry-safe create" --idempotency-ke
|
|
|
131
131
|
|
|
132
132
|
`--backend` picks the agent runtime backend: `codex` (default) or `claude_code`. `--model` must be valid for the chosen backend — codex runs OpenAI models (`gpt-5.4` default, `gpt-5.5`), `claude_code` runs Anthropic models (`claude-opus-4-8` default, `claude-sonnet-4-6`) — and the CLI rejects a mismatch before any network call. `claude_code` sessions require an Anthropic API key configured in Settings.
|
|
133
133
|
|
|
134
|
-
`--wait` blocks until the created prompt finishes and exits non-zero if the prompt finishes with `status: failed`, making it suitable for cron or other schedulers that alert on command failure. `--poll-interval <ms>` tunes the completion check frequency.
|
|
134
|
+
`--wait` blocks until the created prompt finishes, prints the resulting PR/branch line in human-readable mode when it is already available, and exits non-zero if the prompt finishes with `status: failed`, making it suitable for cron or other schedulers that alert on command failure. `--poll-interval <ms>` tunes the completion check frequency.
|
|
135
135
|
|
|
136
136
|
Use `--base-branch <branch>` to target a non-default base branch. The CLI only validates that the branch value is non-empty; branch existence is checked later by the session runtime.
|
|
137
137
|
|
|
@@ -176,6 +176,8 @@ arcanist sessions get abc123
|
|
|
176
176
|
arcanist sessions get abc123 --json
|
|
177
177
|
```
|
|
178
178
|
|
|
179
|
+
Human-readable output includes `PR: <url> (<branch>)` when the session has opened a PR, or `Branch: <branch>` when a produced branch is known but no PR URL is present yet. If publishing is still settling, rerun `sessions get`; JSON mode returns the raw session payload, with result fields at `.session.prUrl`, `.session.publishedBranch`, and `.session.lastBranch`.
|
|
180
|
+
|
|
179
181
|
### `arcanist sessions list`
|
|
180
182
|
|
|
181
183
|
```bash
|
|
@@ -324,6 +326,18 @@ arcanist sessions create your-org/your-repo "verify the new rate limiter is acti
|
|
|
324
326
|
|
|
325
327
|
For cron, prefer `ARCANIST_TOKEN` or a logged-in `~/.arcanist/config.json` over passing `--token` on the command line.
|
|
326
328
|
|
|
329
|
+
To capture the PR URL after a successful run, use `create --wait` for the exit code, then read the session state:
|
|
330
|
+
|
|
331
|
+
```bash
|
|
332
|
+
SESSION_ID=$(arcanist sessions create your-org/your-repo "fix the flaky test" --wait --json | jq -r .sessionId)
|
|
333
|
+
for _ in {1..20}; do
|
|
334
|
+
PR_URL=$(arcanist sessions get "$SESSION_ID" --json | jq -r '.session.prUrl // empty')
|
|
335
|
+
[ -n "$PR_URL" ] && break
|
|
336
|
+
sleep 5
|
|
337
|
+
done
|
|
338
|
+
[ -n "$PR_URL" ] || { echo "PR URL not ready for $SESSION_ID" >&2; exit 1; }
|
|
339
|
+
```
|
|
340
|
+
|
|
327
341
|
## Troubleshooting
|
|
328
342
|
|
|
329
343
|
- **401 on login verification**: token may be invalid or expired. Regenerate in **Settings > CLI Tokens**.
|
package/dist/index.js
CHANGED
|
@@ -1023,22 +1023,6 @@ function noChangeOutcomeCopy(reason) {
|
|
|
1023
1023
|
}
|
|
1024
1024
|
}
|
|
1025
1025
|
|
|
1026
|
-
// ../../shared/session/transient-disconnect.ts
|
|
1027
|
-
var TRANSIENT_DISCONNECT_VISIBILITY_MS = 15e3;
|
|
1028
|
-
function nextDisconnectMask(mask, previous, current, now) {
|
|
1029
|
-
if (!current || current.sandboxSubstate !== "reconnecting") return null;
|
|
1030
|
-
if (mask) return mask;
|
|
1031
|
-
if (!previous || previous.sandboxSubstate === "reconnecting") return null;
|
|
1032
|
-
return { since: now, heldPhase: previous.phase, heldSubstate: previous.sandboxSubstate ?? "none" };
|
|
1033
|
-
}
|
|
1034
|
-
function isDisconnectMaskActive(mask, now) {
|
|
1035
|
-
return mask !== null && now - mask.since < TRANSIENT_DISCONNECT_VISIBILITY_MS;
|
|
1036
|
-
}
|
|
1037
|
-
function applyDisconnectMask(view, mask, now) {
|
|
1038
|
-
if (!isDisconnectMaskActive(mask, now)) return view;
|
|
1039
|
-
return { ...view, phase: mask.heldPhase, sandboxSubstate: mask.heldSubstate };
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
1026
|
// ../../shared/transcript/malformed-search.ts
|
|
1043
1027
|
var MALFORMED_SEARCH_BLOCKED_TRANSCRIPT_PREFIX = "Arcanist blocked this malformed search command before execution.";
|
|
1044
1028
|
var BASH_UNMATCHED_QUOTE_EOF_RE = /\/bin\/bash:\s+-c:\s+line\s+\d+:\s+unexpected EOF while looking for matching [`'"][`'"]?/i;
|
|
@@ -2063,6 +2047,23 @@ ${event.answer ? `**Answer:** ${event.answer}
|
|
|
2063
2047
|
function formatNumber(value) {
|
|
2064
2048
|
return value.toLocaleString();
|
|
2065
2049
|
}
|
|
2050
|
+
function stringField(value) {
|
|
2051
|
+
return typeof value === "string" && value.trim() ? value : null;
|
|
2052
|
+
}
|
|
2053
|
+
function formatSessionResult(session) {
|
|
2054
|
+
const prUrl = stringField(session.prUrl);
|
|
2055
|
+
const publishedBranch = stringField(session.publishedBranch);
|
|
2056
|
+
const lastBranch = stringField(session.lastBranch);
|
|
2057
|
+
const baseBranch = stringField(session.baseBranch);
|
|
2058
|
+
const branch = publishedBranch ?? (lastBranch && lastBranch !== baseBranch ? lastBranch : null);
|
|
2059
|
+
if (prUrl) {
|
|
2060
|
+
return `PR: ${prUrl}${branch ? ` (${branch})` : ""}`;
|
|
2061
|
+
}
|
|
2062
|
+
if (branch) {
|
|
2063
|
+
return `Branch: ${branch}`;
|
|
2064
|
+
}
|
|
2065
|
+
return null;
|
|
2066
|
+
}
|
|
2066
2067
|
function latestCompletedPromptResult(prompts) {
|
|
2067
2068
|
for (let i = prompts.length - 1; i >= 0; i--) {
|
|
2068
2069
|
if (prompts[i].status === "completed") return prompts[i].result;
|
|
@@ -2314,6 +2315,22 @@ function renderWatchEvent(event, state) {
|
|
|
2314
2315
|
}
|
|
2315
2316
|
}
|
|
2316
2317
|
|
|
2318
|
+
// ../../shared/session/transient-disconnect.ts
|
|
2319
|
+
var TRANSIENT_DISCONNECT_VISIBILITY_MS = 15e3;
|
|
2320
|
+
function nextDisconnectMask(mask, previous, current, now) {
|
|
2321
|
+
if (!current || current.sandboxSubstate !== "reconnecting") return null;
|
|
2322
|
+
if (mask) return mask;
|
|
2323
|
+
if (!previous || previous.sandboxSubstate === "reconnecting") return null;
|
|
2324
|
+
return { since: now, heldPhase: previous.phase, heldSubstate: previous.sandboxSubstate ?? "none" };
|
|
2325
|
+
}
|
|
2326
|
+
function isDisconnectMaskActive(mask, now) {
|
|
2327
|
+
return mask !== null && now - mask.since < TRANSIENT_DISCONNECT_VISIBILITY_MS;
|
|
2328
|
+
}
|
|
2329
|
+
function applyDisconnectMask(view, mask, now) {
|
|
2330
|
+
if (!isDisconnectMaskActive(mask, now)) return view;
|
|
2331
|
+
return { ...view, phase: mask.heldPhase, sandboxSubstate: mask.heldSubstate };
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2317
2334
|
// src/commands/watch.ts
|
|
2318
2335
|
function parsePollInterval(raw) {
|
|
2319
2336
|
if (!raw) return DEFAULT_WATCH_POLL_INTERVAL_MS;
|
|
@@ -2621,6 +2638,15 @@ async function createCommand(repoUrl, promptArg, options, command) {
|
|
|
2621
2638
|
if (sessionData.sessionUrl) console.log(`URL: ${sessionData.sessionUrl}`);
|
|
2622
2639
|
}
|
|
2623
2640
|
await waitForCreatedPrompt(sessionId, promptId, sessionData.sessionUrl, waitPollIntervalMs, runtime, command);
|
|
2641
|
+
if (!isJson(command, options)) {
|
|
2642
|
+
try {
|
|
2643
|
+
const sessionPayload = await apiFetch(config, `/api/sessions/${sessionId}`);
|
|
2644
|
+
const sessionState = sessionPayload.session && typeof sessionPayload.session === "object" ? sessionPayload.session : sessionPayload;
|
|
2645
|
+
const resultLine = formatSessionResult(sessionState);
|
|
2646
|
+
if (resultLine) console.log(resultLine);
|
|
2647
|
+
} catch {
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2624
2650
|
}
|
|
2625
2651
|
if (isJson(command, options)) {
|
|
2626
2652
|
const output = { sessionId };
|
|
@@ -2640,6 +2666,155 @@ async function createCommand(repoUrl, promptArg, options, command) {
|
|
|
2640
2666
|
if (sessionData.sessionUrl) console.log(`URL: ${sessionData.sessionUrl}`);
|
|
2641
2667
|
}
|
|
2642
2668
|
|
|
2669
|
+
// src/commands/egress.ts
|
|
2670
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
2671
|
+
|
|
2672
|
+
// ../../shared/types/business-egress-policy.ts
|
|
2673
|
+
var MAX_BUSINESS_EGRESS_DOMAINS = 100;
|
|
2674
|
+
var MAX_DOMAIN_LENGTH = 253;
|
|
2675
|
+
function normalizeEgressDomains(entries, key = "egressAllowlist") {
|
|
2676
|
+
const unique = [...new Set(entries.map((entry) => entry.trim().toLowerCase()).filter(Boolean))].sort();
|
|
2677
|
+
if (unique.length > MAX_BUSINESS_EGRESS_DOMAINS) {
|
|
2678
|
+
throw new Error(`${key} must contain at most ${MAX_BUSINESS_EGRESS_DOMAINS} domain names`);
|
|
2679
|
+
}
|
|
2680
|
+
for (const entry of unique) {
|
|
2681
|
+
assertValidEgressDomain(entry, key);
|
|
2682
|
+
}
|
|
2683
|
+
return unique;
|
|
2684
|
+
}
|
|
2685
|
+
function assertValidEgressDomain(value, key = "egressAllowlist") {
|
|
2686
|
+
if (value.includes("*") || value.startsWith(".") || value.endsWith(".") || value.includes("/") || value.includes(":")) {
|
|
2687
|
+
throw new Error(`${key} entries must be exact domain names`);
|
|
2688
|
+
}
|
|
2689
|
+
if (value.length > MAX_DOMAIN_LENGTH) {
|
|
2690
|
+
throw new Error(`${key} entries must be exact domain names`);
|
|
2691
|
+
}
|
|
2692
|
+
const labels = value.split(".");
|
|
2693
|
+
if (labels.length === 4 && labels.every((label) => /^\d+$/.test(label) && Number(label) >= 0 && Number(label) <= 255)) {
|
|
2694
|
+
throw new Error(`${key} entries must be exact domain names`);
|
|
2695
|
+
}
|
|
2696
|
+
if (labels.length < 2 || labels.some(
|
|
2697
|
+
(label) => label.length < 1 || label.length > 63 || !/^[a-z0-9-]+$/.test(label) || label.startsWith("-") || label.endsWith("-")
|
|
2698
|
+
)) {
|
|
2699
|
+
throw new Error(`${key} entries must be exact domain names`);
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
// ../../shared/egress-allowlist/parser.ts
|
|
2704
|
+
var EGRESS_ALLOWLIST_SOURCE_PATH = ".arcanist/egress-allowlist.txt";
|
|
2705
|
+
function parseEgressAllowlistSourceFile(content, key = EGRESS_ALLOWLIST_SOURCE_PATH) {
|
|
2706
|
+
const domains = content.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
2707
|
+
return normalizeEgressDomains(domains, key);
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
// src/commands/egress.ts
|
|
2711
|
+
function parseRepoArg(value) {
|
|
2712
|
+
const trimmed = value.trim().replace(/^https:\/\/github\.com\//, "").replace(/\.git$/, "");
|
|
2713
|
+
const [owner, repo, extra] = trimmed.split("/");
|
|
2714
|
+
if (!owner || !repo || extra) throw new CliError("user", "repo must be owner/name or a GitHub URL");
|
|
2715
|
+
return { owner, repo };
|
|
2716
|
+
}
|
|
2717
|
+
function encodePathSegment(value) {
|
|
2718
|
+
return encodeURIComponent(value);
|
|
2719
|
+
}
|
|
2720
|
+
async function resolveBusinessId(config, options) {
|
|
2721
|
+
if (options.business && options.business.trim()) return options.business.trim();
|
|
2722
|
+
const whoami = await apiFetch(config, "/api/auth/whoami");
|
|
2723
|
+
if (whoami.businessId && whoami.businessId.trim()) return whoami.businessId;
|
|
2724
|
+
throw new CliError("user", "--business is required when the authenticated token has no business context.");
|
|
2725
|
+
}
|
|
2726
|
+
async function egressSourceSetCommand(sourceRepoArg, options = {}, command) {
|
|
2727
|
+
const runtime = getRuntimeOptions(command, options);
|
|
2728
|
+
const config = requireConfig(runtime);
|
|
2729
|
+
const businessId = await resolveBusinessId(config, options);
|
|
2730
|
+
const source = parseRepoArg(sourceRepoArg);
|
|
2731
|
+
const payload = await apiFetch(
|
|
2732
|
+
config,
|
|
2733
|
+
`/api/businesses/${encodePathSegment(businessId)}/egress-allowlist/source`,
|
|
2734
|
+
{
|
|
2735
|
+
method: "PUT",
|
|
2736
|
+
body: JSON.stringify({ sourceRepoOwner: source.owner, sourceRepoName: source.repo })
|
|
2737
|
+
}
|
|
2738
|
+
);
|
|
2739
|
+
if (isJson(command, options)) {
|
|
2740
|
+
writeJson(payload);
|
|
2741
|
+
return;
|
|
2742
|
+
}
|
|
2743
|
+
console.log(`Set egress allowlist source to ${source.owner}/${source.repo}:${EGRESS_ALLOWLIST_SOURCE_PATH}.`);
|
|
2744
|
+
}
|
|
2745
|
+
async function egressSourceGetCommand(options = {}, command) {
|
|
2746
|
+
const runtime = getRuntimeOptions(command, options);
|
|
2747
|
+
const config = requireConfig(runtime);
|
|
2748
|
+
const businessId = await resolveBusinessId(config, options);
|
|
2749
|
+
const payload = await apiFetch(
|
|
2750
|
+
config,
|
|
2751
|
+
`/api/businesses/${encodePathSegment(businessId)}/egress-allowlist/source`
|
|
2752
|
+
);
|
|
2753
|
+
if (isJson(command, options)) {
|
|
2754
|
+
writeJson(payload);
|
|
2755
|
+
return;
|
|
2756
|
+
}
|
|
2757
|
+
if (!payload.source) {
|
|
2758
|
+
console.log("No egress allowlist source configured.");
|
|
2759
|
+
return;
|
|
2760
|
+
}
|
|
2761
|
+
console.log(
|
|
2762
|
+
`${payload.source.sourceRepoOwner}/${payload.source.sourceRepoName}:${payload.path} (${payload.defaultBranch ?? "default branch"})`
|
|
2763
|
+
);
|
|
2764
|
+
console.log(`${payload.domains.length} ${payload.domains.length === 1 ? "domain" : "domains"}.`);
|
|
2765
|
+
}
|
|
2766
|
+
async function egressAddCommand(domains, options = {}, command) {
|
|
2767
|
+
const runtime = getRuntimeOptions(command, options);
|
|
2768
|
+
const config = requireConfig(runtime);
|
|
2769
|
+
const businessId = await resolveBusinessId(config, options);
|
|
2770
|
+
const normalized = normalizeEgressDomains(domains, "domains");
|
|
2771
|
+
const payload = await apiFetch(
|
|
2772
|
+
config,
|
|
2773
|
+
`/api/businesses/${encodePathSegment(businessId)}/egress-allowlist/pull-request`,
|
|
2774
|
+
{
|
|
2775
|
+
method: "POST",
|
|
2776
|
+
body: JSON.stringify({ domains: normalized, ...options.reason ? { reason: options.reason } : {} })
|
|
2777
|
+
}
|
|
2778
|
+
);
|
|
2779
|
+
if (isJson(command, options)) {
|
|
2780
|
+
writeJson(payload);
|
|
2781
|
+
return;
|
|
2782
|
+
}
|
|
2783
|
+
if (payload.status === "unchanged") {
|
|
2784
|
+
console.log("All requested domains are already present in the source file.");
|
|
2785
|
+
return;
|
|
2786
|
+
}
|
|
2787
|
+
console.log(`Egress allowlist PR ${payload.status}: ${payload.prUrl}`);
|
|
2788
|
+
console.log(`Added: ${payload.addedDomains.join(", ")}`);
|
|
2789
|
+
}
|
|
2790
|
+
async function egressSyncCommand(options = {}, command) {
|
|
2791
|
+
const runtime = getRuntimeOptions(command, options);
|
|
2792
|
+
const config = requireConfig(runtime);
|
|
2793
|
+
const businessId = await resolveBusinessId(config, options);
|
|
2794
|
+
const payload = await apiFetch(
|
|
2795
|
+
config,
|
|
2796
|
+
`/api/businesses/${encodePathSegment(businessId)}/egress-allowlist/sync`,
|
|
2797
|
+
{ method: "POST" }
|
|
2798
|
+
);
|
|
2799
|
+
if (isJson(command, options)) {
|
|
2800
|
+
writeJson(payload);
|
|
2801
|
+
return;
|
|
2802
|
+
}
|
|
2803
|
+
console.log(
|
|
2804
|
+
`Applied ${payload.egressAllowlist.length} egress allowlist domains from ${payload.source.sourceRepoOwner}/${payload.source.sourceRepoName}:${payload.path}.`
|
|
2805
|
+
);
|
|
2806
|
+
}
|
|
2807
|
+
async function egressValidateCommand(path = EGRESS_ALLOWLIST_SOURCE_PATH, options = {}, command) {
|
|
2808
|
+
const content = readFileSync2(path, "utf8");
|
|
2809
|
+
const domains = parseEgressAllowlistSourceFile(content, path);
|
|
2810
|
+
if (isJson(command, options)) {
|
|
2811
|
+
writeJson({ ok: true, path, domains });
|
|
2812
|
+
return;
|
|
2813
|
+
}
|
|
2814
|
+
console.log(`Valid egress allowlist: ${path}`);
|
|
2815
|
+
console.log(`${domains.length} ${domains.length === 1 ? "domain" : "domains"}.`);
|
|
2816
|
+
}
|
|
2817
|
+
|
|
2643
2818
|
// src/commands/login.ts
|
|
2644
2819
|
async function loginCommand(options, command) {
|
|
2645
2820
|
const runtime = getRuntimeOptions(command, options);
|
|
@@ -2709,7 +2884,7 @@ async function messageCommand(sessionId, promptArg, options = {}, command) {
|
|
|
2709
2884
|
|
|
2710
2885
|
// src/commands/sandbox.ts
|
|
2711
2886
|
import { execFileSync } from "child_process";
|
|
2712
|
-
import { existsSync, mkdirSync as mkdirSync2, readFileSync as
|
|
2887
|
+
import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
2713
2888
|
import { dirname } from "path";
|
|
2714
2889
|
|
|
2715
2890
|
// ../../shared/sandbox-layer/parser.ts
|
|
@@ -3270,7 +3445,7 @@ function assertCleanAndPushed() {
|
|
|
3270
3445
|
function assertManifestExists(path) {
|
|
3271
3446
|
if (!existsSync(path)) throw new CliError("user", `Missing sandbox manifest: ${path}`);
|
|
3272
3447
|
}
|
|
3273
|
-
function
|
|
3448
|
+
function parseRepoArg2(value, fallback) {
|
|
3274
3449
|
if (!value) return fallback();
|
|
3275
3450
|
const parsed = parseGithubRepoFullName(value);
|
|
3276
3451
|
if (!parsed) throw new CliError("user", "repo must be owner/name or a GitHub URL");
|
|
@@ -3279,10 +3454,10 @@ function parseRepoArg(value, fallback) {
|
|
|
3279
3454
|
function repoPath(repo) {
|
|
3280
3455
|
return `${repo.owner}/${repo.repo}`;
|
|
3281
3456
|
}
|
|
3282
|
-
function
|
|
3457
|
+
function encodePathSegment2(value) {
|
|
3283
3458
|
return encodeURIComponent(value);
|
|
3284
3459
|
}
|
|
3285
|
-
async function
|
|
3460
|
+
async function resolveBusinessId2(config, options) {
|
|
3286
3461
|
if (options.business && options.business.trim()) return options.business.trim();
|
|
3287
3462
|
const whoami = await apiFetch(config, "/api/auth/whoami");
|
|
3288
3463
|
if (whoami.businessId && whoami.businessId.trim()) return whoami.businessId;
|
|
@@ -3421,7 +3596,7 @@ function buildLogsPath(businessId, buildId, options = {}) {
|
|
|
3421
3596
|
if (options.afterSequence != null) params.set("afterSequence", String(options.afterSequence));
|
|
3422
3597
|
if (options.limit != null) params.set("limit", String(options.limit));
|
|
3423
3598
|
const suffix = params.toString() ? `?${params.toString()}` : "";
|
|
3424
|
-
return `/api/businesses/${
|
|
3599
|
+
return `/api/businesses/${encodePathSegment2(businessId)}/sandbox-layer/build-requests/${encodePathSegment2(
|
|
3425
3600
|
buildId
|
|
3426
3601
|
)}/logs${suffix}`;
|
|
3427
3602
|
}
|
|
@@ -3438,7 +3613,7 @@ async function waitForBuild(config, businessId, buildId, options) {
|
|
|
3438
3613
|
}
|
|
3439
3614
|
const status = await apiFetch(
|
|
3440
3615
|
config,
|
|
3441
|
-
`/api/businesses/${
|
|
3616
|
+
`/api/businesses/${encodePathSegment2(businessId)}/sandbox-layer/build-requests/${encodePathSegment2(buildId)}`
|
|
3442
3617
|
);
|
|
3443
3618
|
if (TERMINAL_BUILD_STATUSES.has(status.buildRequest.status)) {
|
|
3444
3619
|
return status.buildRequest;
|
|
@@ -3527,12 +3702,12 @@ async function sandboxInitCommand(options = {}, command) {
|
|
|
3527
3702
|
async function sandboxValidateCommand(options = {}, command) {
|
|
3528
3703
|
const manifestPath = options.manifest ?? DEFAULT_MANIFEST_PATH;
|
|
3529
3704
|
assertManifestExists(manifestPath);
|
|
3530
|
-
const manifestText =
|
|
3705
|
+
const manifestText = readFileSync3(manifestPath, "utf8");
|
|
3531
3706
|
let layerPath;
|
|
3532
3707
|
try {
|
|
3533
3708
|
layerPath = parseSandboxLayerManifest(manifestPath, manifestText).layer.dockerfile;
|
|
3534
3709
|
assertManifestExists(layerPath);
|
|
3535
|
-
const layerText =
|
|
3710
|
+
const layerText = readFileSync3(layerPath, "utf8");
|
|
3536
3711
|
const parsed = await parseSandboxLayerSource({ manifestPath, manifestText, layerPath, layerText });
|
|
3537
3712
|
const payload = formatValidationPayload({ manifestPath, layerPath, parsed });
|
|
3538
3713
|
if (isJson(command, options)) {
|
|
@@ -3556,13 +3731,13 @@ async function sandboxBuildCommand(sourceRepoArg, options = {}, command) {
|
|
|
3556
3731
|
const manifestPath = options.manifest ?? DEFAULT_MANIFEST_PATH;
|
|
3557
3732
|
assertManifestExists(manifestPath);
|
|
3558
3733
|
if (!options.ref) assertCleanAndPushed();
|
|
3559
|
-
const sourceRepo =
|
|
3560
|
-
const targetRepo = options.targetRepo ?
|
|
3561
|
-
const businessId = await
|
|
3734
|
+
const sourceRepo = parseRepoArg2(sourceRepoArg, currentRepo);
|
|
3735
|
+
const targetRepo = options.targetRepo ? parseRepoArg2(options.targetRepo, currentRepo) : null;
|
|
3736
|
+
const businessId = await resolveBusinessId2(config, options);
|
|
3562
3737
|
const ref = options.ref ?? currentRef();
|
|
3563
3738
|
const payload = await apiFetch(
|
|
3564
3739
|
config,
|
|
3565
|
-
`/api/businesses/${
|
|
3740
|
+
`/api/businesses/${encodePathSegment2(businessId)}/repos/${encodePathSegment2(sourceRepo.owner)}/${encodePathSegment2(
|
|
3566
3741
|
sourceRepo.repo
|
|
3567
3742
|
)}/sandbox-layer/build-requests`,
|
|
3568
3743
|
{
|
|
@@ -3592,11 +3767,11 @@ async function sandboxBuildCommand(sourceRepoArg, options = {}, command) {
|
|
|
3592
3767
|
async function sandboxStatusCommand(repoArg, options = {}, command) {
|
|
3593
3768
|
const runtime = getRuntimeOptions(command, options);
|
|
3594
3769
|
const config = requireConfig(runtime);
|
|
3595
|
-
const businessId = await
|
|
3596
|
-
const repo =
|
|
3770
|
+
const businessId = await resolveBusinessId2(config, options);
|
|
3771
|
+
const repo = parseRepoArg2(repoArg, currentRepo);
|
|
3597
3772
|
const payload = await apiFetch(
|
|
3598
3773
|
config,
|
|
3599
|
-
`/api/businesses/${
|
|
3774
|
+
`/api/businesses/${encodePathSegment2(businessId)}/repos/${encodePathSegment2(repo.owner)}/${encodePathSegment2(
|
|
3600
3775
|
repo.repo
|
|
3601
3776
|
)}/sandbox-layer/resolution`
|
|
3602
3777
|
);
|
|
@@ -3609,10 +3784,10 @@ async function sandboxStatusCommand(repoArg, options = {}, command) {
|
|
|
3609
3784
|
async function sandboxHistoryCommand(sourceRepoArg, options = {}, command) {
|
|
3610
3785
|
const runtime = getRuntimeOptions(command, options);
|
|
3611
3786
|
const config = requireConfig(runtime);
|
|
3612
|
-
const businessId = await
|
|
3613
|
-
const sourceRepo =
|
|
3787
|
+
const businessId = await resolveBusinessId2(config, options);
|
|
3788
|
+
const sourceRepo = parseRepoArg2(sourceRepoArg, currentRepo);
|
|
3614
3789
|
const params = new URLSearchParams({ sourceRepo: repoPath(sourceRepo) });
|
|
3615
|
-
if (options.targetRepo) params.set("targetRepo", repoPath(
|
|
3790
|
+
if (options.targetRepo) params.set("targetRepo", repoPath(parseRepoArg2(options.targetRepo, currentRepo)));
|
|
3616
3791
|
if (options.status) params.set("status", options.status);
|
|
3617
3792
|
if (options.limit) {
|
|
3618
3793
|
const limit = Number(options.limit);
|
|
@@ -3623,7 +3798,7 @@ async function sandboxHistoryCommand(sourceRepoArg, options = {}, command) {
|
|
|
3623
3798
|
}
|
|
3624
3799
|
const payload = await apiFetch(
|
|
3625
3800
|
config,
|
|
3626
|
-
`/api/businesses/${
|
|
3801
|
+
`/api/businesses/${encodePathSegment2(businessId)}/sandbox-layer/build-requests?${params.toString()}`
|
|
3627
3802
|
);
|
|
3628
3803
|
if (isJson(command, options)) {
|
|
3629
3804
|
writeJson(payload);
|
|
@@ -3640,7 +3815,7 @@ async function sandboxRebuildStaleCommand(options = {}, command) {
|
|
|
3640
3815
|
if (options.all && options.business) {
|
|
3641
3816
|
throw new CliError("user", "Use either --all or --business, not both.");
|
|
3642
3817
|
}
|
|
3643
|
-
const businessId = options.all ? null : await
|
|
3818
|
+
const businessId = options.all ? null : await resolveBusinessId2(config, options);
|
|
3644
3819
|
let payload = await apiFetch(config, "/api/admin/sandbox-layer/rebuild-campaigns", {
|
|
3645
3820
|
method: "POST",
|
|
3646
3821
|
body: JSON.stringify({
|
|
@@ -3668,7 +3843,7 @@ async function followRebuildCampaign(config, campaignId) {
|
|
|
3668
3843
|
for (; ; ) {
|
|
3669
3844
|
const payload = await apiFetch(
|
|
3670
3845
|
config,
|
|
3671
|
-
`/api/admin/sandbox-layer/rebuild-campaigns/${
|
|
3846
|
+
`/api/admin/sandbox-layer/rebuild-campaigns/${encodePathSegment2(campaignId)}`
|
|
3672
3847
|
);
|
|
3673
3848
|
if (isCampaignTerminal(payload.campaign.status)) return payload;
|
|
3674
3849
|
await sleep2(DEFAULT_POLL_INTERVAL_MS);
|
|
@@ -3697,7 +3872,7 @@ function printRebuildCampaign(payload) {
|
|
|
3697
3872
|
async function sandboxLogsCommand(buildId, options = {}, command) {
|
|
3698
3873
|
const runtime = getRuntimeOptions(command, options);
|
|
3699
3874
|
const config = requireConfig(runtime);
|
|
3700
|
-
const businessId = await
|
|
3875
|
+
const businessId = await resolveBusinessId2(config, options);
|
|
3701
3876
|
let afterSequence = options.afterSequence == null ? void 0 : Number(options.afterSequence);
|
|
3702
3877
|
if (afterSequence != null && (!Number.isInteger(afterSequence) || afterSequence < 0)) {
|
|
3703
3878
|
throw new CliError("user", "--after-sequence must be a non-negative integer.");
|
|
@@ -3721,12 +3896,12 @@ async function sandboxLogsCommand(buildId, options = {}, command) {
|
|
|
3721
3896
|
async function sandboxAssignDefaultCommand(sourceRepoArg, options = {}, command) {
|
|
3722
3897
|
const runtime = getRuntimeOptions(command, options);
|
|
3723
3898
|
const config = requireConfig(runtime);
|
|
3724
|
-
const businessId = await
|
|
3725
|
-
const sourceRepo =
|
|
3899
|
+
const businessId = await resolveBusinessId2(config, options);
|
|
3900
|
+
const sourceRepo = parseRepoArg2(sourceRepoArg, currentRepo);
|
|
3726
3901
|
const manifestPath = options.manifest ?? DEFAULT_MANIFEST_PATH;
|
|
3727
3902
|
const payload = await apiFetch(
|
|
3728
3903
|
config,
|
|
3729
|
-
`/api/businesses/${
|
|
3904
|
+
`/api/businesses/${encodePathSegment2(businessId)}/sandbox-layer/default-source`,
|
|
3730
3905
|
{ method: "PUT", body: JSON.stringify(sourceBody(sourceRepo, manifestPath)) }
|
|
3731
3906
|
);
|
|
3732
3907
|
if (isJson(command, options)) {
|
|
@@ -3741,13 +3916,13 @@ async function sandboxAssignDefaultCommand(sourceRepoArg, options = {}, command)
|
|
|
3741
3916
|
async function sandboxAssignRepoCommand(targetRepoArg, sourceRepoArg, options = {}, command) {
|
|
3742
3917
|
const runtime = getRuntimeOptions(command, options);
|
|
3743
3918
|
const config = requireConfig(runtime);
|
|
3744
|
-
const businessId = await
|
|
3745
|
-
const targetRepo =
|
|
3746
|
-
const sourceRepo =
|
|
3919
|
+
const businessId = await resolveBusinessId2(config, options);
|
|
3920
|
+
const targetRepo = parseRepoArg2(targetRepoArg, currentRepo);
|
|
3921
|
+
const sourceRepo = parseRepoArg2(sourceRepoArg, currentRepo);
|
|
3747
3922
|
const manifestPath = options.manifest ?? DEFAULT_MANIFEST_PATH;
|
|
3748
3923
|
const payload = await apiFetch(
|
|
3749
3924
|
config,
|
|
3750
|
-
`/api/businesses/${
|
|
3925
|
+
`/api/businesses/${encodePathSegment2(businessId)}/repos/${encodePathSegment2(targetRepo.owner)}/${encodePathSegment2(
|
|
3751
3926
|
targetRepo.repo
|
|
3752
3927
|
)}/sandbox-layer/assignment`,
|
|
3753
3928
|
{ method: "PUT", body: JSON.stringify(sourceBody(sourceRepo, manifestPath)) }
|
|
@@ -3766,11 +3941,11 @@ async function sandboxAssignRepoCommand(targetRepoArg, sourceRepoArg, options =
|
|
|
3766
3941
|
async function sandboxUnassignDefaultCommand(options = {}, command) {
|
|
3767
3942
|
const runtime = getRuntimeOptions(command, options);
|
|
3768
3943
|
const config = requireConfig(runtime);
|
|
3769
|
-
const businessId = await
|
|
3944
|
+
const businessId = await resolveBusinessId2(config, options);
|
|
3770
3945
|
if (!options.yes) await confirmOrThrow("Clear the business default sandbox layer source?");
|
|
3771
3946
|
const payload = await apiFetch(
|
|
3772
3947
|
config,
|
|
3773
|
-
`/api/businesses/${
|
|
3948
|
+
`/api/businesses/${encodePathSegment2(businessId)}/sandbox-layer/default-source`,
|
|
3774
3949
|
{ method: "DELETE" }
|
|
3775
3950
|
);
|
|
3776
3951
|
if (isJson(command, options)) {
|
|
@@ -3782,12 +3957,12 @@ async function sandboxUnassignDefaultCommand(options = {}, command) {
|
|
|
3782
3957
|
async function sandboxUnassignRepoCommand(targetRepoArg, options = {}, command) {
|
|
3783
3958
|
const runtime = getRuntimeOptions(command, options);
|
|
3784
3959
|
const config = requireConfig(runtime);
|
|
3785
|
-
const businessId = await
|
|
3786
|
-
const targetRepo =
|
|
3960
|
+
const businessId = await resolveBusinessId2(config, options);
|
|
3961
|
+
const targetRepo = parseRepoArg2(targetRepoArg, currentRepo);
|
|
3787
3962
|
if (!options.yes) await confirmOrThrow(`Clear sandbox layer assignment for ${repoPath(targetRepo)}?`);
|
|
3788
3963
|
const payload = await apiFetch(
|
|
3789
3964
|
config,
|
|
3790
|
-
`/api/businesses/${
|
|
3965
|
+
`/api/businesses/${encodePathSegment2(businessId)}/repos/${encodePathSegment2(targetRepo.owner)}/${encodePathSegment2(
|
|
3791
3966
|
targetRepo.repo
|
|
3792
3967
|
)}/sandbox-layer/assignment`,
|
|
3793
3968
|
{ method: "DELETE" }
|
|
@@ -3846,6 +4021,8 @@ async function getSessionCommand(sessionId, options, command) {
|
|
|
3846
4021
|
console.log(`Status: ${String(session.phase ?? session.status ?? "unknown")}`);
|
|
3847
4022
|
if (session.repoUrl) console.log(`Repo: ${String(session.repoUrl)}`);
|
|
3848
4023
|
if (session.title) console.log(`Title: ${String(session.title)}`);
|
|
4024
|
+
const resultLine = formatSessionResult(session);
|
|
4025
|
+
if (resultLine) console.log(resultLine);
|
|
3849
4026
|
}
|
|
3850
4027
|
async function sessionEventsCommand(sessionId, options, command) {
|
|
3851
4028
|
const afterSequence = resolveSequenceOption(options.afterSequence, options.after, "--after-sequence", "--after");
|
|
@@ -3969,8 +4146,8 @@ function parseStopBlocked(err) {
|
|
|
3969
4146
|
}
|
|
3970
4147
|
|
|
3971
4148
|
// src/commands/test-creds.ts
|
|
3972
|
-
import { readFileSync as
|
|
3973
|
-
function
|
|
4149
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
4150
|
+
function parseRepoArg3(repo) {
|
|
3974
4151
|
const trimmed = repo.trim();
|
|
3975
4152
|
const slashIndex = trimmed.indexOf("/");
|
|
3976
4153
|
if (slashIndex <= 0 || slashIndex === trimmed.length - 1) {
|
|
@@ -3987,13 +4164,13 @@ function readValue(options) {
|
|
|
3987
4164
|
throw new CliError("user", "Provide exactly one of --value, --value-file, or --value-stdin.");
|
|
3988
4165
|
}
|
|
3989
4166
|
if (options.value !== void 0) return options.value;
|
|
3990
|
-
const raw = options.valueFile ?
|
|
4167
|
+
const raw = options.valueFile ? readFileSync4(options.valueFile, "utf8") : readFileSync4(0, "utf8");
|
|
3991
4168
|
return raw.replace(/\r?\n$/, "");
|
|
3992
4169
|
}
|
|
3993
4170
|
async function listTestCredentialsCommand(repo, options, command) {
|
|
3994
4171
|
const runtime = getRuntimeOptions(command, options);
|
|
3995
4172
|
const config = requireConfig(runtime);
|
|
3996
|
-
const repoArg =
|
|
4173
|
+
const repoArg = parseRepoArg3(repo);
|
|
3997
4174
|
const payload = await apiFetch(config, basePath(options.business, repoArg));
|
|
3998
4175
|
if (isJson(command, options)) {
|
|
3999
4176
|
writeJson(payload);
|
|
@@ -4011,7 +4188,7 @@ async function listTestCredentialsCommand(repo, options, command) {
|
|
|
4011
4188
|
async function setTestCredentialCommand(repo, name, options, command) {
|
|
4012
4189
|
const runtime = getRuntimeOptions(command, options);
|
|
4013
4190
|
const config = requireConfig(runtime);
|
|
4014
|
-
const repoArg =
|
|
4191
|
+
const repoArg = parseRepoArg3(repo);
|
|
4015
4192
|
const value = readValue(options);
|
|
4016
4193
|
const payload = await apiFetch(
|
|
4017
4194
|
config,
|
|
@@ -4029,12 +4206,12 @@ async function deleteTestCredentialCommand(repo, name, options, command) {
|
|
|
4029
4206
|
throw new CliError("user", "`test-creds delete --json` requires --yes.");
|
|
4030
4207
|
}
|
|
4031
4208
|
if (options.yes !== true) {
|
|
4032
|
-
const repoArg2 =
|
|
4209
|
+
const repoArg2 = parseRepoArg3(repo);
|
|
4033
4210
|
await confirmOrThrow(`Delete test credential '${name}' for ${repoArg2.owner}/${repoArg2.name}?`);
|
|
4034
4211
|
}
|
|
4035
4212
|
const runtime = getRuntimeOptions(command, options);
|
|
4036
4213
|
const config = requireConfig(runtime);
|
|
4037
|
-
const repoArg =
|
|
4214
|
+
const repoArg = parseRepoArg3(repo);
|
|
4038
4215
|
const payload = await apiFetch(
|
|
4039
4216
|
config,
|
|
4040
4217
|
`${basePath(options.business, repoArg)}/${encodeURIComponent(name)}`,
|
|
@@ -4321,6 +4498,13 @@ sandboxAssign.command("repo").description("Assign a sandbox layer source to one
|
|
|
4321
4498
|
var sandboxUnassign = sandbox.command("unassign").description("Remove sandbox layer source assignments");
|
|
4322
4499
|
sandboxUnassign.command("default").description("Clear the business default sandbox layer source").option("--business <id>", "Business ID; defaults to authenticated user's business").option("--yes", "Skip confirmation").action((options, command) => sandboxUnassignDefaultCommand(options, command));
|
|
4323
4500
|
sandboxUnassign.command("repo").description("Clear a target repo sandbox layer assignment").argument("<target-repo>", "Target repository owner/name").option("--business <id>", "Business ID; defaults to authenticated user's business").option("--yes", "Skip confirmation").action((targetRepo, options, command) => sandboxUnassignRepoCommand(targetRepo, options, command));
|
|
4501
|
+
var egress = program.command("egress").description("Workspace egress allowlist commands");
|
|
4502
|
+
var egressSource = egress.command("source").description("Manage the workspace egress allowlist source repo");
|
|
4503
|
+
egressSource.command("set").description("Set the source repo for .arcanist/egress-allowlist.txt").argument("<source-repo>", "Source repository owner/name").option("--business <id>", "Business ID; defaults to authenticated user's business").action((sourceRepo, options, command) => egressSourceSetCommand(sourceRepo, options, command));
|
|
4504
|
+
egressSource.command("get").description("Show the configured egress allowlist source").option("--business <id>", "Business ID; defaults to authenticated user's business").action((options, command) => egressSourceGetCommand(options, command));
|
|
4505
|
+
egress.command("add").description("Open or update a PR adding domains to the egress allowlist source file").argument("<domains...>", "Exact domain names to add").option("--business <id>", "Business ID; defaults to authenticated user's business").option("--reason <text>", "Reason to include in the PR body").action((domains, options, command) => egressAddCommand(domains, options, command));
|
|
4506
|
+
egress.command("sync").description("Apply the merged egress allowlist source file to the runtime business policy").option("--business <id>", "Business ID; defaults to authenticated user's business").action((options, command) => egressSyncCommand(options, command));
|
|
4507
|
+
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));
|
|
4324
4508
|
var tokens = program.command("tokens").description("CLI token commands");
|
|
4325
4509
|
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));
|
|
4326
4510
|
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(
|