@tryarcanist/cli 0.1.160 → 0.1.161

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 +194 -265
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -138,6 +138,16 @@ async function apiFetchText(config, path, init) {
138
138
  const res = await apiRequest(config, path, init);
139
139
  return res.text();
140
140
  }
141
+ async function resolveBusinessId(config, options) {
142
+ if (options.business && options.business.trim()) return options.business.trim();
143
+ const whoami = await apiFetch(config, "/api/auth/whoami");
144
+ if (whoami.businessId && whoami.businessId.trim()) return whoami.businessId;
145
+ throw new CliError("user", "--business is required when the authenticated token has no business context.");
146
+ }
147
+
148
+ // src/runtime.ts
149
+ import { randomUUID } from "crypto";
150
+ import { createInterface } from "readline/promises";
141
151
 
142
152
  // src/config.ts
143
153
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
@@ -289,8 +299,6 @@ function isLoopbackHost(parsed) {
289
299
  }
290
300
 
291
301
  // src/runtime.ts
292
- import { randomUUID } from "crypto";
293
- import { createInterface } from "readline/promises";
294
302
  var ASCII_FIRST_PRINTABLE = 32;
295
303
  var ASCII_DELETE = 127;
296
304
  var ANSI_CONTROL_SEQUENCE = /\u001b\[[0-9:;<=>?]*[ -/]*[@-~]/g;
@@ -306,6 +314,17 @@ function writeJson(value) {
306
314
  process.stdout.write(`${JSON.stringify(value)}
307
315
  `);
308
316
  }
317
+ function emit(command, options, payload, printer) {
318
+ if (isJson(command, options)) {
319
+ writeJson(payload);
320
+ return;
321
+ }
322
+ printer(payload);
323
+ }
324
+ function resolveBusinessContext(command, options = {}) {
325
+ const runtime = getRuntimeOptions(command, options);
326
+ return { runtime, config: requireConfig(runtime) };
327
+ }
309
328
  async function readStdin() {
310
329
  const chunks = [];
311
330
  for await (const chunk of process.stdin) {
@@ -468,15 +487,12 @@ function isControlCharacter(char) {
468
487
 
469
488
  // src/commands/auth.ts
470
489
  async function whoamiCommand(options, command) {
471
- const runtime = getRuntimeOptions(command, options);
472
- const config = requireConfig(runtime);
490
+ const { config } = resolveBusinessContext(command, options);
473
491
  const payload = await apiFetch(config, "/api/auth/whoami");
474
- if (isJson(command, options)) {
475
- writeJson(payload);
476
- return;
477
- }
478
- console.log(`User: ${String(payload.email ?? payload.userId ?? "unknown")}`);
479
- if (payload.tokenScope) console.log(`Token scope: ${String(payload.tokenScope)}`);
492
+ emit(command, options, payload, (whoami) => {
493
+ console.log(`User: ${String(whoami.email ?? whoami.userId ?? "unknown")}`);
494
+ if (whoami.tokenScope) console.log(`Token scope: ${String(whoami.tokenScope)}`);
495
+ });
480
496
  }
481
497
 
482
498
  // ../../shared/github/repo-url.ts
@@ -1517,6 +1533,7 @@ function mergeToolCall(previous, incoming) {
1517
1533
  ...incoming.inputEstimatedTokens === void 0 && previous.inputEstimatedTokens !== void 0 ? { inputEstimatedTokens: previous.inputEstimatedTokens } : {},
1518
1534
  ...incoming.outputEstimatedTokens === void 0 && previous.outputEstimatedTokens !== void 0 ? { outputEstimatedTokens: previous.outputEstimatedTokens } : {},
1519
1535
  ...incoming.outputChars === void 0 && previous.outputChars !== void 0 ? { outputChars: previous.outputChars } : {},
1536
+ ...incoming.failure === void 0 && previous.failure !== void 0 ? { failure: previous.failure } : {},
1520
1537
  ...incoming.duplicateCount === void 0 && previous.duplicateCount !== void 0 ? { duplicateCount: previous.duplicateCount } : {}
1521
1538
  };
1522
1539
  }
@@ -1530,6 +1547,7 @@ function applyToolCallUpdate(previous, data) {
1530
1547
  ...toolStatus ? { toolStatus } : {},
1531
1548
  ...typeof data?.outputEstimatedTokens === "number" ? { outputEstimatedTokens: data.outputEstimatedTokens } : {},
1532
1549
  ...typeof data?.outputChars === "number" ? { outputChars: data.outputChars } : {},
1550
+ ...isRecord(data?.failure) ? { failure: data.failure } : {},
1533
1551
  ...typeof data?.truncated === "boolean" ? { truncated: data.truncated } : {}
1534
1552
  };
1535
1553
  }
@@ -1800,7 +1818,8 @@ function projectToolCall(data, state) {
1800
1818
  ...typeof data?.truncated === "boolean" ? { truncated: data.truncated } : {},
1801
1819
  ...typeof data?.inputEstimatedTokens === "number" ? { inputEstimatedTokens: data.inputEstimatedTokens } : {},
1802
1820
  ...typeof data?.outputEstimatedTokens === "number" ? { outputEstimatedTokens: data.outputEstimatedTokens } : {},
1803
- ...typeof data?.outputChars === "number" ? { outputChars: data.outputChars } : {}
1821
+ ...typeof data?.outputChars === "number" ? { outputChars: data.outputChars } : {},
1822
+ ...isRecord(data?.failure) ? { failure: data.failure } : {}
1804
1823
  };
1805
1824
  state.merged[existingIdx] = mergeToolCall(previous, nextEntry);
1806
1825
  }
@@ -1817,7 +1836,8 @@ function projectToolCall(data, state) {
1817
1836
  ...typeof data?.truncated === "boolean" ? { truncated: data.truncated } : {},
1818
1837
  ...typeof data?.inputEstimatedTokens === "number" ? { inputEstimatedTokens: data.inputEstimatedTokens } : {},
1819
1838
  ...typeof data?.outputEstimatedTokens === "number" ? { outputEstimatedTokens: data.outputEstimatedTokens } : {},
1820
- ...typeof data?.outputChars === "number" ? { outputChars: data.outputChars } : {}
1839
+ ...typeof data?.outputChars === "number" ? { outputChars: data.outputChars } : {},
1840
+ ...isRecord(data?.failure) ? { failure: data.failure } : {}
1821
1841
  };
1822
1842
  state.toolCallIndexById.set(id, state.merged.length);
1823
1843
  state.merged.push(nextEntry);
@@ -2972,105 +2992,88 @@ function parseRepoArg(value) {
2972
2992
  if (!owner || !repo || extra) throw new CliError("user", "repo must be owner/name or a GitHub URL");
2973
2993
  return { owner, repo };
2974
2994
  }
2975
- function encodePathSegment(value) {
2976
- return encodeURIComponent(value);
2977
- }
2978
- async function resolveBusinessId(config, options) {
2979
- if (options.business && options.business.trim()) return options.business.trim();
2980
- const whoami = await apiFetch(config, "/api/auth/whoami");
2981
- if (whoami.businessId && whoami.businessId.trim()) return whoami.businessId;
2982
- throw new CliError("user", "--business is required when the authenticated token has no business context.");
2983
- }
2984
2995
  async function egressSourceSetCommand(sourceRepoArg, options = {}, command) {
2985
- const runtime = getRuntimeOptions(command, options);
2986
- const config = requireConfig(runtime);
2996
+ const { config } = resolveBusinessContext(command, options);
2987
2997
  const businessId = await resolveBusinessId(config, options);
2988
2998
  const source = parseRepoArg(sourceRepoArg);
2989
2999
  const payload = await apiFetch(
2990
3000
  config,
2991
- `/api/businesses/${encodePathSegment(businessId)}/egress-allowlist/source`,
3001
+ `/api/businesses/${encodeURIComponent(businessId)}/egress-allowlist/source`,
2992
3002
  {
2993
3003
  method: "PUT",
2994
3004
  body: JSON.stringify({ sourceRepoOwner: source.owner, sourceRepoName: source.repo })
2995
3005
  }
2996
3006
  );
2997
- if (isJson(command, options)) {
2998
- writeJson(payload);
2999
- return;
3000
- }
3001
- console.log(`Set egress allowlist source to ${source.owner}/${source.repo}:${EGRESS_ALLOWLIST_SOURCE_PATH}.`);
3007
+ emit(
3008
+ command,
3009
+ options,
3010
+ payload,
3011
+ () => console.log(`Set egress allowlist source to ${source.owner}/${source.repo}:${EGRESS_ALLOWLIST_SOURCE_PATH}.`)
3012
+ );
3002
3013
  }
3003
3014
  async function egressSourceGetCommand(options = {}, command) {
3004
- const runtime = getRuntimeOptions(command, options);
3005
- const config = requireConfig(runtime);
3015
+ const { config } = resolveBusinessContext(command, options);
3006
3016
  const businessId = await resolveBusinessId(config, options);
3007
3017
  const payload = await apiFetch(
3008
3018
  config,
3009
- `/api/businesses/${encodePathSegment(businessId)}/egress-allowlist/source`
3019
+ `/api/businesses/${encodeURIComponent(businessId)}/egress-allowlist/source`
3010
3020
  );
3011
- if (isJson(command, options)) {
3012
- writeJson(payload);
3013
- return;
3014
- }
3015
- if (!payload.source) {
3016
- console.log("No egress allowlist source configured.");
3017
- return;
3018
- }
3019
- console.log(
3020
- `${payload.source.sourceRepoOwner}/${payload.source.sourceRepoName}:${payload.path} (${payload.defaultBranch ?? "default branch"})`
3021
- );
3022
- console.log(`${payload.domains.length} ${payload.domains.length === 1 ? "domain" : "domains"}.`);
3021
+ emit(command, options, payload, (sourcePayload) => {
3022
+ if (!sourcePayload.source) {
3023
+ console.log("No egress allowlist source configured.");
3024
+ return;
3025
+ }
3026
+ console.log(
3027
+ `${sourcePayload.source.sourceRepoOwner}/${sourcePayload.source.sourceRepoName}:${sourcePayload.path} (${sourcePayload.defaultBranch ?? "default branch"})`
3028
+ );
3029
+ console.log(`${sourcePayload.domains.length} ${sourcePayload.domains.length === 1 ? "domain" : "domains"}.`);
3030
+ });
3023
3031
  }
3024
3032
  async function egressAddCommand(domains, options = {}, command) {
3025
- const runtime = getRuntimeOptions(command, options);
3026
- const config = requireConfig(runtime);
3033
+ const { config } = resolveBusinessContext(command, options);
3027
3034
  const businessId = await resolveBusinessId(config, options);
3028
3035
  const normalized = normalizeEgressDomains(domains, "domains");
3029
3036
  const payload = await apiFetch(
3030
3037
  config,
3031
- `/api/businesses/${encodePathSegment(businessId)}/egress-allowlist/pull-request`,
3038
+ `/api/businesses/${encodeURIComponent(businessId)}/egress-allowlist/pull-request`,
3032
3039
  {
3033
3040
  method: "POST",
3034
3041
  body: JSON.stringify({ domains: normalized, ...options.reason ? { reason: options.reason } : {} })
3035
3042
  }
3036
3043
  );
3037
- if (isJson(command, options)) {
3038
- writeJson(payload);
3039
- return;
3040
- }
3041
- if (payload.status === "unchanged") {
3042
- console.log("All requested domains are already present in the source file.");
3043
- return;
3044
- }
3045
- console.log(`Egress allowlist PR ${payload.status}: ${payload.prUrl}`);
3046
- console.log(`Added: ${payload.addedDomains.join(", ")}`);
3044
+ emit(command, options, payload, (addPayload) => {
3045
+ if (addPayload.status === "unchanged") {
3046
+ console.log("All requested domains are already present in the source file.");
3047
+ return;
3048
+ }
3049
+ console.log(`Egress allowlist PR ${addPayload.status}: ${addPayload.prUrl}`);
3050
+ console.log(`Added: ${addPayload.addedDomains.join(", ")}`);
3051
+ });
3047
3052
  }
3048
3053
  async function egressSyncCommand(options = {}, command) {
3049
- const runtime = getRuntimeOptions(command, options);
3050
- const config = requireConfig(runtime);
3054
+ const { config } = resolveBusinessContext(command, options);
3051
3055
  const businessId = await resolveBusinessId(config, options);
3052
3056
  const payload = await apiFetch(
3053
3057
  config,
3054
- `/api/businesses/${encodePathSegment(businessId)}/egress-allowlist/sync`,
3058
+ `/api/businesses/${encodeURIComponent(businessId)}/egress-allowlist/sync`,
3055
3059
  { method: "POST" }
3056
3060
  );
3057
- if (isJson(command, options)) {
3058
- writeJson(payload);
3059
- return;
3060
- }
3061
- console.log(
3062
- `Applied ${payload.egressAllowlist.length} egress allowlist domains from ${payload.source.sourceRepoOwner}/${payload.source.sourceRepoName}:${payload.path}.`
3061
+ emit(
3062
+ command,
3063
+ options,
3064
+ payload,
3065
+ (syncPayload) => console.log(
3066
+ `Applied ${syncPayload.egressAllowlist.length} egress allowlist domains from ${syncPayload.source.sourceRepoOwner}/${syncPayload.source.sourceRepoName}:${syncPayload.path}.`
3067
+ )
3063
3068
  );
3064
3069
  }
3065
3070
  async function egressValidateCommand(path = EGRESS_ALLOWLIST_SOURCE_PATH, options = {}, command) {
3066
3071
  const content = readFileSync2(path, "utf8");
3067
3072
  const domains = parseEgressAllowlistSourceFile(content, path);
3068
- if (isJson(command, options)) {
3069
- writeJson({ ok: true, path, domains });
3070
- return;
3071
- }
3072
- console.log(`Valid egress allowlist: ${path}`);
3073
- console.log(`${domains.length} ${domains.length === 1 ? "domain" : "domains"}.`);
3073
+ emit(command, options, { ok: true, path, domains }, (payload) => {
3074
+ console.log(`Valid egress allowlist: ${payload.path}`);
3075
+ console.log(`${payload.domains.length} ${payload.domains.length === 1 ? "domain" : "domains"}.`);
3076
+ });
3074
3077
  }
3075
3078
 
3076
3079
  // src/commands/login.ts
@@ -3714,15 +3717,6 @@ function parseRepoArg2(value, fallback) {
3714
3717
  function repoPath(repo) {
3715
3718
  return `${repo.owner}/${repo.repo}`;
3716
3719
  }
3717
- function encodePathSegment2(value) {
3718
- return encodeURIComponent(value);
3719
- }
3720
- async function resolveBusinessId2(config, options) {
3721
- if (options.business && options.business.trim()) return options.business.trim();
3722
- const whoami = await apiFetch(config, "/api/auth/whoami");
3723
- if (whoami.businessId && whoami.businessId.trim()) return whoami.businessId;
3724
- throw new CliError("user", "--business is required when the authenticated token has no business context.");
3725
- }
3726
3720
  function parsePollInterval2(value) {
3727
3721
  if (!value) return DEFAULT_POLL_INTERVAL_MS;
3728
3722
  const parsed = Number(value);
@@ -3731,9 +3725,6 @@ function parsePollInterval2(value) {
3731
3725
  }
3732
3726
  return parsed;
3733
3727
  }
3734
- function sleep2(ms) {
3735
- return new Promise((resolve) => setTimeout(resolve, ms));
3736
- }
3737
3728
  function printBuildRequest(buildRequest) {
3738
3729
  console.log(
3739
3730
  [
@@ -3856,7 +3847,7 @@ function buildLogsPath(businessId, buildId, options = {}) {
3856
3847
  if (options.afterSequence != null) params.set("afterSequence", String(options.afterSequence));
3857
3848
  if (options.limit != null) params.set("limit", String(options.limit));
3858
3849
  const suffix = params.toString() ? `?${params.toString()}` : "";
3859
- return `/api/businesses/${encodePathSegment2(businessId)}/sandbox-layer/build-requests/${encodePathSegment2(
3850
+ return `/api/businesses/${encodeURIComponent(businessId)}/sandbox-layer/build-requests/${encodeURIComponent(
3860
3851
  buildId
3861
3852
  )}/logs${suffix}`;
3862
3853
  }
@@ -3873,12 +3864,12 @@ async function waitForBuild(config, businessId, buildId, options) {
3873
3864
  }
3874
3865
  const status = await apiFetch(
3875
3866
  config,
3876
- `/api/businesses/${encodePathSegment2(businessId)}/sandbox-layer/build-requests/${encodePathSegment2(buildId)}`
3867
+ `/api/businesses/${encodeURIComponent(businessId)}/sandbox-layer/build-requests/${encodeURIComponent(buildId)}`
3877
3868
  );
3878
3869
  if (TERMINAL_BUILD_STATUSES.has(status.buildRequest.status)) {
3879
3870
  return status.buildRequest;
3880
3871
  }
3881
- await sleep2(options.pollIntervalMs);
3872
+ await sleep(options.pollIntervalMs);
3882
3873
  }
3883
3874
  }
3884
3875
  function formatValidationPayload(input) {
@@ -3953,11 +3944,7 @@ async function sandboxInitCommand(options = {}, command) {
3953
3944
  "utf8"
3954
3945
  );
3955
3946
  const payload = { ok: true, manifestPath: DEFAULT_MANIFEST_PATH, layerPath };
3956
- if (isJson(command, runtime)) {
3957
- writeJson(payload);
3958
- return;
3959
- }
3960
- console.log(`Created ${DEFAULT_MANIFEST_PATH} and ${layerPath}`);
3947
+ emit(command, runtime, payload, () => console.log(`Created ${DEFAULT_MANIFEST_PATH} and ${layerPath}`));
3961
3948
  }
3962
3949
  async function sandboxValidateCommand(options = {}, command) {
3963
3950
  const manifestPath = options.manifest ?? DEFAULT_MANIFEST_PATH;
@@ -3970,34 +3957,31 @@ async function sandboxValidateCommand(options = {}, command) {
3970
3957
  const layerText = readFileSync3(layerPath, "utf8");
3971
3958
  const parsed = await parseSandboxLayerSource({ manifestPath, manifestText, layerPath, layerText });
3972
3959
  const payload = formatValidationPayload({ manifestPath, layerPath, parsed });
3973
- if (isJson(command, options)) {
3974
- writeJson(payload);
3975
- return;
3976
- }
3977
- console.log("Valid sandbox template.");
3978
- console.log(`Manifest: ${payload.manifestPath}`);
3979
- console.log(`Layer: ${payload.layerPath}`);
3980
- console.log(`Normalized source hash: ${payload.normalizedSourceHash}`);
3981
- console.log(`Instructions: ${payload.instructionCount}`);
3982
- console.log(`Smoke commands: ${payload.smokeCommandCount}`);
3983
- console.log(`Next: ${nextBuildCommand(manifestPath)}`);
3960
+ emit(command, options, payload, (validationPayload) => {
3961
+ console.log("Valid sandbox template.");
3962
+ console.log(`Manifest: ${validationPayload.manifestPath}`);
3963
+ console.log(`Layer: ${validationPayload.layerPath}`);
3964
+ console.log(`Normalized source hash: ${validationPayload.normalizedSourceHash}`);
3965
+ console.log(`Instructions: ${validationPayload.instructionCount}`);
3966
+ console.log(`Smoke commands: ${validationPayload.smokeCommandCount}`);
3967
+ console.log(`Next: ${nextBuildCommand(manifestPath)}`);
3968
+ });
3984
3969
  } catch (err) {
3985
3970
  handleValidationError(err, options, command);
3986
3971
  }
3987
3972
  }
3988
3973
  async function sandboxBuildCommand(sourceRepoArg, options = {}, command) {
3989
- const runtime = getRuntimeOptions(command, options);
3990
- const config = requireConfig(runtime);
3974
+ const { config } = resolveBusinessContext(command, options);
3991
3975
  const manifestPath = options.manifest ?? DEFAULT_MANIFEST_PATH;
3992
3976
  assertManifestExists(manifestPath);
3993
3977
  if (!options.ref) assertCleanAndPushed();
3994
3978
  const sourceRepo = parseRepoArg2(sourceRepoArg, currentRepo);
3995
3979
  const targetRepo = options.targetRepo ? parseRepoArg2(options.targetRepo, currentRepo) : null;
3996
- const businessId = await resolveBusinessId2(config, options);
3980
+ const businessId = await resolveBusinessId(config, options);
3997
3981
  const ref = options.ref ?? currentRef();
3998
3982
  const payload = await apiFetch(
3999
3983
  config,
4000
- `/api/businesses/${encodePathSegment2(businessId)}/repos/${encodePathSegment2(sourceRepo.owner)}/${encodePathSegment2(
3984
+ `/api/businesses/${encodeURIComponent(businessId)}/repos/${encodeURIComponent(sourceRepo.owner)}/${encodeURIComponent(
4001
3985
  sourceRepo.repo
4002
3986
  )}/sandbox-layer/build-requests`,
4003
3987
  {
@@ -4009,8 +3993,7 @@ async function sandboxBuildCommand(sourceRepoArg, options = {}, command) {
4009
3993
  })
4010
3994
  }
4011
3995
  );
4012
- if (isJson(command, options)) writeJson(payload);
4013
- else printBuildRequest(payload.buildRequest);
3996
+ emit(command, options, payload, (buildPayload) => printBuildRequest(buildPayload.buildRequest));
4014
3997
  if (!options.wait && !options.follow) return;
4015
3998
  const json = isJson(command, options);
4016
3999
  const finalBuild = await waitForBuild(config, businessId, payload.buildRequest.id, {
@@ -4025,26 +4008,20 @@ async function sandboxBuildCommand(sourceRepoArg, options = {}, command) {
4025
4008
  }
4026
4009
  }
4027
4010
  async function sandboxStatusCommand(repoArg, options = {}, command) {
4028
- const runtime = getRuntimeOptions(command, options);
4029
- const config = requireConfig(runtime);
4030
- const businessId = await resolveBusinessId2(config, options);
4011
+ const { config } = resolveBusinessContext(command, options);
4012
+ const businessId = await resolveBusinessId(config, options);
4031
4013
  const repo = parseRepoArg2(repoArg, currentRepo);
4032
4014
  const payload = await apiFetch(
4033
4015
  config,
4034
- `/api/businesses/${encodePathSegment2(businessId)}/repos/${encodePathSegment2(repo.owner)}/${encodePathSegment2(
4016
+ `/api/businesses/${encodeURIComponent(businessId)}/repos/${encodeURIComponent(repo.owner)}/${encodeURIComponent(
4035
4017
  repo.repo
4036
4018
  )}/sandbox-layer/resolution`
4037
4019
  );
4038
- if (isJson(command, options)) {
4039
- writeJson(payload);
4040
- return;
4041
- }
4042
- printSandboxStatus(payload);
4020
+ emit(command, options, payload, printSandboxStatus);
4043
4021
  }
4044
4022
  async function sandboxHistoryCommand(sourceRepoArg, options = {}, command) {
4045
- const runtime = getRuntimeOptions(command, options);
4046
- const config = requireConfig(runtime);
4047
- const businessId = await resolveBusinessId2(config, options);
4023
+ const { config } = resolveBusinessContext(command, options);
4024
+ const businessId = await resolveBusinessId(config, options);
4048
4025
  const sourceRepo = parseRepoArg2(sourceRepoArg, currentRepo);
4049
4026
  const params = new URLSearchParams({ sourceRepo: repoPath(sourceRepo) });
4050
4027
  if (options.targetRepo) params.set("targetRepo", repoPath(parseRepoArg2(options.targetRepo, currentRepo)));
@@ -4058,24 +4035,19 @@ async function sandboxHistoryCommand(sourceRepoArg, options = {}, command) {
4058
4035
  }
4059
4036
  const payload = await apiFetch(
4060
4037
  config,
4061
- `/api/businesses/${encodePathSegment2(businessId)}/sandbox-layer/build-requests?${params.toString()}`
4038
+ `/api/businesses/${encodeURIComponent(businessId)}/sandbox-layer/build-requests?${params.toString()}`
4062
4039
  );
4063
- if (isJson(command, options)) {
4064
- writeJson(payload);
4065
- return;
4066
- }
4067
- printBuildHistory(payload.builds);
4040
+ emit(command, options, payload, (historyPayload) => printBuildHistory(historyPayload.builds));
4068
4041
  }
4069
4042
  async function sandboxRebuildStaleCommand(options = {}, command) {
4070
- const runtime = getRuntimeOptions(command, options);
4071
- const config = requireConfig(runtime);
4043
+ const { config } = resolveBusinessContext(command, options);
4072
4044
  if (!options.dryRun && !options.yes) {
4073
4045
  throw new CliError("user", "Use --dry-run to preview or --yes to start a rebuild campaign.");
4074
4046
  }
4075
4047
  if (options.all && options.business) {
4076
4048
  throw new CliError("user", "Use either --all or --business, not both.");
4077
4049
  }
4078
- const businessId = options.all ? null : await resolveBusinessId2(config, options);
4050
+ const businessId = options.all ? null : await resolveBusinessId(config, options);
4079
4051
  let payload = await apiFetch(config, "/api/admin/sandbox-layer/rebuild-campaigns", {
4080
4052
  method: "POST",
4081
4053
  body: JSON.stringify({
@@ -4093,20 +4065,16 @@ async function sandboxRebuildStaleCommand(options = {}, command) {
4093
4065
  if (options.follow && !options.dryRun) {
4094
4066
  payload = await followRebuildCampaign(config, payload.campaign.id);
4095
4067
  }
4096
- if (isJson(command, options)) {
4097
- writeJson(payload);
4098
- return;
4099
- }
4100
- printRebuildCampaign(payload);
4068
+ emit(command, options, payload, printRebuildCampaign);
4101
4069
  }
4102
4070
  async function followRebuildCampaign(config, campaignId) {
4103
4071
  for (; ; ) {
4104
4072
  const payload = await apiFetch(
4105
4073
  config,
4106
- `/api/admin/sandbox-layer/rebuild-campaigns/${encodePathSegment2(campaignId)}`
4074
+ `/api/admin/sandbox-layer/rebuild-campaigns/${encodeURIComponent(campaignId)}`
4107
4075
  );
4108
4076
  if (isCampaignTerminal(payload.campaign.status)) return payload;
4109
- await sleep2(DEFAULT_POLL_INTERVAL_MS);
4077
+ await sleep(DEFAULT_POLL_INTERVAL_MS);
4110
4078
  }
4111
4079
  }
4112
4080
  function isCampaignTerminal(status) {
@@ -4130,9 +4098,8 @@ function printRebuildCampaign(payload) {
4130
4098
  }
4131
4099
  }
4132
4100
  async function sandboxLogsCommand(buildId, options = {}, command) {
4133
- const runtime = getRuntimeOptions(command, options);
4134
- const config = requireConfig(runtime);
4135
- const businessId = await resolveBusinessId2(config, options);
4101
+ const { config } = resolveBusinessContext(command, options);
4102
+ const businessId = await resolveBusinessId(config, options);
4136
4103
  let afterSequence = options.afterSequence == null ? void 0 : Number(options.afterSequence);
4137
4104
  if (afterSequence != null && (!Number.isInteger(afterSequence) || afterSequence < 0)) {
4138
4105
  throw new CliError("user", "--after-sequence must be a non-negative integer.");
@@ -4150,88 +4117,72 @@ async function sandboxLogsCommand(buildId, options = {}, command) {
4150
4117
  else printLogs(payload.logs);
4151
4118
  if (!options.follow) return;
4152
4119
  if (payload.logs.length > 0) afterSequence = payload.logs[payload.logs.length - 1].sequence;
4153
- await sleep2(parsePollInterval2(options.pollInterval));
4120
+ await sleep(parsePollInterval2(options.pollInterval));
4154
4121
  }
4155
4122
  }
4156
4123
  async function sandboxAssignDefaultCommand(sourceRepoArg, options = {}, command) {
4157
- const runtime = getRuntimeOptions(command, options);
4158
- const config = requireConfig(runtime);
4159
- const businessId = await resolveBusinessId2(config, options);
4124
+ const { config } = resolveBusinessContext(command, options);
4125
+ const businessId = await resolveBusinessId(config, options);
4160
4126
  const sourceRepo = parseRepoArg2(sourceRepoArg, currentRepo);
4161
4127
  const manifestPath = options.manifest ?? DEFAULT_MANIFEST_PATH;
4162
4128
  const payload = await apiFetch(
4163
4129
  config,
4164
- `/api/businesses/${encodePathSegment2(businessId)}/sandbox-layer/default-source`,
4130
+ `/api/businesses/${encodeURIComponent(businessId)}/sandbox-layer/default-source`,
4165
4131
  { method: "PUT", body: JSON.stringify(sourceBody(sourceRepo, manifestPath)) }
4166
4132
  );
4167
- if (isJson(command, options)) {
4168
- writeJson(payload);
4169
- return;
4170
- }
4171
- console.log(`Set default sandbox layer source to ${repoPath(sourceRepo)}.`);
4172
- console.log(
4173
- `Build missing target coverage with: arcanist sandbox build ${repoPath(sourceRepo)} --business ${businessId}`
4174
- );
4133
+ emit(command, options, payload, () => {
4134
+ console.log(`Set default sandbox layer source to ${repoPath(sourceRepo)}.`);
4135
+ console.log(
4136
+ `Build missing target coverage with: arcanist sandbox build ${repoPath(sourceRepo)} --business ${businessId}`
4137
+ );
4138
+ });
4175
4139
  }
4176
4140
  async function sandboxAssignRepoCommand(targetRepoArg, sourceRepoArg, options = {}, command) {
4177
- const runtime = getRuntimeOptions(command, options);
4178
- const config = requireConfig(runtime);
4179
- const businessId = await resolveBusinessId2(config, options);
4141
+ const { config } = resolveBusinessContext(command, options);
4142
+ const businessId = await resolveBusinessId(config, options);
4180
4143
  const targetRepo = parseRepoArg2(targetRepoArg, currentRepo);
4181
4144
  const sourceRepo = parseRepoArg2(sourceRepoArg, currentRepo);
4182
4145
  const manifestPath = options.manifest ?? DEFAULT_MANIFEST_PATH;
4183
4146
  const payload = await apiFetch(
4184
4147
  config,
4185
- `/api/businesses/${encodePathSegment2(businessId)}/repos/${encodePathSegment2(targetRepo.owner)}/${encodePathSegment2(
4148
+ `/api/businesses/${encodeURIComponent(businessId)}/repos/${encodeURIComponent(targetRepo.owner)}/${encodeURIComponent(
4186
4149
  targetRepo.repo
4187
4150
  )}/sandbox-layer/assignment`,
4188
4151
  { method: "PUT", body: JSON.stringify(sourceBody(sourceRepo, manifestPath)) }
4189
4152
  );
4190
- if (isJson(command, options)) {
4191
- writeJson(payload);
4192
- return;
4193
- }
4194
- console.log(`Assigned ${repoPath(sourceRepo)} as sandbox layer source for ${repoPath(targetRepo)}.`);
4195
- console.log(
4196
- `Build target coverage with: arcanist sandbox build ${repoPath(sourceRepo)} --target-repo ${repoPath(
4197
- targetRepo
4198
- )} --business ${businessId}`
4199
- );
4153
+ emit(command, options, payload, () => {
4154
+ console.log(`Assigned ${repoPath(sourceRepo)} as sandbox layer source for ${repoPath(targetRepo)}.`);
4155
+ console.log(
4156
+ `Build target coverage with: arcanist sandbox build ${repoPath(sourceRepo)} --target-repo ${repoPath(
4157
+ targetRepo
4158
+ )} --business ${businessId}`
4159
+ );
4160
+ });
4200
4161
  }
4201
4162
  async function sandboxUnassignDefaultCommand(options = {}, command) {
4202
- const runtime = getRuntimeOptions(command, options);
4203
- const config = requireConfig(runtime);
4204
- const businessId = await resolveBusinessId2(config, options);
4163
+ const { config } = resolveBusinessContext(command, options);
4164
+ const businessId = await resolveBusinessId(config, options);
4205
4165
  if (!options.yes) await confirmOrThrow("Clear the business default sandbox layer source?");
4206
4166
  const payload = await apiFetch(
4207
4167
  config,
4208
- `/api/businesses/${encodePathSegment2(businessId)}/sandbox-layer/default-source`,
4168
+ `/api/businesses/${encodeURIComponent(businessId)}/sandbox-layer/default-source`,
4209
4169
  { method: "DELETE" }
4210
4170
  );
4211
- if (isJson(command, options)) {
4212
- writeJson(payload);
4213
- return;
4214
- }
4215
- console.log("Cleared default sandbox layer source.");
4171
+ emit(command, options, payload, () => console.log("Cleared default sandbox layer source."));
4216
4172
  }
4217
4173
  async function sandboxUnassignRepoCommand(targetRepoArg, options = {}, command) {
4218
- const runtime = getRuntimeOptions(command, options);
4219
- const config = requireConfig(runtime);
4220
- const businessId = await resolveBusinessId2(config, options);
4174
+ const { config } = resolveBusinessContext(command, options);
4175
+ const businessId = await resolveBusinessId(config, options);
4221
4176
  const targetRepo = parseRepoArg2(targetRepoArg, currentRepo);
4222
4177
  if (!options.yes) await confirmOrThrow(`Clear sandbox layer assignment for ${repoPath(targetRepo)}?`);
4223
4178
  const payload = await apiFetch(
4224
4179
  config,
4225
- `/api/businesses/${encodePathSegment2(businessId)}/repos/${encodePathSegment2(targetRepo.owner)}/${encodePathSegment2(
4180
+ `/api/businesses/${encodeURIComponent(businessId)}/repos/${encodeURIComponent(targetRepo.owner)}/${encodeURIComponent(
4226
4181
  targetRepo.repo
4227
4182
  )}/sandbox-layer/assignment`,
4228
4183
  { method: "DELETE" }
4229
4184
  );
4230
- if (isJson(command, options)) {
4231
- writeJson(payload);
4232
- return;
4233
- }
4234
- console.log(`Cleared sandbox layer assignment for ${repoPath(targetRepo)}.`);
4185
+ emit(command, options, payload, () => console.log(`Cleared sandbox layer assignment for ${repoPath(targetRepo)}.`));
4235
4186
  }
4236
4187
 
4237
4188
  // src/commands/sessions.ts
@@ -4369,8 +4320,7 @@ async function usageCommand(sessionId, options, command) {
4369
4320
 
4370
4321
  // src/commands/stop.ts
4371
4322
  async function stopCommand(sessionId, options = {}, command) {
4372
- const runtime = getRuntimeOptions(command, options);
4373
- const config = requireConfig(runtime);
4323
+ const { config } = resolveBusinessContext(command, options);
4374
4324
  let status;
4375
4325
  try {
4376
4326
  const response = await apiFetch(config, `/api/sessions/${sessionId}/stop`, {
@@ -4382,17 +4332,15 @@ async function stopCommand(sessionId, options = {}, command) {
4382
4332
  if (!parsed) throw err;
4383
4333
  status = parsed.reason;
4384
4334
  }
4385
- if (isJson(command, options)) {
4386
- writeJson({ sessionId, status });
4387
- return;
4388
- }
4389
- if (status === "already_stopped") {
4390
- console.log(`Session ${sessionId} is already stopped.`);
4391
- } else if (status === "stopping" || status === "stopped") {
4392
- console.log(`Stop requested for session ${sessionId}.`);
4393
- } else {
4394
- console.log(`Session ${sessionId} cannot be stopped from phase=${status}.`);
4395
- }
4335
+ emit(command, options, { sessionId, status }, (payload) => {
4336
+ if (payload.status === "already_stopped") {
4337
+ console.log(`Session ${payload.sessionId} is already stopped.`);
4338
+ } else if (payload.status === "stopping" || payload.status === "stopped") {
4339
+ console.log(`Stop requested for session ${payload.sessionId}.`);
4340
+ } else {
4341
+ console.log(`Session ${payload.sessionId} cannot be stopped from phase=${payload.status}.`);
4342
+ }
4343
+ });
4396
4344
  }
4397
4345
  function parseStopBlocked(err) {
4398
4346
  if (!(err instanceof ApiError) || err.status !== 409) return null;
@@ -4428,26 +4376,22 @@ function readValue(options) {
4428
4376
  return raw.replace(/\r?\n$/, "");
4429
4377
  }
4430
4378
  async function listTestCredentialsCommand(repo, options, command) {
4431
- const runtime = getRuntimeOptions(command, options);
4432
- const config = requireConfig(runtime);
4379
+ const { config } = resolveBusinessContext(command, options);
4433
4380
  const repoArg = parseRepoArg3(repo);
4434
4381
  const payload = await apiFetch(config, basePath(options.business, repoArg));
4435
- if (isJson(command, options)) {
4436
- writeJson(payload);
4437
- return;
4438
- }
4439
- if (payload.credentials.length === 0) {
4440
- console.log(`No test credentials configured for ${repoArg.owner}/${repoArg.name}.`);
4441
- return;
4442
- }
4443
- for (const cred of payload.credentials) {
4444
- const ts = new Date(cred.updatedAt).toISOString();
4445
- console.log(`${cred.name} ${ts} ${cred.rotatedByUserId ?? "-"}`);
4446
- }
4382
+ emit(command, options, payload, (listPayload) => {
4383
+ if (listPayload.credentials.length === 0) {
4384
+ console.log(`No test credentials configured for ${repoArg.owner}/${repoArg.name}.`);
4385
+ return;
4386
+ }
4387
+ for (const cred of listPayload.credentials) {
4388
+ const ts = new Date(cred.updatedAt).toISOString();
4389
+ console.log(`${cred.name} ${ts} ${cred.rotatedByUserId ?? "-"}`);
4390
+ }
4391
+ });
4447
4392
  }
4448
4393
  async function setTestCredentialCommand(repo, name, options, command) {
4449
- const runtime = getRuntimeOptions(command, options);
4450
- const config = requireConfig(runtime);
4394
+ const { config } = resolveBusinessContext(command, options);
4451
4395
  const repoArg = parseRepoArg3(repo);
4452
4396
  const value = readValue(options);
4453
4397
  const payload = await apiFetch(
@@ -4455,11 +4399,12 @@ async function setTestCredentialCommand(repo, name, options, command) {
4455
4399
  `${basePath(options.business, repoArg)}/${encodeURIComponent(name)}`,
4456
4400
  { method: "PUT", body: JSON.stringify({ value }) }
4457
4401
  );
4458
- if (isJson(command, options)) {
4459
- writeJson(payload);
4460
- return;
4461
- }
4462
- console.log(`Set test credential '${name}' for ${repoArg.owner}/${repoArg.name}.`);
4402
+ emit(
4403
+ command,
4404
+ options,
4405
+ payload,
4406
+ () => console.log(`Set test credential '${name}' for ${repoArg.owner}/${repoArg.name}.`)
4407
+ );
4463
4408
  }
4464
4409
  async function deleteTestCredentialCommand(repo, name, options, command) {
4465
4410
  if (isJson(command, options) && options.yes !== true) {
@@ -4469,25 +4414,24 @@ async function deleteTestCredentialCommand(repo, name, options, command) {
4469
4414
  const repoArg2 = parseRepoArg3(repo);
4470
4415
  await confirmOrThrow(`Delete test credential '${name}' for ${repoArg2.owner}/${repoArg2.name}?`);
4471
4416
  }
4472
- const runtime = getRuntimeOptions(command, options);
4473
- const config = requireConfig(runtime);
4417
+ const { config } = resolveBusinessContext(command, options);
4474
4418
  const repoArg = parseRepoArg3(repo);
4475
4419
  const payload = await apiFetch(
4476
4420
  config,
4477
4421
  `${basePath(options.business, repoArg)}/${encodeURIComponent(name)}`,
4478
4422
  { method: "DELETE" }
4479
4423
  );
4480
- if (isJson(command, options)) {
4481
- writeJson(payload);
4482
- return;
4483
- }
4484
- console.log(`Deleted test credential '${name}' for ${repoArg.owner}/${repoArg.name}.`);
4424
+ emit(
4425
+ command,
4426
+ options,
4427
+ payload,
4428
+ () => console.log(`Deleted test credential '${name}' for ${repoArg.owner}/${repoArg.name}.`)
4429
+ );
4485
4430
  }
4486
4431
 
4487
4432
  // src/commands/tokens.ts
4488
4433
  async function listTokensCommand(options, command) {
4489
- const runtime = getRuntimeOptions(command, options);
4490
- const config = requireConfig(runtime);
4434
+ const { config } = resolveBusinessContext(command, options);
4491
4435
  const query = new URLSearchParams();
4492
4436
  if (options.limit) query.set("limit", options.limit);
4493
4437
  if (options.cursor) query.set("cursor", options.cursor);
@@ -4495,24 +4439,21 @@ async function listTokensCommand(options, command) {
4495
4439
  config,
4496
4440
  `/api/cli-tokens${query.size ? `?${query.toString()}` : ""}`
4497
4441
  );
4498
- if (isJson(command, options)) {
4499
- writeJson(payload);
4500
- return;
4501
- }
4502
- if (payload.data.length === 0) {
4503
- console.log("No CLI tokens found.");
4504
- return;
4505
- }
4506
- for (const token of payload.data) {
4507
- console.log(
4508
- `${String(token.id)} ${String(token.scope)} ${String(token.tokenPrefix)} ${String(token.revokedAt ? "revoked" : "active")}`
4509
- );
4510
- }
4511
- if (payload.nextCursor) console.log(`Next cursor: ${payload.nextCursor}`);
4442
+ emit(command, options, payload, (listPayload) => {
4443
+ if (listPayload.data.length === 0) {
4444
+ console.log("No CLI tokens found.");
4445
+ return;
4446
+ }
4447
+ for (const token of listPayload.data) {
4448
+ console.log(
4449
+ `${String(token.id)} ${String(token.scope)} ${String(token.tokenPrefix)} ${String(token.revokedAt ? "revoked" : "active")}`
4450
+ );
4451
+ }
4452
+ if (listPayload.nextCursor) console.log(`Next cursor: ${listPayload.nextCursor}`);
4453
+ });
4512
4454
  }
4513
4455
  async function createTokenCommand(options, command) {
4514
- const runtime = getRuntimeOptions(command, options);
4515
- const config = requireConfig(runtime);
4456
+ const { config } = resolveBusinessContext(command, options);
4516
4457
  const expiresInDays = options.expiresInDays === void 0 ? void 0 : Number(options.expiresInDays);
4517
4458
  if (expiresInDays !== void 0 && (!Number.isInteger(expiresInDays) || expiresInDays <= 0)) {
4518
4459
  throw new CliError("user", "expiresInDays must be a positive integer.");
@@ -4524,13 +4465,11 @@ async function createTokenCommand(options, command) {
4524
4465
  ...expiresInDays !== void 0 ? { expiresInDays } : {}
4525
4466
  })
4526
4467
  });
4527
- if (isJson(command, options)) {
4528
- writeJson(payload);
4529
- return;
4530
- }
4531
- console.log(`Token: ${String(payload.token)}`);
4532
- console.log(`ID: ${String(payload.id)}`);
4533
- console.log(`Scope: ${String(payload.scope)}`);
4468
+ emit(command, options, payload, (tokenPayload) => {
4469
+ console.log(`Token: ${String(tokenPayload.token)}`);
4470
+ console.log(`ID: ${String(tokenPayload.id)}`);
4471
+ console.log(`Scope: ${String(tokenPayload.scope)}`);
4472
+ });
4534
4473
  }
4535
4474
  async function revokeTokenCommand(tokenId, options, command) {
4536
4475
  if (isJson(command, options) && options.yes !== true) {
@@ -4539,28 +4478,18 @@ async function revokeTokenCommand(tokenId, options, command) {
4539
4478
  if (options.yes !== true) {
4540
4479
  await confirmOrThrow(`Revoke CLI token ${tokenId}?`);
4541
4480
  }
4542
- const runtime = getRuntimeOptions(command, options);
4543
- const config = requireConfig(runtime);
4481
+ const { config } = resolveBusinessContext(command, options);
4544
4482
  const payload = await apiFetch(config, `/api/cli-tokens/${tokenId}/revoke`, {
4545
4483
  method: "POST"
4546
4484
  });
4547
- if (isJson(command, options)) {
4548
- writeJson(payload);
4549
- return;
4550
- }
4551
- console.log(`Revoked token ${tokenId}.`);
4485
+ emit(command, options, payload, () => console.log(`Revoked token ${tokenId}.`));
4552
4486
  }
4553
4487
 
4554
4488
  // src/commands/transcript.ts
4555
4489
  async function transcriptCommand(sessionId, options, command) {
4556
- const runtime = getRuntimeOptions(command, options);
4557
- const config = requireConfig(runtime);
4490
+ const { config } = resolveBusinessContext(command, options);
4558
4491
  const exportData = await apiFetch(config, `/api/sessions/${sessionId}/export`);
4559
- if (isJson(command, options)) {
4560
- writeJson(exportData);
4561
- return;
4562
- }
4563
- console.log(renderSessionTranscript(exportData));
4492
+ emit(command, options, exportData, (payload) => console.log(renderSessionTranscript(payload)));
4564
4493
  }
4565
4494
 
4566
4495
  // src/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tryarcanist/cli",
3
- "version": "0.1.160",
3
+ "version": "0.1.161",
4
4
  "description": "CLI for Arcanist — create and manage coding agent sessions",
5
5
  "type": "module",
6
6
  "bin": {