@tryarcanist/cli 0.1.168 → 0.1.169

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +2 -1
  2. package/dist/index.js +56 -22
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -116,7 +116,7 @@ Exit codes:
116
116
  130 interrupted
117
117
  ```
118
118
 
119
- Mutation commands send an `Idempotency-Key` header. The CLI does not auto-retry mutation endpoints; retry explicitly with the same `--idempotency-key` when a request may have already reached the server. `--idempotency-key` is available on `sessions create` and `sessions send`.
119
+ Mutation commands send an `Idempotency-Key` header. The CLI does not auto-retry mutation endpoints; retry explicitly with the same `--idempotency-key` when a request may have already reached the server. `--idempotency-key` is available on `sessions create`, `sessions send`, `tokens create`, and `sandbox build`. `tokens create` and `sandbox build` use auto-random keys by default, so only an explicit key makes cross-invocation retries safe.
120
120
 
121
121
  ## Commands
122
122
 
@@ -360,6 +360,7 @@ Creates a CLI token and prints the plaintext token exactly once.
360
360
  ```bash
361
361
  arcanist tokens create --scope read
362
362
  arcanist tokens create --scope read --expires-in-days 30 --json
363
+ arcanist tokens create --scope read --idempotency-key 1f0e6f1a-...
363
364
  ```
364
365
 
365
366
  Read-scoped tokens cannot create write-scoped tokens; the server enforces that and returns 403.
package/dist/index.js CHANGED
@@ -4313,20 +4313,34 @@ async function sandboxBuildCommand(sourceRepoArg, options = {}, command) {
4313
4313
  const targetRepo = options.targetRepo ? parseRepoArg2(options.targetRepo, currentRepo) : null;
4314
4314
  const businessId = await resolveBusinessId(config, options);
4315
4315
  const ref = options.ref ?? currentRef();
4316
- const payload = await apiFetch(
4317
- config,
4318
- `/api/businesses/${encodeURIComponent(businessId)}/repos/${encodeURIComponent(sourceRepo.owner)}/${encodeURIComponent(
4319
- sourceRepo.repo
4320
- )}/sandbox-layer/build-requests`,
4321
- {
4322
- method: "POST",
4323
- body: JSON.stringify({
4324
- ref,
4325
- manifestPath,
4326
- ...targetRepo ? { targetRepo: { owner: targetRepo.owner, name: targetRepo.repo } } : {}
4327
- })
4316
+ let payload;
4317
+ try {
4318
+ payload = await apiFetch(
4319
+ config,
4320
+ `/api/businesses/${encodeURIComponent(businessId)}/repos/${encodeURIComponent(sourceRepo.owner)}/${encodeURIComponent(
4321
+ sourceRepo.repo
4322
+ )}/sandbox-layer/build-requests`,
4323
+ {
4324
+ method: "POST",
4325
+ headers: { "Idempotency-Key": options.idempotencyKey ?? randomIdempotencyKey() },
4326
+ body: JSON.stringify({
4327
+ ref,
4328
+ manifestPath,
4329
+ ...targetRepo ? { targetRepo: { owner: targetRepo.owner, name: targetRepo.repo } } : {}
4330
+ })
4331
+ }
4332
+ );
4333
+ } catch (err) {
4334
+ if (err instanceof ApiError && err.status === 409 && err.data?.serverCode === "duplicate_request") {
4335
+ throw new CliError(err.code, err.message, {
4336
+ exitCode: err.exitCode,
4337
+ requestId: err.requestId,
4338
+ data: err.data,
4339
+ hint: "Use the same --idempotency-key only for retrying the original sandbox build request."
4340
+ });
4328
4341
  }
4329
- );
4342
+ throw err;
4343
+ }
4330
4344
  emit(command, options, payload, (buildPayload) => printBuildRequest(buildPayload.buildRequest));
4331
4345
  if (!options.wait && !options.follow) return;
4332
4346
  const json = isJson(command, options);
@@ -4494,6 +4508,9 @@ async function sandboxAssignRepoCommand(targetRepoArg, sourceRepoArg, options =
4494
4508
  });
4495
4509
  }
4496
4510
  async function sandboxUnassignDefaultCommand(options = {}, command) {
4511
+ if (isJson(command, options) && options.yes !== true) {
4512
+ throw new CliError("user", "`sandbox unassign default --json` requires --yes.");
4513
+ }
4497
4514
  const { config } = resolveBusinessContext(command, options);
4498
4515
  const businessId = await resolveBusinessId(config, options);
4499
4516
  if (!options.yes) await confirmOrThrow("Clear the business default sandbox layer source?");
@@ -4505,6 +4522,9 @@ async function sandboxUnassignDefaultCommand(options = {}, command) {
4505
4522
  emit(command, options, payload, () => console.log("Cleared default sandbox layer source."));
4506
4523
  }
4507
4524
  async function sandboxUnassignRepoCommand(targetRepoArg, options = {}, command) {
4525
+ if (isJson(command, options) && options.yes !== true) {
4526
+ throw new CliError("user", "`sandbox unassign repo --json` requires --yes.");
4527
+ }
4508
4528
  const { config } = resolveBusinessContext(command, options);
4509
4529
  const businessId = await resolveBusinessId(config, options);
4510
4530
  const targetRepo = parseRepoArg2(targetRepoArg, currentRepo);
@@ -4790,13 +4810,27 @@ async function createTokenCommand(options, command) {
4790
4810
  if (expiresInDays !== void 0 && (!Number.isInteger(expiresInDays) || expiresInDays <= 0)) {
4791
4811
  throw new CliError("user", "expiresInDays must be a positive integer.");
4792
4812
  }
4793
- const payload = await apiFetch(config, "/api/cli-tokens", {
4794
- method: "POST",
4795
- body: JSON.stringify({
4796
- scope: options.scope ?? "read",
4797
- ...expiresInDays !== void 0 ? { expiresInDays } : {}
4798
- })
4799
- });
4813
+ let payload;
4814
+ try {
4815
+ payload = await apiFetch(config, "/api/cli-tokens", {
4816
+ method: "POST",
4817
+ headers: { "Idempotency-Key": options.idempotencyKey ?? randomIdempotencyKey() },
4818
+ body: JSON.stringify({
4819
+ scope: options.scope ?? "read",
4820
+ ...expiresInDays !== void 0 ? { expiresInDays } : {}
4821
+ })
4822
+ });
4823
+ } catch (err) {
4824
+ if (err instanceof ApiError && err.status === 409 && err.data?.serverCode === "duplicate_request") {
4825
+ throw new CliError(err.code, err.message, {
4826
+ exitCode: err.exitCode,
4827
+ requestId: err.requestId,
4828
+ data: err.data,
4829
+ hint: "Use a new --idempotency-key for a new token, or retry the original command only if the first result was lost."
4830
+ });
4831
+ }
4832
+ throw err;
4833
+ }
4800
4834
  emit(command, options, payload, (tokenPayload) => {
4801
4835
  console.log(`Token: ${String(tokenPayload.token)}`);
4802
4836
  console.log(`ID: ${String(tokenPayload.id)}`);
@@ -5062,7 +5096,7 @@ Examples:
5062
5096
  var sandbox = program.command("sandbox").description("Sandbox layer commands");
5063
5097
  sandbox.command("init").description("Create sandbox layer source files").action((options, command) => sandboxInitCommand(options, command));
5064
5098
  sandbox.command("validate").description("Validate local sandbox layer source files").option("--manifest <path>", "Manifest path", ".arcanist/sandbox.yaml").action((options, command) => sandboxValidateCommand(options, command));
5065
- sandbox.command("build").description("Build a repo-sourced sandbox layer").argument("[source-repo]", "Source repository owner/name; defaults to current git remote").option("--manifest <path>", "Manifest path", ".arcanist/sandbox.yaml").option("--ref <ref>", "Git ref to build").option("--target-repo <repo>", "Target repository owner/name for resource-profile coverage").option("--business <id>", "Business ID; defaults to authenticated user's business").option("--wait", "Wait for the build to finish").option("--follow", "Follow logs while waiting").option("--poll-interval <ms>", "Polling interval in milliseconds").action((sourceRepo, options, command) => sandboxBuildCommand(sourceRepo, options, command));
5099
+ sandbox.command("build").description("Build a repo-sourced sandbox layer").argument("[source-repo]", "Source repository owner/name; defaults to current git remote").option("--manifest <path>", "Manifest path", ".arcanist/sandbox.yaml").option("--ref <ref>", "Git ref to build").option("--target-repo <repo>", "Target repository owner/name for resource-profile coverage").option("--business <id>", "Business ID; defaults to authenticated user's business").option("--wait", "Wait for the build to finish").option("--follow", "Follow logs while waiting").option("--poll-interval <ms>", "Polling interval in milliseconds").option("--idempotency-key <uuid>", "Request idempotency key for safe manual retries").action((sourceRepo, options, command) => sandboxBuildCommand(sourceRepo, options, command));
5066
5100
  sandbox.command("status").description("Show sandbox layer status").argument("[repo]", "Repository owner/name; defaults to current git remote").option("--business <id>", "Business ID; defaults to authenticated user's business").action((repo, options, command) => sandboxStatusCommand(repo, options, command));
5067
5101
  sandbox.command("history").description("Show sandbox layer build history").argument("[source-repo]", "Source repository owner/name; defaults to current git remote").option("--business <id>", "Business ID; defaults to authenticated user's business").option("--target-repo <repo>", "Target repository owner/name for resource-profile coverage").option("--status <status>", "Filter by build status").option("--limit <n>", "Maximum builds to return").action((sourceRepo, options, command) => sandboxHistoryCommand(sourceRepo, options, command));
5068
5102
  sandbox.command("logs").description("Print sandbox layer build logs").argument("<build-id>", "Build ID").option("--business <id>", "Business ID; defaults to authenticated user's business").option("--follow", "Follow logs").option("--after-sequence <n>", "Return logs after this sequence").option("--limit <n>", "Maximum log chunks to return").option("--poll-interval <ms>", "Polling interval in milliseconds").action((buildId, options, command) => sandboxLogsCommand(buildId, options, command));
@@ -5084,7 +5118,7 @@ egress.command("sync").description("Apply the merged egress allowlist source fil
5084
5118
  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));
5085
5119
  var tokens = program.command("tokens").description("CLI token commands");
5086
5120
  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));
5087
- 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(
5121
+ 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").option("--idempotency-key <uuid>", "Request idempotency key for safe manual retries").addHelpText(
5088
5122
  "after",
5089
5123
  `
5090
5124
  Examples:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tryarcanist/cli",
3
- "version": "0.1.168",
3
+ "version": "0.1.169",
4
4
  "description": "CLI for Arcanist — create and manage coding agent sessions",
5
5
  "type": "module",
6
6
  "bin": {