@tryarcanist/cli 0.1.133 → 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.
Files changed (2) hide show
  1. package/dist/index.js +197 -41
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2666,6 +2666,155 @@ async function createCommand(repoUrl, promptArg, options, command) {
2666
2666
  if (sessionData.sessionUrl) console.log(`URL: ${sessionData.sessionUrl}`);
2667
2667
  }
2668
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
+
2669
2818
  // src/commands/login.ts
2670
2819
  async function loginCommand(options, command) {
2671
2820
  const runtime = getRuntimeOptions(command, options);
@@ -2735,7 +2884,7 @@ async function messageCommand(sessionId, promptArg, options = {}, command) {
2735
2884
 
2736
2885
  // src/commands/sandbox.ts
2737
2886
  import { execFileSync } from "child_process";
2738
- import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
2887
+ import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
2739
2888
  import { dirname } from "path";
2740
2889
 
2741
2890
  // ../../shared/sandbox-layer/parser.ts
@@ -3296,7 +3445,7 @@ function assertCleanAndPushed() {
3296
3445
  function assertManifestExists(path) {
3297
3446
  if (!existsSync(path)) throw new CliError("user", `Missing sandbox manifest: ${path}`);
3298
3447
  }
3299
- function parseRepoArg(value, fallback) {
3448
+ function parseRepoArg2(value, fallback) {
3300
3449
  if (!value) return fallback();
3301
3450
  const parsed = parseGithubRepoFullName(value);
3302
3451
  if (!parsed) throw new CliError("user", "repo must be owner/name or a GitHub URL");
@@ -3305,10 +3454,10 @@ function parseRepoArg(value, fallback) {
3305
3454
  function repoPath(repo) {
3306
3455
  return `${repo.owner}/${repo.repo}`;
3307
3456
  }
3308
- function encodePathSegment(value) {
3457
+ function encodePathSegment2(value) {
3309
3458
  return encodeURIComponent(value);
3310
3459
  }
3311
- async function resolveBusinessId(config, options) {
3460
+ async function resolveBusinessId2(config, options) {
3312
3461
  if (options.business && options.business.trim()) return options.business.trim();
3313
3462
  const whoami = await apiFetch(config, "/api/auth/whoami");
3314
3463
  if (whoami.businessId && whoami.businessId.trim()) return whoami.businessId;
@@ -3447,7 +3596,7 @@ function buildLogsPath(businessId, buildId, options = {}) {
3447
3596
  if (options.afterSequence != null) params.set("afterSequence", String(options.afterSequence));
3448
3597
  if (options.limit != null) params.set("limit", String(options.limit));
3449
3598
  const suffix = params.toString() ? `?${params.toString()}` : "";
3450
- return `/api/businesses/${encodePathSegment(businessId)}/sandbox-layer/build-requests/${encodePathSegment(
3599
+ return `/api/businesses/${encodePathSegment2(businessId)}/sandbox-layer/build-requests/${encodePathSegment2(
3451
3600
  buildId
3452
3601
  )}/logs${suffix}`;
3453
3602
  }
@@ -3464,7 +3613,7 @@ async function waitForBuild(config, businessId, buildId, options) {
3464
3613
  }
3465
3614
  const status = await apiFetch(
3466
3615
  config,
3467
- `/api/businesses/${encodePathSegment(businessId)}/sandbox-layer/build-requests/${encodePathSegment(buildId)}`
3616
+ `/api/businesses/${encodePathSegment2(businessId)}/sandbox-layer/build-requests/${encodePathSegment2(buildId)}`
3468
3617
  );
3469
3618
  if (TERMINAL_BUILD_STATUSES.has(status.buildRequest.status)) {
3470
3619
  return status.buildRequest;
@@ -3553,12 +3702,12 @@ async function sandboxInitCommand(options = {}, command) {
3553
3702
  async function sandboxValidateCommand(options = {}, command) {
3554
3703
  const manifestPath = options.manifest ?? DEFAULT_MANIFEST_PATH;
3555
3704
  assertManifestExists(manifestPath);
3556
- const manifestText = readFileSync2(manifestPath, "utf8");
3705
+ const manifestText = readFileSync3(manifestPath, "utf8");
3557
3706
  let layerPath;
3558
3707
  try {
3559
3708
  layerPath = parseSandboxLayerManifest(manifestPath, manifestText).layer.dockerfile;
3560
3709
  assertManifestExists(layerPath);
3561
- const layerText = readFileSync2(layerPath, "utf8");
3710
+ const layerText = readFileSync3(layerPath, "utf8");
3562
3711
  const parsed = await parseSandboxLayerSource({ manifestPath, manifestText, layerPath, layerText });
3563
3712
  const payload = formatValidationPayload({ manifestPath, layerPath, parsed });
3564
3713
  if (isJson(command, options)) {
@@ -3582,13 +3731,13 @@ async function sandboxBuildCommand(sourceRepoArg, options = {}, command) {
3582
3731
  const manifestPath = options.manifest ?? DEFAULT_MANIFEST_PATH;
3583
3732
  assertManifestExists(manifestPath);
3584
3733
  if (!options.ref) assertCleanAndPushed();
3585
- const sourceRepo = parseRepoArg(sourceRepoArg, currentRepo);
3586
- const targetRepo = options.targetRepo ? parseRepoArg(options.targetRepo, currentRepo) : null;
3587
- const businessId = await resolveBusinessId(config, options);
3734
+ const sourceRepo = parseRepoArg2(sourceRepoArg, currentRepo);
3735
+ const targetRepo = options.targetRepo ? parseRepoArg2(options.targetRepo, currentRepo) : null;
3736
+ const businessId = await resolveBusinessId2(config, options);
3588
3737
  const ref = options.ref ?? currentRef();
3589
3738
  const payload = await apiFetch(
3590
3739
  config,
3591
- `/api/businesses/${encodePathSegment(businessId)}/repos/${encodePathSegment(sourceRepo.owner)}/${encodePathSegment(
3740
+ `/api/businesses/${encodePathSegment2(businessId)}/repos/${encodePathSegment2(sourceRepo.owner)}/${encodePathSegment2(
3592
3741
  sourceRepo.repo
3593
3742
  )}/sandbox-layer/build-requests`,
3594
3743
  {
@@ -3618,11 +3767,11 @@ async function sandboxBuildCommand(sourceRepoArg, options = {}, command) {
3618
3767
  async function sandboxStatusCommand(repoArg, options = {}, command) {
3619
3768
  const runtime = getRuntimeOptions(command, options);
3620
3769
  const config = requireConfig(runtime);
3621
- const businessId = await resolveBusinessId(config, options);
3622
- const repo = parseRepoArg(repoArg, currentRepo);
3770
+ const businessId = await resolveBusinessId2(config, options);
3771
+ const repo = parseRepoArg2(repoArg, currentRepo);
3623
3772
  const payload = await apiFetch(
3624
3773
  config,
3625
- `/api/businesses/${encodePathSegment(businessId)}/repos/${encodePathSegment(repo.owner)}/${encodePathSegment(
3774
+ `/api/businesses/${encodePathSegment2(businessId)}/repos/${encodePathSegment2(repo.owner)}/${encodePathSegment2(
3626
3775
  repo.repo
3627
3776
  )}/sandbox-layer/resolution`
3628
3777
  );
@@ -3635,10 +3784,10 @@ async function sandboxStatusCommand(repoArg, options = {}, command) {
3635
3784
  async function sandboxHistoryCommand(sourceRepoArg, options = {}, command) {
3636
3785
  const runtime = getRuntimeOptions(command, options);
3637
3786
  const config = requireConfig(runtime);
3638
- const businessId = await resolveBusinessId(config, options);
3639
- const sourceRepo = parseRepoArg(sourceRepoArg, currentRepo);
3787
+ const businessId = await resolveBusinessId2(config, options);
3788
+ const sourceRepo = parseRepoArg2(sourceRepoArg, currentRepo);
3640
3789
  const params = new URLSearchParams({ sourceRepo: repoPath(sourceRepo) });
3641
- if (options.targetRepo) params.set("targetRepo", repoPath(parseRepoArg(options.targetRepo, currentRepo)));
3790
+ if (options.targetRepo) params.set("targetRepo", repoPath(parseRepoArg2(options.targetRepo, currentRepo)));
3642
3791
  if (options.status) params.set("status", options.status);
3643
3792
  if (options.limit) {
3644
3793
  const limit = Number(options.limit);
@@ -3649,7 +3798,7 @@ async function sandboxHistoryCommand(sourceRepoArg, options = {}, command) {
3649
3798
  }
3650
3799
  const payload = await apiFetch(
3651
3800
  config,
3652
- `/api/businesses/${encodePathSegment(businessId)}/sandbox-layer/build-requests?${params.toString()}`
3801
+ `/api/businesses/${encodePathSegment2(businessId)}/sandbox-layer/build-requests?${params.toString()}`
3653
3802
  );
3654
3803
  if (isJson(command, options)) {
3655
3804
  writeJson(payload);
@@ -3666,7 +3815,7 @@ async function sandboxRebuildStaleCommand(options = {}, command) {
3666
3815
  if (options.all && options.business) {
3667
3816
  throw new CliError("user", "Use either --all or --business, not both.");
3668
3817
  }
3669
- const businessId = options.all ? null : await resolveBusinessId(config, options);
3818
+ const businessId = options.all ? null : await resolveBusinessId2(config, options);
3670
3819
  let payload = await apiFetch(config, "/api/admin/sandbox-layer/rebuild-campaigns", {
3671
3820
  method: "POST",
3672
3821
  body: JSON.stringify({
@@ -3694,7 +3843,7 @@ async function followRebuildCampaign(config, campaignId) {
3694
3843
  for (; ; ) {
3695
3844
  const payload = await apiFetch(
3696
3845
  config,
3697
- `/api/admin/sandbox-layer/rebuild-campaigns/${encodePathSegment(campaignId)}`
3846
+ `/api/admin/sandbox-layer/rebuild-campaigns/${encodePathSegment2(campaignId)}`
3698
3847
  );
3699
3848
  if (isCampaignTerminal(payload.campaign.status)) return payload;
3700
3849
  await sleep2(DEFAULT_POLL_INTERVAL_MS);
@@ -3723,7 +3872,7 @@ function printRebuildCampaign(payload) {
3723
3872
  async function sandboxLogsCommand(buildId, options = {}, command) {
3724
3873
  const runtime = getRuntimeOptions(command, options);
3725
3874
  const config = requireConfig(runtime);
3726
- const businessId = await resolveBusinessId(config, options);
3875
+ const businessId = await resolveBusinessId2(config, options);
3727
3876
  let afterSequence = options.afterSequence == null ? void 0 : Number(options.afterSequence);
3728
3877
  if (afterSequence != null && (!Number.isInteger(afterSequence) || afterSequence < 0)) {
3729
3878
  throw new CliError("user", "--after-sequence must be a non-negative integer.");
@@ -3747,12 +3896,12 @@ async function sandboxLogsCommand(buildId, options = {}, command) {
3747
3896
  async function sandboxAssignDefaultCommand(sourceRepoArg, options = {}, command) {
3748
3897
  const runtime = getRuntimeOptions(command, options);
3749
3898
  const config = requireConfig(runtime);
3750
- const businessId = await resolveBusinessId(config, options);
3751
- const sourceRepo = parseRepoArg(sourceRepoArg, currentRepo);
3899
+ const businessId = await resolveBusinessId2(config, options);
3900
+ const sourceRepo = parseRepoArg2(sourceRepoArg, currentRepo);
3752
3901
  const manifestPath = options.manifest ?? DEFAULT_MANIFEST_PATH;
3753
3902
  const payload = await apiFetch(
3754
3903
  config,
3755
- `/api/businesses/${encodePathSegment(businessId)}/sandbox-layer/default-source`,
3904
+ `/api/businesses/${encodePathSegment2(businessId)}/sandbox-layer/default-source`,
3756
3905
  { method: "PUT", body: JSON.stringify(sourceBody(sourceRepo, manifestPath)) }
3757
3906
  );
3758
3907
  if (isJson(command, options)) {
@@ -3767,13 +3916,13 @@ async function sandboxAssignDefaultCommand(sourceRepoArg, options = {}, command)
3767
3916
  async function sandboxAssignRepoCommand(targetRepoArg, sourceRepoArg, options = {}, command) {
3768
3917
  const runtime = getRuntimeOptions(command, options);
3769
3918
  const config = requireConfig(runtime);
3770
- const businessId = await resolveBusinessId(config, options);
3771
- const targetRepo = parseRepoArg(targetRepoArg, currentRepo);
3772
- const sourceRepo = parseRepoArg(sourceRepoArg, currentRepo);
3919
+ const businessId = await resolveBusinessId2(config, options);
3920
+ const targetRepo = parseRepoArg2(targetRepoArg, currentRepo);
3921
+ const sourceRepo = parseRepoArg2(sourceRepoArg, currentRepo);
3773
3922
  const manifestPath = options.manifest ?? DEFAULT_MANIFEST_PATH;
3774
3923
  const payload = await apiFetch(
3775
3924
  config,
3776
- `/api/businesses/${encodePathSegment(businessId)}/repos/${encodePathSegment(targetRepo.owner)}/${encodePathSegment(
3925
+ `/api/businesses/${encodePathSegment2(businessId)}/repos/${encodePathSegment2(targetRepo.owner)}/${encodePathSegment2(
3777
3926
  targetRepo.repo
3778
3927
  )}/sandbox-layer/assignment`,
3779
3928
  { method: "PUT", body: JSON.stringify(sourceBody(sourceRepo, manifestPath)) }
@@ -3792,11 +3941,11 @@ async function sandboxAssignRepoCommand(targetRepoArg, sourceRepoArg, options =
3792
3941
  async function sandboxUnassignDefaultCommand(options = {}, command) {
3793
3942
  const runtime = getRuntimeOptions(command, options);
3794
3943
  const config = requireConfig(runtime);
3795
- const businessId = await resolveBusinessId(config, options);
3944
+ const businessId = await resolveBusinessId2(config, options);
3796
3945
  if (!options.yes) await confirmOrThrow("Clear the business default sandbox layer source?");
3797
3946
  const payload = await apiFetch(
3798
3947
  config,
3799
- `/api/businesses/${encodePathSegment(businessId)}/sandbox-layer/default-source`,
3948
+ `/api/businesses/${encodePathSegment2(businessId)}/sandbox-layer/default-source`,
3800
3949
  { method: "DELETE" }
3801
3950
  );
3802
3951
  if (isJson(command, options)) {
@@ -3808,12 +3957,12 @@ async function sandboxUnassignDefaultCommand(options = {}, command) {
3808
3957
  async function sandboxUnassignRepoCommand(targetRepoArg, options = {}, command) {
3809
3958
  const runtime = getRuntimeOptions(command, options);
3810
3959
  const config = requireConfig(runtime);
3811
- const businessId = await resolveBusinessId(config, options);
3812
- const targetRepo = parseRepoArg(targetRepoArg, currentRepo);
3960
+ const businessId = await resolveBusinessId2(config, options);
3961
+ const targetRepo = parseRepoArg2(targetRepoArg, currentRepo);
3813
3962
  if (!options.yes) await confirmOrThrow(`Clear sandbox layer assignment for ${repoPath(targetRepo)}?`);
3814
3963
  const payload = await apiFetch(
3815
3964
  config,
3816
- `/api/businesses/${encodePathSegment(businessId)}/repos/${encodePathSegment(targetRepo.owner)}/${encodePathSegment(
3965
+ `/api/businesses/${encodePathSegment2(businessId)}/repos/${encodePathSegment2(targetRepo.owner)}/${encodePathSegment2(
3817
3966
  targetRepo.repo
3818
3967
  )}/sandbox-layer/assignment`,
3819
3968
  { method: "DELETE" }
@@ -3997,8 +4146,8 @@ function parseStopBlocked(err) {
3997
4146
  }
3998
4147
 
3999
4148
  // src/commands/test-creds.ts
4000
- import { readFileSync as readFileSync3 } from "fs";
4001
- function parseRepoArg2(repo) {
4149
+ import { readFileSync as readFileSync4 } from "fs";
4150
+ function parseRepoArg3(repo) {
4002
4151
  const trimmed = repo.trim();
4003
4152
  const slashIndex = trimmed.indexOf("/");
4004
4153
  if (slashIndex <= 0 || slashIndex === trimmed.length - 1) {
@@ -4015,13 +4164,13 @@ function readValue(options) {
4015
4164
  throw new CliError("user", "Provide exactly one of --value, --value-file, or --value-stdin.");
4016
4165
  }
4017
4166
  if (options.value !== void 0) return options.value;
4018
- const raw = options.valueFile ? readFileSync3(options.valueFile, "utf8") : readFileSync3(0, "utf8");
4167
+ const raw = options.valueFile ? readFileSync4(options.valueFile, "utf8") : readFileSync4(0, "utf8");
4019
4168
  return raw.replace(/\r?\n$/, "");
4020
4169
  }
4021
4170
  async function listTestCredentialsCommand(repo, options, command) {
4022
4171
  const runtime = getRuntimeOptions(command, options);
4023
4172
  const config = requireConfig(runtime);
4024
- const repoArg = parseRepoArg2(repo);
4173
+ const repoArg = parseRepoArg3(repo);
4025
4174
  const payload = await apiFetch(config, basePath(options.business, repoArg));
4026
4175
  if (isJson(command, options)) {
4027
4176
  writeJson(payload);
@@ -4039,7 +4188,7 @@ async function listTestCredentialsCommand(repo, options, command) {
4039
4188
  async function setTestCredentialCommand(repo, name, options, command) {
4040
4189
  const runtime = getRuntimeOptions(command, options);
4041
4190
  const config = requireConfig(runtime);
4042
- const repoArg = parseRepoArg2(repo);
4191
+ const repoArg = parseRepoArg3(repo);
4043
4192
  const value = readValue(options);
4044
4193
  const payload = await apiFetch(
4045
4194
  config,
@@ -4057,12 +4206,12 @@ async function deleteTestCredentialCommand(repo, name, options, command) {
4057
4206
  throw new CliError("user", "`test-creds delete --json` requires --yes.");
4058
4207
  }
4059
4208
  if (options.yes !== true) {
4060
- const repoArg2 = parseRepoArg2(repo);
4209
+ const repoArg2 = parseRepoArg3(repo);
4061
4210
  await confirmOrThrow(`Delete test credential '${name}' for ${repoArg2.owner}/${repoArg2.name}?`);
4062
4211
  }
4063
4212
  const runtime = getRuntimeOptions(command, options);
4064
4213
  const config = requireConfig(runtime);
4065
- const repoArg = parseRepoArg2(repo);
4214
+ const repoArg = parseRepoArg3(repo);
4066
4215
  const payload = await apiFetch(
4067
4216
  config,
4068
4217
  `${basePath(options.business, repoArg)}/${encodeURIComponent(name)}`,
@@ -4349,6 +4498,13 @@ sandboxAssign.command("repo").description("Assign a sandbox layer source to one
4349
4498
  var sandboxUnassign = sandbox.command("unassign").description("Remove sandbox layer source assignments");
4350
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));
4351
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));
4352
4508
  var tokens = program.command("tokens").description("CLI token commands");
4353
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));
4354
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(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tryarcanist/cli",
3
- "version": "0.1.133",
3
+ "version": "0.1.134",
4
4
  "description": "CLI for Arcanist — create and manage coding agent sessions",
5
5
  "type": "module",
6
6
  "bin": {