deepspace 0.10.3 → 0.12.0

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/dist/cli.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli/cli.ts
4
- import { defineCommand as defineCommand13, runMain, runCommand } from "citty";
4
+ import { defineCommand as defineCommand14, runMain, runCommand } from "citty";
5
5
  import { sync as spawnSync8 } from "cross-spawn";
6
- import { readFileSync as readFileSync27, existsSync as existsSync25 } from "fs";
6
+ import { readFileSync as readFileSync28, existsSync as existsSync26 } from "fs";
7
7
  import { dirname as dirname10, join as join24 } from "path";
8
8
  import { fileURLToPath as fileURLToPath3 } from "url";
9
9
 
@@ -709,7 +709,7 @@ async function provisionProfile(jwt) {
709
709
  }
710
710
  }
711
711
  function sleep(ms) {
712
- return new Promise((resolve18) => setTimeout(resolve18, ms));
712
+ return new Promise((resolve19) => setTimeout(resolve19, ms));
713
713
  }
714
714
  function base64url(bytes) {
715
715
  return bytes.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
@@ -929,6 +929,7 @@ import { dirname as dirname2, join as join5, resolve as resolve3 } from "path";
929
929
  // src/cli/lib/git/process.ts
930
930
  import { sync as spawnSync } from "cross-spawn";
931
931
  var MAX_GIT_BUFFER = 512 * 1024 * 1024;
932
+ var GIT_TIMEOUT_MS = 3e5;
932
933
  var GitError = class extends Error {
933
934
  constructor(message, code) {
934
935
  super(message);
@@ -937,10 +938,16 @@ var GitError = class extends Error {
937
938
  }
938
939
  };
939
940
  function runGit(cwd, args, opts = {}) {
941
+ const timeout = opts.timeoutMs ?? GIT_TIMEOUT_MS;
940
942
  const result = spawnSync("git", args, {
941
943
  cwd,
942
944
  input: opts.input,
943
945
  maxBuffer: MAX_GIT_BUFFER,
946
+ timeout,
947
+ // SIGKILL, not the default SIGTERM: a child that ignores or traps TERM
948
+ // would otherwise keep running for its full duration and the "timeout"
949
+ // would bound nothing.
950
+ killSignal: "SIGKILL",
944
951
  stdio: ["pipe", "pipe", "pipe"],
945
952
  env: {
946
953
  ...process.env,
@@ -949,6 +956,12 @@ function runGit(cwd, args, opts = {}) {
949
956
  ...opts.env ?? {}
950
957
  }
951
958
  });
959
+ if (result.error?.code === "ETIMEDOUT") {
960
+ throw new GitError(
961
+ `git ${args[0]} did not finish within ${Math.round(timeout / 1e3)}s and was stopped. If this was a push, the history is probably too large to transfer \u2014 see \`deepspace push\` for the size limits; otherwise retry, and report it with \`deepspace feedback\` if it persists.`,
962
+ "git_timeout"
963
+ );
964
+ }
952
965
  if (result.error) {
953
966
  const code = result.error.code;
954
967
  if (code === "ENOENT") {
@@ -2534,10 +2547,10 @@ function formatSchemaLintFindings(findings) {
2534
2547
  import { createServer } from "net";
2535
2548
  var DEFAULT_PORT = 5173;
2536
2549
  function checkPortAvailable(port, host = "0.0.0.0") {
2537
- return new Promise((resolve18) => {
2550
+ return new Promise((resolve19) => {
2538
2551
  const srv = createServer();
2539
- srv.once("error", () => resolve18(false));
2540
- srv.once("listening", () => srv.close(() => resolve18(true)));
2552
+ srv.once("error", () => resolve19(false));
2553
+ srv.once("listening", () => srv.close(() => resolve19(true)));
2541
2554
  srv.listen(port, host);
2542
2555
  });
2543
2556
  }
@@ -3679,8 +3692,8 @@ var clear = defineDeepspaceCommand({
3679
3692
  failures.push({ email: a.email, error: err.message });
3680
3693
  }
3681
3694
  }
3682
- const deleted = targets.filter((target) => !failures.some((failure) => failure.email === target.email));
3683
- removeTestAccounts(deleted.map((target) => target.id), deleted.map((target) => target.email));
3695
+ const deleted = targets.filter((target2) => !failures.some((failure) => failure.email === target2.email));
3696
+ removeTestAccounts(deleted.map((target2) => target2.id), deleted.map((target2) => target2.email));
3684
3697
  if (failures.length > 0) {
3685
3698
  throw new Refusal(
3686
3699
  `Deleted ${ok2}/${targets.length} test account(s).
@@ -3715,6 +3728,7 @@ import { resolve as resolve12 } from "path";
3715
3728
  // src/cli/commands/deploy/build.ts
3716
3729
  import * as p6 from "@clack/prompts";
3717
3730
  import { execSync as execSync2 } from "child_process";
3731
+ import { createHash as createHash2 } from "crypto";
3718
3732
  import { existsSync as existsSync15, readFileSync as readFileSync17, readdirSync as readdirSync4 } from "fs";
3719
3733
  import { dirname as dirname5, join as join16, resolve as resolve10 } from "path";
3720
3734
 
@@ -4174,9 +4188,12 @@ function collectAssets(dir) {
4174
4188
  if (entry.isDirectory()) {
4175
4189
  walk(fullPath, relativePath);
4176
4190
  } else {
4191
+ const bytes = readFileSync17(fullPath);
4177
4192
  assets.push({
4178
4193
  path: `/${relativePath}`,
4179
- contentBase64: readFileSync17(fullPath).toString("base64")
4194
+ hash: createHash2("sha256").update(bytes).digest("hex"),
4195
+ size: bytes.byteLength,
4196
+ sourcePath: fullPath
4180
4197
  });
4181
4198
  }
4182
4199
  }
@@ -4463,7 +4480,7 @@ function createDeployOutput(json) {
4463
4480
 
4464
4481
  // src/cli/commands/deploy/request.ts
4465
4482
  import * as p10 from "@clack/prompts";
4466
- import { randomUUID as randomUUID2 } from "crypto";
4483
+ import { readFileSync as readFileSync18 } from "fs";
4467
4484
 
4468
4485
  // src/cli/commands/deploy/repository.ts
4469
4486
  import * as p9 from "@clack/prompts";
@@ -4653,6 +4670,37 @@ function repoApi(deployUrl, token, appId) {
4653
4670
  };
4654
4671
  }
4655
4672
 
4673
+ // src/shared/app-files.ts
4674
+ var MAX_APP_FILE_BYTES = 25 * 1024 * 1024;
4675
+ function formatBytes(bytes) {
4676
+ if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
4677
+ if (bytes >= 1024) return `${Math.round(bytes / 1024)} KiB`;
4678
+ return `${bytes} B`;
4679
+ }
4680
+ function encodeKeyPath(key) {
4681
+ return key.split("/").map(encodeURIComponent).join("/");
4682
+ }
4683
+ function oversizeMessage(bytes) {
4684
+ return `That file is ${formatBytes(bytes)}; the limit is ${formatBytes(MAX_APP_FILE_BYTES)} per file.`;
4685
+ }
4686
+ function describeFilesFailure(status3, body) {
4687
+ const trimmed = body.trim();
4688
+ if (trimmed.startsWith("{")) {
4689
+ try {
4690
+ const parsed = JSON.parse(trimmed);
4691
+ if (parsed.error) return parsed.error;
4692
+ } catch {
4693
+ }
4694
+ }
4695
+ if (status3 === 413) {
4696
+ return `The file is too large to upload. The limit is ${formatBytes(MAX_APP_FILE_BYTES)} per file; larger uploads are rejected by the network before the app receives them.`;
4697
+ }
4698
+ if (status3 === 401 || status3 === 403) return "Not authorized to write these files.";
4699
+ if (status3 === 415) return "That file type is not accepted.";
4700
+ if (status3 >= 500) return `The files service failed (HTTP ${status3}). Retry shortly.`;
4701
+ return `Upload failed (HTTP ${status3}).`;
4702
+ }
4703
+
4656
4704
  // src/cli/lib/vc-remote.ts
4657
4705
  import { existsSync as existsSync17 } from "fs";
4658
4706
  import { tmpdir } from "os";
@@ -4817,7 +4865,7 @@ function isThinPackRejection(result) {
4817
4865
  function isRecoverablePushFailure(status3) {
4818
4866
  return status3 === "non_fast_forward" || status3 === "ref_conflict";
4819
4867
  }
4820
- function classifyPushTransportFailure(error) {
4868
+ function classifyPushTransportFailure(error, cwd) {
4821
4869
  const message = error instanceof Error ? error.message : String(error);
4822
4870
  if (/(?:HTTP |error: )409\b/i.test(message)) {
4823
4871
  return {
@@ -4837,6 +4885,9 @@ function classifyPushTransportFailure(error) {
4837
4885
  error: "The cloud repo rate-limited this push. Wait a few seconds, then retry."
4838
4886
  };
4839
4887
  }
4888
+ if (/(?:HTTP |error: )413\b/i.test(message)) {
4889
+ return { code: "push_too_large", error: pushTooLargeMessage(cwd) };
4890
+ }
4840
4891
  return null;
4841
4892
  }
4842
4893
  function pushToSpace(cwd, token, refspec, opts = {}) {
@@ -4861,18 +4912,28 @@ function pushToSpace(cwd, token, refspec, opts = {}) {
4861
4912
  }
4862
4913
  var OVERSIZED_PUSH_RE = /too large|exceeds|\bLFS\b/i;
4863
4914
  var SERVER_OBJECT_CAP_BYTES = 20 * 1024 * 1024;
4864
- var OVERSIZED_PUSH_FIX = `Find the offending object with \`git rev-list --objects --all | git cat-file --batch-check='%(objecttype) %(objectsize) %(rest)' | sort -k2 -n | tail\`, then .gitignore it (or move it to Git LFS) and re-commit.`;
4865
- function formatBytes(bytes) {
4866
- if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
4867
- if (bytes >= 1024) return `${Math.round(bytes / 1024)} KiB`;
4868
- return `${bytes} B`;
4869
- }
4870
- function oversizedPushFix(cwd, cap = SERVER_OBJECT_CAP_BYTES) {
4871
- const objects = cwd ? findOversizedObjects(cwd, cap) : [];
4872
- if (objects.length === 0) return OVERSIZED_PUSH_FIX;
4915
+ var SERVER_PACK_CAP_BYTES = 32 * 1024 * 1024;
4916
+ var OVERSIZED_PUSH_FIX = `Find the offending object with \`git rev-list --objects --all | git cat-file --batch-check='%(objecttype) %(objectsize) %(rest)' | sort -k2 -n | tail\`.`;
4917
+ function removeFromHistoryAdvice(path) {
4918
+ const file = path ? shQuote(path) : "<file>";
4919
+ return `Media belongs in the app's files rather than Git history \u2014 \`deepspace app files put ${file}\` stores it in the app's own allocation and serves it over HTTP. Then get it out of the push: add it to .gitignore, and drop it from the commits that carry it \u2014 if those commits are still local, \`git reset --soft <last pushed commit>\` and re-commit without the file. \`git rm --cached\` plus a new commit is NOT sufficient on its own: the blob stays reachable from the earlier commit and the push is rejected the same way. If it already reached pushed history, rewrite it out (\`git filter-repo --path ${file} --invert-paths\`).`;
4920
+ }
4921
+ function fixForObjects(objects) {
4922
+ if (objects.length === 0) return `${OVERSIZED_PUSH_FIX} ${removeFromHistoryAdvice(null)}`;
4873
4923
  const one = objects.length === 1;
4874
4924
  const named = objects.map((object) => `${object.path} (${formatBytes(object.bytes)})`).join(", ");
4875
- return `The oversized ${one ? "file is" : "files are"} ${named}. Remove ${one ? "it" : "them"} from history (\`git rm --cached ${one ? shQuote(objects[0].path) : "<path>"}\`, add to .gitignore, re-commit) or track ${one ? "it" : "them"} with Git LFS.`;
4925
+ return `The oversized ${one ? "file is" : "files are"} ${named}. ${removeFromHistoryAdvice(one ? objects[0].path : null)}`;
4926
+ }
4927
+ function oversizedPushFix(cwd, cap = SERVER_OBJECT_CAP_BYTES) {
4928
+ return fixForObjects(cwd ? findOversizedObjects(cwd, cap) : []);
4929
+ }
4930
+ function pushTooLargeMessage(cwd) {
4931
+ const ceilings = `The cloud repo refused this push as too large (HTTP 413). The ceilings are ${formatBytes(SERVER_OBJECT_CAP_BYTES)} per file and ${formatBytes(SERVER_PACK_CAP_BYTES)} of compressed history per push. Large media never compresses, so every clone would pay for it forever.`;
4932
+ const objects = cwd ? findOversizedObjects(cwd, SERVER_OBJECT_CAP_BYTES) : [];
4933
+ if (objects.length === 0) {
4934
+ return `${ceilings} No single file is over the per-file cap, so this is the volume of history \u2014 push it in smaller batches (\`git push space <earlier-commit>:refs/heads/<branch>\` first, then the rest). ${OVERSIZED_PUSH_FIX}`;
4935
+ }
4936
+ return `${ceilings} ${fixForObjects(objects)}`;
4876
4937
  }
4877
4938
  function pushFailureMessage(label, push, cwd) {
4878
4939
  const reason = push.reason ?? push.summary ?? "";
@@ -4962,7 +5023,7 @@ function remotePublicRefs(cwd, remote) {
4962
5023
  function remoteBranchOid(cwd, remote, branch) {
4963
5024
  return remotePublicRefs(cwd, remote).find((ref) => ref.name === `refs/heads/${branch}`)?.oid ?? null;
4964
5025
  }
4965
- function localTransferRefs(cwd, prefix, target) {
5026
+ function localTransferRefs(cwd, prefix, target2) {
4966
5027
  const output = runGit(cwd, [
4967
5028
  "for-each-ref",
4968
5029
  "--format=%(refname) %(objectname)",
@@ -4970,7 +5031,7 @@ function localTransferRefs(cwd, prefix, target) {
4970
5031
  ]).stdout.toString("utf-8");
4971
5032
  return output.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => {
4972
5033
  const [name, oid] = line.split(/\s+/, 2);
4973
- return { name: `${target}/${name.slice(`${prefix}/`.length)}`, oid };
5034
+ return { name: `${target2}/${name.slice(`${prefix}/`.length)}`, oid };
4974
5035
  }).filter((ref) => /^[0-9a-f]{40}$/.test(ref.oid));
4975
5036
  }
4976
5037
 
@@ -5083,7 +5144,7 @@ async function syncDeployRepository(options) {
5083
5144
  }
5084
5145
  } else if (pushResult.status === "rejected") {
5085
5146
  output.die(
5086
- `Cloud repo rejected ${branch}${pushResult.reason ? `: ${pushResult.reason}` : ""}. Resolve the rejection and publish the commit before deploying.`,
5147
+ `${pushFailureMessage("Cloud repo push", pushResult, appDir)} Publish the commit before deploying.`,
5087
5148
  "vc_push_rejected"
5088
5149
  );
5089
5150
  } else {
@@ -5127,7 +5188,7 @@ async function syncDeployRepository(options) {
5127
5188
  sourceRevision = claimed.revision;
5128
5189
  }
5129
5190
  } catch (error) {
5130
- const failure = deployRepositoryFailure(error);
5191
+ const failure = deployRepositoryFailure(error, appDir);
5131
5192
  output.die(failure.error, failure.code);
5132
5193
  }
5133
5194
  return { commitOid, recoverable, deployKey, source, sourceRevision, baseReleaseId };
@@ -5154,8 +5215,8 @@ function classifyRemoteState(appDir, token, branch, localTip, remote) {
5154
5215
  function errorMessage3(error) {
5155
5216
  return error instanceof Error ? error.message : String(error);
5156
5217
  }
5157
- function deployRepositoryFailure(error) {
5158
- const transportFailure = classifyPushTransportFailure(error);
5218
+ function deployRepositoryFailure(error, cwd) {
5219
+ const transportFailure = classifyPushTransportFailure(error, cwd);
5159
5220
  if (transportFailure) return transportFailure;
5160
5221
  const code = errorCode(error);
5161
5222
  const message = errorMessage3(error);
@@ -5165,15 +5226,18 @@ function deployRepositoryFailure(error) {
5165
5226
  error: `Version-control sync failed: ${message}`
5166
5227
  };
5167
5228
  }
5229
+ var PUSH_RETRY_BUDGET_MS = 12e4;
5168
5230
  async function pushWithTransientRetry(push) {
5169
5231
  const backoffMs = [500, 1500, 3500];
5232
+ const deadline = Date.now() + PUSH_RETRY_BUDGET_MS;
5170
5233
  for (let attempt = 0; ; attempt++) {
5171
5234
  try {
5172
5235
  return push();
5173
5236
  } catch (error) {
5174
5237
  const message = errorMessage3(error);
5175
- if (/(?:HTTP |error: )(?:429|503)\b/i.test(message) && attempt < backoffMs.length) {
5176
- await new Promise((resolve18) => setTimeout(resolve18, backoffMs[attempt]));
5238
+ const transient = /(?:HTTP |error: )(?:429|503)\b/i.test(message);
5239
+ if (transient && attempt < backoffMs.length && Date.now() + backoffMs[attempt] < deadline) {
5240
+ await new Promise((resolve19) => setTimeout(resolve19, backoffMs[attempt]));
5177
5241
  continue;
5178
5242
  }
5179
5243
  throw error;
@@ -5203,12 +5267,9 @@ function detachedHeadRefusal() {
5203
5267
 
5204
5268
  // src/cli/commands/deploy/request.ts
5205
5269
  var CLOUDFLARE_DEPLOY_ERROR_HINT = "Deployment reached the DeepSpace deploy worker, but Cloudflare's deployment control-plane API failed. This is often caused by a Cloudflare Dashboard/API incident or Workers for Platforms entitlement outage. Already deployed apps should keep serving; wait for Cloudflare to recover and retry.";
5206
- var MAX_GROUP_BYTES = 40 * 1024 * 1024;
5207
- var GROUP_BYTES = (() => {
5208
- const configured = Number(process.env.DEEPSPACE_DEPLOY_GROUP_BYTES);
5209
- const bytes = Number.isInteger(configured) && configured > 0 ? configured : 3 * 1024 * 1024;
5210
- return Math.min(bytes, MAX_GROUP_BYTES);
5211
- })();
5270
+ var DEPLOY_SERVICE_LIMIT_HINT = "The DeepSpace deploy service hit a resource limit processing this deploy (the deploy worker was terminated before it could answer). This is a DeepSpace-side failure, not a Cloudflare incident \u2014 your app was not changed. Retry; if it keeps happening, report the app id and asset count.";
5271
+ var REQUIRED_ASSET_TRANSPORT = "content-addressed-v1";
5272
+ var UPLOAD_CONCURRENCY = 4;
5212
5273
  async function deployBuiltBundle(options) {
5213
5274
  const {
5214
5275
  deployUrl,
@@ -5229,44 +5290,11 @@ async function deployBuiltBundle(options) {
5229
5290
  } else if (bundle.extraRoutes.length) {
5230
5291
  p10.log.info(`Custom worker-first routes: ${bundle.extraRoutes.join(", ")}`);
5231
5292
  }
5232
- const assetGroups = packAssetGroups(bundle.assets, GROUP_BYTES);
5233
- if (assetGroups.length === 0) assetGroups.push([]);
5234
- const uploadId = randomUUID2();
5235
- const totalGroups = assetGroups.length;
5236
- try {
5237
- for (let index = 0; index < totalGroups; index++) {
5238
- const groupJson = JSON.stringify(assetGroups[index]);
5239
- spinner2.message(`Uploading assets \u2014 group ${index + 1}/${totalGroups}...`);
5240
- const groupResponse = await postWithRetry(
5241
- `${deployUrl}/api/deploy/${appId}/assets?uploadId=${uploadId}&groupIndex=${index}&totalGroups=${totalGroups}`,
5242
- () => ({
5243
- method: "POST",
5244
- headers: {
5245
- Authorization: `Bearer ${token}`,
5246
- "Content-Type": "application/json"
5247
- },
5248
- body: groupJson
5249
- })
5250
- );
5251
- if (!groupResponse.ok) {
5252
- const errorBody = await groupResponse.json().catch(() => ({}));
5253
- await abortStagedUpload(deployUrl, appId, uploadId, totalGroups, token);
5254
- spinner2.stop("Deploy failed");
5255
- output.die(
5256
- `Asset upload failed on group ${index + 1}/${totalGroups}: ${errorBody.error ?? `HTTP ${groupResponse.status}`}`,
5257
- "upload_failed"
5258
- );
5259
- }
5260
- }
5261
- } catch (error) {
5262
- await abortStagedUpload(deployUrl, appId, uploadId, totalGroups, token);
5263
- spinner2.stop("Deploy failed");
5264
- output.die(`Asset upload failed (network): ${errorMessage4(error)}`, "upload_failed");
5265
- }
5293
+ await requireAssetTransport(deployUrl, spinner2, output);
5294
+ await uploadDeployAssets({ deployUrl, appId, token, assets: bundle.assets, output, spinner: spinner2 });
5266
5295
  spinner2.message(`Deploying ${appName}...`);
5267
5296
  let confirmRename = options.rename;
5268
5297
  const bail = async (message, stopLabel = "Deploy failed", code, actionRequired = false, action) => {
5269
- await abortStagedUpload(deployUrl, appId, uploadId, totalGroups, token);
5270
5298
  if (stopLabel !== null) spinner2.stop(stopLabel);
5271
5299
  p10.cancel(code ? withSlug(message, code) : message);
5272
5300
  if (action) printAction(action);
@@ -5288,8 +5316,7 @@ async function deployBuiltBundle(options) {
5288
5316
  new Blob([bundle.workerJs], { type: "application/javascript" }),
5289
5317
  "worker.js"
5290
5318
  );
5291
- form.append("uploadId", uploadId);
5292
- form.append("totalGroups", String(totalGroups));
5319
+ form.append("assetManifest", JSON.stringify(assetManifest(bundle.assets)));
5293
5320
  form.append("appMigrations", JSON.stringify(bundle.appMigrations));
5294
5321
  if (bundle.doManifest) form.append("doManifest", JSON.stringify(bundle.doManifest));
5295
5322
  if (bundle.customBindings.length) {
@@ -5368,7 +5395,11 @@ async function deployBuiltBundle(options) {
5368
5395
  );
5369
5396
  }
5370
5397
  if (!response.ok || !body.success) {
5371
- await bail(formatDeployWorkerError(response.status, body.error), "Deploy failed", body.code);
5398
+ await bail(
5399
+ formatDeployWorkerError(response.status, body.error, body.code),
5400
+ "Deploy failed",
5401
+ body.code
5402
+ );
5372
5403
  }
5373
5404
  if (body.onBehalfOfOwner) {
5374
5405
  p10.log.warn(`Deployed on behalf of owner ${body.onBehalfOfOwner}`);
@@ -5385,17 +5416,139 @@ async function deployBuiltBundle(options) {
5385
5416
  }
5386
5417
  return body;
5387
5418
  }
5388
- async function abortStagedUpload(deployUrl, appId, uploadId, totalGroups, token) {
5419
+ function assetManifest(assets) {
5420
+ return assets.map((asset) => ({ path: asset.path, hash: asset.hash, size: asset.size }));
5421
+ }
5422
+ async function requireAssetTransport(deployUrl, spinner2, output) {
5423
+ let advertised;
5389
5424
  try {
5390
- await fetch(
5391
- `${deployUrl}/api/deploy/${appId}/assets?uploadId=${uploadId}&totalGroups=${totalGroups}`,
5392
- { method: "DELETE", headers: { Authorization: `Bearer ${token}` } }
5425
+ const response = await fetch(`${deployUrl}/api/health`);
5426
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
5427
+ const body = await response.json();
5428
+ advertised = body.capabilities?.assetTransport;
5429
+ } catch (error) {
5430
+ spinner2.stop("Deploy failed");
5431
+ output.die(
5432
+ `Could not reach the DeepSpace deploy service at ${deployUrl} to check its capabilities: ${errorMessage4(error)}`,
5433
+ "deploy_service_unreachable"
5393
5434
  );
5394
- } catch {
5435
+ }
5436
+ if (advertised !== REQUIRED_ASSET_TRANSPORT) {
5437
+ spinner2.stop("Deploy failed");
5438
+ output.die(
5439
+ `The DeepSpace deploy service at ${deployUrl} does not support this CLI's asset transport (needs "${REQUIRED_ASSET_TRANSPORT}", found ${advertised === void 0 ? "none" : `"${String(advertised)}"`}). Wait for the platform to finish updating, or pin an older deepspace CLI.`,
5440
+ "asset_transport_unsupported"
5441
+ );
5442
+ }
5443
+ }
5444
+ async function uploadDeployAssets(options) {
5445
+ const { deployUrl, appId, token, assets, spinner: spinner2 } = options;
5446
+ const output = options.output;
5447
+ const byHash = new Map(assets.map((asset) => [asset.hash, asset]));
5448
+ spinner2.message("Checking which assets are already uploaded...");
5449
+ let planResponse;
5450
+ try {
5451
+ planResponse = await postWithRetry(`${deployUrl}/api/deploy/${appId}/asset-plan`, () => ({
5452
+ method: "POST",
5453
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
5454
+ body: JSON.stringify({
5455
+ assets: [...byHash.values()].map((asset) => ({ hash: asset.hash, size: asset.size }))
5456
+ })
5457
+ }));
5458
+ } catch (error) {
5459
+ spinner2.stop("Deploy failed");
5460
+ output.die(`Asset plan failed (network): ${errorMessage4(error)}`, "upload_failed");
5461
+ }
5462
+ const planBody = await planResponse.json().catch(() => ({}));
5463
+ if (!planResponse.ok) {
5464
+ spinner2.stop("Deploy failed");
5465
+ output.die(
5466
+ formatDeployWorkerError(planResponse.status, planBody.error, planBody.code),
5467
+ planBody.code ?? "upload_failed"
5468
+ );
5469
+ }
5470
+ const missing = [];
5471
+ for (const hash of planBody.missing ?? []) {
5472
+ const asset = byHash.get(hash);
5473
+ if (!asset) {
5474
+ spinner2.stop("Deploy failed");
5475
+ output.die(
5476
+ `The deploy service asked for asset ${hash}, which is not part of this build. Re-run the deploy; if it persists, report the app id.`,
5477
+ "upload_failed"
5478
+ );
5479
+ }
5480
+ missing.push(asset);
5481
+ }
5482
+ if (missing.length === 0) {
5483
+ p10.log.info(`Assets: ${byHash.size} file(s), all already uploaded`);
5484
+ return;
5485
+ }
5486
+ const missingBytes = missing.reduce((total, asset) => total + asset.size, 0);
5487
+ p10.log.info(
5488
+ `Assets: ${byHash.size} file(s), uploading ${missing.length} (${Math.ceil(missingBytes / 1024)} KiB); the rest are already stored`
5489
+ );
5490
+ let uploaded = 0;
5491
+ const next = () => missing.pop();
5492
+ const worker = async () => {
5493
+ for (let asset = next(); asset; asset = next()) {
5494
+ const response = await postWithRetry(
5495
+ `${deployUrl}/api/deploy/${appId}/assets/${asset.hash}`,
5496
+ () => ({
5497
+ method: "PUT",
5498
+ headers: {
5499
+ Authorization: `Bearer ${token}`,
5500
+ "Content-Type": "application/octet-stream"
5501
+ },
5502
+ // Read whole, per attempt. The platform matches Content-Length
5503
+ // against the plan before it touches storage, and a stream body
5504
+ // would be sent chunked with no length at all; a retry also needs a
5505
+ // body it can send again.
5506
+ body: readFileSync18(asset.sourcePath)
5507
+ })
5508
+ );
5509
+ if (!response.ok) {
5510
+ const body = await response.json().catch(() => ({}));
5511
+ throw new AssetUploadError(
5512
+ body.error ?? `Upload of ${asset.path} failed: HTTP ${response.status}`,
5513
+ body.code ?? "upload_failed",
5514
+ response.status
5515
+ );
5516
+ }
5517
+ uploaded++;
5518
+ spinner2.message(`Uploading assets \u2014 ${uploaded}/${uploaded + missing.length}...`);
5519
+ }
5520
+ };
5521
+ try {
5522
+ await Promise.all(
5523
+ Array.from({ length: Math.min(UPLOAD_CONCURRENCY, missing.length) }, worker)
5524
+ );
5525
+ } catch (error) {
5526
+ spinner2.stop("Deploy failed");
5527
+ if (error instanceof AssetUploadError) {
5528
+ output.die(formatDeployWorkerError(error.status, error.message, error.code), error.code);
5529
+ }
5530
+ output.die(`Asset upload failed (network): ${errorMessage4(error)}`, "upload_failed");
5395
5531
  }
5396
5532
  }
5397
- function formatDeployWorkerError(status3, error) {
5533
+ var AssetUploadError = class extends Error {
5534
+ constructor(message, code, status3) {
5535
+ super(message);
5536
+ this.code = code;
5537
+ this.status = status3;
5538
+ }
5539
+ };
5540
+ function isDeployServiceResourceLimit(status3, error) {
5541
+ if (!error) return status3 === 500 || status3 === 502;
5542
+ return /\b110[12]\b/.test(error) && /worker|error code/i.test(error);
5543
+ }
5544
+ function formatDeployWorkerError(status3, error, code) {
5398
5545
  const detail = error ?? `Deployment error (${status3})`;
5546
+ if (code === "cli_outdated") return detail;
5547
+ if (isDeployServiceResourceLimit(status3, error)) {
5548
+ return `${DEPLOY_SERVICE_LIMIT_HINT}
5549
+
5550
+ Underlying error: ${detail}`;
5551
+ }
5399
5552
  if (status3 < 500 && ![
5400
5553
  "Upload session failed",
5401
5554
  "Asset upload failed",
@@ -5412,23 +5565,6 @@ Underlying error: ${detail}`;
5412
5565
  function errorMessage4(error) {
5413
5566
  return error instanceof Error ? error.message : String(error);
5414
5567
  }
5415
- function packAssetGroups(assets, maxBytes) {
5416
- const groups = [];
5417
- let current = [];
5418
- let currentBytes = 2;
5419
- for (const asset of assets) {
5420
- const entryBytes = Buffer.byteLength(JSON.stringify(asset), "utf-8") + 1;
5421
- if (current.length > 0 && currentBytes + entryBytes > maxBytes) {
5422
- groups.push(current);
5423
- current = [];
5424
- currentBytes = 2;
5425
- }
5426
- current.push(asset);
5427
- currentBytes += entryBytes;
5428
- }
5429
- if (current.length > 0) groups.push(current);
5430
- return groups;
5431
- }
5432
5568
  async function postWithRetry(url, makeInit, {
5433
5569
  attempts = 4,
5434
5570
  retryServerErrors = true
@@ -5445,7 +5581,7 @@ async function postWithRetry(url, makeInit, {
5445
5581
  lastError = error;
5446
5582
  }
5447
5583
  if (attempt < attempts) {
5448
- await new Promise((resolve18) => setTimeout(resolve18, Math.min(8e3, 500 * 2 ** (attempt - 1))));
5584
+ await new Promise((resolve19) => setTimeout(resolve19, Math.min(8e3, 500 * 2 ** (attempt - 1))));
5449
5585
  }
5450
5586
  }
5451
5587
  if (lastResponse) return lastResponse;
@@ -5454,7 +5590,7 @@ async function postWithRetry(url, makeInit, {
5454
5590
 
5455
5591
  // src/cli/commands/deploy/secrets.ts
5456
5592
  import * as p11 from "@clack/prompts";
5457
- import { existsSync as existsSync18, readFileSync as readFileSync18 } from "fs";
5593
+ import { existsSync as existsSync18, readFileSync as readFileSync19 } from "fs";
5458
5594
  function classifyDevVarsSecrets(options) {
5459
5595
  const store = new Set(options.storeSecretNames);
5460
5596
  const strayKeys = options.handEditedDevVarKeys.filter((key) => !store.has(key));
@@ -5553,7 +5689,7 @@ function prepareDeploySecrets(options) {
5553
5689
  }
5554
5690
  function readCustomDevVars(path, output) {
5555
5691
  if (!existsSync18(path)) return {};
5556
- const section = extractCustomDevVars(stripGeneratedSecretsCache(readFileSync18(path, "utf-8")));
5692
+ const section = extractCustomDevVars(stripGeneratedSecretsCache(readFileSync19(path, "utf-8")));
5557
5693
  if (!section) return {};
5558
5694
  try {
5559
5695
  return parseDevVars(section);
@@ -5827,7 +5963,7 @@ async function waitForAssetsReady(url, timeoutMs) {
5827
5963
  if (!body.includes("Assets have not yet deployed") && !body.includes("No app configured")) {
5828
5964
  streak++;
5829
5965
  if (streak >= 3) return true;
5830
- await new Promise((resolve18) => setTimeout(resolve18, 1500));
5966
+ await new Promise((resolve19) => setTimeout(resolve19, 1500));
5831
5967
  continue;
5832
5968
  }
5833
5969
  streak = 0;
@@ -5835,7 +5971,7 @@ async function waitForAssetsReady(url, timeoutMs) {
5835
5971
  streak = 0;
5836
5972
  }
5837
5973
  const waitMs = Math.min(8e3, 1e3 * 2 ** (attempt - 1));
5838
- await new Promise((resolve18) => setTimeout(resolve18, waitMs));
5974
+ await new Promise((resolve19) => setTimeout(resolve19, waitMs));
5839
5975
  }
5840
5976
  return false;
5841
5977
  }
@@ -6479,7 +6615,7 @@ var usage_default = defineDeepspaceCommand({
6479
6615
  });
6480
6616
 
6481
6617
  // src/cli/commands/add.ts
6482
- import { existsSync as existsSync20, readFileSync as readFileSync20, realpathSync as realpathSync3 } from "fs";
6618
+ import { existsSync as existsSync20, readFileSync as readFileSync21, realpathSync as realpathSync3 } from "fs";
6483
6619
  import { dirname as dirname7, join as join19, resolve as resolve14 } from "path";
6484
6620
  import { pathToFileURL as pathToFileURL3 } from "url";
6485
6621
 
@@ -6492,7 +6628,7 @@ import {
6492
6628
  mkdirSync as mkdirSync6,
6493
6629
  mkdtempSync,
6494
6630
  openSync as openSync2,
6495
- readFileSync as readFileSync19,
6631
+ readFileSync as readFileSync20,
6496
6632
  readSync,
6497
6633
  readdirSync as readdirSync5,
6498
6634
  rmSync as rmSync4,
@@ -6509,7 +6645,7 @@ function featurePackageRoot(moduleUrl = import.meta.url) {
6509
6645
  const packagePath = join18(dir, "package.json");
6510
6646
  if (existsSync19(packagePath)) {
6511
6647
  try {
6512
- const pkg2 = JSON.parse(readFileSync19(packagePath, "utf-8"));
6648
+ const pkg2 = JSON.parse(readFileSync20(packagePath, "utf-8"));
6513
6649
  if (pkg2.name === "deepspace") return dir;
6514
6650
  } catch {
6515
6651
  }
@@ -6526,7 +6662,7 @@ function readFeatureConfig(id, packageRoot = featurePackageRoot()) {
6526
6662
  const configPath = join18(packageRoot, "features", id, "feature.json");
6527
6663
  if (!existsSync19(configPath)) return null;
6528
6664
  try {
6529
- const config = JSON.parse(readFileSync19(configPath, "utf-8"));
6665
+ const config = JSON.parse(readFileSync20(configPath, "utf-8"));
6530
6666
  if (typeof config.id !== "string" || typeof config.name !== "string" || !Array.isArray(config.files)) {
6531
6667
  return null;
6532
6668
  }
@@ -6591,7 +6727,7 @@ function copyFile(src, dest) {
6591
6727
  function copyFileWithImportShift(src, dest, levels) {
6592
6728
  mkdirSync6(dirname6(dest), { recursive: true });
6593
6729
  const prefix = "../".repeat(levels);
6594
- const shifted = readFileSync19(src, "utf-8").replace(
6730
+ const shifted = readFileSync20(src, "utf-8").replace(
6595
6731
  /(from\s+|import\s+|import\()(['"])(\.\.\/)/g,
6596
6732
  `$1$2${prefix}$3`
6597
6733
  );
@@ -6650,7 +6786,7 @@ function integrateSchema(config, targetDir, emit) {
6650
6786
  if (!existsSync19(schemasPath)) {
6651
6787
  return [unresolvedSchema(config.schema, "src/schemas.ts was not found.", emit)];
6652
6788
  }
6653
- let content = readFileSync19(schemasPath, "utf-8");
6789
+ let content = readFileSync20(schemasPath, "utf-8");
6654
6790
  const { exportName, importPath, spreadOperator } = config.schema;
6655
6791
  const importPattern = new RegExp(`^import\\s+\\{[^}]*\\b${exportName}\\b`, "m");
6656
6792
  if (importPattern.test(content)) {
@@ -6707,7 +6843,7 @@ function integrateActions(config, targetDir, emit) {
6707
6843
  if (!existsSync19(actionsPath)) {
6708
6844
  return [unresolvedActions(config.actions, "src/actions/index.ts was not found.", emit)];
6709
6845
  }
6710
- let content = readFileSync19(actionsPath, "utf-8");
6846
+ let content = readFileSync20(actionsPath, "utf-8");
6711
6847
  const registryPattern = /export const actions:\s*Record<string,\s*ActionHandler(?:<[^>]*>)?>\s*=\s*\{/;
6712
6848
  if (!registryPattern.test(content)) {
6713
6849
  const reason = "src/actions/index.ts has an unexpected structure.";
@@ -6762,7 +6898,7 @@ function integrateCode(config, targetDir, emit) {
6762
6898
  });
6763
6899
  continue;
6764
6900
  }
6765
- const original = readFileSync19(targetPath, "utf-8");
6901
+ const original = readFileSync20(targetPath, "utf-8");
6766
6902
  if (original.includes(insertion.insert.trim())) {
6767
6903
  emit(` Integration already present: ${insertion.file}`);
6768
6904
  continue;
@@ -6798,7 +6934,7 @@ ${content.slice(importIndex < 0 ? 0 : importIndex)}`;
6798
6934
  function integrateIgnore(config, targetDir) {
6799
6935
  if (!config.ignore?.length) return;
6800
6936
  const targetPath = join18(targetDir, ".gitignore");
6801
- const current = existsSync19(targetPath) ? readFileSync19(targetPath, "utf-8") : "";
6937
+ const current = existsSync19(targetPath) ? readFileSync20(targetPath, "utf-8") : "";
6802
6938
  const present = new Set(current.split(/\r?\n/).map((line) => line.trim()));
6803
6939
  const missing = config.ignore.filter((pattern) => !present.has(pattern));
6804
6940
  if (missing.length === 0) return;
@@ -6818,7 +6954,7 @@ function integrateWrangler(config, targetDir, emit) {
6818
6954
  if (!existsSync19(targetPath)) {
6819
6955
  return [{ area: "wrangler", reason: "wrangler.toml was not found.", steps }];
6820
6956
  }
6821
- let source = readFileSync19(targetPath, "utf-8");
6957
+ let source = readFileSync20(targetPath, "utf-8");
6822
6958
  if (runWorkerFirst && !/^\[assets\]\s*$/m.test(source)) {
6823
6959
  return [{ area: "wrangler", reason: "wrangler.toml has no [assets] section.", steps }];
6824
6960
  }
@@ -6907,7 +7043,7 @@ function integrateCss(packageRoot, config, targetDir, emit) {
6907
7043
  }
6908
7044
  ];
6909
7045
  }
6910
- let styles = readFileSync19(stylesPath, "utf-8");
7046
+ let styles = readFileSync20(stylesPath, "utf-8");
6911
7047
  let integrated = 0;
6912
7048
  const unresolved = [];
6913
7049
  for (const cssFile of config.css) {
@@ -6921,7 +7057,7 @@ function integrateCss(packageRoot, config, targetDir, emit) {
6921
7057
  });
6922
7058
  continue;
6923
7059
  }
6924
- const css = readFileSync19(srcPath, "utf-8");
7060
+ const css = readFileSync20(srcPath, "utf-8");
6925
7061
  const fingerprint = css.match(/\.([\w-]+)\s*\{|@keyframes\s+([\w-]+)/);
6926
7062
  const marker = fingerprint?.[1] ?? fingerprint?.[2];
6927
7063
  if (marker && styles.includes(marker)) {
@@ -6952,7 +7088,7 @@ function integrateRoute(config, targetDir, emit) {
6952
7088
  if (!existsSync19(navPath)) {
6953
7089
  return [unresolvedRoute(config.route, config, "src/nav.ts was not found.", emit)];
6954
7090
  }
6955
- let content = readFileSync19(navPath, "utf-8");
7091
+ let content = readFileSync20(navPath, "utf-8");
6956
7092
  const routePath = config.route.path;
6957
7093
  if (content.includes(`'${routePath}'`)) {
6958
7094
  emit(` Nav already present: ${routePath}`);
@@ -7033,7 +7169,7 @@ function integrateDependencies(config, targetDir, runInstall, outputVisible, emi
7033
7169
  }
7034
7170
  let pkg2;
7035
7171
  try {
7036
- pkg2 = JSON.parse(readFileSync19(packagePath, "utf-8"));
7172
+ pkg2 = JSON.parse(readFileSync20(packagePath, "utf-8"));
7037
7173
  } catch (error) {
7038
7174
  emit(
7039
7175
  ` Could not patch package.json (parse error: ${error instanceof Error ? error.message : String(error)})`
@@ -7093,7 +7229,7 @@ ${child.diagnosticTail}` : "";
7093
7229
  }
7094
7230
  function dependencySnapshot(targetDir) {
7095
7231
  try {
7096
- const pkg2 = JSON.parse(readFileSync19(join18(targetDir, "package.json"), "utf-8"));
7232
+ const pkg2 = JSON.parse(readFileSync20(join18(targetDir, "package.json"), "utf-8"));
7097
7233
  return {
7098
7234
  dependencies: pkg2.dependencies ?? {},
7099
7235
  devDependencies: pkg2.devDependencies ?? {}
@@ -7342,7 +7478,7 @@ function isDeepSpaceApp(dir) {
7342
7478
  const packagePath = join19(dir, "package.json");
7343
7479
  if (!existsSync20(packagePath)) return false;
7344
7480
  try {
7345
- const pkg2 = JSON.parse(readFileSync20(packagePath, "utf-8"));
7481
+ const pkg2 = JSON.parse(readFileSync21(packagePath, "utf-8"));
7346
7482
  return Boolean(pkg2.dependencies?.deepspace || pkg2.devDependencies?.deepspace);
7347
7483
  } catch {
7348
7484
  return false;
@@ -7545,11 +7681,11 @@ function fmtDate(iso) {
7545
7681
  }
7546
7682
  async function confirm4(message) {
7547
7683
  process.stdout.write(`${message} [y/N] `);
7548
- return new Promise((resolve18) => {
7684
+ return new Promise((resolve19) => {
7549
7685
  process.stdin.setEncoding("utf-8");
7550
7686
  process.stdin.once("data", (data) => {
7551
7687
  const answer = data.toString().trim().toLowerCase();
7552
- resolve18(answer === "y" || answer === "yes");
7688
+ resolve19(answer === "y" || answer === "yes");
7553
7689
  });
7554
7690
  });
7555
7691
  }
@@ -7837,8 +7973,289 @@ var domain_default = defineCommand5({
7837
7973
  subCommands: { search, buy, list: list2, status, attach, detach, renew }
7838
7974
  });
7839
7975
 
7840
- // src/cli/commands/collaborators.ts
7976
+ // src/cli/commands/app-files.ts
7841
7977
  import { defineCommand as defineCommand6 } from "citty";
7978
+ import { basename as basename4, resolve as resolve15 } from "path";
7979
+ import { existsSync as existsSync21, statSync as statSync2 } from "fs";
7980
+
7981
+ // src/cli/lib/app-files-api.ts
7982
+ import { createWriteStream, openAsBlob, statSync } from "fs";
7983
+ import { basename as basename3, extname } from "path";
7984
+ import { Readable } from "stream";
7985
+ import { pipeline } from "stream/promises";
7986
+ import { randomUUID as randomUUID2 } from "crypto";
7987
+ var CONTENT_TYPES = {
7988
+ ".avif": "image/avif",
7989
+ ".css": "text/css",
7990
+ ".csv": "text/csv",
7991
+ ".gif": "image/gif",
7992
+ ".ico": "image/x-icon",
7993
+ ".jpeg": "image/jpeg",
7994
+ ".jpg": "image/jpeg",
7995
+ ".json": "application/json",
7996
+ ".md": "text/markdown",
7997
+ ".mp3": "audio/mpeg",
7998
+ ".mp4": "video/mp4",
7999
+ ".otf": "font/otf",
8000
+ ".pdf": "application/pdf",
8001
+ ".png": "image/png",
8002
+ ".txt": "text/plain",
8003
+ ".wasm": "application/wasm",
8004
+ ".wav": "audio/wav",
8005
+ ".webm": "video/webm",
8006
+ ".webp": "image/webp",
8007
+ ".woff": "font/woff",
8008
+ ".woff2": "font/woff2",
8009
+ ".zip": "application/zip"
8010
+ };
8011
+ function contentTypeFor(path) {
8012
+ return CONTENT_TYPES[extname(path).toLowerCase()] ?? "application/octet-stream";
8013
+ }
8014
+ function filesPath(appId, rest = "") {
8015
+ return `/api/app-files/${appId}${rest}`;
8016
+ }
8017
+ async function request(baseUrl, token, path, init = {}) {
8018
+ let res;
8019
+ try {
8020
+ res = await fetch(`${baseUrl}${path}`, {
8021
+ ...init,
8022
+ headers: { Authorization: `Bearer ${token}`, ...init.headers ?? {} }
8023
+ });
8024
+ } catch (err) {
8025
+ throw new ApiError(
8026
+ `Could not reach the DeepSpace platform at ${baseUrl}: ${err instanceof Error ? err.message : String(err)}. Check your connection (and DEEPSPACE_PLATFORM_URL), then retry.`,
8027
+ 0,
8028
+ "network_error",
8029
+ path
8030
+ );
8031
+ }
8032
+ if (res.ok) return res;
8033
+ const text2 = await res.text();
8034
+ let code;
8035
+ try {
8036
+ code = JSON.parse(text2.trim()).code;
8037
+ } catch {
8038
+ }
8039
+ throw new ApiError(
8040
+ describeFilesFailure(res.status, text2),
8041
+ res.status,
8042
+ code ?? (res.status === 413 ? "file_too_large" : res.status >= 500 ? "server_error" : "http_error"),
8043
+ path
8044
+ );
8045
+ }
8046
+ async function readJson(res, path) {
8047
+ const text2 = await res.text();
8048
+ try {
8049
+ return JSON.parse(text2);
8050
+ } catch {
8051
+ throw new ApiError(
8052
+ `The DeepSpace platform returned a malformed (non-JSON) response.`,
8053
+ res.status,
8054
+ "invalid_response",
8055
+ path
8056
+ );
8057
+ }
8058
+ }
8059
+ async function multipartBody(localPath, fileName, contentType) {
8060
+ const boundary = `----deepspace${randomUUID2().replace(/-/g, "")}`;
8061
+ const safeName = fileName.replace(/["\\\r\n]/g, "_");
8062
+ const head = `--${boundary}\r
8063
+ Content-Disposition: form-data; name="file"; filename="${safeName}"\r
8064
+ Content-Type: ${contentType}\r
8065
+ \r
8066
+ `;
8067
+ const file = await openAsBlob(localPath, { type: contentType });
8068
+ return {
8069
+ blob: new Blob([head, file, `\r
8070
+ --${boundary}--\r
8071
+ `]),
8072
+ contentType: `multipart/form-data; boundary=${boundary}`
8073
+ };
8074
+ }
8075
+ async function uploadAppFile(baseUrl, token, appId, localPath, key) {
8076
+ const path = filesPath(appId, `/upload?key=${encodeKeyPath(key)}`);
8077
+ const size = statSync(localPath).size;
8078
+ if (size > MAX_APP_FILE_BYTES) {
8079
+ throw new ApiError(oversizeMessage(size), 0, "file_too_large", path);
8080
+ }
8081
+ const { blob, contentType } = await multipartBody(
8082
+ localPath,
8083
+ basename3(localPath),
8084
+ contentTypeFor(localPath)
8085
+ );
8086
+ const res = await request(baseUrl, token, path, {
8087
+ method: "POST",
8088
+ headers: { "Content-Type": contentType },
8089
+ body: blob
8090
+ });
8091
+ const body = await readJson(res, path);
8092
+ return { key: body.key, name: body.name, size };
8093
+ }
8094
+ async function listAppFiles(baseUrl, token, appId, query) {
8095
+ const path = filesPath(appId, `?${query}`);
8096
+ return readJson(await request(baseUrl, token, path), path);
8097
+ }
8098
+ async function deleteAppFile(baseUrl, token, appId, key) {
8099
+ await request(baseUrl, token, filesPath(appId, `/${encodeKeyPath(key)}`), { method: "DELETE" });
8100
+ }
8101
+ async function downloadAppFile(baseUrl, token, appId, key, destination) {
8102
+ const path = filesPath(appId, `/${encodeKeyPath(key)}`);
8103
+ const res = await request(baseUrl, token, path);
8104
+ if (!res.body) {
8105
+ throw new ApiError("The server returned an empty response", 502, "invalid_response", path);
8106
+ }
8107
+ let bytes = 0;
8108
+ await pipeline(
8109
+ Readable.fromWeb(res.body),
8110
+ async function* (chunks) {
8111
+ for await (const chunk of chunks) {
8112
+ bytes += chunk.length;
8113
+ yield chunk;
8114
+ }
8115
+ },
8116
+ createWriteStream(destination)
8117
+ );
8118
+ return { bytes, contentType: res.headers.get("content-type") ?? "application/octet-stream" };
8119
+ }
8120
+
8121
+ // src/cli/commands/app-files.ts
8122
+ var DEPLOY_URL8 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
8123
+ var PLATFORM_URL = process.env.DEEPSPACE_PLATFORM_URL ?? PLATFORM_URLS.platform;
8124
+ async function target(app2) {
8125
+ const token = await ensureToken();
8126
+ const appId = await resolveAppTarget(DEPLOY_URL8, token, app2);
8127
+ return { token, appId };
8128
+ }
8129
+ var MAX_LIST_LIMIT = 1e3;
8130
+ function parseLimit(raw) {
8131
+ const limit = Number(raw ?? 100);
8132
+ if (!Number.isInteger(limit) || limit < 1 || limit > MAX_LIST_LIMIT) {
8133
+ throw new Refusal(`--limit must be a whole number from 1 to ${MAX_LIST_LIMIT}.`, "invalid_limit");
8134
+ }
8135
+ return limit;
8136
+ }
8137
+ function requireKey(raw) {
8138
+ const key = raw.trim().replace(/^\/+/, "");
8139
+ if (!key) throw new Refusal("A key is required (for example `logo.png`).", "invalid_key");
8140
+ if (key.split("/").some((segment) => segment === ".." || segment === ".")) {
8141
+ throw new Refusal(
8142
+ `"${raw}" contains a path traversal segment. Keys are relative to the app, e.g. \`img/hero.jpg\`.`,
8143
+ "invalid_key"
8144
+ );
8145
+ }
8146
+ return key;
8147
+ }
8148
+ function appPrefixOf(key) {
8149
+ const segments = key.split("/");
8150
+ return segments.length > 2 ? `${segments[0]}/${segments[1]}/` : "";
8151
+ }
8152
+ function relativeKey(key, prefix) {
8153
+ return prefix && key.startsWith(prefix) ? key.slice(prefix.length) : key;
8154
+ }
8155
+ function servedPath(key) {
8156
+ return `/api/files/${encodeKeyPath(key)}?scope=app`;
8157
+ }
8158
+ var put = defineDeepspaceCommand({
8159
+ meta: { name: "put", description: "Upload a file to the app\u2019s files" },
8160
+ args: {
8161
+ file: { type: "positional", description: "Local file to upload", required: true },
8162
+ key: { type: "string", description: "Key to store it under (default: the file name)" },
8163
+ app: { type: "string", description: "App id or live name (defaults to ./wrangler.toml)" }
8164
+ },
8165
+ async run({ args }) {
8166
+ const localPath = resolve15(String(args.file));
8167
+ if (!existsSync21(localPath) || !statSync2(localPath).isFile()) {
8168
+ throw new Refusal(`No such file: ${args.file}`, "file_not_found");
8169
+ }
8170
+ const key = requireKey(args.key ?? basename4(localPath));
8171
+ const { token, appId } = await target(args.app);
8172
+ const result = await uploadAppFile(PLATFORM_URL, token, appId, localPath, key);
8173
+ if (!args.json) {
8174
+ console.log(`\u2713 ${key} (${formatBytes(result.size)})`);
8175
+ console.log(` Served from your app at ${servedPath(result.key)}`);
8176
+ }
8177
+ return { data: { appId, key, storedKey: result.key, size: result.size, path: servedPath(result.key) } };
8178
+ }
8179
+ });
8180
+ var list3 = defineDeepspaceCommand({
8181
+ meta: { name: "list", description: "List the app\u2019s files" },
8182
+ args: {
8183
+ prefix: { type: "string", description: "Only keys starting with this prefix" },
8184
+ limit: {
8185
+ type: "string",
8186
+ description: `Max entries, 1-${MAX_LIST_LIMIT} (default 100)`,
8187
+ default: "100"
8188
+ },
8189
+ app: { type: "string", description: "App id or live name (defaults to ./wrangler.toml)" }
8190
+ },
8191
+ async run({ args }) {
8192
+ const limit = parseLimit(args.limit);
8193
+ const { token, appId } = await target(args.app);
8194
+ const query = new URLSearchParams({ limit: String(limit) });
8195
+ if (args.prefix) query.set("prefix", String(args.prefix));
8196
+ const result = await listAppFiles(PLATFORM_URL, token, appId, query);
8197
+ const prefix = result.files.length ? appPrefixOf(result.files[0].key) : "";
8198
+ const files = result.files.map((file) => ({
8199
+ key: relativeKey(file.key, prefix),
8200
+ size: file.size,
8201
+ uploaded: file.uploaded,
8202
+ path: servedPath(file.key)
8203
+ }));
8204
+ if (!args.json) {
8205
+ if (files.length === 0) {
8206
+ console.log("No files. Upload one with `deepspace app files put <file>`.");
8207
+ } else {
8208
+ const keyWidth = Math.max(3, ...files.map((file) => file.key.length));
8209
+ console.log(`${"KEY".padEnd(keyWidth)} ${"SIZE".padStart(9)} UPLOADED`);
8210
+ for (const file of files) {
8211
+ console.log(
8212
+ `${file.key.padEnd(keyWidth)} ${formatBytes(file.size).padStart(9)} ${file.uploaded.slice(0, 10)}`
8213
+ );
8214
+ }
8215
+ const more = limit < MAX_LIST_LIMIT ? "raise --limit or narrow with --prefix" : "narrow with --prefix";
8216
+ if (result.truncated) console.log(`(truncated at ${limit} \u2014 ${more})`);
8217
+ }
8218
+ }
8219
+ return { data: { appId, files, truncated: result.truncated } };
8220
+ }
8221
+ });
8222
+ var get = defineDeepspaceCommand({
8223
+ meta: { name: "get", description: "Download one file" },
8224
+ args: {
8225
+ key: { type: "positional", description: "Key to download", required: true },
8226
+ out: { type: "string", description: "Write here (default: the key\u2019s file name)" },
8227
+ app: { type: "string", description: "App id or live name (defaults to ./wrangler.toml)" }
8228
+ },
8229
+ async run({ args }) {
8230
+ const key = requireKey(String(args.key));
8231
+ const destination = resolve15(String(args.out ?? basename4(key)));
8232
+ const { token, appId } = await target(args.app);
8233
+ const result = await downloadAppFile(PLATFORM_URL, token, appId, key, destination);
8234
+ if (!args.json) console.log(`\u2713 ${key} \u2192 ${destination} (${formatBytes(result.bytes)})`);
8235
+ return { data: { appId, key, out: destination, size: result.bytes, contentType: result.contentType } };
8236
+ }
8237
+ });
8238
+ var rm = defineDeepspaceCommand({
8239
+ meta: { name: "rm", description: "Delete one file" },
8240
+ args: {
8241
+ key: { type: "positional", description: "Key to delete", required: true },
8242
+ app: { type: "string", description: "App id or live name (defaults to ./wrangler.toml)" }
8243
+ },
8244
+ async run({ args }) {
8245
+ const key = requireKey(String(args.key));
8246
+ const { token, appId } = await target(args.app);
8247
+ await deleteAppFile(PLATFORM_URL, token, appId, key);
8248
+ if (!args.json) console.log(`\u2713 deleted ${key}`);
8249
+ return { data: { appId, key, deleted: true } };
8250
+ }
8251
+ });
8252
+ var app_files_default = defineCommand6({
8253
+ meta: { name: "files", description: "Upload and manage the app\u2019s files (images, media)" },
8254
+ subCommands: { put, list: list3, get, rm }
8255
+ });
8256
+
8257
+ // src/cli/commands/collaborators.ts
8258
+ import { defineCommand as defineCommand7 } from "citty";
7842
8259
 
7843
8260
  // src/server/auth/testAccounts.ts
7844
8261
  var TEST_ACCOUNT_EMAIL_SUFFIX = "@deepspace.test";
@@ -7848,18 +8265,18 @@ function isTestAccountEmail(email) {
7848
8265
 
7849
8266
  // src/cli/commands/collaborators.ts
7850
8267
  var API_URL5 = process.env.DEEPSPACE_API_URL ?? PLATFORM_URLS.api;
7851
- var DEPLOY_URL8 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
8268
+ var DEPLOY_URL9 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
7852
8269
  function api2(token, path, init) {
7853
8270
  return apiFetch(API_URL5, token, path, init);
7854
8271
  }
7855
- var list3 = defineDeepspaceCommand({
8272
+ var list4 = defineDeepspaceCommand({
7856
8273
  meta: { name: "list", description: "List collaborators on your app" },
7857
8274
  args: {
7858
8275
  app: { type: "string", alias: "a", description: "App id or name (defaults to ./wrangler.toml)" }
7859
8276
  },
7860
8277
  async run({ args }) {
7861
8278
  const token = await ensureToken();
7862
- const app2 = await resolveAppTarget(DEPLOY_URL8, token, args.app);
8279
+ const app2 = await resolveAppTarget(DEPLOY_URL9, token, args.app);
7863
8280
  const { collaborators, pending = [] } = await api2(token, `/api/app-collaborators/${encodeURIComponent(app2)}`);
7864
8281
  if (!collaborators.length && !pending.length) {
7865
8282
  if (!args.json) {
@@ -7897,7 +8314,7 @@ var add = defineDeepspaceCommand({
7897
8314
  async run({ args }) {
7898
8315
  const email = String(args.email);
7899
8316
  const token = await ensureToken();
7900
- const app2 = await resolveAppTarget(DEPLOY_URL8, token, args.app);
8317
+ const app2 = await resolveAppTarget(DEPLOY_URL9, token, args.app);
7901
8318
  if (isTestAccountEmail(email)) {
7902
8319
  throw new Refusal(
7903
8320
  "Test accounts (@deepspace.test) cannot be added as collaborators. Use a real DeepSpace account.",
@@ -7939,13 +8356,13 @@ var remove = defineDeepspaceCommand({
7939
8356
  async run({ args }) {
7940
8357
  const email = String(args.email);
7941
8358
  const token = await ensureToken();
7942
- const app2 = await resolveAppTarget(DEPLOY_URL8, token, args.app);
8359
+ const app2 = await resolveAppTarget(DEPLOY_URL9, token, args.app);
7943
8360
  const { collaborators } = await api2(
7944
8361
  token,
7945
8362
  `/api/app-collaborators/${encodeURIComponent(app2)}`
7946
8363
  );
7947
- const target = email.trim().toLowerCase();
7948
- const found = collaborators.find((c) => c.emailDisplay.toLowerCase() === target);
8364
+ const target2 = email.trim().toLowerCase();
8365
+ const found = collaborators.find((c) => c.emailDisplay.toLowerCase() === target2);
7949
8366
  if (!found) {
7950
8367
  throw new Refusal(`${email} is not a collaborator on ${app2}`, "not_a_collaborator", {
7951
8368
  action: cliAction("deepspace", "app", "collaborators", "list", "--app", app2)
@@ -7969,13 +8386,13 @@ var cancel3 = defineDeepspaceCommand({
7969
8386
  async run({ args }) {
7970
8387
  const email = String(args.email);
7971
8388
  const token = await ensureToken();
7972
- const app2 = await resolveAppTarget(DEPLOY_URL8, token, args.app);
8389
+ const app2 = await resolveAppTarget(DEPLOY_URL9, token, args.app);
7973
8390
  const { pending = [] } = await api2(
7974
8391
  token,
7975
8392
  `/api/app-collaborators/${encodeURIComponent(app2)}`
7976
8393
  );
7977
- const target = email.trim().toLowerCase();
7978
- const found = pending.find((p34) => p34.email.toLowerCase() === target);
8394
+ const target2 = email.trim().toLowerCase();
8395
+ const found = pending.find((p34) => p34.email.toLowerCase() === target2);
7979
8396
  if (!found) {
7980
8397
  throw new Refusal(`${email} has no pending invite on ${app2}`, "no_pending_invite", {
7981
8398
  action: cliAction("deepspace", "app", "collaborators", "list", "--app", app2)
@@ -7990,16 +8407,16 @@ var cancel3 = defineDeepspaceCommand({
7990
8407
  return { data: { cancelled: found } };
7991
8408
  }
7992
8409
  });
7993
- var collaborators_default = defineCommand6({
8410
+ var collaborators_default = defineCommand7({
7994
8411
  meta: { name: "collaborators", description: "Manage who can deploy your app" },
7995
- subCommands: { list: list3, add, remove, cancel: cancel3 }
8412
+ subCommands: { list: list4, add, remove, cancel: cancel3 }
7996
8413
  });
7997
8414
 
7998
8415
  // src/cli/commands/transfer.ts
7999
- import { defineCommand as defineCommand7 } from "citty";
8416
+ import { defineCommand as defineCommand8 } from "citty";
8000
8417
  import * as p14 from "@clack/prompts";
8001
8418
  var API_URL6 = process.env.DEEPSPACE_API_URL ?? PLATFORM_URLS.api;
8002
- var DEPLOY_URL9 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
8419
+ var DEPLOY_URL10 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
8003
8420
  var offer = defineDeepspaceCommand({
8004
8421
  meta: { name: "offer", description: "Offer this app to another DeepSpace user" },
8005
8422
  args: {
@@ -8013,7 +8430,7 @@ var offer = defineDeepspaceCommand({
8013
8430
  async run({ args }) {
8014
8431
  const email = String(args.email);
8015
8432
  const token = await ensureToken();
8016
- const app2 = await resolveAppTarget(DEPLOY_URL9, token, args.app);
8433
+ const app2 = await resolveAppTarget(DEPLOY_URL10, token, args.app);
8017
8434
  const pending = await apiFetch(
8018
8435
  API_URL6,
8019
8436
  token,
@@ -8065,7 +8482,7 @@ var status2 = defineDeepspaceCommand({
8065
8482
  },
8066
8483
  async run({ args }) {
8067
8484
  const token = await ensureToken();
8068
- const app2 = await resolveAppTarget(DEPLOY_URL9, token, args.app);
8485
+ const app2 = await resolveAppTarget(DEPLOY_URL10, token, args.app);
8069
8486
  const { transfer } = await apiFetch(API_URL6, token, `/api/app-transfers/${encodeURIComponent(app2)}`);
8070
8487
  if (!transfer) {
8071
8488
  if (!args.json) console.log(`No pending transfer for ${app2}.`);
@@ -8092,7 +8509,7 @@ var accept = defineDeepspaceCommand({
8092
8509
  const msg = err instanceof Error ? err.message : String(err);
8093
8510
  throw new Refusal(msg, msg.startsWith("No app id") ? "not_in_app_repo" : "invalid_app");
8094
8511
  }
8095
- await apiFetch(DEPLOY_URL9, token, `/api/apps/${encodeURIComponent(app2)}/transfer/accept`, {
8512
+ await apiFetch(DEPLOY_URL10, token, `/api/apps/${encodeURIComponent(app2)}/transfer/accept`, {
8096
8513
  method: "POST"
8097
8514
  });
8098
8515
  if (!args.json) {
@@ -8110,37 +8527,37 @@ var cancel4 = defineDeepspaceCommand({
8110
8527
  },
8111
8528
  async run({ args }) {
8112
8529
  const token = await ensureToken();
8113
- const app2 = await resolveAppTarget(DEPLOY_URL9, token, args.app);
8530
+ const app2 = await resolveAppTarget(DEPLOY_URL10, token, args.app);
8114
8531
  await apiFetch(API_URL6, token, `/api/app-transfers/${encodeURIComponent(app2)}`, { method: "DELETE" });
8115
8532
  if (!args.json) console.log(`\u2713 Transfer offer for ${app2} cancelled.`);
8116
8533
  return { data: { app: app2, cancelled: true } };
8117
8534
  }
8118
8535
  });
8119
- var transfer_default = defineCommand7({
8536
+ var transfer_default = defineCommand8({
8120
8537
  meta: { name: "transfer", description: "Transfer app ownership to another user" },
8121
8538
  subCommands: { offer, status: status2, accept, cancel: cancel4 }
8122
8539
  });
8123
8540
 
8124
8541
  // src/cli/commands/integrations.ts
8125
- import { defineCommand as defineCommand8 } from "citty";
8542
+ import { defineCommand as defineCommand9 } from "citty";
8126
8543
 
8127
8544
  // src/cli/commands/_invoke-impl.ts
8128
- import { readFileSync as readFileSync21 } from "fs";
8545
+ import { readFileSync as readFileSync22 } from "fs";
8129
8546
  import * as p15 from "@clack/prompts";
8130
8547
  var API_URL7 = process.env.DEEPSPACE_API_URL ?? PLATFORM_URLS.api;
8131
8548
  var DEFAULT_TIMEOUT_MS = 12e4;
8132
- function parseTarget(target) {
8133
- if (!target || typeof target !== "string") {
8549
+ function parseTarget(target2) {
8550
+ if (!target2 || typeof target2 !== "string") {
8134
8551
  throw new Refusal(
8135
8552
  "Missing target. Expected '<integration>/<endpoint>' (e.g. 'openai/chat-completion').",
8136
8553
  "missing_target",
8137
8554
  { action: cliAction("deepspace", "integrations", "list") }
8138
8555
  );
8139
8556
  }
8140
- const parts = target.split("/");
8557
+ const parts = target2.split("/");
8141
8558
  if (parts.length !== 2 || !parts[0] || !parts[1]) {
8142
8559
  throw new Refusal(
8143
- `Bad target '${target}'. Expected '<integration>/<endpoint>' (e.g. 'openai/chat-completion').`,
8560
+ `Bad target '${target2}'. Expected '<integration>/<endpoint>' (e.g. 'openai/chat-completion').`,
8144
8561
  "bad_target",
8145
8562
  { action: cliAction("deepspace", "integrations", "list") }
8146
8563
  );
@@ -8166,7 +8583,7 @@ function resolveBody(opts) {
8166
8583
  if (opts.body != null) {
8167
8584
  raw = opts.body;
8168
8585
  } else if (opts.bodyFile != null) {
8169
- raw = opts.bodyFile === "-" ? readFileSync21(0, "utf-8") : readFileSync21(opts.bodyFile, "utf-8");
8586
+ raw = opts.bodyFile === "-" ? readFileSync22(0, "utf-8") : readFileSync22(opts.bodyFile, "utf-8");
8170
8587
  } else {
8171
8588
  return "{}";
8172
8589
  }
@@ -8360,7 +8777,7 @@ async function runInvoke(args) {
8360
8777
  }
8361
8778
 
8362
8779
  // src/cli/commands/integrations.ts
8363
- var list4 = defineDeepspaceCommand({
8780
+ var list5 = defineDeepspaceCommand({
8364
8781
  meta: {
8365
8782
  name: "list",
8366
8783
  description: "List all available integrations"
@@ -8431,13 +8848,13 @@ var invoke = defineDeepspaceCommand({
8431
8848
  });
8432
8849
  }
8433
8850
  });
8434
- var integrations_default = defineCommand8({
8851
+ var integrations_default = defineCommand9({
8435
8852
  meta: {
8436
8853
  name: "integrations",
8437
8854
  description: "List, inspect, and invoke platform integrations"
8438
8855
  },
8439
8856
  subCommands: {
8440
- list: list4,
8857
+ list: list5,
8441
8858
  info,
8442
8859
  invoke
8443
8860
  }
@@ -8445,7 +8862,7 @@ var integrations_default = defineCommand8({
8445
8862
 
8446
8863
  // src/cli/commands/feedback.ts
8447
8864
  import * as p16 from "@clack/prompts";
8448
- import { existsSync as existsSync21, readFileSync as readFileSync22 } from "fs";
8865
+ import { existsSync as existsSync22, readFileSync as readFileSync23 } from "fs";
8449
8866
  import { dirname as dirname8, join as join20 } from "path";
8450
8867
  import { fileURLToPath as fileURLToPath2 } from "url";
8451
8868
  import { release } from "os";
@@ -8473,8 +8890,8 @@ function readCliVersion() {
8473
8890
  join20(here, "..", "..", "..", "package.json")
8474
8891
  ];
8475
8892
  for (const path of candidates) {
8476
- if (!existsSync21(path)) continue;
8477
- const pkg2 = JSON.parse(readFileSync22(path, "utf-8"));
8893
+ if (!existsSync22(path)) continue;
8894
+ const pkg2 = JSON.parse(readFileSync23(path, "utf-8"));
8478
8895
  if (pkg2.name === "deepspace" && pkg2.version) return pkg2.version;
8479
8896
  }
8480
8897
  } catch {
@@ -8669,8 +9086,8 @@ Context: deepspace ${payload.cliVersion} \xB7 ${payload.nodeVersion} \xB7 ${payl
8669
9086
  });
8670
9087
 
8671
9088
  // src/cli/commands/secrets.ts
8672
- import { defineCommand as defineCommand9 } from "citty";
8673
- import { readFileSync as readFileSync23 } from "fs";
9089
+ import { defineCommand as defineCommand10 } from "citty";
9090
+ import { readFileSync as readFileSync24 } from "fs";
8674
9091
 
8675
9092
  // src/cli/lib/citty-args.ts
8676
9093
  function dedupePositionals(bound, rest) {
@@ -8703,7 +9120,7 @@ function parseCursorArg(raw) {
8703
9120
  }
8704
9121
 
8705
9122
  // src/cli/commands/secrets.ts
8706
- var DEPLOY_URL10 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
9123
+ var DEPLOY_URL11 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
8707
9124
  var COMMON_ARGS = {
8708
9125
  app: {
8709
9126
  type: "string",
@@ -8773,7 +9190,7 @@ function warnIfDebugRoutesEnabled(secrets) {
8773
9190
  }
8774
9191
  var APPLY_HINT = "Run `deepspace deploy` to apply \u2014 secrets take effect at deploy time.";
8775
9192
  var LOCAL_DEV_HINT = "Running `deepspace dev start`? Restart it to load the change locally.";
8776
- var list5 = defineCommand9({
9193
+ var list6 = defineCommand10({
8777
9194
  meta: { name: "list", description: "List masked secrets in a config" },
8778
9195
  args: {
8779
9196
  ...COMMON_ARGS,
@@ -8786,7 +9203,7 @@ var list5 = defineCommand9({
8786
9203
  async run({ args }) {
8787
9204
  try {
8788
9205
  const t = await resolveTarget(args);
8789
- const { secrets } = await listSecrets(DEPLOY_URL10, t.token, t.appId, t.configName);
9206
+ const { secrets } = await listSecrets(DEPLOY_URL11, t.token, t.appId, t.configName);
8790
9207
  if (args.json) {
8791
9208
  console.log(JSON.stringify({ ok: true, appId: t.appId, config: t.configName, secrets }));
8792
9209
  return;
@@ -8803,7 +9220,7 @@ var list5 = defineCommand9({
8803
9220
  }
8804
9221
  }
8805
9222
  });
8806
- var set = defineCommand9({
9223
+ var set = defineCommand10({
8807
9224
  meta: { name: "set", description: "Set secrets from KEY=value pairs" },
8808
9225
  args: {
8809
9226
  ...COMMON_ARGS,
@@ -8831,9 +9248,9 @@ var set = defineCommand9({
8831
9248
  }
8832
9249
  if (Object.keys(secrets).length === 1) {
8833
9250
  const [[k, v]] = Object.entries(secrets);
8834
- await setSecret(DEPLOY_URL10, t.token, t.appId, t.configName, k, v);
9251
+ await setSecret(DEPLOY_URL11, t.token, t.appId, t.configName, k, v);
8835
9252
  } else {
8836
- await uploadSecrets(DEPLOY_URL10, t.token, t.appId, t.configName, secrets, false);
9253
+ await uploadSecrets(DEPLOY_URL11, t.token, t.appId, t.configName, secrets, false);
8837
9254
  }
8838
9255
  const n = Object.keys(secrets).length;
8839
9256
  ok(
@@ -8852,7 +9269,7 @@ var set = defineCommand9({
8852
9269
  }
8853
9270
  }
8854
9271
  });
8855
- var get = defineCommand9({
9272
+ var get2 = defineCommand10({
8856
9273
  meta: { name: "get", description: "Get a secret; pass --plain to print the value" },
8857
9274
  args: {
8858
9275
  ...COMMON_ARGS,
@@ -8864,11 +9281,11 @@ var get = defineCommand9({
8864
9281
  const t = await resolveTarget(args);
8865
9282
  const key = validateSecretName(args.key);
8866
9283
  if (args.plain) {
8867
- const { value } = await getSecretPlain(DEPLOY_URL10, t.token, t.appId, t.configName, key);
9284
+ const { value } = await getSecretPlain(DEPLOY_URL11, t.token, t.appId, t.configName, key);
8868
9285
  process.stdout.write(process.stdout.isTTY ? value + "\n" : value);
8869
9286
  return;
8870
9287
  }
8871
- const { secrets } = await listSecrets(DEPLOY_URL10, t.token, t.appId, t.configName);
9288
+ const { secrets } = await listSecrets(DEPLOY_URL11, t.token, t.appId, t.configName);
8872
9289
  const item = secrets.find((s) => s.key === key);
8873
9290
  if (!item)
8874
9291
  fail(new InputError(`Secret "${key}" not found in ${t.configName}`, "secret_not_found"));
@@ -8888,7 +9305,7 @@ var get = defineCommand9({
8888
9305
  }
8889
9306
  }
8890
9307
  });
8891
- var del2 = defineCommand9({
9308
+ var del2 = defineCommand10({
8892
9309
  meta: { name: "delete", description: "Delete one or more secrets" },
8893
9310
  args: {
8894
9311
  ...COMMON_ARGS,
@@ -8903,7 +9320,7 @@ var del2 = defineCommand9({
8903
9320
  for (const key of keys) {
8904
9321
  const name = validateSecretName(key);
8905
9322
  try {
8906
- await deleteSecret(DEPLOY_URL10, t.token, t.appId, t.configName, name);
9323
+ await deleteSecret(DEPLOY_URL11, t.token, t.appId, t.configName, name);
8907
9324
  deleted++;
8908
9325
  } catch (err) {
8909
9326
  if (err?.status === 404) absent++;
@@ -8922,7 +9339,7 @@ var del2 = defineCommand9({
8922
9339
  }
8923
9340
  }
8924
9341
  });
8925
- var upload = defineCommand9({
9342
+ var upload = defineCommand10({
8926
9343
  meta: { name: "upload", description: "Upload secrets from a dotenv or JSON file" },
8927
9344
  args: {
8928
9345
  ...COMMON_ARGS,
@@ -8940,11 +9357,11 @@ var upload = defineCommand9({
8940
9357
  async run({ args }) {
8941
9358
  try {
8942
9359
  const t = await resolveTarget(args);
8943
- const content = args.file === "-" ? readFileSync23(0, "utf-8") : readFileSync23(args.file, "utf-8");
9360
+ const content = args.file === "-" ? readFileSync24(0, "utf-8") : readFileSync24(args.file, "utf-8");
8944
9361
  const secrets = parseSecretsUpload(content);
8945
9362
  if (Object.keys(secrets).length === 0)
8946
9363
  fail(new InputError("No secrets found in the input.", "empty_input"));
8947
- await uploadSecrets(DEPLOY_URL10, t.token, t.appId, t.configName, secrets, args.replace);
9364
+ await uploadSecrets(DEPLOY_URL11, t.token, t.appId, t.configName, secrets, args.replace);
8948
9365
  const uploaded = Object.keys(secrets);
8949
9366
  ok(
8950
9367
  args.json === true,
@@ -8959,7 +9376,7 @@ var upload = defineCommand9({
8959
9376
  }
8960
9377
  }
8961
9378
  });
8962
- var download = defineCommand9({
9379
+ var download = defineCommand10({
8963
9380
  meta: { name: "download", description: "Download a config\u2019s secrets (dotenv/json/shell)" },
8964
9381
  args: {
8965
9382
  ...COMMON_ARGS,
@@ -8981,14 +9398,14 @@ var download = defineCommand9({
8981
9398
  )
8982
9399
  );
8983
9400
  }
8984
- const { secrets } = await fetchSecretsValues(DEPLOY_URL10, t.token, t.appId, t.configName);
9401
+ const { secrets } = await fetchSecretsValues(DEPLOY_URL11, t.token, t.appId, t.configName);
8985
9402
  process.stdout.write(formatSecretsDownload(secrets, format));
8986
9403
  } catch (err) {
8987
9404
  fail(err);
8988
9405
  }
8989
9406
  }
8990
9407
  });
8991
- var pull = defineCommand9({
9408
+ var pull = defineCommand10({
8992
9409
  meta: { name: "pull", description: "Refresh the .dev.vars cache from the app store" },
8993
9410
  args: { ...COMMON_ARGS },
8994
9411
  async run({ args }) {
@@ -9004,7 +9421,7 @@ var pull = defineCommand9({
9004
9421
  );
9005
9422
  const t = await resolveTarget(args);
9006
9423
  const ownerId = decodeJwtPayload(t.token).sub;
9007
- const pulled = await pullAppSecretsCache(DEPLOY_URL10, t.token, t.appId, t.configName);
9424
+ const pulled = await pullAppSecretsCache(DEPLOY_URL11, t.token, t.appId, t.configName);
9008
9425
  if (!pulled) {
9009
9426
  ok(
9010
9427
  args.json === true,
@@ -9031,13 +9448,13 @@ var pull = defineCommand9({
9031
9448
  }
9032
9449
  }
9033
9450
  });
9034
- var configsList = defineCommand9({
9451
+ var configsList = defineCommand10({
9035
9452
  meta: { name: "list", description: "List the app\u2019s configs" },
9036
9453
  args: { ...COMMON_ARGS },
9037
9454
  async run({ args }) {
9038
9455
  try {
9039
9456
  const t = await resolveTarget(args);
9040
- const { configs: configs2 } = await listConfigs(DEPLOY_URL10, t.token, t.appId);
9457
+ const { configs: configs2 } = await listConfigs(DEPLOY_URL11, t.token, t.appId);
9041
9458
  ok(args.json === true, { appId: t.appId, configs: configs2 }, () => {
9042
9459
  if (configs2.length === 0) {
9043
9460
  console.log("No configs yet \u2014 the first `secrets set` creates one.");
@@ -9052,7 +9469,7 @@ var configsList = defineCommand9({
9052
9469
  }
9053
9470
  }
9054
9471
  });
9055
- var configsCreate = defineCommand9({
9472
+ var configsCreate = defineCommand10({
9056
9473
  meta: { name: "create", description: "Create a config (optionally copying another)" },
9057
9474
  args: {
9058
9475
  ...COMMON_ARGS,
@@ -9064,7 +9481,7 @@ var configsCreate = defineCommand9({
9064
9481
  const t = await resolveTarget(args);
9065
9482
  const name = validateConfigName(args.name);
9066
9483
  const copyFrom = args["copy-from"] || void 0;
9067
- const { configs: configs2 } = await listConfigs(DEPLOY_URL10, t.token, t.appId);
9484
+ const { configs: configs2 } = await listConfigs(DEPLOY_URL11, t.token, t.appId);
9068
9485
  if (configs2.some((c) => c.name === name)) {
9069
9486
  if (copyFrom) {
9070
9487
  fail(
@@ -9081,7 +9498,7 @@ var configsCreate = defineCommand9({
9081
9498
  );
9082
9499
  return;
9083
9500
  }
9084
- await createConfig(DEPLOY_URL10, t.token, t.appId, name, copyFrom);
9501
+ await createConfig(DEPLOY_URL11, t.token, t.appId, name, copyFrom);
9085
9502
  ok(
9086
9503
  args.json === true,
9087
9504
  { appId: t.appId, config: name, created: true, ...copyFrom ? { copyFrom } : {} },
@@ -9092,7 +9509,7 @@ var configsCreate = defineCommand9({
9092
9509
  }
9093
9510
  }
9094
9511
  });
9095
- var configsDelete = defineCommand9({
9512
+ var configsDelete = defineCommand10({
9096
9513
  meta: { name: "delete", description: "Delete a config and all of its secrets" },
9097
9514
  args: {
9098
9515
  ...COMMON_ARGS,
@@ -9102,7 +9519,7 @@ var configsDelete = defineCommand9({
9102
9519
  try {
9103
9520
  const t = await resolveTarget(args);
9104
9521
  const name = validateConfigName(args.name);
9105
- await deleteConfig(DEPLOY_URL10, t.token, t.appId, name);
9522
+ await deleteConfig(DEPLOY_URL11, t.token, t.appId, name);
9106
9523
  ok(
9107
9524
  args.json === true,
9108
9525
  { appId: t.appId, config: name, deleted: true },
@@ -9113,16 +9530,16 @@ var configsDelete = defineCommand9({
9113
9530
  }
9114
9531
  }
9115
9532
  });
9116
- var configs = defineCommand9({
9533
+ var configs = defineCommand10({
9117
9534
  meta: { name: "configs", description: "Manage the app\u2019s secrets configs" },
9118
9535
  subCommands: { list: configsList, create: configsCreate, delete: configsDelete }
9119
9536
  });
9120
- var secrets_default = defineCommand9({
9537
+ var secrets_default = defineCommand10({
9121
9538
  meta: { name: "secrets", description: "Manage the app\u2019s secrets store" },
9122
9539
  subCommands: {
9123
- list: list5,
9540
+ list: list6,
9124
9541
  set,
9125
- get,
9542
+ get: get2,
9126
9543
  delete: del2,
9127
9544
  upload,
9128
9545
  download,
@@ -9194,9 +9611,9 @@ var init_default = defineDeepspaceCommand({
9194
9611
  });
9195
9612
 
9196
9613
  // src/cli/commands/status.ts
9197
- import { defineCommand as defineCommand10 } from "citty";
9614
+ import { defineCommand as defineCommand11 } from "citty";
9198
9615
  import * as p17 from "@clack/prompts";
9199
- import { existsSync as existsSync22, readFileSync as readFileSync24 } from "fs";
9616
+ import { existsSync as existsSync23, readFileSync as readFileSync25 } from "fs";
9200
9617
  function timeAgo(iso) {
9201
9618
  const ms = Date.now() - Date.parse(iso);
9202
9619
  if (!Number.isFinite(ms) || ms < 0) return iso;
@@ -9229,7 +9646,7 @@ function resolveStatusApp(appDir, wranglerEnv) {
9229
9646
  if (!resolved.ok) throw new InputError(resolved.reason, "invalid_env");
9230
9647
  return { appName: resolved.name, appId: readAppId(appDir, wranglerEnv) };
9231
9648
  }
9232
- var status_default = defineCommand10({
9649
+ var status_default = defineCommand11({
9233
9650
  meta: {
9234
9651
  name: "status",
9235
9652
  description: "One screenful of current state: session, app, workspace, and live release"
@@ -9281,7 +9698,7 @@ var status_default = defineCommand10({
9281
9698
  json.services = urls;
9282
9699
  if (urlOverrides.length > 0) json.urlOverrides = urlOverrides;
9283
9700
  }
9284
- facts.loggedIn = existsSync22(SESSION_PATH);
9701
+ facts.loggedIn = existsSync23(SESSION_PATH);
9285
9702
  let userId = null;
9286
9703
  if (!facts.loggedIn) {
9287
9704
  lines.push(["Session", "not logged in"]);
@@ -9290,7 +9707,7 @@ var status_default = defineCommand10({
9290
9707
  let who = "logged in";
9291
9708
  try {
9292
9709
  const payload = decodeJwtPayload(
9293
- readFileSync24(TOKEN_PATH, "utf-8").trim()
9710
+ readFileSync25(TOKEN_PATH, "utf-8").trim()
9294
9711
  );
9295
9712
  if (payload.email) who = payload.email;
9296
9713
  userId = payload.sub ?? null;
@@ -9520,8 +9937,8 @@ var push_default = defineDeepspaceCommand({
9520
9937
  const branchArg = args.branch === void 0 ? void 0 : String(args.branch);
9521
9938
  const appArg = typeof args.app === "string" ? args.app : void 0;
9522
9939
  const force = args.force === true;
9940
+ const appDir = findAppDir();
9523
9941
  try {
9524
- const appDir = findAppDir();
9525
9942
  if (!appDir)
9526
9943
  throw new Refusal(
9527
9944
  "No wrangler.toml found \u2014 run from inside an app directory.",
@@ -9650,7 +10067,7 @@ var push_default = defineDeepspaceCommand({
9650
10067
  }
9651
10068
  const recoverable = isRecoverablePushFailure(result.status);
9652
10069
  const rejectReason = result.reason ?? result.summary;
9653
- const errorMsg = ok2 ? null : result.status === "ref_conflict" ? `The cloud repo's ${branch} moved while you worked (${rejectReason}). Run \`deepspace pull --app ${shQuote(appId)} --branch ${shQuote(branch)}\` to integrate, then push again.` : result.status === "rejected" ? `The cloud repo rejected ${branch}: ${rejectReason}.` + (OVERSIZED_PUSH_RE.test(rejectReason) ? ` ${oversizedPushFix(appDir)}` : "") : `The cloud repo's ${branch} has commit(s) your local ${branch} doesn't. Run \`deepspace pull --app ${shQuote(appId)} --branch ${shQuote(branch)}\` and merge (or rebase), then push. Avoid --force here \u2014 it's guarded and will refuse rather than silently drop a peer's commits.`;
10070
+ const errorMsg = ok2 ? null : result.status === "ref_conflict" ? `The cloud repo's ${branch} moved while you worked (${rejectReason}). Run \`deepspace pull --app ${shQuote(appId)} --branch ${shQuote(branch)}\` to integrate, then push again.` : result.status === "rejected" ? pushFailureMessage(`The cloud repo rejected ${branch}`, result, appDir ?? void 0) : `The cloud repo's ${branch} has commit(s) your local ${branch} doesn't. Run \`deepspace pull --app ${shQuote(appId)} --branch ${shQuote(branch)}\` and merge (or rebase), then push. Avoid --force here \u2014 it's guarded and will refuse rather than silently drop a peer's commits.`;
9654
10071
  if (!args.json && ok2) p18.log.info(`App: ${appId}`);
9655
10072
  const action = recoverable ? pullRecoveryAction(pullRecoveryCwd, appId, branch) : void 0;
9656
10073
  if (!ok2) {
@@ -9663,7 +10080,7 @@ var push_default = defineDeepspaceCommand({
9663
10080
  return { data: { status: result.status, appId, branch, oid: tipOid } };
9664
10081
  } catch (err) {
9665
10082
  if (err instanceof Refusal) throw err;
9666
- const transportFailure = classifyPushTransportFailure(err);
10083
+ const transportFailure = classifyPushTransportFailure(err, appDir ?? void 0);
9667
10084
  if (transportFailure) {
9668
10085
  throw new Refusal(transportFailure.error, transportFailure.code);
9669
10086
  }
@@ -9955,14 +10372,14 @@ var pull_default = defineDeepspaceCommand({
9955
10372
 
9956
10373
  // src/cli/commands/clone.ts
9957
10374
  import * as p20 from "@clack/prompts";
9958
- import { existsSync as existsSync23, readdirSync as readdirSync6, rmSync as rmSync5 } from "fs";
9959
- import { resolve as resolve15, basename as basename3, join as join21 } from "path";
10375
+ import { existsSync as existsSync24, readdirSync as readdirSync6, rmSync as rmSync5 } from "fs";
10376
+ import { resolve as resolve16, basename as basename5, join as join21 } from "path";
9960
10377
  function cleanFailedCloneDir(dir) {
9961
10378
  let entries;
9962
10379
  try {
9963
10380
  entries = readdirSync6(dir);
9964
10381
  } catch {
9965
- return existsSync23(dir) ? ["<unreadable>"] : [];
10382
+ return existsSync24(dir) ? ["<unreadable>"] : [];
9966
10383
  }
9967
10384
  if (entries.length === 1 && entries[0] === ".git") {
9968
10385
  try {
@@ -9975,7 +10392,7 @@ function cleanFailedCloneDir(dir) {
9975
10392
  return entries;
9976
10393
  }
9977
10394
  function validateCloneTarget(dir) {
9978
- if (!existsSync23(dir)) return false;
10395
+ if (!existsSync24(dir)) return false;
9979
10396
  let entries;
9980
10397
  try {
9981
10398
  entries = readdirSync6(dir);
@@ -10010,7 +10427,7 @@ var clone_default = defineDeepspaceCommand({
10010
10427
  async run({ args }) {
10011
10428
  assertAppTargetResolvable(String(args.app));
10012
10429
  const selector = String(args.app).trim();
10013
- const dir = resolve15(String(args.dir ?? selector));
10430
+ const dir = resolve16(String(args.dir ?? selector));
10014
10431
  const dirPreexisted = validateCloneTarget(dir);
10015
10432
  const spinner2 = args.json ? null : createSpinner();
10016
10433
  spinner2?.start(`Resolving ${selector}\u2026`);
@@ -10053,7 +10470,7 @@ var clone_default = defineDeepspaceCommand({
10053
10470
  const head = resolveCommit(dir, "HEAD");
10054
10471
  const branch = currentBranch(dir);
10055
10472
  spinner2?.stop(
10056
- `Cloned ${selector} (${branches.length} ${branches.length === 1 ? "branch" : "branches"}) into ${basename3(dir)}/ at ${head?.slice(0, 10) ?? "(unborn)"}.`
10473
+ `Cloned ${selector} (${branches.length} ${branches.length === 1 ? "branch" : "branches"}) into ${basename5(dir)}/ at ${head?.slice(0, 10) ?? "(unborn)"}.`
10057
10474
  );
10058
10475
  if (!args.json && !branch) {
10059
10476
  p20.log.warn("HEAD is detached \u2014 check `git branch -a`.");
@@ -10076,7 +10493,7 @@ var clone_default = defineDeepspaceCommand({
10076
10493
 
10077
10494
  // src/cli/commands/releases.ts
10078
10495
  import * as p21 from "@clack/prompts";
10079
- var DEPLOY_URL11 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
10496
+ var DEPLOY_URL12 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
10080
10497
  var releases_default = defineDeepspaceCommand({
10081
10498
  meta: {
10082
10499
  name: "releases",
@@ -10111,8 +10528,8 @@ var releases_default = defineDeepspaceCommand({
10111
10528
  const spinner2 = args.json ? null : createSpinner();
10112
10529
  spinner2?.start("Loading release history\u2026");
10113
10530
  const token = await ensureToken();
10114
- const appId = await resolveAppTarget(DEPLOY_URL11, token, appArg, { wranglerEnv: envArg });
10115
- const { releases } = await repoApi(DEPLOY_URL11, token, appId).listReleases(limit);
10531
+ const appId = await resolveAppTarget(DEPLOY_URL12, token, appArg, { wranglerEnv: envArg });
10532
+ const { releases } = await repoApi(DEPLOY_URL12, token, appId).listReleases(limit);
10116
10533
  spinner2?.stop(`Loaded ${releases.length} ${releases.length === 1 ? "release" : "releases"}.`);
10117
10534
  if (!args.json) {
10118
10535
  if (releases.length === 0) {
@@ -10144,7 +10561,7 @@ Retry in a moment, or re-run with --allow-do-deletion to accept the risk.`,
10144
10561
  "do_guard_unavailable"
10145
10562
  );
10146
10563
  }
10147
- var DEPLOY_URL12 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
10564
+ var DEPLOY_URL13 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
10148
10565
  function pickPreviousRelease(releases) {
10149
10566
  if (releases.length === 0) {
10150
10567
  return { error: "No releases recorded yet \u2014 nothing to roll back to.", code: "no_releases" };
@@ -10206,8 +10623,8 @@ var rollback_default = defineDeepspaceCommand({
10206
10623
  const spinner2 = args.json ? null : createSpinner();
10207
10624
  spinner2?.start("Preparing rollback\u2026");
10208
10625
  const token = await ensureToken();
10209
- const appId = await resolveAppTarget(DEPLOY_URL12, token, appArg, { wranglerEnv: envArg });
10210
- const api4 = repoApi(DEPLOY_URL12, token, appId);
10626
+ const appId = await resolveAppTarget(DEPLOY_URL13, token, appArg, { wranglerEnv: envArg });
10627
+ const api4 = repoApi(DEPLOY_URL13, token, appId);
10211
10628
  let releaseId = args.release ? String(args.release).trim() : "";
10212
10629
  if (!releaseId) {
10213
10630
  const { releases } = await api4.listReleases(2);
@@ -10218,7 +10635,7 @@ var rollback_default = defineDeepspaceCommand({
10218
10635
  spinner2?.message(`Rolling back to ${releaseId}\u2026`);
10219
10636
  let result;
10220
10637
  try {
10221
- result = await apiFetch(DEPLOY_URL12, token, `/api/deploy/${appId}/rollback`, {
10638
+ result = await apiFetch(DEPLOY_URL13, token, `/api/deploy/${appId}/rollback`, {
10222
10639
  method: "POST",
10223
10640
  body: JSON.stringify({
10224
10641
  releaseId,
@@ -10279,11 +10696,11 @@ Re-run with --allow-do-deletion to accept the data loss.`,
10279
10696
  });
10280
10697
 
10281
10698
  // src/cli/commands/workspace.ts
10282
- import { defineCommand as defineCommand11 } from "citty";
10699
+ import { defineCommand as defineCommand12 } from "citty";
10283
10700
 
10284
10701
  // src/cli/commands/workspace/new.ts
10285
10702
  import * as p25 from "@clack/prompts";
10286
- import { isAbsolute as isAbsolute6, resolve as resolve16 } from "path";
10703
+ import { isAbsolute as isAbsolute6, resolve as resolve17 } from "path";
10287
10704
 
10288
10705
  // src/cli/commands/workspace/analysis.ts
10289
10706
  import * as p23 from "@clack/prompts";
@@ -10408,7 +10825,7 @@ function printOverlaps(overlaps, json) {
10408
10825
 
10409
10826
  // src/cli/commands/workspace/local.ts
10410
10827
  import * as p24 from "@clack/prompts";
10411
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync25, realpathSync as realpathSync5, renameSync as renameSync2, statSync, writeFileSync as writeFileSync9 } from "fs";
10828
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync26, realpathSync as realpathSync5, renameSync as renameSync2, statSync as statSync3, writeFileSync as writeFileSync9 } from "fs";
10412
10829
  import { dirname as dirname9, isAbsolute as isAbsolute5, join as join22, relative as relative2, sep as sep2 } from "path";
10413
10830
  var WORKSPACE_MARKER = "deepspace-workspace.json";
10414
10831
  function excludeWorktreeDir(appDir, dir) {
@@ -10466,7 +10883,7 @@ function isManagedWorkspaceWorktree(worktreeRoot, workspaceId) {
10466
10883
  const markerPath = worktreeMarkerPath(worktreeRoot);
10467
10884
  if (!markerPath) return false;
10468
10885
  try {
10469
- const marker = JSON.parse(readFileSync25(markerPath, "utf-8"));
10886
+ const marker = JSON.parse(readFileSync26(markerPath, "utf-8"));
10470
10887
  return marker.version === 1 && marker.workspaceId === workspaceId && marker.mode === "managed-linked";
10471
10888
  } catch {
10472
10889
  return false;
@@ -10497,7 +10914,7 @@ function materializeWorkspaceWorktree(appDir, worktreeRoot, branch, startPoint,
10497
10914
  function inOwnLinkedWorktree(appDir, id) {
10498
10915
  if (workspaceIdFromBranch(currentBranch(appDir)) !== id) return false;
10499
10916
  try {
10500
- return statSync(join22(repoToplevel(appDir), ".git")).isFile();
10917
+ return statSync3(join22(repoToplevel(appDir), ".git")).isFile();
10501
10918
  } catch {
10502
10919
  return false;
10503
10920
  }
@@ -10843,7 +11260,7 @@ var newWorkspaceCommand = defineDeepspaceCommand({
10843
11260
  idempotencyKey: mintIdempotencyKey()
10844
11261
  });
10845
11262
  const dirInput = typeof args.dir === "string" ? args.dir.trim() : "";
10846
- const worktreeRoot = dirInput ? isAbsolute6(dirInput) ? dirInput : resolve16(appDir, dirInput) : defaultWorkspaceRoot(appDir, id);
11263
+ const worktreeRoot = dirInput ? isAbsolute6(dirInput) ? dirInput : resolve17(appDir, dirInput) : defaultWorkspaceRoot(appDir, id);
10847
11264
  const dir = appDirInWorktree(appDir, worktreeRoot);
10848
11265
  try {
10849
11266
  materializeWorkspaceWorktree(appDir, worktreeRoot, branch, baseOid, id);
@@ -10881,8 +11298,8 @@ var newWorkspaceCommand = defineDeepspaceCommand({
10881
11298
 
10882
11299
  // src/cli/commands/workspace/attach.ts
10883
11300
  import * as p26 from "@clack/prompts";
10884
- import { existsSync as existsSync24, readdirSync as readdirSync7, rmSync as rmSync6 } from "fs";
10885
- import { isAbsolute as isAbsolute7, resolve as resolve17 } from "path";
11301
+ import { existsSync as existsSync25, readdirSync as readdirSync7, rmSync as rmSync6 } from "fs";
11302
+ import { isAbsolute as isAbsolute7, resolve as resolve18 } from "path";
10886
11303
  function finishedWorkspaceMessage(id, view) {
10887
11304
  const workspace = view.workspace;
10888
11305
  if (workspace.status === "landed") {
@@ -10891,7 +11308,7 @@ function finishedWorkspaceMessage(id, view) {
10891
11308
  return `Workspace ${id} is ${workspace.status} \u2014 its ref is retained briefly (fetch ${workspace.ref} to inspect), but attach only works on active workspaces.`;
10892
11309
  }
10893
11310
  function cleanFailedFreshAttachDir(dir) {
10894
- if (!existsSync24(dir)) return [];
11311
+ if (!existsSync25(dir)) return [];
10895
11312
  let entries;
10896
11313
  try {
10897
11314
  entries = readdirSync7(dir);
@@ -10967,7 +11384,7 @@ var attachWorkspaceCommand = defineDeepspaceCommand({
10967
11384
  );
10968
11385
  }
10969
11386
  const requestedDir = typeof args.dir === "string" ? args.dir.trim() : "";
10970
- const worktreeRoot = requestedDir ? isAbsolute7(requestedDir) ? requestedDir : resolve17(appDir, requestedDir) : defaultWorkspaceRoot(appDir, id);
11387
+ const worktreeRoot = requestedDir ? isAbsolute7(requestedDir) ? requestedDir : resolve18(appDir, requestedDir) : defaultWorkspaceRoot(appDir, id);
10971
11388
  const dir2 = appDirInWorktree(appDir, worktreeRoot);
10972
11389
  if (resolveCommit(appDir, `refs/heads/${branch}`)) {
10973
11390
  throw new Refusal(
@@ -11006,11 +11423,11 @@ var attachWorkspaceCommand = defineDeepspaceCommand({
11006
11423
  extra: { status: view.workspace.status }
11007
11424
  });
11008
11425
  }
11009
- const dir = resolve17(
11426
+ const dir = resolve18(
11010
11427
  process.cwd(),
11011
11428
  (typeof args.dir === "string" ? args.dir.trim() : "") || `ws-${id.slice(3).toLowerCase()}`
11012
11429
  );
11013
- if (existsSync24(dir)) {
11430
+ if (existsSync25(dir)) {
11014
11431
  spinner2?.stop("Target exists.");
11015
11432
  throw new Refusal(`${dir} already exists \u2014 pass a different directory.`, "dir_exists");
11016
11433
  }
@@ -11328,13 +11745,13 @@ import * as p30 from "@clack/prompts";
11328
11745
 
11329
11746
  // src/cli/lib/validation.ts
11330
11747
  import { sync as spawnSync7 } from "cross-spawn";
11331
- import { readFileSync as readFileSync26 } from "fs";
11748
+ import { readFileSync as readFileSync27 } from "fs";
11332
11749
  import { join as join23 } from "path";
11333
11750
  var MAX_SUMMARY_CHARS = 4e3;
11334
11751
  var MAX_OUTPUT_BUFFER = 64 * 1024 * 1024;
11335
11752
  function configuredCommand(appDir) {
11336
11753
  try {
11337
- const pkg2 = JSON.parse(readFileSync26(join23(appDir, "package.json"), "utf-8"));
11754
+ const pkg2 = JSON.parse(readFileSync27(join23(appDir, "package.json"), "utf-8"));
11338
11755
  if (pkg2.scripts?.validate) return `${detectPackageManager(appDir)} run --silent validate`;
11339
11756
  } catch {
11340
11757
  }
@@ -11754,7 +12171,7 @@ var dropWorkspaceCommand = defineDeepspaceCommand({
11754
12171
  });
11755
12172
 
11756
12173
  // src/cli/commands/workspace.ts
11757
- var workspace_default = defineCommand11({
12174
+ var workspace_default = defineCommand12({
11758
12175
  meta: {
11759
12176
  name: "workspace",
11760
12177
  description: "Durable parallel-agent workspaces (new/attach/sync/list/status/land/drop)"
@@ -11925,7 +12342,7 @@ var activity_default = defineDeepspaceCommand({
11925
12342
  });
11926
12343
 
11927
12344
  // src/cli/commands/git-credential.ts
11928
- import { defineCommand as defineCommand12 } from "citty";
12345
+ import { defineCommand as defineCommand13 } from "citty";
11929
12346
  function parseFields(input) {
11930
12347
  const fields = /* @__PURE__ */ new Map();
11931
12348
  for (const line of input.split("\n")) {
@@ -11974,7 +12391,7 @@ async function readStdin() {
11974
12391
  for await (const chunk of process.stdin) data += chunk;
11975
12392
  return data;
11976
12393
  }
11977
- var git_credential_default = defineCommand12({
12394
+ var git_credential_default = defineCommand13({
11978
12395
  meta: {
11979
12396
  name: "git-credential",
11980
12397
  description: "Git credential helper (installed automatically; not for direct use)"
@@ -12092,8 +12509,8 @@ function reportSource(appId, state, json) {
12092
12509
  async function moveToGitHub(input) {
12093
12510
  const { appDir, appId, token, deployUrl, state, remoteArg } = input;
12094
12511
  const selected = requireGitHubRemote(appDir, remoteArg);
12095
- const target = { provider: "github", repository: selected.repository };
12096
- if (sameSource(state.source, target)) return { source: target, revision: state.revision };
12512
+ const target2 = { provider: "github", repository: selected.repository };
12513
+ if (sameSource(state.source, target2)) return { source: target2, revision: state.revision };
12097
12514
  if (state.source?.provider === "deepspace") {
12098
12515
  requireLocalHead(appDir);
12099
12516
  const api4 = repoApi(deployUrl, token, appId);
@@ -12149,7 +12566,7 @@ async function moveToGitHub(input) {
12149
12566
  );
12150
12567
  }
12151
12568
  return setAppSource(deployUrl, token, appId, {
12152
- source: target,
12569
+ source: target2,
12153
12570
  expectedRevision: state.revision,
12154
12571
  refs: wanted,
12155
12572
  ...releaseExpectation(latest)
@@ -12157,17 +12574,17 @@ async function moveToGitHub(input) {
12157
12574
  }
12158
12575
  requirePublishedHead(appDir, selected, appId);
12159
12576
  return setAppSource(deployUrl, token, appId, {
12160
- source: target,
12577
+ source: target2,
12161
12578
  expectedRevision: state.revision
12162
12579
  });
12163
12580
  }
12164
12581
  async function moveToDeepSpace(input) {
12165
12582
  const { appDir, appId, token, deployUrl, state, remoteArg } = input;
12166
- const target = { provider: "deepspace" };
12167
- if (sameSource(state.source, target)) return { source: target, revision: state.revision };
12583
+ const target2 = { provider: "deepspace" };
12584
+ if (sameSource(state.source, target2)) return { source: target2, revision: state.revision };
12168
12585
  if (state.source?.provider !== "github") {
12169
12586
  return setAppSource(deployUrl, token, appId, {
12170
- source: target,
12587
+ source: target2,
12171
12588
  expectedRevision: state.revision
12172
12589
  });
12173
12590
  }
@@ -12201,7 +12618,7 @@ async function moveToDeepSpace(input) {
12201
12618
  );
12202
12619
  }
12203
12620
  return setAppSource(deployUrl, token, appId, {
12204
- source: target,
12621
+ source: target2,
12205
12622
  expectedRevision: state.revision,
12206
12623
  branch,
12207
12624
  commitOid: oid,
@@ -12828,11 +13245,11 @@ function reportMigration(migration, json) {
12828
13245
 
12829
13246
  // src/cli/cli.ts
12830
13247
  var __dirname = dirname10(fileURLToPath3(import.meta.url));
12831
- var pkg = JSON.parse(readFileSync27(join24(__dirname, "..", "package.json"), "utf-8"));
12832
- if (process.platform === "linux" && !process.env.SSL_CERT_FILE && existsSync25("/etc/ssl/certs/ca-certificates.crt")) {
13248
+ var pkg = JSON.parse(readFileSync28(join24(__dirname, "..", "package.json"), "utf-8"));
13249
+ if (process.platform === "linux" && !process.env.SSL_CERT_FILE && existsSync26("/etc/ssl/certs/ca-certificates.crt")) {
12833
13250
  process.env.SSL_CERT_FILE = "/etc/ssl/certs/ca-certificates.crt";
12834
13251
  }
12835
- var create2 = defineCommand13({
13252
+ var create2 = defineCommand14({
12836
13253
  meta: {
12837
13254
  name: "create",
12838
13255
  description: "Create a new DeepSpace app (runs create-deepspace; all flags are forwarded)"
@@ -12871,19 +13288,20 @@ var create2 = defineCommand13({
12871
13288
  }
12872
13289
  });
12873
13290
  var rename = (cmd, name) => ({ ...cmd, meta: { ...cmd.meta ?? {}, name } });
12874
- var auth = defineCommand13({
13291
+ var auth = defineCommand14({
12875
13292
  meta: { name: "auth", description: "Session: login, logout, whoami" },
12876
13293
  subCommands: { login: login_default, logout: logout_default, whoami: whoami_default }
12877
13294
  });
12878
- var app = defineCommand13({
13295
+ var app = defineCommand14({
12879
13296
  meta: {
12880
13297
  name: "app",
12881
- description: "The app itself: create, init, list, source, migrate, undeploy, transfer, collaborators, domain, usage"
13298
+ description: "The app itself: create, init, list, files, source, migrate, undeploy, transfer, collaborators, domain, usage"
12882
13299
  },
12883
13300
  subCommands: {
12884
13301
  create: create2,
12885
13302
  init: init_default,
12886
13303
  list: rename(apps_default, "list"),
13304
+ files: app_files_default,
12887
13305
  undeploy: undeploy_default,
12888
13306
  transfer: transfer_default,
12889
13307
  collaborators: collaborators_default,
@@ -12893,11 +13311,11 @@ var app = defineCommand13({
12893
13311
  migrate: migrate_default
12894
13312
  }
12895
13313
  });
12896
- var devGroup = defineCommand13({
13314
+ var devGroup = defineCommand14({
12897
13315
  meta: { name: "dev", description: "Local development" },
12898
13316
  subCommands: { start: rename(dev_default, "start"), kill: kill_default }
12899
13317
  });
12900
- var testGroup = defineCommand13({
13318
+ var testGroup = defineCommand14({
12901
13319
  meta: { name: "test", description: "App tests and test tooling" },
12902
13320
  subCommands: {
12903
13321
  run: rename(test_default, "run"),
@@ -12905,7 +13323,7 @@ var testGroup = defineCommand13({
12905
13323
  accounts: rename(test_accounts_default, "accounts")
12906
13324
  }
12907
13325
  });
12908
- var main = defineCommand13({
13326
+ var main = defineCommand14({
12909
13327
  meta: {
12910
13328
  name: "deepspace",
12911
13329
  version: pkg.version,