deepspace 0.11.0 → 0.13.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, renderUsage } from "citty";
5
5
  import { sync as spawnSync8 } from "cross-spawn";
6
- import { readFileSync as readFileSync28, 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
 
@@ -44,6 +44,13 @@ import * as p2 from "@clack/prompts";
44
44
  import { isAbsolute } from "path";
45
45
 
46
46
  // src/cli/lib/cli-format.ts
47
+ var ANSI_RE = /\[[0-9;]*m/g;
48
+ function stripAnsi(s) {
49
+ return s.replace(ANSI_RE, "");
50
+ }
51
+ function forTerminal(s) {
52
+ return process.stdout.isTTY ? s : stripAnsi(s);
53
+ }
47
54
  function shQuote(s) {
48
55
  return `'${s.replace(/'/g, "'\\''")}'`;
49
56
  }
@@ -176,7 +183,10 @@ var API_ERROR_HINTS = {
176
183
  test_account_cannot_be_collaborator: "Test accounts cannot be added as collaborators. Use a real DeepSpace account.",
177
184
  user_not_found: "No DeepSpace user with that email. They need to log in to DeepSpace at least once (`deepspace auth login`, or sign in to any app) before they can be referenced by email.",
178
185
  insufficient_credits: "Out of credits. Inviting a new collaborator by email sends them a transactional email billed to your account \u2014 top up your credits and try again.",
179
- invite_email_failed: "The invite email could not be sent. You were not charged \u2014 please try again in a moment."
186
+ // "Try again" alone sends callers into a loop when the address itself is the
187
+ // problem — the send is what failed, and an undeliverable address fails the
188
+ // same way every time.
189
+ invite_email_failed: "The invite itself was not created and the charge was voided \u2014 nothing to undo. Retrying helps only if the send was transient; check the address is real and deliverable first. An email is needed ONLY for someone with no DeepSpace account yet: an existing user is added instantly, so having them sign in once and re-running this skips email entirely. If a retry now reports `already_invited` with no email sent, cancel the stuck invite (`deepspace app collaborators cancel <email>`) and invite again."
180
190
  };
181
191
  function formatCliError(err) {
182
192
  const message = err instanceof Error ? err.message : String(err);
@@ -222,6 +232,18 @@ function wrapCommandErrors(cmd) {
222
232
  }
223
233
 
224
234
  // src/cli/lib/command-suggestions.ts
235
+ function splitTokenPath(token, table) {
236
+ const parts = token.trim().split(/\s+/);
237
+ if (parts.length < 2) return null;
238
+ let node = table;
239
+ for (const part of parts) {
240
+ const next = node?.[part];
241
+ if (!next) return null;
242
+ node = next.subCommands;
243
+ }
244
+ return parts;
245
+ }
246
+ var DESTRUCTIVE_VERBS = /* @__PURE__ */ new Set(["rm", "delete", "remove", "drop", "clear", "undeploy", "kill"]);
225
247
  function editDistance(a, b) {
226
248
  const left = a.toLowerCase();
227
249
  const right = b.toLowerCase();
@@ -263,12 +285,15 @@ function findUnknownCommand(argv, rootCommands) {
263
285
  if (!table || token.startsWith("-")) return null;
264
286
  const command = table[token];
265
287
  if (!command) {
266
- const relativeSuggestion = closestCommandPath(token, table);
288
+ const split = splitTokenPath(token, table);
289
+ const relativeSuggestion = split ?? closestCommandPath(token, table);
267
290
  if (!relativeSuggestion) return null;
268
291
  return {
269
292
  attemptedPath: ["deepspace", ...accepted, token],
270
293
  helpPath: ["deepspace", ...accepted],
271
- suggestion: ["deepspace", ...accepted, ...relativeSuggestion]
294
+ suggestion: ["deepspace", ...accepted, ...relativeSuggestion],
295
+ executable: !DESTRUCTIVE_VERBS.has(relativeSuggestion.at(-1) ?? ""),
296
+ ...split ? { quotedToken: true } : {}
272
297
  };
273
298
  }
274
299
  accepted.push(token);
@@ -709,7 +734,7 @@ async function provisionProfile(jwt) {
709
734
  }
710
735
  }
711
736
  function sleep(ms) {
712
- return new Promise((resolve18) => setTimeout(resolve18, ms));
737
+ return new Promise((resolve19) => setTimeout(resolve19, ms));
713
738
  }
714
739
  function base64url(bytes) {
715
740
  return bytes.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
@@ -929,6 +954,7 @@ import { dirname as dirname2, join as join5, resolve as resolve3 } from "path";
929
954
  // src/cli/lib/git/process.ts
930
955
  import { sync as spawnSync } from "cross-spawn";
931
956
  var MAX_GIT_BUFFER = 512 * 1024 * 1024;
957
+ var GIT_TIMEOUT_MS = 3e5;
932
958
  var GitError = class extends Error {
933
959
  constructor(message, code) {
934
960
  super(message);
@@ -937,10 +963,16 @@ var GitError = class extends Error {
937
963
  }
938
964
  };
939
965
  function runGit(cwd, args, opts = {}) {
966
+ const timeout = opts.timeoutMs ?? GIT_TIMEOUT_MS;
940
967
  const result = spawnSync("git", args, {
941
968
  cwd,
942
969
  input: opts.input,
943
970
  maxBuffer: MAX_GIT_BUFFER,
971
+ timeout,
972
+ // SIGKILL, not the default SIGTERM: a child that ignores or traps TERM
973
+ // would otherwise keep running for its full duration and the "timeout"
974
+ // would bound nothing.
975
+ killSignal: "SIGKILL",
944
976
  stdio: ["pipe", "pipe", "pipe"],
945
977
  env: {
946
978
  ...process.env,
@@ -949,6 +981,12 @@ function runGit(cwd, args, opts = {}) {
949
981
  ...opts.env ?? {}
950
982
  }
951
983
  });
984
+ if (result.error?.code === "ETIMEDOUT") {
985
+ throw new GitError(
986
+ `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.`,
987
+ "git_timeout"
988
+ );
989
+ }
952
990
  if (result.error) {
953
991
  const code = result.error.code;
954
992
  if (code === "ENOENT") {
@@ -2531,16 +2569,31 @@ function formatSchemaLintFindings(findings) {
2531
2569
  }
2532
2570
 
2533
2571
  // src/cli/lib/port.ts
2534
- import { createServer } from "net";
2572
+ import { createServer, Socket } from "net";
2535
2573
  var DEFAULT_PORT = 5173;
2536
- function checkPortAvailable(port, host = "0.0.0.0") {
2537
- return new Promise((resolve18) => {
2538
- const srv = createServer();
2539
- srv.once("error", () => resolve18(false));
2540
- srv.once("listening", () => srv.close(() => resolve18(true)));
2541
- srv.listen(port, host);
2574
+ function isPortListening(port, host, timeoutMs = 500) {
2575
+ const target2 = host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host;
2576
+ return new Promise((resolve19) => {
2577
+ const socket = new Socket();
2578
+ const done = (listening) => {
2579
+ socket.destroy();
2580
+ resolve19(listening);
2581
+ };
2582
+ socket.setTimeout(timeoutMs);
2583
+ socket.once("connect", () => done(true));
2584
+ socket.once("timeout", () => done(false));
2585
+ socket.once("error", () => done(false));
2586
+ socket.connect(port, target2);
2542
2587
  });
2543
2588
  }
2589
+ async function waitForPortListening(port, host, timeoutMs) {
2590
+ const deadline = Date.now() + timeoutMs;
2591
+ while (Date.now() < deadline) {
2592
+ if (await isPortListening(port, host)) return true;
2593
+ await new Promise((resolve19) => setTimeout(resolve19, 250));
2594
+ }
2595
+ return false;
2596
+ }
2544
2597
  function resolvePort(arg) {
2545
2598
  const raw = arg ?? process.env.DEEPSPACE_PORT;
2546
2599
  if (!raw) return DEFAULT_PORT;
@@ -2552,6 +2605,8 @@ function resolvePort(arg) {
2552
2605
  }
2553
2606
 
2554
2607
  // src/cli/commands/dev.ts
2608
+ var DEFAULT_DEV_HOST = "0.0.0.0";
2609
+ var READINESS_TIMEOUT_MS = 12e4;
2555
2610
  var DEPLOY_URL = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
2556
2611
  function noAppDirMessage(start, children) {
2557
2612
  const lines = [`No wrangler.toml found at or above ${start}.`];
@@ -2587,6 +2642,11 @@ var dev_default = defineDeepspaceCommand({
2587
2642
  type: "string",
2588
2643
  description: `Port to bind (default ${DEFAULT_PORT}, or $DEEPSPACE_PORT)`,
2589
2644
  required: false
2645
+ },
2646
+ host: {
2647
+ type: "string",
2648
+ description: `Interface to bind (default ${DEFAULT_DEV_HOST} \u2014 reachable from outside a container)`,
2649
+ required: false
2590
2650
  }
2591
2651
  },
2592
2652
  async run({ args }) {
@@ -2595,6 +2655,7 @@ var dev_default = defineDeepspaceCommand({
2595
2655
  };
2596
2656
  preflightNodeVersion("dev start");
2597
2657
  const wranglerEnv = typeof args.env === "string" && args.env.trim() ? args.env.trim() : void 0;
2658
+ const host = typeof args.host === "string" && args.host.trim() ? args.host.trim() : DEFAULT_DEV_HOST;
2598
2659
  const start = resolve7(args.dir ?? ".");
2599
2660
  const appDir = findAppDir(start);
2600
2661
  if (!appDir) {
@@ -2658,6 +2719,7 @@ var dev_default = defineDeepspaceCommand({
2658
2719
  say(`Logged in as ${payload.name ?? payload.email}`);
2659
2720
  if (wranglerEnv) say(`Wrangler env: ${wranglerEnv}`);
2660
2721
  say(`Port: ${port}`);
2722
+ if (host !== DEFAULT_DEV_HOST) say(`Host: ${host}`);
2661
2723
  let generatedSecretsCache;
2662
2724
  const appIdForSecrets = readAppId(appDir, wranglerEnv);
2663
2725
  if (appIdForSecrets) {
@@ -2685,7 +2747,7 @@ var dev_default = defineDeepspaceCommand({
2685
2747
  { sharedDevVarsCache }
2686
2748
  );
2687
2749
  preflightWindowsWorkerd(appDir);
2688
- if (!await checkPortAvailable(port)) {
2750
+ if (await isPortListening(port, host)) {
2689
2751
  const killArgv = port === DEFAULT_PORT ? ["deepspace", "dev", "kill"] : ["deepspace", "dev", "kill", "--port", String(port)];
2690
2752
  const killCmd = killArgv.join(" ");
2691
2753
  wranglerConfig.cleanup();
@@ -2696,12 +2758,34 @@ Free it with \`${killCmd}\`, or start on another port: \`deepspace dev start --p
2696
2758
  { extra: { port } }
2697
2759
  );
2698
2760
  }
2761
+ const url = `http://${host === DEFAULT_DEV_HOST ? "localhost" : host}:${port}`;
2699
2762
  say("Starting dev server...\n");
2700
- const vite = spawn("npx", ["vite", "--port", String(port), "--strictPort", "--host"], {
2701
- cwd: appDir,
2702
- stdio: "inherit",
2703
- env: wranglerViteEnv(process.env, wranglerConfig, { DEEPSPACE_PORT: String(port) })
2704
- });
2763
+ const vite = spawn(
2764
+ "npx",
2765
+ ["vite", "--port", String(port), "--strictPort", "--host", host],
2766
+ {
2767
+ cwd: appDir,
2768
+ stdio: "inherit",
2769
+ env: wranglerViteEnv(process.env, wranglerConfig, { DEEPSPACE_PORT: String(port) })
2770
+ }
2771
+ );
2772
+ if (args.json) {
2773
+ void waitForPortListening(port, host, READINESS_TIMEOUT_MS).then((ready) => {
2774
+ console.log(
2775
+ JSON.stringify(
2776
+ ready ? { ok: true, ready: true, url, port, host, appDir } : {
2777
+ ok: false,
2778
+ code: "dev_server_not_ready",
2779
+ error: `The dev server did not answer on ${url} within ${READINESS_TIMEOUT_MS / 1e3}s.`,
2780
+ url,
2781
+ port,
2782
+ host,
2783
+ appDir
2784
+ }
2785
+ )
2786
+ );
2787
+ });
2788
+ }
2705
2789
  let stopping = false;
2706
2790
  const stop = () => {
2707
2791
  stopping = true;
@@ -2723,7 +2807,7 @@ Free it with \`${killCmd}\`, or start on another port: \`deepspace dev start --p
2723
2807
  extra: { port, appDir, exitCode: code }
2724
2808
  });
2725
2809
  }
2726
- return { data: { port, appDir, wranglerEnv: wranglerEnv ?? null } };
2810
+ return { data: { port, host, url, appDir, wranglerEnv: wranglerEnv ?? null } };
2727
2811
  }
2728
2812
  });
2729
2813
 
@@ -3679,8 +3763,8 @@ var clear = defineDeepspaceCommand({
3679
3763
  failures.push({ email: a.email, error: err.message });
3680
3764
  }
3681
3765
  }
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));
3766
+ const deleted = targets.filter((target2) => !failures.some((failure) => failure.email === target2.email));
3767
+ removeTestAccounts(deleted.map((target2) => target2.id), deleted.map((target2) => target2.email));
3684
3768
  if (failures.length > 0) {
3685
3769
  throw new Refusal(
3686
3770
  `Deleted ${ok2}/${targets.length} test account(s).
@@ -3712,6 +3796,96 @@ import * as p12 from "@clack/prompts";
3712
3796
  import { defineCommand as defineCommand3 } from "citty";
3713
3797
  import { resolve as resolve12 } from "path";
3714
3798
 
3799
+ // src/cli/lib/edge-propagation.ts
3800
+ var NOT_SERVING = ["Assets have not yet deployed", "No app configured"];
3801
+ async function waitForEdgePropagation(url, timeoutMs) {
3802
+ const deadline = Date.now() + timeoutMs;
3803
+ let attempt = 0;
3804
+ let streak = 0;
3805
+ while (Date.now() < deadline) {
3806
+ attempt++;
3807
+ try {
3808
+ const response = await fetch(url, { redirect: "manual" });
3809
+ const body = await response.text();
3810
+ if (!NOT_SERVING.some((marker) => body.includes(marker))) {
3811
+ streak++;
3812
+ if (streak >= 3) return true;
3813
+ await new Promise((resolve19) => setTimeout(resolve19, 1500));
3814
+ continue;
3815
+ }
3816
+ streak = 0;
3817
+ } catch {
3818
+ streak = 0;
3819
+ }
3820
+ const waitMs = Math.min(8e3, 1e3 * 2 ** (attempt - 1));
3821
+ await new Promise((resolve19) => setTimeout(resolve19, waitMs));
3822
+ }
3823
+ return false;
3824
+ }
3825
+
3826
+ // src/shared/app-files.ts
3827
+ var MAX_APP_FILE_BYTES = 1024 * 1024 * 1024;
3828
+ var MAX_UPLOAD_REQUEST_BYTES = 25 * 1024 * 1024;
3829
+ var UPLOAD_PART_BYTES = 20 * 1024 * 1024;
3830
+ var MAX_UPLOAD_PARTS = Math.ceil(MAX_APP_FILE_BYTES / UPLOAD_PART_BYTES);
3831
+ var MAX_BASE64_UPLOAD_BYTES = Math.floor(MAX_UPLOAD_REQUEST_BYTES * 3 / 4);
3832
+ function planUploadParts(totalBytes, partSize) {
3833
+ const parts = [];
3834
+ for (let start = 0; start < totalBytes; start += partSize) {
3835
+ parts.push({
3836
+ partNumber: parts.length + 1,
3837
+ start,
3838
+ end: Math.min(start + partSize, totalBytes)
3839
+ });
3840
+ }
3841
+ return parts;
3842
+ }
3843
+ var MAX_DEPLOY_ASSET_FILE_BYTES = 25 * 1024 * 1024;
3844
+ var DANGEROUS_MIME_TYPES = /* @__PURE__ */ new Set([
3845
+ "text/html",
3846
+ "application/xhtml+xml",
3847
+ "image/svg+xml",
3848
+ "application/xml",
3849
+ "text/xml",
3850
+ "application/javascript",
3851
+ "text/javascript",
3852
+ "application/ecmascript",
3853
+ "text/ecmascript"
3854
+ ]);
3855
+ function activeContentMessage(mimeType) {
3856
+ return `${mimeType} is not accepted: the files API serves your app's own origin, so stored HTML/SVG/JavaScript would execute there with your users' sessions. Ship it as a build asset under \`public/\` instead, where it is served as part of your app rather than as user-uploaded content.`;
3857
+ }
3858
+ function formatBytes(bytes) {
3859
+ if (bytes >= 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GiB`;
3860
+ if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
3861
+ if (bytes >= 1024) return `${Math.round(bytes / 1024)} KiB`;
3862
+ return `${bytes} B`;
3863
+ }
3864
+ function encodeKeyPath(key) {
3865
+ return key.split("/").map(encodeURIComponent).join("/");
3866
+ }
3867
+ function oversizeMessage(bytes, limit = MAX_APP_FILE_BYTES) {
3868
+ return `That file is ${formatBytes(bytes)}; the limit is ${formatBytes(limit)} per file.`;
3869
+ }
3870
+ function describeFilesFailure(status3, body) {
3871
+ const trimmed = body.trim();
3872
+ if (trimmed.startsWith("{")) {
3873
+ try {
3874
+ const parsed = JSON.parse(trimmed);
3875
+ if (parsed.error) return parsed.error;
3876
+ } catch {
3877
+ }
3878
+ }
3879
+ if (status3 === 413) {
3880
+ return `The upload is too large. Files are limited to ${formatBytes(MAX_APP_FILE_BYTES)}, and one request may carry ${formatBytes(MAX_UPLOAD_REQUEST_BYTES)} \u2014 a larger request is rejected by the network before the app receives it.`;
3881
+ }
3882
+ if (status3 === 411) return "The upload did not declare its length.";
3883
+ if (status3 === 401 || status3 === 403) return "Not authorized to write these files.";
3884
+ if (status3 === 415) return "That file type is not accepted.";
3885
+ if (status3 >= 500) return `The files service failed (HTTP ${status3}). Retry shortly.`;
3886
+ return `Upload failed (HTTP ${status3}).`;
3887
+ }
3888
+
3715
3889
  // src/cli/commands/deploy/build.ts
3716
3890
  import * as p6 from "@clack/prompts";
3717
3891
  import { execSync as execSync2 } from "child_process";
@@ -4031,7 +4205,14 @@ var RESERVED_RUN_WORKER_FIRST = /* @__PURE__ */ new Set([
4031
4205
  "/_documentation",
4032
4206
  "/_documentation/*",
4033
4207
  "/_documentation-root",
4034
- "/_documentation-root/*"
4208
+ "/_documentation-root/*",
4209
+ // Agent-protocol paths: the worker must see them so they cannot fall through
4210
+ // to the SPA shell (templates/base/src/server/http-routes.ts reserves them).
4211
+ "/llms.txt",
4212
+ "/llms-full.txt",
4213
+ "/.well-known/mcp",
4214
+ "/.well-known/mcp.json",
4215
+ "/.well-known/mcp/*"
4035
4216
  ]);
4036
4217
  async function buildDeployBundle(options) {
4037
4218
  const { appDir, appName, envName, sharedDevVarsCache, output, spinner: spinner2 } = options;
@@ -4165,6 +4346,12 @@ function resolveDeployRunWorkerFirst(config) {
4165
4346
  const routes = extractRunWorkerFirst(config);
4166
4347
  return routes;
4167
4348
  }
4349
+ function oversizedAssetRefusal(assets) {
4350
+ const tooBig = assets.find((asset) => asset.size > MAX_DEPLOY_ASSET_FILE_BYTES);
4351
+ if (!tooBig) return null;
4352
+ return `${tooBig.path} is ${formatBytes(tooBig.size)}; the per-file limit for deploy assets is ${formatBytes(MAX_DEPLOY_ASSET_FILE_BYTES)}. ${LARGE_MEDIA_HOME}`;
4353
+ }
4354
+ var LARGE_MEDIA_HOME = "Large media belongs in your app files (`deepspace app files put`), not in the deploy bundle \u2014 move it out of `public/` and reference it at runtime.";
4168
4355
  function collectAssets(dir) {
4169
4356
  const assets = [];
4170
4357
  const walk = (currentDir, prefix) => {
@@ -4821,7 +5008,7 @@ function isThinPackRejection(result) {
4821
5008
  function isRecoverablePushFailure(status3) {
4822
5009
  return status3 === "non_fast_forward" || status3 === "ref_conflict";
4823
5010
  }
4824
- function classifyPushTransportFailure(error) {
5011
+ function classifyPushTransportFailure(error, cwd) {
4825
5012
  const message = error instanceof Error ? error.message : String(error);
4826
5013
  if (/(?:HTTP |error: )409\b/i.test(message)) {
4827
5014
  return {
@@ -4841,6 +5028,9 @@ function classifyPushTransportFailure(error) {
4841
5028
  error: "The cloud repo rate-limited this push. Wait a few seconds, then retry."
4842
5029
  };
4843
5030
  }
5031
+ if (/(?:HTTP |error: )413\b/i.test(message)) {
5032
+ return { code: "push_too_large", error: pushTooLargeMessage(cwd) };
5033
+ }
4844
5034
  return null;
4845
5035
  }
4846
5036
  function pushToSpace(cwd, token, refspec, opts = {}) {
@@ -4865,18 +5055,29 @@ function pushToSpace(cwd, token, refspec, opts = {}) {
4865
5055
  }
4866
5056
  var OVERSIZED_PUSH_RE = /too large|exceeds|\bLFS\b/i;
4867
5057
  var SERVER_OBJECT_CAP_BYTES = 20 * 1024 * 1024;
4868
- 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.`;
4869
- function formatBytes(bytes) {
4870
- if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
4871
- if (bytes >= 1024) return `${Math.round(bytes / 1024)} KiB`;
4872
- return `${bytes} B`;
4873
- }
4874
- function oversizedPushFix(cwd, cap = SERVER_OBJECT_CAP_BYTES) {
4875
- const objects = cwd ? findOversizedObjects(cwd, cap) : [];
4876
- if (objects.length === 0) return OVERSIZED_PUSH_FIX;
5058
+ var SERVER_PACK_CAP_BYTES = 32 * 1024 * 1024;
5059
+ var PUSH_CEILINGS = `${formatBytes(SERVER_OBJECT_CAP_BYTES)} per file, ${formatBytes(SERVER_PACK_CAP_BYTES)} per push`;
5060
+ 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\`.`;
5061
+ function removeFromHistoryAdvice(path) {
5062
+ const file = path ? shQuote(path) : "<file>";
5063
+ 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 both the push and the bundle, in this order: (1) move it OUT of \`public/\` \u2014 .gitignore keeps it out of Git but not out of the deploy bundle, which uploads every file under \`public/\`; (2) if the commits that carry it are still local, \`git reset --soft <last pushed commit>\`, then \`git restore --staged ${file}\` \u2014 reset --soft moves HEAD only and leaves the file staged, so skipping this step re-commits the same blob; (3) commit what remains, then push \u2014 if git says "nothing to commit", that commit held only the file, so skip straight to the push and "Everything up-to-date" means you are done. If it already reached pushed history, rewrite it out instead (\`git filter-repo --path ${file} --invert-paths\`).`;
5064
+ }
5065
+ function fixForObjects(objects) {
5066
+ if (objects.length === 0) return `${OVERSIZED_PUSH_FIX} ${removeFromHistoryAdvice(null)}`;
4877
5067
  const one = objects.length === 1;
4878
5068
  const named = objects.map((object) => `${object.path} (${formatBytes(object.bytes)})`).join(", ");
4879
- 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.`;
5069
+ return `The oversized ${one ? "file is" : "files are"} ${named}. ${removeFromHistoryAdvice(one ? objects[0].path : null)}`;
5070
+ }
5071
+ function oversizedPushFix(cwd, cap = SERVER_OBJECT_CAP_BYTES) {
5072
+ return fixForObjects(cwd ? findOversizedObjects(cwd, cap) : []);
5073
+ }
5074
+ function pushTooLargeMessage(cwd) {
5075
+ 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.`;
5076
+ const objects = cwd ? findOversizedObjects(cwd, SERVER_OBJECT_CAP_BYTES) : [];
5077
+ if (objects.length === 0) {
5078
+ 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}`;
5079
+ }
5080
+ return `${ceilings} ${fixForObjects(objects)}`;
4880
5081
  }
4881
5082
  function pushFailureMessage(label, push, cwd) {
4882
5083
  const reason = push.reason ?? push.summary ?? "";
@@ -4966,7 +5167,7 @@ function remotePublicRefs(cwd, remote) {
4966
5167
  function remoteBranchOid(cwd, remote, branch) {
4967
5168
  return remotePublicRefs(cwd, remote).find((ref) => ref.name === `refs/heads/${branch}`)?.oid ?? null;
4968
5169
  }
4969
- function localTransferRefs(cwd, prefix, target) {
5170
+ function localTransferRefs(cwd, prefix, target2) {
4970
5171
  const output = runGit(cwd, [
4971
5172
  "for-each-ref",
4972
5173
  "--format=%(refname) %(objectname)",
@@ -4974,7 +5175,7 @@ function localTransferRefs(cwd, prefix, target) {
4974
5175
  ]).stdout.toString("utf-8");
4975
5176
  return output.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => {
4976
5177
  const [name, oid] = line.split(/\s+/, 2);
4977
- return { name: `${target}/${name.slice(`${prefix}/`.length)}`, oid };
5178
+ return { name: `${target2}/${name.slice(`${prefix}/`.length)}`, oid };
4978
5179
  }).filter((ref) => /^[0-9a-f]{40}$/.test(ref.oid));
4979
5180
  }
4980
5181
 
@@ -5087,7 +5288,7 @@ async function syncDeployRepository(options) {
5087
5288
  }
5088
5289
  } else if (pushResult.status === "rejected") {
5089
5290
  output.die(
5090
- `Cloud repo rejected ${branch}${pushResult.reason ? `: ${pushResult.reason}` : ""}. Resolve the rejection and publish the commit before deploying.`,
5291
+ `${pushFailureMessage("Cloud repo push", pushResult, appDir)} Publish the commit before deploying.`,
5091
5292
  "vc_push_rejected"
5092
5293
  );
5093
5294
  } else {
@@ -5131,7 +5332,7 @@ async function syncDeployRepository(options) {
5131
5332
  sourceRevision = claimed.revision;
5132
5333
  }
5133
5334
  } catch (error) {
5134
- const failure = deployRepositoryFailure(error);
5335
+ const failure = deployRepositoryFailure(error, appDir);
5135
5336
  output.die(failure.error, failure.code);
5136
5337
  }
5137
5338
  return { commitOid, recoverable, deployKey, source, sourceRevision, baseReleaseId };
@@ -5158,8 +5359,8 @@ function classifyRemoteState(appDir, token, branch, localTip, remote) {
5158
5359
  function errorMessage3(error) {
5159
5360
  return error instanceof Error ? error.message : String(error);
5160
5361
  }
5161
- function deployRepositoryFailure(error) {
5162
- const transportFailure = classifyPushTransportFailure(error);
5362
+ function deployRepositoryFailure(error, cwd) {
5363
+ const transportFailure = classifyPushTransportFailure(error, cwd);
5163
5364
  if (transportFailure) return transportFailure;
5164
5365
  const code = errorCode(error);
5165
5366
  const message = errorMessage3(error);
@@ -5169,15 +5370,18 @@ function deployRepositoryFailure(error) {
5169
5370
  error: `Version-control sync failed: ${message}`
5170
5371
  };
5171
5372
  }
5373
+ var PUSH_RETRY_BUDGET_MS = 12e4;
5172
5374
  async function pushWithTransientRetry(push) {
5173
5375
  const backoffMs = [500, 1500, 3500];
5376
+ const deadline = Date.now() + PUSH_RETRY_BUDGET_MS;
5174
5377
  for (let attempt = 0; ; attempt++) {
5175
5378
  try {
5176
5379
  return push();
5177
5380
  } catch (error) {
5178
5381
  const message = errorMessage3(error);
5179
- if (/(?:HTTP |error: )(?:429|503)\b/i.test(message) && attempt < backoffMs.length) {
5180
- await new Promise((resolve18) => setTimeout(resolve18, backoffMs[attempt]));
5382
+ const transient = /(?:HTTP |error: )(?:429|503)\b/i.test(message);
5383
+ if (transient && attempt < backoffMs.length && Date.now() + backoffMs[attempt] < deadline) {
5384
+ await new Promise((resolve19) => setTimeout(resolve19, backoffMs[attempt]));
5181
5385
  continue;
5182
5386
  }
5183
5387
  throw error;
@@ -5342,7 +5546,7 @@ async function deployBuiltBundle(options) {
5342
5546
  );
5343
5547
  }
5344
5548
  if (body.onBehalfOfOwner) {
5345
- p10.log.warn(`Deployed on behalf of owner ${body.onBehalfOfOwner}`);
5549
+ p10.log.warn("This release is attributed to the app's owner, not to your account.");
5346
5550
  }
5347
5551
  if (body.staleBaseGuard === "skipped") {
5348
5552
  p10.log.warn(
@@ -5423,10 +5627,11 @@ async function uploadDeployAssets(options) {
5423
5627
  p10.log.info(`Assets: ${byHash.size} file(s), all already uploaded`);
5424
5628
  return;
5425
5629
  }
5426
- const missingBytes = missing.reduce((total, asset) => total + asset.size, 0);
5630
+ const missingBytes = missing.reduce((total2, asset) => total2 + asset.size, 0);
5427
5631
  p10.log.info(
5428
5632
  `Assets: ${byHash.size} file(s), uploading ${missing.length} (${Math.ceil(missingBytes / 1024)} KiB); the rest are already stored`
5429
5633
  );
5634
+ const total = missing.length;
5430
5635
  let uploaded = 0;
5431
5636
  const next = () => missing.pop();
5432
5637
  const worker = async () => {
@@ -5455,7 +5660,7 @@ async function uploadDeployAssets(options) {
5455
5660
  );
5456
5661
  }
5457
5662
  uploaded++;
5458
- spinner2.message(`Uploading assets \u2014 ${uploaded}/${uploaded + missing.length}...`);
5663
+ spinner2.message(`Uploading assets \u2014 ${uploaded}/${total}...`);
5459
5664
  }
5460
5665
  };
5461
5666
  try {
@@ -5521,7 +5726,7 @@ async function postWithRetry(url, makeInit, {
5521
5726
  lastError = error;
5522
5727
  }
5523
5728
  if (attempt < attempts) {
5524
- await new Promise((resolve18) => setTimeout(resolve18, Math.min(8e3, 500 * 2 ** (attempt - 1))));
5729
+ await new Promise((resolve19) => setTimeout(resolve19, Math.min(8e3, 500 * 2 ** (attempt - 1))));
5525
5730
  }
5526
5731
  }
5527
5732
  if (lastResponse) return lastResponse;
@@ -5686,7 +5891,11 @@ var DEPLOY_URL3 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
5686
5891
  var deploy_default = defineCommand3({
5687
5892
  meta: {
5688
5893
  name: "deploy",
5689
- description: "Build and deploy your DeepSpace app"
5894
+ // The per-file ceiling belongs in --help: it was previously discoverable
5895
+ // only by hitting it, after the build and the push had already run. The
5896
+ // per-deploy total is deliberately absent — it is env-configurable, so any
5897
+ // number stated here would be wrong for some environment.
5898
+ description: `Build and deploy your DeepSpace app (assets: ${formatBytes(MAX_DEPLOY_ASSET_FILE_BYTES)} per file)`
5690
5899
  },
5691
5900
  args: {
5692
5901
  dir: {
@@ -5782,6 +5991,20 @@ var deploy_default = defineCommand3({
5782
5991
  output
5783
5992
  });
5784
5993
  const sourceState = await getAppSource(DEPLOY_URL3, token, appId);
5994
+ const spinner2 = createSpinner();
5995
+ const bundle = await buildDeployBundle({
5996
+ appDir,
5997
+ appName,
5998
+ envName,
5999
+ sharedDevVarsCache: secretsCache.linked !== null,
6000
+ output,
6001
+ spinner: spinner2
6002
+ });
6003
+ const oversized = oversizedAssetRefusal(bundle.assets);
6004
+ if (oversized) {
6005
+ spinner2.stop("Deploy failed");
6006
+ output.die(oversized, "assets_too_large");
6007
+ }
5785
6008
  const repository = await syncDeployRepository({
5786
6009
  deployUrl: DEPLOY_URL3,
5787
6010
  appDir,
@@ -5792,15 +6015,6 @@ var deploy_default = defineCommand3({
5792
6015
  output,
5793
6016
  sourceState
5794
6017
  });
5795
- const spinner2 = createSpinner();
5796
- const bundle = await buildDeployBundle({
5797
- appDir,
5798
- appName,
5799
- envName,
5800
- sharedDevVarsCache: secretsCache.linked !== null,
5801
- output,
5802
- spinner: spinner2
5803
- });
5804
6018
  const secrets = prepareDeploySecrets({
5805
6019
  appDir,
5806
6020
  envName,
@@ -5826,7 +6040,7 @@ var deploy_default = defineCommand3({
5826
6040
  });
5827
6041
  if (body.url) {
5828
6042
  spinner2.message("Waiting for edge propagation...");
5829
- const ready = await waitForAssetsReady(body.url, 9e4);
6043
+ const ready = await waitForEdgePropagation(body.url, 9e4);
5830
6044
  if (!ready) {
5831
6045
  spinner2.stop("Deployed (edge propagation still in progress after 90s)");
5832
6046
  p12.log.warn(
@@ -5891,30 +6105,6 @@ function blankSelectorRefusal(args) {
5891
6105
  function staleBaseGuardFields(body) {
5892
6106
  return body.staleBaseGuard === "skipped" ? { staleBaseGuard: "skipped" } : {};
5893
6107
  }
5894
- async function waitForAssetsReady(url, timeoutMs) {
5895
- const deadline = Date.now() + timeoutMs;
5896
- let attempt = 0;
5897
- let streak = 0;
5898
- while (Date.now() < deadline) {
5899
- attempt++;
5900
- try {
5901
- const response = await fetch(url, { redirect: "manual" });
5902
- const body = await response.text();
5903
- if (!body.includes("Assets have not yet deployed") && !body.includes("No app configured")) {
5904
- streak++;
5905
- if (streak >= 3) return true;
5906
- await new Promise((resolve18) => setTimeout(resolve18, 1500));
5907
- continue;
5908
- }
5909
- streak = 0;
5910
- } catch {
5911
- streak = 0;
5912
- }
5913
- const waitMs = Math.min(8e3, 1e3 * 2 ** (attempt - 1));
5914
- await new Promise((resolve18) => setTimeout(resolve18, waitMs));
5915
- }
5916
- return false;
5917
- }
5918
6108
  function errorMessage6(error) {
5919
6109
  return error instanceof Error ? error.message : String(error);
5920
6110
  }
@@ -5954,6 +6144,19 @@ function requireAppIdArg(explicit) {
5954
6144
  }
5955
6145
  return id;
5956
6146
  }
6147
+ function liveAppUrl(app2) {
6148
+ return app2.status === "active" && app2.deployedAt !== null ? app2.url : null;
6149
+ }
6150
+ async function listApps(deployUrl, token) {
6151
+ const { apps } = await apiFetch(deployUrl, token, "/api/apps");
6152
+ if (!Array.isArray(apps)) {
6153
+ throw new InputError(
6154
+ `The deploy service at ${deployUrl} returned an unexpected response shape for the app list \u2014 check DEEPSPACE_DEPLOY_URL (is this the service the app lives on?).`,
6155
+ "invalid_response"
6156
+ );
6157
+ }
6158
+ return apps;
6159
+ }
5957
6160
  function matchAppSelector(apps, selector) {
5958
6161
  const byId = apps.find((app2) => app2.appId === selector);
5959
6162
  if (byId) return byId.appId;
@@ -5963,14 +6166,7 @@ function matchAppSelector(apps, selector) {
5963
6166
  }
5964
6167
  async function resolveAppSelector(deployUrl, token, selector) {
5965
6168
  if (STRICT_APP_ID_RE.test(selector)) return selector;
5966
- const { apps } = await apiFetch(deployUrl, token, "/api/apps");
5967
- if (!Array.isArray(apps)) {
5968
- throw new InputError(
5969
- `The deploy service at ${deployUrl} returned an unexpected response shape for the app list \u2014 check DEEPSPACE_DEPLOY_URL (is this the service the app lives on?).`,
5970
- "invalid_response"
5971
- );
5972
- }
5973
- const id = matchAppSelector(apps, selector);
6169
+ const id = matchAppSelector(await listApps(deployUrl, token), selector);
5974
6170
  if (id) return id;
5975
6171
  const shared = await resolveAppIdByName(deployUrl, token, selector);
5976
6172
  if (shared) return shared;
@@ -6053,7 +6249,7 @@ function assertAppTargetResolvable(appArg, options = {}) {
6053
6249
  async function warnIfPhantomApp(deployUrl, token, appId, explicit) {
6054
6250
  if (!explicit || !STRICT_APP_ID_RE.test(explicit.trim())) return;
6055
6251
  try {
6056
- const { apps } = await apiFetch(deployUrl, token, "/api/apps");
6252
+ const apps = await listApps(deployUrl, token);
6057
6253
  if (!apps.some((app2) => app2.appId === appId)) {
6058
6254
  process.stderr.write(
6059
6255
  `warning: ${appId} is not among your apps. If it exists and you have access (collaborator/admin), the server will use it; if it doesn't exist, this will register it as a NEW app under your account \u2014 check the id if that's not what you meant (a subdomain name also works with -a).
@@ -6167,7 +6363,6 @@ var whoami_default = defineDeepspaceCommand({
6167
6363
  if (!args.json) {
6168
6364
  console.log(`Name: ${payload.name ?? "(none)"}`);
6169
6365
  console.log(`Email: ${payload.email ?? "(none)"}`);
6170
- console.log(`UserID: ${payload.sub ?? "(none)"}`);
6171
6366
  console.log(`Type: ${accountType}`);
6172
6367
  if (payload.iss) console.log(`Issuer: ${payload.iss}`);
6173
6368
  console.log(`Dashboard: ${DASHBOARD_URL}`);
@@ -6199,7 +6394,7 @@ var apps_default = defineDeepspaceCommand({
6199
6394
  },
6200
6395
  async run({ args }) {
6201
6396
  const token = await ensureToken();
6202
- const { apps } = await apiFetch(DEPLOY_URL5, token, "/api/apps");
6397
+ const apps = await listApps(DEPLOY_URL5, token);
6203
6398
  if (!args.json) {
6204
6399
  if (!apps.length) {
6205
6400
  console.log("No apps yet. Create one with `npx create-deepspace <name>` and `deepspace deploy`.");
@@ -6207,16 +6402,18 @@ var apps_default = defineDeepspaceCommand({
6207
6402
  const rows = [...apps].sort(
6208
6403
  (left, right) => Number(right.status !== "undeployed") - Number(left.status !== "undeployed")
6209
6404
  );
6405
+ const roleOf = (a) => a.role ?? "owner";
6406
+ const roleWidth = Math.max(4, ...rows.map((a) => roleOf(a).length));
6210
6407
  const nameWidth = Math.max(4, ...rows.map((a) => (a.name ?? "\u2014").length));
6211
6408
  const statusWidth = Math.max(6, ...rows.map((a) => a.status.length));
6212
6409
  const idWidth = Math.max(6, ...rows.map((a) => a.appId.length));
6213
6410
  console.log(
6214
- `${"NAME".padEnd(nameWidth)} ${"STATUS".padEnd(statusWidth)} ${"APP ID".padEnd(idWidth)} URL`
6411
+ `${"NAME".padEnd(nameWidth)} ${"STATUS".padEnd(statusWidth)} ${"ROLE".padEnd(roleWidth)} ${"APP ID".padEnd(idWidth)} URL`
6215
6412
  );
6216
6413
  for (const a of rows) {
6217
- const url = a.url ?? "(not deployed)";
6414
+ const url = liveAppUrl(a) ?? "(not deployed)";
6218
6415
  console.log(
6219
- `${(a.name ?? "\u2014").padEnd(nameWidth)} ${a.status.padEnd(statusWidth)} ${a.appId.padEnd(idWidth)} ${url}`
6416
+ `${(a.name ?? "\u2014").padEnd(nameWidth)} ${a.status.padEnd(statusWidth)} ${roleOf(a).padEnd(roleWidth)} ${a.appId.padEnd(idWidth)} ${url}`
6220
6417
  );
6221
6418
  }
6222
6419
  }
@@ -7621,11 +7818,11 @@ function fmtDate(iso) {
7621
7818
  }
7622
7819
  async function confirm4(message) {
7623
7820
  process.stdout.write(`${message} [y/N] `);
7624
- return new Promise((resolve18) => {
7821
+ return new Promise((resolve19) => {
7625
7822
  process.stdin.setEncoding("utf-8");
7626
7823
  process.stdin.once("data", (data) => {
7627
7824
  const answer = data.toString().trim().toLowerCase();
7628
- resolve18(answer === "y" || answer === "yes");
7825
+ resolve19(answer === "y" || answer === "yes");
7629
7826
  });
7630
7827
  });
7631
7828
  }
@@ -7913,8 +8110,401 @@ var domain_default = defineCommand5({
7913
8110
  subCommands: { search, buy, list: list2, status, attach, detach, renew }
7914
8111
  });
7915
8112
 
7916
- // src/cli/commands/collaborators.ts
8113
+ // src/cli/commands/app-files.ts
7917
8114
  import { defineCommand as defineCommand6 } from "citty";
8115
+ import { basename as basename4, resolve as resolve15 } from "path";
8116
+ import { existsSync as existsSync21, statSync as statSync2 } from "fs";
8117
+
8118
+ // src/cli/lib/app-files-api.ts
8119
+ import { createWriteStream, openAsBlob, statSync } from "fs";
8120
+ import { basename as basename3, extname } from "path";
8121
+ import { Readable } from "stream";
8122
+ import { pipeline } from "stream/promises";
8123
+ import { randomUUID as randomUUID2 } from "crypto";
8124
+ var CONTENT_TYPES = {
8125
+ ".avif": "image/avif",
8126
+ ".css": "text/css",
8127
+ ".csv": "text/csv",
8128
+ ".gif": "image/gif",
8129
+ ".htm": "text/html",
8130
+ ".html": "text/html",
8131
+ ".ico": "image/x-icon",
8132
+ ".jpeg": "image/jpeg",
8133
+ ".jpg": "image/jpeg",
8134
+ ".js": "text/javascript",
8135
+ ".json": "application/json",
8136
+ ".md": "text/markdown",
8137
+ ".mjs": "text/javascript",
8138
+ ".svg": "image/svg+xml",
8139
+ ".mp3": "audio/mpeg",
8140
+ ".mp4": "video/mp4",
8141
+ ".otf": "font/otf",
8142
+ ".pdf": "application/pdf",
8143
+ ".png": "image/png",
8144
+ ".txt": "text/plain",
8145
+ ".wasm": "application/wasm",
8146
+ ".wav": "audio/wav",
8147
+ ".webm": "video/webm",
8148
+ ".webp": "image/webp",
8149
+ ".woff": "font/woff",
8150
+ ".woff2": "font/woff2",
8151
+ ".zip": "application/zip"
8152
+ };
8153
+ function contentTypeFor(path) {
8154
+ return CONTENT_TYPES[extname(path).toLowerCase()] ?? "application/octet-stream";
8155
+ }
8156
+ function filesPath(appId, rest = "") {
8157
+ return `/api/app-files/${appId}${rest}`;
8158
+ }
8159
+ async function request(baseUrl, token, path, init = {}) {
8160
+ let res;
8161
+ try {
8162
+ res = await fetch(`${baseUrl}${path}`, {
8163
+ ...init,
8164
+ headers: { Authorization: `Bearer ${token}`, ...init.headers ?? {} }
8165
+ });
8166
+ } catch (err) {
8167
+ throw new ApiError(
8168
+ `Could not reach the DeepSpace platform at ${baseUrl}: ${err instanceof Error ? err.message : String(err)}. Check your connection (and DEEPSPACE_PLATFORM_URL), then retry.`,
8169
+ 0,
8170
+ "network_error",
8171
+ path
8172
+ );
8173
+ }
8174
+ if (res.ok) return res;
8175
+ const text2 = await res.text();
8176
+ let code;
8177
+ try {
8178
+ code = JSON.parse(text2.trim()).code;
8179
+ } catch {
8180
+ }
8181
+ throw new ApiError(
8182
+ describeFilesFailure(res.status, text2),
8183
+ res.status,
8184
+ // Same slug the worker emits for the same condition — the edge cutting an
8185
+ // oversized request short is not a different failure from the worker
8186
+ // refusing it, and two codes made it look like one.
8187
+ code ?? (res.status === 413 ? "too_large" : res.status >= 500 ? "server_error" : "http_error"),
8188
+ path
8189
+ );
8190
+ }
8191
+ async function readJson(res, path) {
8192
+ const text2 = await res.text();
8193
+ try {
8194
+ return JSON.parse(text2);
8195
+ } catch {
8196
+ throw new ApiError(
8197
+ `The DeepSpace platform returned a malformed (non-JSON) response.`,
8198
+ res.status,
8199
+ "invalid_response",
8200
+ path
8201
+ );
8202
+ }
8203
+ }
8204
+ async function multipartBody(localPath, fileName, contentType) {
8205
+ const boundary = `----deepspace${randomUUID2().replace(/-/g, "")}`;
8206
+ const safeName = fileName.replace(/["\\\r\n]/g, "_");
8207
+ const head = `--${boundary}\r
8208
+ Content-Disposition: form-data; name="file"; filename="${safeName}"\r
8209
+ Content-Type: ${contentType}\r
8210
+ \r
8211
+ `;
8212
+ const file = await openAsBlob(localPath, { type: contentType });
8213
+ return {
8214
+ blob: new Blob([head, file, `\r
8215
+ --${boundary}--\r
8216
+ `]),
8217
+ contentType: `multipart/form-data; boundary=${boundary}`
8218
+ };
8219
+ }
8220
+ async function uploadAppFile(baseUrl, token, appId, localPath, key, onProgress) {
8221
+ const path = filesPath(appId, `/upload?key=${encodeKeyPath(key)}`);
8222
+ const size = statSync(localPath).size;
8223
+ if (size > MAX_APP_FILE_BYTES) {
8224
+ throw new ApiError(oversizeMessage(size), 0, "too_large", path);
8225
+ }
8226
+ const declaredType = contentTypeFor(localPath);
8227
+ if (DANGEROUS_MIME_TYPES.has(declaredType)) {
8228
+ throw new ApiError(activeContentMessage(declaredType), 0, "active_content", path);
8229
+ }
8230
+ if (size > UPLOAD_PART_BYTES) {
8231
+ return uploadInParts(baseUrl, token, appId, localPath, key, size, onProgress);
8232
+ }
8233
+ const { blob, contentType } = await multipartBody(localPath, basename3(localPath), declaredType);
8234
+ const res = await request(baseUrl, token, path, {
8235
+ method: "POST",
8236
+ headers: { "Content-Type": contentType },
8237
+ body: blob
8238
+ });
8239
+ const body = await readJson(res, path);
8240
+ onProgress?.(size, size, 1, 1);
8241
+ return { key: body.key, name: body.name, size, parts: 1 };
8242
+ }
8243
+ function isRetryable(err) {
8244
+ if (!(err instanceof ApiError)) return false;
8245
+ return err.status === 0 || err.status === 429 || err.status >= 500;
8246
+ }
8247
+ async function uploadInParts(baseUrl, token, appId, localPath, key, size, onProgress) {
8248
+ const contentType = contentTypeFor(localPath);
8249
+ const initPath = filesPath(appId, `/multipart?key=${encodeKeyPath(key)}`);
8250
+ const session = await readJson(
8251
+ await request(baseUrl, token, initPath, {
8252
+ method: "POST",
8253
+ headers: { "Content-Type": "application/json" },
8254
+ body: JSON.stringify({ name: basename3(localPath), mimeType: contentType, size })
8255
+ }),
8256
+ initPath
8257
+ );
8258
+ const query = `uploadKey=${encodeKeyPath(session.uploadKey)}&uploadId=${encodeURIComponent(session.uploadId)}`;
8259
+ if (!Number.isSafeInteger(session.partSize) || session.partSize < 1) {
8260
+ throw new ApiError(
8261
+ `The platform advertised an unusable part size (${session.partSize}).`,
8262
+ 0,
8263
+ "invalid_response",
8264
+ initPath
8265
+ );
8266
+ }
8267
+ const plan = planUploadParts(size, session.partSize);
8268
+ const file = await openAsBlob(localPath, { type: contentType });
8269
+ const uploaded = [];
8270
+ try {
8271
+ for (const part of plan) {
8272
+ const partPath = filesPath(appId, `/multipart/part?${query}&partNumber=${part.partNumber}`);
8273
+ const send = () => request(baseUrl, token, partPath, {
8274
+ method: "PUT",
8275
+ body: file.slice(part.start, part.end, contentType)
8276
+ });
8277
+ const res = await send().catch(async (err) => {
8278
+ if (!isRetryable(err)) throw err;
8279
+ return send();
8280
+ });
8281
+ uploaded.push(await readJson(res, partPath));
8282
+ onProgress?.(part.end, size, part.partNumber, plan.length);
8283
+ }
8284
+ const completePath = filesPath(appId, `/multipart/complete?${query}`);
8285
+ const body = await readJson(
8286
+ await request(baseUrl, token, completePath, {
8287
+ method: "POST",
8288
+ headers: { "Content-Type": "application/json" },
8289
+ body: JSON.stringify({ parts: uploaded })
8290
+ }),
8291
+ completePath
8292
+ );
8293
+ return { key: body.key, name: body.name, size, parts: plan.length };
8294
+ } catch (err) {
8295
+ await request(baseUrl, token, filesPath(appId, `/multipart?${query}`), {
8296
+ method: "DELETE"
8297
+ }).catch(() => void 0);
8298
+ throw err;
8299
+ }
8300
+ }
8301
+ async function listAppFiles(baseUrl, token, appId, query) {
8302
+ const path = filesPath(appId, `?${query}`);
8303
+ return readJson(await request(baseUrl, token, path), path);
8304
+ }
8305
+ async function deleteAppFile(baseUrl, token, appId, key) {
8306
+ const path = filesPath(appId, `/${encodeKeyPath(key)}`);
8307
+ const res = await request(baseUrl, token, path, { method: "DELETE" });
8308
+ const body = await readJson(res, path);
8309
+ return { existed: body.existed !== false };
8310
+ }
8311
+ async function downloadAppFile(baseUrl, token, appId, key, destination) {
8312
+ const path = filesPath(appId, `/${encodeKeyPath(key)}`);
8313
+ const res = await request(baseUrl, token, path);
8314
+ if (!res.body) {
8315
+ throw new ApiError("The server returned an empty response", 502, "invalid_response", path);
8316
+ }
8317
+ let bytes = 0;
8318
+ await pipeline(
8319
+ Readable.fromWeb(res.body),
8320
+ async function* (chunks) {
8321
+ for await (const chunk of chunks) {
8322
+ bytes += chunk.length;
8323
+ yield chunk;
8324
+ }
8325
+ },
8326
+ createWriteStream(destination)
8327
+ );
8328
+ return { bytes, contentType: res.headers.get("content-type") ?? "application/octet-stream" };
8329
+ }
8330
+
8331
+ // src/cli/commands/app-files.ts
8332
+ var DEPLOY_URL8 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
8333
+ var PLATFORM_URL = process.env.DEEPSPACE_PLATFORM_URL ?? PLATFORM_URLS.platform;
8334
+ async function target(app2) {
8335
+ const token = await ensureToken();
8336
+ const appId = await resolveAppTarget(DEPLOY_URL8, token, app2);
8337
+ return { token, appId };
8338
+ }
8339
+ var MAX_LIST_LIMIT = 1e3;
8340
+ function parseLimit(raw) {
8341
+ const limit = Number(raw ?? 100);
8342
+ if (!Number.isInteger(limit) || limit < 1 || limit > MAX_LIST_LIMIT) {
8343
+ throw new Refusal(`--limit must be a whole number from 1 to ${MAX_LIST_LIMIT}.`, "invalid_limit");
8344
+ }
8345
+ return limit;
8346
+ }
8347
+ function requireKey(raw) {
8348
+ const key = raw.trim().replace(/^\/+/, "");
8349
+ if (!key) throw new Refusal("A key is required (for example `logo.png`).", "invalid_key");
8350
+ if (key.split("/").some((segment) => segment === ".." || segment === ".")) {
8351
+ throw new Refusal(
8352
+ `"${raw}" contains a path traversal segment. Keys are relative to the app, e.g. \`img/hero.jpg\`.`,
8353
+ "invalid_key"
8354
+ );
8355
+ }
8356
+ return key;
8357
+ }
8358
+ function appPrefixOf(key) {
8359
+ const segments = key.split("/");
8360
+ return segments.length > 2 ? `${segments[0]}/${segments[1]}/` : "";
8361
+ }
8362
+ function relativeKey(key, prefix) {
8363
+ return prefix && key.startsWith(prefix) ? key.slice(prefix.length) : key;
8364
+ }
8365
+ function servedPath(key) {
8366
+ return `/api/files/${encodeKeyPath(key)}?scope=app`;
8367
+ }
8368
+ var put = defineDeepspaceCommand({
8369
+ meta: {
8370
+ name: "put",
8371
+ description: `Upload a file to the app\u2019s files (${formatBytes(MAX_APP_FILE_BYTES)} per file)`
8372
+ },
8373
+ args: {
8374
+ file: { type: "positional", description: "Local file to upload", required: true },
8375
+ key: { type: "string", description: "Key to store it under (default: the file name)" },
8376
+ app: { type: "string", description: "App id or live name (defaults to ./wrangler.toml)" }
8377
+ },
8378
+ async run({ args }) {
8379
+ const localPath = resolve15(String(args.file));
8380
+ if (!existsSync21(localPath) || !statSync2(localPath).isFile()) {
8381
+ throw new Refusal(`No such file: ${args.file}`, "file_not_found");
8382
+ }
8383
+ const key = requireKey(args.key ?? basename4(localPath));
8384
+ const { token, appId } = await target(args.app);
8385
+ const spinner2 = args.json ? null : createSpinner();
8386
+ let started = false;
8387
+ const result = await uploadAppFile(
8388
+ PLATFORM_URL,
8389
+ token,
8390
+ appId,
8391
+ localPath,
8392
+ key,
8393
+ (sent, total, part, parts) => {
8394
+ if (parts === 1 || !spinner2) return;
8395
+ if (!started) {
8396
+ spinner2.start(`Uploading ${key} in ${parts} parts`);
8397
+ started = true;
8398
+ }
8399
+ spinner2.message(
8400
+ `${key} \u2014 part ${part}/${parts} (${formatBytes(sent)} of ${formatBytes(total)})`
8401
+ );
8402
+ }
8403
+ ).finally(() => {
8404
+ if (started) spinner2?.stop();
8405
+ });
8406
+ if (!args.json) {
8407
+ const how = result.parts > 1 ? ` in ${result.parts} parts` : "";
8408
+ console.log(`\u2713 ${key} (${formatBytes(result.size)})${how}`);
8409
+ console.log(` Served from your app at ${servedPath(result.key)}`);
8410
+ }
8411
+ return {
8412
+ data: {
8413
+ appId,
8414
+ key,
8415
+ storedKey: result.key,
8416
+ size: result.size,
8417
+ parts: result.parts,
8418
+ path: servedPath(result.key)
8419
+ }
8420
+ };
8421
+ }
8422
+ });
8423
+ var list3 = defineDeepspaceCommand({
8424
+ meta: { name: "list", description: "List the app\u2019s files" },
8425
+ args: {
8426
+ prefix: { type: "string", description: "Only keys starting with this prefix" },
8427
+ limit: {
8428
+ type: "string",
8429
+ description: `Max entries, 1-${MAX_LIST_LIMIT} (default 100)`,
8430
+ default: "100"
8431
+ },
8432
+ app: { type: "string", description: "App id or live name (defaults to ./wrangler.toml)" }
8433
+ },
8434
+ async run({ args }) {
8435
+ const limit = parseLimit(args.limit);
8436
+ const { token, appId } = await target(args.app);
8437
+ const query = new URLSearchParams({ limit: String(limit) });
8438
+ if (args.prefix) query.set("prefix", String(args.prefix));
8439
+ const result = await listAppFiles(PLATFORM_URL, token, appId, query);
8440
+ const prefix = result.files.length ? appPrefixOf(result.files[0].key) : "";
8441
+ const files = result.files.map((file) => ({
8442
+ key: relativeKey(file.key, prefix),
8443
+ size: file.size,
8444
+ uploaded: file.uploaded,
8445
+ path: servedPath(file.key)
8446
+ }));
8447
+ if (!args.json) {
8448
+ if (files.length === 0) {
8449
+ console.log("No files. Upload one with `deepspace app files put <file>`.");
8450
+ } else {
8451
+ const keyWidth = Math.max(3, ...files.map((file) => file.key.length));
8452
+ console.log(`${"KEY".padEnd(keyWidth)} ${"SIZE".padStart(9)} UPLOADED`);
8453
+ for (const file of files) {
8454
+ console.log(
8455
+ `${file.key.padEnd(keyWidth)} ${formatBytes(file.size).padStart(9)} ${file.uploaded.slice(0, 10)}`
8456
+ );
8457
+ }
8458
+ const more = limit < MAX_LIST_LIMIT ? "raise --limit or narrow with --prefix" : "narrow with --prefix";
8459
+ if (result.truncated) console.log(`(truncated at ${limit} \u2014 ${more})`);
8460
+ }
8461
+ }
8462
+ return { data: { appId, files, truncated: result.truncated } };
8463
+ }
8464
+ });
8465
+ var get = defineDeepspaceCommand({
8466
+ meta: { name: "get", description: "Download one file" },
8467
+ args: {
8468
+ key: { type: "positional", description: "Key to download", required: true },
8469
+ out: { type: "string", description: "Write here (default: the key\u2019s file name)" },
8470
+ app: { type: "string", description: "App id or live name (defaults to ./wrangler.toml)" }
8471
+ },
8472
+ async run({ args }) {
8473
+ const key = requireKey(String(args.key));
8474
+ const destination = resolve15(String(args.out ?? basename4(key)));
8475
+ const { token, appId } = await target(args.app);
8476
+ const result = await downloadAppFile(PLATFORM_URL, token, appId, key, destination);
8477
+ if (!args.json) console.log(`\u2713 ${key} \u2192 ${destination} (${formatBytes(result.bytes)})`);
8478
+ return { data: { appId, key, out: destination, size: result.bytes, contentType: result.contentType } };
8479
+ }
8480
+ });
8481
+ var rm = defineDeepspaceCommand({
8482
+ meta: { name: "rm", description: "Delete one file" },
8483
+ args: {
8484
+ key: { type: "positional", description: "Key to delete", required: true },
8485
+ app: { type: "string", description: "App id or live name (defaults to ./wrangler.toml)" }
8486
+ },
8487
+ async run({ args }) {
8488
+ const key = requireKey(String(args.key));
8489
+ const { token, appId } = await target(args.app);
8490
+ const { existed } = await deleteAppFile(PLATFORM_URL, token, appId, key);
8491
+ if (!existed) {
8492
+ throw new Refusal(
8493
+ `No file at ${key} \u2014 nothing was deleted. Check the key with \`deepspace app files list\`.`,
8494
+ "file_not_found"
8495
+ );
8496
+ }
8497
+ if (!args.json) console.log(`\u2713 deleted ${key}`);
8498
+ return { data: { appId, key, deleted: true } };
8499
+ }
8500
+ });
8501
+ var app_files_default = defineCommand6({
8502
+ meta: { name: "files", description: "Upload and manage the app\u2019s files (images, media)" },
8503
+ subCommands: { put, list: list3, get, rm }
8504
+ });
8505
+
8506
+ // src/cli/commands/collaborators.ts
8507
+ import { defineCommand as defineCommand7 } from "citty";
7918
8508
 
7919
8509
  // src/server/auth/testAccounts.ts
7920
8510
  var TEST_ACCOUNT_EMAIL_SUFFIX = "@deepspace.test";
@@ -7924,18 +8514,18 @@ function isTestAccountEmail(email) {
7924
8514
 
7925
8515
  // src/cli/commands/collaborators.ts
7926
8516
  var API_URL5 = process.env.DEEPSPACE_API_URL ?? PLATFORM_URLS.api;
7927
- var DEPLOY_URL8 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
8517
+ var DEPLOY_URL9 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
7928
8518
  function api2(token, path, init) {
7929
8519
  return apiFetch(API_URL5, token, path, init);
7930
8520
  }
7931
- var list3 = defineDeepspaceCommand({
8521
+ var list4 = defineDeepspaceCommand({
7932
8522
  meta: { name: "list", description: "List collaborators on your app" },
7933
8523
  args: {
7934
8524
  app: { type: "string", alias: "a", description: "App id or name (defaults to ./wrangler.toml)" }
7935
8525
  },
7936
8526
  async run({ args }) {
7937
8527
  const token = await ensureToken();
7938
- const app2 = await resolveAppTarget(DEPLOY_URL8, token, args.app);
8528
+ const app2 = await resolveAppTarget(DEPLOY_URL9, token, args.app);
7939
8529
  const { collaborators, pending = [] } = await api2(token, `/api/app-collaborators/${encodeURIComponent(app2)}`);
7940
8530
  if (!collaborators.length && !pending.length) {
7941
8531
  if (!args.json) {
@@ -7973,7 +8563,7 @@ var add = defineDeepspaceCommand({
7973
8563
  async run({ args }) {
7974
8564
  const email = String(args.email);
7975
8565
  const token = await ensureToken();
7976
- const app2 = await resolveAppTarget(DEPLOY_URL8, token, args.app);
8566
+ const app2 = await resolveAppTarget(DEPLOY_URL9, token, args.app);
7977
8567
  if (isTestAccountEmail(email)) {
7978
8568
  throw new Refusal(
7979
8569
  "Test accounts (@deepspace.test) cannot be added as collaborators. Use a real DeepSpace account.",
@@ -8015,13 +8605,13 @@ var remove = defineDeepspaceCommand({
8015
8605
  async run({ args }) {
8016
8606
  const email = String(args.email);
8017
8607
  const token = await ensureToken();
8018
- const app2 = await resolveAppTarget(DEPLOY_URL8, token, args.app);
8608
+ const app2 = await resolveAppTarget(DEPLOY_URL9, token, args.app);
8019
8609
  const { collaborators } = await api2(
8020
8610
  token,
8021
8611
  `/api/app-collaborators/${encodeURIComponent(app2)}`
8022
8612
  );
8023
- const target = email.trim().toLowerCase();
8024
- const found = collaborators.find((c) => c.emailDisplay.toLowerCase() === target);
8613
+ const target2 = email.trim().toLowerCase();
8614
+ const found = collaborators.find((c) => c.emailDisplay.toLowerCase() === target2);
8025
8615
  if (!found) {
8026
8616
  throw new Refusal(`${email} is not a collaborator on ${app2}`, "not_a_collaborator", {
8027
8617
  action: cliAction("deepspace", "app", "collaborators", "list", "--app", app2)
@@ -8045,13 +8635,13 @@ var cancel3 = defineDeepspaceCommand({
8045
8635
  async run({ args }) {
8046
8636
  const email = String(args.email);
8047
8637
  const token = await ensureToken();
8048
- const app2 = await resolveAppTarget(DEPLOY_URL8, token, args.app);
8638
+ const app2 = await resolveAppTarget(DEPLOY_URL9, token, args.app);
8049
8639
  const { pending = [] } = await api2(
8050
8640
  token,
8051
8641
  `/api/app-collaborators/${encodeURIComponent(app2)}`
8052
8642
  );
8053
- const target = email.trim().toLowerCase();
8054
- const found = pending.find((p34) => p34.email.toLowerCase() === target);
8643
+ const target2 = email.trim().toLowerCase();
8644
+ const found = pending.find((p34) => p34.email.toLowerCase() === target2);
8055
8645
  if (!found) {
8056
8646
  throw new Refusal(`${email} has no pending invite on ${app2}`, "no_pending_invite", {
8057
8647
  action: cliAction("deepspace", "app", "collaborators", "list", "--app", app2)
@@ -8066,16 +8656,16 @@ var cancel3 = defineDeepspaceCommand({
8066
8656
  return { data: { cancelled: found } };
8067
8657
  }
8068
8658
  });
8069
- var collaborators_default = defineCommand6({
8659
+ var collaborators_default = defineCommand7({
8070
8660
  meta: { name: "collaborators", description: "Manage who can deploy your app" },
8071
- subCommands: { list: list3, add, remove, cancel: cancel3 }
8661
+ subCommands: { list: list4, add, remove, cancel: cancel3 }
8072
8662
  });
8073
8663
 
8074
8664
  // src/cli/commands/transfer.ts
8075
- import { defineCommand as defineCommand7 } from "citty";
8665
+ import { defineCommand as defineCommand8 } from "citty";
8076
8666
  import * as p14 from "@clack/prompts";
8077
8667
  var API_URL6 = process.env.DEEPSPACE_API_URL ?? PLATFORM_URLS.api;
8078
- var DEPLOY_URL9 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
8668
+ var DEPLOY_URL10 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
8079
8669
  var offer = defineDeepspaceCommand({
8080
8670
  meta: { name: "offer", description: "Offer this app to another DeepSpace user" },
8081
8671
  args: {
@@ -8089,7 +8679,7 @@ var offer = defineDeepspaceCommand({
8089
8679
  async run({ args }) {
8090
8680
  const email = String(args.email);
8091
8681
  const token = await ensureToken();
8092
- const app2 = await resolveAppTarget(DEPLOY_URL9, token, args.app);
8682
+ const app2 = await resolveAppTarget(DEPLOY_URL10, token, args.app);
8093
8683
  const pending = await apiFetch(
8094
8684
  API_URL6,
8095
8685
  token,
@@ -8141,7 +8731,7 @@ var status2 = defineDeepspaceCommand({
8141
8731
  },
8142
8732
  async run({ args }) {
8143
8733
  const token = await ensureToken();
8144
- const app2 = await resolveAppTarget(DEPLOY_URL9, token, args.app);
8734
+ const app2 = await resolveAppTarget(DEPLOY_URL10, token, args.app);
8145
8735
  const { transfer } = await apiFetch(API_URL6, token, `/api/app-transfers/${encodeURIComponent(app2)}`);
8146
8736
  if (!transfer) {
8147
8737
  if (!args.json) console.log(`No pending transfer for ${app2}.`);
@@ -8168,7 +8758,7 @@ var accept = defineDeepspaceCommand({
8168
8758
  const msg = err instanceof Error ? err.message : String(err);
8169
8759
  throw new Refusal(msg, msg.startsWith("No app id") ? "not_in_app_repo" : "invalid_app");
8170
8760
  }
8171
- await apiFetch(DEPLOY_URL9, token, `/api/apps/${encodeURIComponent(app2)}/transfer/accept`, {
8761
+ await apiFetch(DEPLOY_URL10, token, `/api/apps/${encodeURIComponent(app2)}/transfer/accept`, {
8172
8762
  method: "POST"
8173
8763
  });
8174
8764
  if (!args.json) {
@@ -8186,37 +8776,37 @@ var cancel4 = defineDeepspaceCommand({
8186
8776
  },
8187
8777
  async run({ args }) {
8188
8778
  const token = await ensureToken();
8189
- const app2 = await resolveAppTarget(DEPLOY_URL9, token, args.app);
8779
+ const app2 = await resolveAppTarget(DEPLOY_URL10, token, args.app);
8190
8780
  await apiFetch(API_URL6, token, `/api/app-transfers/${encodeURIComponent(app2)}`, { method: "DELETE" });
8191
8781
  if (!args.json) console.log(`\u2713 Transfer offer for ${app2} cancelled.`);
8192
8782
  return { data: { app: app2, cancelled: true } };
8193
8783
  }
8194
8784
  });
8195
- var transfer_default = defineCommand7({
8785
+ var transfer_default = defineCommand8({
8196
8786
  meta: { name: "transfer", description: "Transfer app ownership to another user" },
8197
8787
  subCommands: { offer, status: status2, accept, cancel: cancel4 }
8198
8788
  });
8199
8789
 
8200
8790
  // src/cli/commands/integrations.ts
8201
- import { defineCommand as defineCommand8 } from "citty";
8791
+ import { defineCommand as defineCommand9 } from "citty";
8202
8792
 
8203
8793
  // src/cli/commands/_invoke-impl.ts
8204
8794
  import { readFileSync as readFileSync22 } from "fs";
8205
8795
  import * as p15 from "@clack/prompts";
8206
8796
  var API_URL7 = process.env.DEEPSPACE_API_URL ?? PLATFORM_URLS.api;
8207
8797
  var DEFAULT_TIMEOUT_MS = 12e4;
8208
- function parseTarget(target) {
8209
- if (!target || typeof target !== "string") {
8798
+ function parseTarget(target2) {
8799
+ if (!target2 || typeof target2 !== "string") {
8210
8800
  throw new Refusal(
8211
8801
  "Missing target. Expected '<integration>/<endpoint>' (e.g. 'openai/chat-completion').",
8212
8802
  "missing_target",
8213
8803
  { action: cliAction("deepspace", "integrations", "list") }
8214
8804
  );
8215
8805
  }
8216
- const parts = target.split("/");
8806
+ const parts = target2.split("/");
8217
8807
  if (parts.length !== 2 || !parts[0] || !parts[1]) {
8218
8808
  throw new Refusal(
8219
- `Bad target '${target}'. Expected '<integration>/<endpoint>' (e.g. 'openai/chat-completion').`,
8809
+ `Bad target '${target2}'. Expected '<integration>/<endpoint>' (e.g. 'openai/chat-completion').`,
8220
8810
  "bad_target",
8221
8811
  { action: cliAction("deepspace", "integrations", "list") }
8222
8812
  );
@@ -8436,7 +9026,7 @@ async function runInvoke(args) {
8436
9026
  }
8437
9027
 
8438
9028
  // src/cli/commands/integrations.ts
8439
- var list4 = defineDeepspaceCommand({
9029
+ var list5 = defineDeepspaceCommand({
8440
9030
  meta: {
8441
9031
  name: "list",
8442
9032
  description: "List all available integrations"
@@ -8507,13 +9097,13 @@ var invoke = defineDeepspaceCommand({
8507
9097
  });
8508
9098
  }
8509
9099
  });
8510
- var integrations_default = defineCommand8({
9100
+ var integrations_default = defineCommand9({
8511
9101
  meta: {
8512
9102
  name: "integrations",
8513
9103
  description: "List, inspect, and invoke platform integrations"
8514
9104
  },
8515
9105
  subCommands: {
8516
- list: list4,
9106
+ list: list5,
8517
9107
  info,
8518
9108
  invoke
8519
9109
  }
@@ -8521,7 +9111,7 @@ var integrations_default = defineCommand8({
8521
9111
 
8522
9112
  // src/cli/commands/feedback.ts
8523
9113
  import * as p16 from "@clack/prompts";
8524
- import { existsSync as existsSync21, readFileSync as readFileSync23 } from "fs";
9114
+ import { existsSync as existsSync22, readFileSync as readFileSync23 } from "fs";
8525
9115
  import { dirname as dirname8, join as join20 } from "path";
8526
9116
  import { fileURLToPath as fileURLToPath2 } from "url";
8527
9117
  import { release } from "os";
@@ -8549,7 +9139,7 @@ function readCliVersion() {
8549
9139
  join20(here, "..", "..", "..", "package.json")
8550
9140
  ];
8551
9141
  for (const path of candidates) {
8552
- if (!existsSync21(path)) continue;
9142
+ if (!existsSync22(path)) continue;
8553
9143
  const pkg2 = JSON.parse(readFileSync23(path, "utf-8"));
8554
9144
  if (pkg2.name === "deepspace" && pkg2.version) return pkg2.version;
8555
9145
  }
@@ -8745,7 +9335,7 @@ Context: deepspace ${payload.cliVersion} \xB7 ${payload.nodeVersion} \xB7 ${payl
8745
9335
  });
8746
9336
 
8747
9337
  // src/cli/commands/secrets.ts
8748
- import { defineCommand as defineCommand9 } from "citty";
9338
+ import { defineCommand as defineCommand10 } from "citty";
8749
9339
  import { readFileSync as readFileSync24 } from "fs";
8750
9340
 
8751
9341
  // src/cli/lib/citty-args.ts
@@ -8779,7 +9369,7 @@ function parseCursorArg(raw) {
8779
9369
  }
8780
9370
 
8781
9371
  // src/cli/commands/secrets.ts
8782
- var DEPLOY_URL10 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
9372
+ var DEPLOY_URL11 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
8783
9373
  var COMMON_ARGS = {
8784
9374
  app: {
8785
9375
  type: "string",
@@ -8849,7 +9439,7 @@ function warnIfDebugRoutesEnabled(secrets) {
8849
9439
  }
8850
9440
  var APPLY_HINT = "Run `deepspace deploy` to apply \u2014 secrets take effect at deploy time.";
8851
9441
  var LOCAL_DEV_HINT = "Running `deepspace dev start`? Restart it to load the change locally.";
8852
- var list5 = defineCommand9({
9442
+ var list6 = defineCommand10({
8853
9443
  meta: { name: "list", description: "List masked secrets in a config" },
8854
9444
  args: {
8855
9445
  ...COMMON_ARGS,
@@ -8862,7 +9452,7 @@ var list5 = defineCommand9({
8862
9452
  async run({ args }) {
8863
9453
  try {
8864
9454
  const t = await resolveTarget(args);
8865
- const { secrets } = await listSecrets(DEPLOY_URL10, t.token, t.appId, t.configName);
9455
+ const { secrets } = await listSecrets(DEPLOY_URL11, t.token, t.appId, t.configName);
8866
9456
  if (args.json) {
8867
9457
  console.log(JSON.stringify({ ok: true, appId: t.appId, config: t.configName, secrets }));
8868
9458
  return;
@@ -8879,7 +9469,7 @@ var list5 = defineCommand9({
8879
9469
  }
8880
9470
  }
8881
9471
  });
8882
- var set = defineCommand9({
9472
+ var set = defineCommand10({
8883
9473
  meta: { name: "set", description: "Set secrets from KEY=value pairs" },
8884
9474
  args: {
8885
9475
  ...COMMON_ARGS,
@@ -8907,9 +9497,9 @@ var set = defineCommand9({
8907
9497
  }
8908
9498
  if (Object.keys(secrets).length === 1) {
8909
9499
  const [[k, v]] = Object.entries(secrets);
8910
- await setSecret(DEPLOY_URL10, t.token, t.appId, t.configName, k, v);
9500
+ await setSecret(DEPLOY_URL11, t.token, t.appId, t.configName, k, v);
8911
9501
  } else {
8912
- await uploadSecrets(DEPLOY_URL10, t.token, t.appId, t.configName, secrets, false);
9502
+ await uploadSecrets(DEPLOY_URL11, t.token, t.appId, t.configName, secrets, false);
8913
9503
  }
8914
9504
  const n = Object.keys(secrets).length;
8915
9505
  ok(
@@ -8928,7 +9518,7 @@ var set = defineCommand9({
8928
9518
  }
8929
9519
  }
8930
9520
  });
8931
- var get = defineCommand9({
9521
+ var get2 = defineCommand10({
8932
9522
  meta: { name: "get", description: "Get a secret; pass --plain to print the value" },
8933
9523
  args: {
8934
9524
  ...COMMON_ARGS,
@@ -8940,11 +9530,11 @@ var get = defineCommand9({
8940
9530
  const t = await resolveTarget(args);
8941
9531
  const key = validateSecretName(args.key);
8942
9532
  if (args.plain) {
8943
- const { value } = await getSecretPlain(DEPLOY_URL10, t.token, t.appId, t.configName, key);
9533
+ const { value } = await getSecretPlain(DEPLOY_URL11, t.token, t.appId, t.configName, key);
8944
9534
  process.stdout.write(process.stdout.isTTY ? value + "\n" : value);
8945
9535
  return;
8946
9536
  }
8947
- const { secrets } = await listSecrets(DEPLOY_URL10, t.token, t.appId, t.configName);
9537
+ const { secrets } = await listSecrets(DEPLOY_URL11, t.token, t.appId, t.configName);
8948
9538
  const item = secrets.find((s) => s.key === key);
8949
9539
  if (!item)
8950
9540
  fail(new InputError(`Secret "${key}" not found in ${t.configName}`, "secret_not_found"));
@@ -8964,7 +9554,7 @@ var get = defineCommand9({
8964
9554
  }
8965
9555
  }
8966
9556
  });
8967
- var del2 = defineCommand9({
9557
+ var del2 = defineCommand10({
8968
9558
  meta: { name: "delete", description: "Delete one or more secrets" },
8969
9559
  args: {
8970
9560
  ...COMMON_ARGS,
@@ -8979,7 +9569,7 @@ var del2 = defineCommand9({
8979
9569
  for (const key of keys) {
8980
9570
  const name = validateSecretName(key);
8981
9571
  try {
8982
- await deleteSecret(DEPLOY_URL10, t.token, t.appId, t.configName, name);
9572
+ await deleteSecret(DEPLOY_URL11, t.token, t.appId, t.configName, name);
8983
9573
  deleted++;
8984
9574
  } catch (err) {
8985
9575
  if (err?.status === 404) absent++;
@@ -8998,7 +9588,7 @@ var del2 = defineCommand9({
8998
9588
  }
8999
9589
  }
9000
9590
  });
9001
- var upload = defineCommand9({
9591
+ var upload = defineCommand10({
9002
9592
  meta: { name: "upload", description: "Upload secrets from a dotenv or JSON file" },
9003
9593
  args: {
9004
9594
  ...COMMON_ARGS,
@@ -9020,7 +9610,7 @@ var upload = defineCommand9({
9020
9610
  const secrets = parseSecretsUpload(content);
9021
9611
  if (Object.keys(secrets).length === 0)
9022
9612
  fail(new InputError("No secrets found in the input.", "empty_input"));
9023
- await uploadSecrets(DEPLOY_URL10, t.token, t.appId, t.configName, secrets, args.replace);
9613
+ await uploadSecrets(DEPLOY_URL11, t.token, t.appId, t.configName, secrets, args.replace);
9024
9614
  const uploaded = Object.keys(secrets);
9025
9615
  ok(
9026
9616
  args.json === true,
@@ -9035,7 +9625,7 @@ var upload = defineCommand9({
9035
9625
  }
9036
9626
  }
9037
9627
  });
9038
- var download = defineCommand9({
9628
+ var download = defineCommand10({
9039
9629
  meta: { name: "download", description: "Download a config\u2019s secrets (dotenv/json/shell)" },
9040
9630
  args: {
9041
9631
  ...COMMON_ARGS,
@@ -9057,14 +9647,14 @@ var download = defineCommand9({
9057
9647
  )
9058
9648
  );
9059
9649
  }
9060
- const { secrets } = await fetchSecretsValues(DEPLOY_URL10, t.token, t.appId, t.configName);
9650
+ const { secrets } = await fetchSecretsValues(DEPLOY_URL11, t.token, t.appId, t.configName);
9061
9651
  process.stdout.write(formatSecretsDownload(secrets, format));
9062
9652
  } catch (err) {
9063
9653
  fail(err);
9064
9654
  }
9065
9655
  }
9066
9656
  });
9067
- var pull = defineCommand9({
9657
+ var pull = defineCommand10({
9068
9658
  meta: { name: "pull", description: "Refresh the .dev.vars cache from the app store" },
9069
9659
  args: { ...COMMON_ARGS },
9070
9660
  async run({ args }) {
@@ -9080,7 +9670,7 @@ var pull = defineCommand9({
9080
9670
  );
9081
9671
  const t = await resolveTarget(args);
9082
9672
  const ownerId = decodeJwtPayload(t.token).sub;
9083
- const pulled = await pullAppSecretsCache(DEPLOY_URL10, t.token, t.appId, t.configName);
9673
+ const pulled = await pullAppSecretsCache(DEPLOY_URL11, t.token, t.appId, t.configName);
9084
9674
  if (!pulled) {
9085
9675
  ok(
9086
9676
  args.json === true,
@@ -9107,13 +9697,13 @@ var pull = defineCommand9({
9107
9697
  }
9108
9698
  }
9109
9699
  });
9110
- var configsList = defineCommand9({
9700
+ var configsList = defineCommand10({
9111
9701
  meta: { name: "list", description: "List the app\u2019s configs" },
9112
9702
  args: { ...COMMON_ARGS },
9113
9703
  async run({ args }) {
9114
9704
  try {
9115
9705
  const t = await resolveTarget(args);
9116
- const { configs: configs2 } = await listConfigs(DEPLOY_URL10, t.token, t.appId);
9706
+ const { configs: configs2 } = await listConfigs(DEPLOY_URL11, t.token, t.appId);
9117
9707
  ok(args.json === true, { appId: t.appId, configs: configs2 }, () => {
9118
9708
  if (configs2.length === 0) {
9119
9709
  console.log("No configs yet \u2014 the first `secrets set` creates one.");
@@ -9128,7 +9718,7 @@ var configsList = defineCommand9({
9128
9718
  }
9129
9719
  }
9130
9720
  });
9131
- var configsCreate = defineCommand9({
9721
+ var configsCreate = defineCommand10({
9132
9722
  meta: { name: "create", description: "Create a config (optionally copying another)" },
9133
9723
  args: {
9134
9724
  ...COMMON_ARGS,
@@ -9140,7 +9730,7 @@ var configsCreate = defineCommand9({
9140
9730
  const t = await resolveTarget(args);
9141
9731
  const name = validateConfigName(args.name);
9142
9732
  const copyFrom = args["copy-from"] || void 0;
9143
- const { configs: configs2 } = await listConfigs(DEPLOY_URL10, t.token, t.appId);
9733
+ const { configs: configs2 } = await listConfigs(DEPLOY_URL11, t.token, t.appId);
9144
9734
  if (configs2.some((c) => c.name === name)) {
9145
9735
  if (copyFrom) {
9146
9736
  fail(
@@ -9157,7 +9747,7 @@ var configsCreate = defineCommand9({
9157
9747
  );
9158
9748
  return;
9159
9749
  }
9160
- await createConfig(DEPLOY_URL10, t.token, t.appId, name, copyFrom);
9750
+ await createConfig(DEPLOY_URL11, t.token, t.appId, name, copyFrom);
9161
9751
  ok(
9162
9752
  args.json === true,
9163
9753
  { appId: t.appId, config: name, created: true, ...copyFrom ? { copyFrom } : {} },
@@ -9168,7 +9758,7 @@ var configsCreate = defineCommand9({
9168
9758
  }
9169
9759
  }
9170
9760
  });
9171
- var configsDelete = defineCommand9({
9761
+ var configsDelete = defineCommand10({
9172
9762
  meta: { name: "delete", description: "Delete a config and all of its secrets" },
9173
9763
  args: {
9174
9764
  ...COMMON_ARGS,
@@ -9178,7 +9768,7 @@ var configsDelete = defineCommand9({
9178
9768
  try {
9179
9769
  const t = await resolveTarget(args);
9180
9770
  const name = validateConfigName(args.name);
9181
- await deleteConfig(DEPLOY_URL10, t.token, t.appId, name);
9771
+ await deleteConfig(DEPLOY_URL11, t.token, t.appId, name);
9182
9772
  ok(
9183
9773
  args.json === true,
9184
9774
  { appId: t.appId, config: name, deleted: true },
@@ -9189,16 +9779,16 @@ var configsDelete = defineCommand9({
9189
9779
  }
9190
9780
  }
9191
9781
  });
9192
- var configs = defineCommand9({
9782
+ var configs = defineCommand10({
9193
9783
  meta: { name: "configs", description: "Manage the app\u2019s secrets configs" },
9194
9784
  subCommands: { list: configsList, create: configsCreate, delete: configsDelete }
9195
9785
  });
9196
- var secrets_default = defineCommand9({
9786
+ var secrets_default = defineCommand10({
9197
9787
  meta: { name: "secrets", description: "Manage the app\u2019s secrets store" },
9198
9788
  subCommands: {
9199
- list: list5,
9789
+ list: list6,
9200
9790
  set,
9201
- get,
9791
+ get: get2,
9202
9792
  delete: del2,
9203
9793
  upload,
9204
9794
  download,
@@ -9270,9 +9860,9 @@ var init_default = defineDeepspaceCommand({
9270
9860
  });
9271
9861
 
9272
9862
  // src/cli/commands/status.ts
9273
- import { defineCommand as defineCommand10 } from "citty";
9863
+ import { defineCommand as defineCommand11 } from "citty";
9274
9864
  import * as p17 from "@clack/prompts";
9275
- import { existsSync as existsSync22, readFileSync as readFileSync25 } from "fs";
9865
+ import { existsSync as existsSync23, readFileSync as readFileSync25 } from "fs";
9276
9866
  function timeAgo(iso) {
9277
9867
  const ms = Date.now() - Date.parse(iso);
9278
9868
  if (!Number.isFinite(ms) || ms < 0) return iso;
@@ -9305,7 +9895,7 @@ function resolveStatusApp(appDir, wranglerEnv) {
9305
9895
  if (!resolved.ok) throw new InputError(resolved.reason, "invalid_env");
9306
9896
  return { appName: resolved.name, appId: readAppId(appDir, wranglerEnv) };
9307
9897
  }
9308
- var status_default = defineCommand10({
9898
+ var status_default = defineCommand11({
9309
9899
  meta: {
9310
9900
  name: "status",
9311
9901
  description: "One screenful of current state: session, app, workspace, and live release"
@@ -9357,7 +9947,7 @@ var status_default = defineCommand10({
9357
9947
  json.services = urls;
9358
9948
  if (urlOverrides.length > 0) json.urlOverrides = urlOverrides;
9359
9949
  }
9360
- facts.loggedIn = existsSync22(SESSION_PATH);
9950
+ facts.loggedIn = existsSync23(SESSION_PATH);
9361
9951
  let userId = null;
9362
9952
  if (!facts.loggedIn) {
9363
9953
  lines.push(["Session", "not logged in"]);
@@ -9454,24 +10044,36 @@ var status_default = defineCommand10({
9454
10044
  const failure = statusRemoteFailure(error);
9455
10045
  lines.push([label, failure.human]);
9456
10046
  json[key] = failure.json;
9457
- }, reportRelease2 = function(rel) {
10047
+ }, reportLive2 = function(rel, entry) {
10048
+ if (rel === null) {
10049
+ lines.push(["Live", "never deployed"]);
10050
+ json.live = null;
10051
+ return;
10052
+ }
9458
10053
  const byYou = userId !== null && rel.actor === userId;
9459
- lines.push([
9460
- "Live",
9461
- `release #${rel.seq} (${rel.kind} of ${rel.commitOid?.slice(0, 10) ?? "unknown source"}, ${timeAgo(rel.createdAt)}${byYou ? ", by you" : ""})` + (rel.url ? ` \xB7 ${rel.url}` : "")
9462
- ]);
9463
- json.live = {
10054
+ const release2 = `release #${rel.seq} (${rel.kind} of ${rel.commitOid?.slice(0, 10) ?? "unknown source"}, ${timeAgo(rel.createdAt)}${byYou ? ", by you" : ""})`;
10055
+ const details = {
9464
10056
  seq: rel.seq,
9465
10057
  kind: rel.kind,
9466
10058
  commitOid: rel.commitOid,
9467
10059
  createdAt: rel.createdAt,
9468
- url: rel.url,
9469
10060
  byYou,
9470
10061
  source: rel.source,
9471
10062
  sourceRevision: rel.sourceRevision
9472
10063
  };
10064
+ if (!entry) {
10065
+ lines.push(["Live", `${release2} \xB7 serving state not visible from your account`]);
10066
+ json.live = { ...details, serving: null, url: null };
10067
+ return;
10068
+ }
10069
+ const url = liveAppUrl(entry);
10070
+ lines.push([
10071
+ "Live",
10072
+ url ? `${release2} \xB7 ${url}` : `not serving \u2014 ${entry.status}, last ${release2}`
10073
+ ]);
10074
+ json.live = { ...details, serving: url !== null, url };
9473
10075
  };
9474
- var reportWorkspace = reportWorkspace2, reportRemoteFailure = reportRemoteFailure2, reportRelease = reportRelease2;
10076
+ var reportWorkspace = reportWorkspace2, reportRemoteFailure = reportRemoteFailure2, reportLive = reportLive2;
9475
10077
  const api4 = repoApi(deployBaseUrl(), token, appId);
9476
10078
  const sourceResult = await observeRemote(getAppSource(deployBaseUrl(), token, appId));
9477
10079
  const source = sourceResult.ok ? sourceResult.value.source : null;
@@ -9491,10 +10093,11 @@ var status_default = defineCommand10({
9491
10093
  lines.push(["Source", "unclaimed \xB7 next normal deploy defaults to DeepSpace"]);
9492
10094
  json.source = null;
9493
10095
  }
9494
- const [refsResult, wsResult, releaseResult] = await Promise.all([
10096
+ const [refsResult, wsResult, releaseResult, appsResult] = await Promise.all([
9495
10097
  githubSource ? Promise.resolve({ ok: true, value: null }) : observeRemote(api4.getRefs()),
9496
10098
  facts.workspaceId && !githubSource ? observeRemote(api4.getWorkspace(facts.workspaceId).then((r) => r.view)) : Promise.resolve({ ok: true, value: null }),
9497
- observeRemote(api4.latestRelease().then((r) => r.release))
10099
+ observeRemote(api4.latestRelease().then((r) => r.release)),
10100
+ observeRemote(listApps(deployBaseUrl(), token))
9498
10101
  ]);
9499
10102
  if (facts.workspaceId) {
9500
10103
  if (githubSource) {
@@ -9523,12 +10126,8 @@ var status_default = defineCommand10({
9523
10126
  }
9524
10127
  }
9525
10128
  if (!releaseResult.ok) reportRemoteFailure2("Live", "live", releaseResult.error);
9526
- else if (releaseResult.value === null) {
9527
- lines.push(["Live", "never deployed"]);
9528
- json.live = null;
9529
- } else {
9530
- reportRelease2(releaseResult.value);
9531
- }
10129
+ else if (!appsResult.ok) reportRemoteFailure2("Live", "live", appsResult.error);
10130
+ else reportLive2(releaseResult.value, appsResult.value.find((a) => a.appId === appId));
9532
10131
  }
9533
10132
  spinner2?.stop("Cloud state checked.");
9534
10133
  }
@@ -9571,7 +10170,7 @@ function workspaceBranchPushRefusal(branch) {
9571
10170
  var push_default = defineDeepspaceCommand({
9572
10171
  meta: {
9573
10172
  name: "push",
9574
- description: "Push local git commits to the app's cloud repo"
10173
+ description: `Push local git commits to the app's cloud repo (${PUSH_CEILINGS})`
9575
10174
  },
9576
10175
  args: {
9577
10176
  branch: {
@@ -9596,8 +10195,8 @@ var push_default = defineDeepspaceCommand({
9596
10195
  const branchArg = args.branch === void 0 ? void 0 : String(args.branch);
9597
10196
  const appArg = typeof args.app === "string" ? args.app : void 0;
9598
10197
  const force = args.force === true;
10198
+ const appDir = findAppDir();
9599
10199
  try {
9600
- const appDir = findAppDir();
9601
10200
  if (!appDir)
9602
10201
  throw new Refusal(
9603
10202
  "No wrangler.toml found \u2014 run from inside an app directory.",
@@ -9726,7 +10325,7 @@ var push_default = defineDeepspaceCommand({
9726
10325
  }
9727
10326
  const recoverable = isRecoverablePushFailure(result.status);
9728
10327
  const rejectReason = result.reason ?? result.summary;
9729
- 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.`;
10328
+ 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.`;
9730
10329
  if (!args.json && ok2) p18.log.info(`App: ${appId}`);
9731
10330
  const action = recoverable ? pullRecoveryAction(pullRecoveryCwd, appId, branch) : void 0;
9732
10331
  if (!ok2) {
@@ -9739,7 +10338,7 @@ var push_default = defineDeepspaceCommand({
9739
10338
  return { data: { status: result.status, appId, branch, oid: tipOid } };
9740
10339
  } catch (err) {
9741
10340
  if (err instanceof Refusal) throw err;
9742
- const transportFailure = classifyPushTransportFailure(err);
10341
+ const transportFailure = classifyPushTransportFailure(err, appDir ?? void 0);
9743
10342
  if (transportFailure) {
9744
10343
  throw new Refusal(transportFailure.error, transportFailure.code);
9745
10344
  }
@@ -10031,14 +10630,14 @@ var pull_default = defineDeepspaceCommand({
10031
10630
 
10032
10631
  // src/cli/commands/clone.ts
10033
10632
  import * as p20 from "@clack/prompts";
10034
- import { existsSync as existsSync23, readdirSync as readdirSync6, rmSync as rmSync5 } from "fs";
10035
- import { resolve as resolve15, basename as basename3, join as join21 } from "path";
10633
+ import { existsSync as existsSync24, readdirSync as readdirSync6, rmSync as rmSync5 } from "fs";
10634
+ import { resolve as resolve16, basename as basename5, join as join21 } from "path";
10036
10635
  function cleanFailedCloneDir(dir) {
10037
10636
  let entries;
10038
10637
  try {
10039
10638
  entries = readdirSync6(dir);
10040
10639
  } catch {
10041
- return existsSync23(dir) ? ["<unreadable>"] : [];
10640
+ return existsSync24(dir) ? ["<unreadable>"] : [];
10042
10641
  }
10043
10642
  if (entries.length === 1 && entries[0] === ".git") {
10044
10643
  try {
@@ -10051,7 +10650,7 @@ function cleanFailedCloneDir(dir) {
10051
10650
  return entries;
10052
10651
  }
10053
10652
  function validateCloneTarget(dir) {
10054
- if (!existsSync23(dir)) return false;
10653
+ if (!existsSync24(dir)) return false;
10055
10654
  let entries;
10056
10655
  try {
10057
10656
  entries = readdirSync6(dir);
@@ -10086,7 +10685,7 @@ var clone_default = defineDeepspaceCommand({
10086
10685
  async run({ args }) {
10087
10686
  assertAppTargetResolvable(String(args.app));
10088
10687
  const selector = String(args.app).trim();
10089
- const dir = resolve15(String(args.dir ?? selector));
10688
+ const dir = resolve16(String(args.dir ?? selector));
10090
10689
  const dirPreexisted = validateCloneTarget(dir);
10091
10690
  const spinner2 = args.json ? null : createSpinner();
10092
10691
  spinner2?.start(`Resolving ${selector}\u2026`);
@@ -10129,7 +10728,7 @@ var clone_default = defineDeepspaceCommand({
10129
10728
  const head = resolveCommit(dir, "HEAD");
10130
10729
  const branch = currentBranch(dir);
10131
10730
  spinner2?.stop(
10132
- `Cloned ${selector} (${branches.length} ${branches.length === 1 ? "branch" : "branches"}) into ${basename3(dir)}/ at ${head?.slice(0, 10) ?? "(unborn)"}.`
10731
+ `Cloned ${selector} (${branches.length} ${branches.length === 1 ? "branch" : "branches"}) into ${basename5(dir)}/ at ${head?.slice(0, 10) ?? "(unborn)"}.`
10133
10732
  );
10134
10733
  if (!args.json && !branch) {
10135
10734
  p20.log.warn("HEAD is detached \u2014 check `git branch -a`.");
@@ -10152,7 +10751,28 @@ var clone_default = defineDeepspaceCommand({
10152
10751
 
10153
10752
  // src/cli/commands/releases.ts
10154
10753
  import * as p21 from "@clack/prompts";
10155
- var DEPLOY_URL11 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
10754
+
10755
+ // src/cli/lib/actor-labels.ts
10756
+ var API_URL9 = process.env.DEEPSPACE_API_URL ?? PLATFORM_URLS.api;
10757
+ async function actorLabels(token, appId) {
10758
+ const labels = /* @__PURE__ */ new Map();
10759
+ try {
10760
+ const { sub, email } = decodeJwtPayload(token);
10761
+ if (sub && email) labels.set(sub, `${email} (you)`);
10762
+ } catch {
10763
+ }
10764
+ try {
10765
+ const { collaborators } = await apiFetch(API_URL9, token, `/api/app-collaborators/${encodeURIComponent(appId)}`);
10766
+ for (const c of collaborators) {
10767
+ if (!labels.has(c.userId)) labels.set(c.userId, c.emailDisplay);
10768
+ }
10769
+ } catch {
10770
+ }
10771
+ return labels;
10772
+ }
10773
+
10774
+ // src/cli/commands/releases.ts
10775
+ var DEPLOY_URL12 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
10156
10776
  var releases_default = defineDeepspaceCommand({
10157
10777
  meta: {
10158
10778
  name: "releases",
@@ -10187,18 +10807,19 @@ var releases_default = defineDeepspaceCommand({
10187
10807
  const spinner2 = args.json ? null : createSpinner();
10188
10808
  spinner2?.start("Loading release history\u2026");
10189
10809
  const token = await ensureToken();
10190
- const appId = await resolveAppTarget(DEPLOY_URL11, token, appArg, { wranglerEnv: envArg });
10191
- const { releases } = await repoApi(DEPLOY_URL11, token, appId).listReleases(limit);
10810
+ const appId = await resolveAppTarget(DEPLOY_URL12, token, appArg, { wranglerEnv: envArg });
10811
+ const { releases } = await repoApi(DEPLOY_URL12, token, appId).listReleases(limit);
10192
10812
  spinner2?.stop(`Loaded ${releases.length} ${releases.length === 1 ? "release" : "releases"}.`);
10193
10813
  if (!args.json) {
10194
10814
  if (releases.length === 0) {
10195
10815
  p21.log.info("No releases yet \u2014 the history starts with the next `deepspace deploy`.");
10196
10816
  } else {
10817
+ const actors = await actorLabels(token, appId);
10197
10818
  for (const r of releases) {
10198
10819
  const source = r.commitOid ? `commit ${r.commitOid.slice(0, 10)}` : "(no source recorded)";
10199
10820
  const rollback = r.rollbackAvailable ? "rollback available" : "rollback unavailable";
10200
10821
  console.log(
10201
- `#${r.seq} ${r.id} ${r.kind.padEnd(8)} ${r.createdAt} ${r.actor} ${source} ${rollback}`
10822
+ `#${r.seq} ${r.id} ${r.kind.padEnd(8)} ${r.createdAt} ${actors.get(r.actor) ?? r.actor} ${source} ${rollback}`
10202
10823
  );
10203
10824
  }
10204
10825
  p21.log.info(
@@ -10220,7 +10841,7 @@ Retry in a moment, or re-run with --allow-do-deletion to accept the risk.`,
10220
10841
  "do_guard_unavailable"
10221
10842
  );
10222
10843
  }
10223
- var DEPLOY_URL12 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
10844
+ var DEPLOY_URL13 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
10224
10845
  function pickPreviousRelease(releases) {
10225
10846
  if (releases.length === 0) {
10226
10847
  return { error: "No releases recorded yet \u2014 nothing to roll back to.", code: "no_releases" };
@@ -10282,8 +10903,8 @@ var rollback_default = defineDeepspaceCommand({
10282
10903
  const spinner2 = args.json ? null : createSpinner();
10283
10904
  spinner2?.start("Preparing rollback\u2026");
10284
10905
  const token = await ensureToken();
10285
- const appId = await resolveAppTarget(DEPLOY_URL12, token, appArg, { wranglerEnv: envArg });
10286
- const api4 = repoApi(DEPLOY_URL12, token, appId);
10906
+ const appId = await resolveAppTarget(DEPLOY_URL13, token, appArg, { wranglerEnv: envArg });
10907
+ const api4 = repoApi(DEPLOY_URL13, token, appId);
10287
10908
  let releaseId = args.release ? String(args.release).trim() : "";
10288
10909
  if (!releaseId) {
10289
10910
  const { releases } = await api4.listReleases(2);
@@ -10294,7 +10915,7 @@ var rollback_default = defineDeepspaceCommand({
10294
10915
  spinner2?.message(`Rolling back to ${releaseId}\u2026`);
10295
10916
  let result;
10296
10917
  try {
10297
- result = await apiFetch(DEPLOY_URL12, token, `/api/deploy/${appId}/rollback`, {
10918
+ result = await apiFetch(DEPLOY_URL13, token, `/api/deploy/${appId}/rollback`, {
10298
10919
  method: "POST",
10299
10920
  body: JSON.stringify({
10300
10921
  releaseId,
@@ -10321,10 +10942,20 @@ Re-run with --allow-do-deletion to accept the data loss.`,
10321
10942
  "invalid_response"
10322
10943
  );
10323
10944
  }
10324
- spinner2?.stop(`Rolled back to ${result.rolledBackTo ?? releaseId}.`);
10945
+ let edgePropagating = false;
10946
+ if (result.url) {
10947
+ spinner2?.message("Waiting for edge propagation...");
10948
+ edgePropagating = !await waitForEdgePropagation(result.url, 9e4);
10949
+ }
10950
+ spinner2?.stop(
10951
+ edgePropagating ? `Rolled back to ${result.rolledBackTo ?? releaseId} (edge propagation still in progress after 90s).` : `Rolled back to ${result.rolledBackTo ?? releaseId}.`
10952
+ );
10325
10953
  const doClassGuard = result.doClassGuard === "unverified" ? "unverified" : void 0;
10326
10954
  if (!args.json) {
10327
- if (result.url) p22.log.success(`Live at: ${result.url}`);
10955
+ if (result.url) {
10956
+ if (edgePropagating) p22.log.warn(`URL: ${result.url} (accepted; serving not yet verified)`);
10957
+ else p22.log.success(`Live at: ${result.url}`);
10958
+ }
10328
10959
  if (result.bundleRetained === false) {
10329
10960
  p22.log.warn(
10330
10961
  "The rollback is live, but its bundle was concurrently retired and is not available for a future rollback."
@@ -10348,6 +10979,7 @@ Re-run with --allow-do-deletion to accept the data loss.`,
10348
10979
  url: result.url ?? null,
10349
10980
  versionId: result.versionId ?? null,
10350
10981
  wranglerEnv: wranglerEnv ?? null,
10982
+ ...edgePropagating ? { edgePropagating: true } : {},
10351
10983
  ...doClassGuard ? { doClassGuard } : {}
10352
10984
  }
10353
10985
  };
@@ -10355,11 +10987,11 @@ Re-run with --allow-do-deletion to accept the data loss.`,
10355
10987
  });
10356
10988
 
10357
10989
  // src/cli/commands/workspace.ts
10358
- import { defineCommand as defineCommand11 } from "citty";
10990
+ import { defineCommand as defineCommand12 } from "citty";
10359
10991
 
10360
10992
  // src/cli/commands/workspace/new.ts
10361
10993
  import * as p25 from "@clack/prompts";
10362
- import { isAbsolute as isAbsolute6, resolve as resolve16 } from "path";
10994
+ import { isAbsolute as isAbsolute6, resolve as resolve17 } from "path";
10363
10995
 
10364
10996
  // src/cli/commands/workspace/analysis.ts
10365
10997
  import * as p23 from "@clack/prompts";
@@ -10484,7 +11116,7 @@ function printOverlaps(overlaps, json) {
10484
11116
 
10485
11117
  // src/cli/commands/workspace/local.ts
10486
11118
  import * as p24 from "@clack/prompts";
10487
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync26, realpathSync as realpathSync5, renameSync as renameSync2, statSync, writeFileSync as writeFileSync9 } from "fs";
11119
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync26, realpathSync as realpathSync5, renameSync as renameSync2, statSync as statSync3, writeFileSync as writeFileSync9 } from "fs";
10488
11120
  import { dirname as dirname9, isAbsolute as isAbsolute5, join as join22, relative as relative2, sep as sep2 } from "path";
10489
11121
  var WORKSPACE_MARKER = "deepspace-workspace.json";
10490
11122
  function excludeWorktreeDir(appDir, dir) {
@@ -10573,7 +11205,7 @@ function materializeWorkspaceWorktree(appDir, worktreeRoot, branch, startPoint,
10573
11205
  function inOwnLinkedWorktree(appDir, id) {
10574
11206
  if (workspaceIdFromBranch(currentBranch(appDir)) !== id) return false;
10575
11207
  try {
10576
- return statSync(join22(repoToplevel(appDir), ".git")).isFile();
11208
+ return statSync3(join22(repoToplevel(appDir), ".git")).isFile();
10577
11209
  } catch {
10578
11210
  return false;
10579
11211
  }
@@ -10919,7 +11551,7 @@ var newWorkspaceCommand = defineDeepspaceCommand({
10919
11551
  idempotencyKey: mintIdempotencyKey()
10920
11552
  });
10921
11553
  const dirInput = typeof args.dir === "string" ? args.dir.trim() : "";
10922
- const worktreeRoot = dirInput ? isAbsolute6(dirInput) ? dirInput : resolve16(appDir, dirInput) : defaultWorkspaceRoot(appDir, id);
11554
+ const worktreeRoot = dirInput ? isAbsolute6(dirInput) ? dirInput : resolve17(appDir, dirInput) : defaultWorkspaceRoot(appDir, id);
10923
11555
  const dir = appDirInWorktree(appDir, worktreeRoot);
10924
11556
  try {
10925
11557
  materializeWorkspaceWorktree(appDir, worktreeRoot, branch, baseOid, id);
@@ -10957,8 +11589,8 @@ var newWorkspaceCommand = defineDeepspaceCommand({
10957
11589
 
10958
11590
  // src/cli/commands/workspace/attach.ts
10959
11591
  import * as p26 from "@clack/prompts";
10960
- import { existsSync as existsSync24, readdirSync as readdirSync7, rmSync as rmSync6 } from "fs";
10961
- import { isAbsolute as isAbsolute7, resolve as resolve17 } from "path";
11592
+ import { existsSync as existsSync25, readdirSync as readdirSync7, rmSync as rmSync6 } from "fs";
11593
+ import { isAbsolute as isAbsolute7, resolve as resolve18 } from "path";
10962
11594
  function finishedWorkspaceMessage(id, view) {
10963
11595
  const workspace = view.workspace;
10964
11596
  if (workspace.status === "landed") {
@@ -10967,7 +11599,7 @@ function finishedWorkspaceMessage(id, view) {
10967
11599
  return `Workspace ${id} is ${workspace.status} \u2014 its ref is retained briefly (fetch ${workspace.ref} to inspect), but attach only works on active workspaces.`;
10968
11600
  }
10969
11601
  function cleanFailedFreshAttachDir(dir) {
10970
- if (!existsSync24(dir)) return [];
11602
+ if (!existsSync25(dir)) return [];
10971
11603
  let entries;
10972
11604
  try {
10973
11605
  entries = readdirSync7(dir);
@@ -11043,7 +11675,7 @@ var attachWorkspaceCommand = defineDeepspaceCommand({
11043
11675
  );
11044
11676
  }
11045
11677
  const requestedDir = typeof args.dir === "string" ? args.dir.trim() : "";
11046
- const worktreeRoot = requestedDir ? isAbsolute7(requestedDir) ? requestedDir : resolve17(appDir, requestedDir) : defaultWorkspaceRoot(appDir, id);
11678
+ const worktreeRoot = requestedDir ? isAbsolute7(requestedDir) ? requestedDir : resolve18(appDir, requestedDir) : defaultWorkspaceRoot(appDir, id);
11047
11679
  const dir2 = appDirInWorktree(appDir, worktreeRoot);
11048
11680
  if (resolveCommit(appDir, `refs/heads/${branch}`)) {
11049
11681
  throw new Refusal(
@@ -11082,11 +11714,11 @@ var attachWorkspaceCommand = defineDeepspaceCommand({
11082
11714
  extra: { status: view.workspace.status }
11083
11715
  });
11084
11716
  }
11085
- const dir = resolve17(
11717
+ const dir = resolve18(
11086
11718
  process.cwd(),
11087
11719
  (typeof args.dir === "string" ? args.dir.trim() : "") || `ws-${id.slice(3).toLowerCase()}`
11088
11720
  );
11089
- if (existsSync24(dir)) {
11721
+ if (existsSync25(dir)) {
11090
11722
  spinner2?.stop("Target exists.");
11091
11723
  throw new Refusal(`${dir} already exists \u2014 pass a different directory.`, "dir_exists");
11092
11724
  }
@@ -11232,27 +11864,6 @@ var syncWorkspaceCommand = defineDeepspaceCommand({
11232
11864
 
11233
11865
  // src/cli/commands/workspace/list.ts
11234
11866
  import * as p28 from "@clack/prompts";
11235
-
11236
- // src/cli/lib/actor-labels.ts
11237
- var API_URL9 = process.env.DEEPSPACE_API_URL ?? PLATFORM_URLS.api;
11238
- async function actorLabels(token, appId) {
11239
- const labels = /* @__PURE__ */ new Map();
11240
- try {
11241
- const { sub, email } = decodeJwtPayload(token);
11242
- if (sub && email) labels.set(sub, `${email} (you)`);
11243
- } catch {
11244
- }
11245
- try {
11246
- const { collaborators } = await apiFetch(API_URL9, token, `/api/app-collaborators/${encodeURIComponent(appId)}`);
11247
- for (const c of collaborators) {
11248
- if (!labels.has(c.userId)) labels.set(c.userId, c.emailDisplay);
11249
- }
11250
- } catch {
11251
- }
11252
- return labels;
11253
- }
11254
-
11255
- // src/cli/commands/workspace/list.ts
11256
11867
  function withWorkspaceOverlaps(views, overlapsById) {
11257
11868
  return views.map((view) => ({
11258
11869
  ...view,
@@ -11830,7 +12441,7 @@ var dropWorkspaceCommand = defineDeepspaceCommand({
11830
12441
  });
11831
12442
 
11832
12443
  // src/cli/commands/workspace.ts
11833
- var workspace_default = defineCommand11({
12444
+ var workspace_default = defineCommand12({
11834
12445
  meta: {
11835
12446
  name: "workspace",
11836
12447
  description: "Durable parallel-agent workspaces (new/attach/sync/list/status/land/drop)"
@@ -12001,7 +12612,7 @@ var activity_default = defineDeepspaceCommand({
12001
12612
  });
12002
12613
 
12003
12614
  // src/cli/commands/git-credential.ts
12004
- import { defineCommand as defineCommand12 } from "citty";
12615
+ import { defineCommand as defineCommand13 } from "citty";
12005
12616
  function parseFields(input) {
12006
12617
  const fields = /* @__PURE__ */ new Map();
12007
12618
  for (const line of input.split("\n")) {
@@ -12050,7 +12661,7 @@ async function readStdin() {
12050
12661
  for await (const chunk of process.stdin) data += chunk;
12051
12662
  return data;
12052
12663
  }
12053
- var git_credential_default = defineCommand12({
12664
+ var git_credential_default = defineCommand13({
12054
12665
  meta: {
12055
12666
  name: "git-credential",
12056
12667
  description: "Git credential helper (installed automatically; not for direct use)"
@@ -12168,8 +12779,8 @@ function reportSource(appId, state, json) {
12168
12779
  async function moveToGitHub(input) {
12169
12780
  const { appDir, appId, token, deployUrl, state, remoteArg } = input;
12170
12781
  const selected = requireGitHubRemote(appDir, remoteArg);
12171
- const target = { provider: "github", repository: selected.repository };
12172
- if (sameSource(state.source, target)) return { source: target, revision: state.revision };
12782
+ const target2 = { provider: "github", repository: selected.repository };
12783
+ if (sameSource(state.source, target2)) return { source: target2, revision: state.revision };
12173
12784
  if (state.source?.provider === "deepspace") {
12174
12785
  requireLocalHead(appDir);
12175
12786
  const api4 = repoApi(deployUrl, token, appId);
@@ -12225,7 +12836,7 @@ async function moveToGitHub(input) {
12225
12836
  );
12226
12837
  }
12227
12838
  return setAppSource(deployUrl, token, appId, {
12228
- source: target,
12839
+ source: target2,
12229
12840
  expectedRevision: state.revision,
12230
12841
  refs: wanted,
12231
12842
  ...releaseExpectation(latest)
@@ -12233,17 +12844,17 @@ async function moveToGitHub(input) {
12233
12844
  }
12234
12845
  requirePublishedHead(appDir, selected, appId);
12235
12846
  return setAppSource(deployUrl, token, appId, {
12236
- source: target,
12847
+ source: target2,
12237
12848
  expectedRevision: state.revision
12238
12849
  });
12239
12850
  }
12240
12851
  async function moveToDeepSpace(input) {
12241
12852
  const { appDir, appId, token, deployUrl, state, remoteArg } = input;
12242
- const target = { provider: "deepspace" };
12243
- if (sameSource(state.source, target)) return { source: target, revision: state.revision };
12853
+ const target2 = { provider: "deepspace" };
12854
+ if (sameSource(state.source, target2)) return { source: target2, revision: state.revision };
12244
12855
  if (state.source?.provider !== "github") {
12245
12856
  return setAppSource(deployUrl, token, appId, {
12246
- source: target,
12857
+ source: target2,
12247
12858
  expectedRevision: state.revision
12248
12859
  });
12249
12860
  }
@@ -12277,7 +12888,7 @@ async function moveToDeepSpace(input) {
12277
12888
  );
12278
12889
  }
12279
12890
  return setAppSource(deployUrl, token, appId, {
12280
- source: target,
12891
+ source: target2,
12281
12892
  expectedRevision: state.revision,
12282
12893
  branch,
12283
12894
  commitOid: oid,
@@ -12905,10 +13516,10 @@ function reportMigration(migration, json) {
12905
13516
  // src/cli/cli.ts
12906
13517
  var __dirname = dirname10(fileURLToPath3(import.meta.url));
12907
13518
  var pkg = JSON.parse(readFileSync28(join24(__dirname, "..", "package.json"), "utf-8"));
12908
- if (process.platform === "linux" && !process.env.SSL_CERT_FILE && existsSync25("/etc/ssl/certs/ca-certificates.crt")) {
13519
+ if (process.platform === "linux" && !process.env.SSL_CERT_FILE && existsSync26("/etc/ssl/certs/ca-certificates.crt")) {
12909
13520
  process.env.SSL_CERT_FILE = "/etc/ssl/certs/ca-certificates.crt";
12910
13521
  }
12911
- var create2 = defineCommand13({
13522
+ var create2 = defineCommand14({
12912
13523
  meta: {
12913
13524
  name: "create",
12914
13525
  description: "Create a new DeepSpace app (runs create-deepspace; all flags are forwarded)"
@@ -12947,19 +13558,20 @@ var create2 = defineCommand13({
12947
13558
  }
12948
13559
  });
12949
13560
  var rename = (cmd, name) => ({ ...cmd, meta: { ...cmd.meta ?? {}, name } });
12950
- var auth = defineCommand13({
13561
+ var auth = defineCommand14({
12951
13562
  meta: { name: "auth", description: "Session: login, logout, whoami" },
12952
13563
  subCommands: { login: login_default, logout: logout_default, whoami: whoami_default }
12953
13564
  });
12954
- var app = defineCommand13({
13565
+ var app = defineCommand14({
12955
13566
  meta: {
12956
13567
  name: "app",
12957
- description: "The app itself: create, init, list, source, migrate, undeploy, transfer, collaborators, domain, usage"
13568
+ description: "The app itself: create, init, list, files, source, migrate, undeploy, transfer, collaborators, domain, usage"
12958
13569
  },
12959
13570
  subCommands: {
12960
13571
  create: create2,
12961
13572
  init: init_default,
12962
13573
  list: rename(apps_default, "list"),
13574
+ files: app_files_default,
12963
13575
  undeploy: undeploy_default,
12964
13576
  transfer: transfer_default,
12965
13577
  collaborators: collaborators_default,
@@ -12969,11 +13581,11 @@ var app = defineCommand13({
12969
13581
  migrate: migrate_default
12970
13582
  }
12971
13583
  });
12972
- var devGroup = defineCommand13({
13584
+ var devGroup = defineCommand14({
12973
13585
  meta: { name: "dev", description: "Local development" },
12974
13586
  subCommands: { start: rename(dev_default, "start"), kill: kill_default }
12975
13587
  });
12976
- var testGroup = defineCommand13({
13588
+ var testGroup = defineCommand14({
12977
13589
  meta: { name: "test", description: "App tests and test tooling" },
12978
13590
  subCommands: {
12979
13591
  run: rename(test_default, "run"),
@@ -12981,7 +13593,7 @@ var testGroup = defineCommand13({
12981
13593
  accounts: rename(test_accounts_default, "accounts")
12982
13594
  }
12983
13595
  });
12984
- var main = defineCommand13({
13596
+ var main = defineCommand14({
12985
13597
  meta: {
12986
13598
  name: "deepspace",
12987
13599
  version: pkg.version,
@@ -13021,7 +13633,7 @@ function commandTree() {
13021
13633
  return main.subCommands ?? {};
13022
13634
  }
13023
13635
  function unknownCommandMessage(unknown) {
13024
- return `Unknown command: ${unknown.attemptedPath.join(" ")} \u2014 did you mean \`${unknown.suggestion.join(" ")}\`?`;
13636
+ return `Unknown command: ${humanCommand(unknown.attemptedPath)} \u2014 did you mean \`${humanCommand(unknown.suggestion)}\`?` + (unknown.quotedToken ? " The whole command arrived as one quoted argument; pass each word separately." : "");
13025
13637
  }
13026
13638
  function assertKnownCommandPath(argv) {
13027
13639
  const unknown = findUnknownCommand(argv, commandTree());
@@ -13030,7 +13642,7 @@ function assertKnownCommandPath(argv) {
13030
13642
  `${unknownCommandMessage(unknown)}
13031
13643
  Run \`${unknown.helpPath.join(" ")} --help\` for the command list.`
13032
13644
  );
13033
- printAction({ cwd: process.cwd(), argv: unknown.suggestion });
13645
+ if (unknown.executable) printAction({ cwd: process.cwd(), argv: unknown.suggestion });
13034
13646
  process.exit(1);
13035
13647
  }
13036
13648
  var rawArgs = process.argv.slice(2);
@@ -13052,7 +13664,7 @@ if (rawArgs.includes("--json") && !wantsHelp) {
13052
13664
  ok: false,
13053
13665
  code: "unknown_command",
13054
13666
  error: unknownCommandMessage(unknown),
13055
- action: { cwd: process.cwd(), argv: unknown.suggestion }
13667
+ ...unknown.executable ? { action: { cwd: process.cwd(), argv: unknown.suggestion } } : {}
13056
13668
  })
13057
13669
  );
13058
13670
  process.exit(1);
@@ -13061,16 +13673,20 @@ if (rawArgs.includes("--json") && !wantsHelp) {
13061
13673
  stopActiveSpinner();
13062
13674
  const cittyCode = err instanceof Error && err.name === "CLIError" ? err.code : void 0;
13063
13675
  const code = cittyCode === "EARG" ? "missing_argument" : cittyCode === "E_UNKNOWN_COMMAND" ? "unknown_command" : cittyCode === "E_NO_COMMAND" ? "no_command" : errorCode(err);
13064
- const message = (err instanceof Error ? err.message : String(err)).replace(
13065
- // eslint-disable-next-line no-control-regex
13066
- /\u001b\[[0-9;]*m/g,
13067
- ""
13068
- );
13676
+ const message = stripAnsi(err instanceof Error ? err.message : String(err));
13069
13677
  console.log(JSON.stringify({ ok: false, error: message, ...code ? { code } : {} }));
13070
13678
  process.exit(1);
13071
13679
  });
13072
13680
  } else {
13073
13681
  assertKnownCommandPath(rawArgs);
13074
- runMain(wrapCommandErrors(main));
13682
+ runMain(wrapCommandErrors(main), {
13683
+ showUsage: async (cmd, parent) => {
13684
+ try {
13685
+ console.log(forTerminal(await renderUsage(cmd, parent)) + "\n");
13686
+ } catch (error) {
13687
+ console.error(error);
13688
+ }
13689
+ }
13690
+ });
13075
13691
  }
13076
13692
  //# sourceMappingURL=cli.js.map