deepspace 0.12.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,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli/cli.ts
4
- import { defineCommand as defineCommand14, runMain, runCommand } from "citty";
4
+ import { defineCommand as defineCommand14, runMain, runCommand, renderUsage } from "citty";
5
5
  import { sync as spawnSync8 } from "cross-spawn";
6
6
  import { readFileSync as readFileSync28, existsSync as existsSync26 } from "fs";
7
7
  import { dirname as dirname10, join as join24 } from "path";
@@ -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);
@@ -2544,16 +2569,31 @@ function formatSchemaLintFindings(findings) {
2544
2569
  }
2545
2570
 
2546
2571
  // src/cli/lib/port.ts
2547
- import { createServer } from "net";
2572
+ import { createServer, Socket } from "net";
2548
2573
  var DEFAULT_PORT = 5173;
2549
- function checkPortAvailable(port, host = "0.0.0.0") {
2574
+ function isPortListening(port, host, timeoutMs = 500) {
2575
+ const target2 = host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host;
2550
2576
  return new Promise((resolve19) => {
2551
- const srv = createServer();
2552
- srv.once("error", () => resolve19(false));
2553
- srv.once("listening", () => srv.close(() => resolve19(true)));
2554
- srv.listen(port, host);
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);
2555
2587
  });
2556
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
+ }
2557
2597
  function resolvePort(arg) {
2558
2598
  const raw = arg ?? process.env.DEEPSPACE_PORT;
2559
2599
  if (!raw) return DEFAULT_PORT;
@@ -2565,6 +2605,8 @@ function resolvePort(arg) {
2565
2605
  }
2566
2606
 
2567
2607
  // src/cli/commands/dev.ts
2608
+ var DEFAULT_DEV_HOST = "0.0.0.0";
2609
+ var READINESS_TIMEOUT_MS = 12e4;
2568
2610
  var DEPLOY_URL = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
2569
2611
  function noAppDirMessage(start, children) {
2570
2612
  const lines = [`No wrangler.toml found at or above ${start}.`];
@@ -2600,6 +2642,11 @@ var dev_default = defineDeepspaceCommand({
2600
2642
  type: "string",
2601
2643
  description: `Port to bind (default ${DEFAULT_PORT}, or $DEEPSPACE_PORT)`,
2602
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
2603
2650
  }
2604
2651
  },
2605
2652
  async run({ args }) {
@@ -2608,6 +2655,7 @@ var dev_default = defineDeepspaceCommand({
2608
2655
  };
2609
2656
  preflightNodeVersion("dev start");
2610
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;
2611
2659
  const start = resolve7(args.dir ?? ".");
2612
2660
  const appDir = findAppDir(start);
2613
2661
  if (!appDir) {
@@ -2671,6 +2719,7 @@ var dev_default = defineDeepspaceCommand({
2671
2719
  say(`Logged in as ${payload.name ?? payload.email}`);
2672
2720
  if (wranglerEnv) say(`Wrangler env: ${wranglerEnv}`);
2673
2721
  say(`Port: ${port}`);
2722
+ if (host !== DEFAULT_DEV_HOST) say(`Host: ${host}`);
2674
2723
  let generatedSecretsCache;
2675
2724
  const appIdForSecrets = readAppId(appDir, wranglerEnv);
2676
2725
  if (appIdForSecrets) {
@@ -2698,7 +2747,7 @@ var dev_default = defineDeepspaceCommand({
2698
2747
  { sharedDevVarsCache }
2699
2748
  );
2700
2749
  preflightWindowsWorkerd(appDir);
2701
- if (!await checkPortAvailable(port)) {
2750
+ if (await isPortListening(port, host)) {
2702
2751
  const killArgv = port === DEFAULT_PORT ? ["deepspace", "dev", "kill"] : ["deepspace", "dev", "kill", "--port", String(port)];
2703
2752
  const killCmd = killArgv.join(" ");
2704
2753
  wranglerConfig.cleanup();
@@ -2709,12 +2758,34 @@ Free it with \`${killCmd}\`, or start on another port: \`deepspace dev start --p
2709
2758
  { extra: { port } }
2710
2759
  );
2711
2760
  }
2761
+ const url = `http://${host === DEFAULT_DEV_HOST ? "localhost" : host}:${port}`;
2712
2762
  say("Starting dev server...\n");
2713
- const vite = spawn("npx", ["vite", "--port", String(port), "--strictPort", "--host"], {
2714
- cwd: appDir,
2715
- stdio: "inherit",
2716
- env: wranglerViteEnv(process.env, wranglerConfig, { DEEPSPACE_PORT: String(port) })
2717
- });
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
+ }
2718
2789
  let stopping = false;
2719
2790
  const stop = () => {
2720
2791
  stopping = true;
@@ -2736,7 +2807,7 @@ Free it with \`${killCmd}\`, or start on another port: \`deepspace dev start --p
2736
2807
  extra: { port, appDir, exitCode: code }
2737
2808
  });
2738
2809
  }
2739
- return { data: { port, appDir, wranglerEnv: wranglerEnv ?? null } };
2810
+ return { data: { port, host, url, appDir, wranglerEnv: wranglerEnv ?? null } };
2740
2811
  }
2741
2812
  });
2742
2813
 
@@ -3725,6 +3796,96 @@ import * as p12 from "@clack/prompts";
3725
3796
  import { defineCommand as defineCommand3 } from "citty";
3726
3797
  import { resolve as resolve12 } from "path";
3727
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
+
3728
3889
  // src/cli/commands/deploy/build.ts
3729
3890
  import * as p6 from "@clack/prompts";
3730
3891
  import { execSync as execSync2 } from "child_process";
@@ -4044,7 +4205,14 @@ var RESERVED_RUN_WORKER_FIRST = /* @__PURE__ */ new Set([
4044
4205
  "/_documentation",
4045
4206
  "/_documentation/*",
4046
4207
  "/_documentation-root",
4047
- "/_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/*"
4048
4216
  ]);
4049
4217
  async function buildDeployBundle(options) {
4050
4218
  const { appDir, appName, envName, sharedDevVarsCache, output, spinner: spinner2 } = options;
@@ -4178,6 +4346,12 @@ function resolveDeployRunWorkerFirst(config) {
4178
4346
  const routes = extractRunWorkerFirst(config);
4179
4347
  return routes;
4180
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.";
4181
4355
  function collectAssets(dir) {
4182
4356
  const assets = [];
4183
4357
  const walk = (currentDir, prefix) => {
@@ -4670,37 +4844,6 @@ function repoApi(deployUrl, token, appId) {
4670
4844
  };
4671
4845
  }
4672
4846
 
4673
- // src/shared/app-files.ts
4674
- var MAX_APP_FILE_BYTES = 25 * 1024 * 1024;
4675
- function formatBytes(bytes) {
4676
- if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
4677
- if (bytes >= 1024) return `${Math.round(bytes / 1024)} KiB`;
4678
- return `${bytes} B`;
4679
- }
4680
- function encodeKeyPath(key) {
4681
- return key.split("/").map(encodeURIComponent).join("/");
4682
- }
4683
- function oversizeMessage(bytes) {
4684
- return `That file is ${formatBytes(bytes)}; the limit is ${formatBytes(MAX_APP_FILE_BYTES)} per file.`;
4685
- }
4686
- function describeFilesFailure(status3, body) {
4687
- const trimmed = body.trim();
4688
- if (trimmed.startsWith("{")) {
4689
- try {
4690
- const parsed = JSON.parse(trimmed);
4691
- if (parsed.error) return parsed.error;
4692
- } catch {
4693
- }
4694
- }
4695
- if (status3 === 413) {
4696
- return `The file is too large to upload. The limit is ${formatBytes(MAX_APP_FILE_BYTES)} per file; larger uploads are rejected by the network before the app receives them.`;
4697
- }
4698
- if (status3 === 401 || status3 === 403) return "Not authorized to write these files.";
4699
- if (status3 === 415) return "That file type is not accepted.";
4700
- if (status3 >= 500) return `The files service failed (HTTP ${status3}). Retry shortly.`;
4701
- return `Upload failed (HTTP ${status3}).`;
4702
- }
4703
-
4704
4847
  // src/cli/lib/vc-remote.ts
4705
4848
  import { existsSync as existsSync17 } from "fs";
4706
4849
  import { tmpdir } from "os";
@@ -4913,10 +5056,11 @@ function pushToSpace(cwd, token, refspec, opts = {}) {
4913
5056
  var OVERSIZED_PUSH_RE = /too large|exceeds|\bLFS\b/i;
4914
5057
  var SERVER_OBJECT_CAP_BYTES = 20 * 1024 * 1024;
4915
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`;
4916
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\`.`;
4917
5061
  function removeFromHistoryAdvice(path) {
4918
5062
  const file = path ? shQuote(path) : "<file>";
4919
- return `Media belongs in the app's files rather than Git history \u2014 \`deepspace app files put ${file}\` stores it in the app's own allocation and serves it over HTTP. Then get it out of the push: add it to .gitignore, and drop it from the commits that carry it \u2014 if those commits are still local, \`git reset --soft <last pushed commit>\` and re-commit without the file. \`git rm --cached\` plus a new commit is NOT sufficient on its own: the blob stays reachable from the earlier commit and the push is rejected the same way. If it already reached pushed history, rewrite it out (\`git filter-repo --path ${file} --invert-paths\`).`;
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\`).`;
4920
5064
  }
4921
5065
  function fixForObjects(objects) {
4922
5066
  if (objects.length === 0) return `${OVERSIZED_PUSH_FIX} ${removeFromHistoryAdvice(null)}`;
@@ -5402,7 +5546,7 @@ async function deployBuiltBundle(options) {
5402
5546
  );
5403
5547
  }
5404
5548
  if (body.onBehalfOfOwner) {
5405
- 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.");
5406
5550
  }
5407
5551
  if (body.staleBaseGuard === "skipped") {
5408
5552
  p10.log.warn(
@@ -5483,10 +5627,11 @@ async function uploadDeployAssets(options) {
5483
5627
  p10.log.info(`Assets: ${byHash.size} file(s), all already uploaded`);
5484
5628
  return;
5485
5629
  }
5486
- const missingBytes = missing.reduce((total, asset) => total + asset.size, 0);
5630
+ const missingBytes = missing.reduce((total2, asset) => total2 + asset.size, 0);
5487
5631
  p10.log.info(
5488
5632
  `Assets: ${byHash.size} file(s), uploading ${missing.length} (${Math.ceil(missingBytes / 1024)} KiB); the rest are already stored`
5489
5633
  );
5634
+ const total = missing.length;
5490
5635
  let uploaded = 0;
5491
5636
  const next = () => missing.pop();
5492
5637
  const worker = async () => {
@@ -5515,7 +5660,7 @@ async function uploadDeployAssets(options) {
5515
5660
  );
5516
5661
  }
5517
5662
  uploaded++;
5518
- spinner2.message(`Uploading assets \u2014 ${uploaded}/${uploaded + missing.length}...`);
5663
+ spinner2.message(`Uploading assets \u2014 ${uploaded}/${total}...`);
5519
5664
  }
5520
5665
  };
5521
5666
  try {
@@ -5746,7 +5891,11 @@ var DEPLOY_URL3 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
5746
5891
  var deploy_default = defineCommand3({
5747
5892
  meta: {
5748
5893
  name: "deploy",
5749
- 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)`
5750
5899
  },
5751
5900
  args: {
5752
5901
  dir: {
@@ -5842,6 +5991,20 @@ var deploy_default = defineCommand3({
5842
5991
  output
5843
5992
  });
5844
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
+ }
5845
6008
  const repository = await syncDeployRepository({
5846
6009
  deployUrl: DEPLOY_URL3,
5847
6010
  appDir,
@@ -5852,15 +6015,6 @@ var deploy_default = defineCommand3({
5852
6015
  output,
5853
6016
  sourceState
5854
6017
  });
5855
- const spinner2 = createSpinner();
5856
- const bundle = await buildDeployBundle({
5857
- appDir,
5858
- appName,
5859
- envName,
5860
- sharedDevVarsCache: secretsCache.linked !== null,
5861
- output,
5862
- spinner: spinner2
5863
- });
5864
6018
  const secrets = prepareDeploySecrets({
5865
6019
  appDir,
5866
6020
  envName,
@@ -5886,7 +6040,7 @@ var deploy_default = defineCommand3({
5886
6040
  });
5887
6041
  if (body.url) {
5888
6042
  spinner2.message("Waiting for edge propagation...");
5889
- const ready = await waitForAssetsReady(body.url, 9e4);
6043
+ const ready = await waitForEdgePropagation(body.url, 9e4);
5890
6044
  if (!ready) {
5891
6045
  spinner2.stop("Deployed (edge propagation still in progress after 90s)");
5892
6046
  p12.log.warn(
@@ -5951,30 +6105,6 @@ function blankSelectorRefusal(args) {
5951
6105
  function staleBaseGuardFields(body) {
5952
6106
  return body.staleBaseGuard === "skipped" ? { staleBaseGuard: "skipped" } : {};
5953
6107
  }
5954
- async function waitForAssetsReady(url, timeoutMs) {
5955
- const deadline = Date.now() + timeoutMs;
5956
- let attempt = 0;
5957
- let streak = 0;
5958
- while (Date.now() < deadline) {
5959
- attempt++;
5960
- try {
5961
- const response = await fetch(url, { redirect: "manual" });
5962
- const body = await response.text();
5963
- if (!body.includes("Assets have not yet deployed") && !body.includes("No app configured")) {
5964
- streak++;
5965
- if (streak >= 3) return true;
5966
- await new Promise((resolve19) => setTimeout(resolve19, 1500));
5967
- continue;
5968
- }
5969
- streak = 0;
5970
- } catch {
5971
- streak = 0;
5972
- }
5973
- const waitMs = Math.min(8e3, 1e3 * 2 ** (attempt - 1));
5974
- await new Promise((resolve19) => setTimeout(resolve19, waitMs));
5975
- }
5976
- return false;
5977
- }
5978
6108
  function errorMessage6(error) {
5979
6109
  return error instanceof Error ? error.message : String(error);
5980
6110
  }
@@ -6014,6 +6144,19 @@ function requireAppIdArg(explicit) {
6014
6144
  }
6015
6145
  return id;
6016
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
+ }
6017
6160
  function matchAppSelector(apps, selector) {
6018
6161
  const byId = apps.find((app2) => app2.appId === selector);
6019
6162
  if (byId) return byId.appId;
@@ -6023,14 +6166,7 @@ function matchAppSelector(apps, selector) {
6023
6166
  }
6024
6167
  async function resolveAppSelector(deployUrl, token, selector) {
6025
6168
  if (STRICT_APP_ID_RE.test(selector)) return selector;
6026
- const { apps } = await apiFetch(deployUrl, token, "/api/apps");
6027
- if (!Array.isArray(apps)) {
6028
- throw new InputError(
6029
- `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?).`,
6030
- "invalid_response"
6031
- );
6032
- }
6033
- const id = matchAppSelector(apps, selector);
6169
+ const id = matchAppSelector(await listApps(deployUrl, token), selector);
6034
6170
  if (id) return id;
6035
6171
  const shared = await resolveAppIdByName(deployUrl, token, selector);
6036
6172
  if (shared) return shared;
@@ -6113,7 +6249,7 @@ function assertAppTargetResolvable(appArg, options = {}) {
6113
6249
  async function warnIfPhantomApp(deployUrl, token, appId, explicit) {
6114
6250
  if (!explicit || !STRICT_APP_ID_RE.test(explicit.trim())) return;
6115
6251
  try {
6116
- const { apps } = await apiFetch(deployUrl, token, "/api/apps");
6252
+ const apps = await listApps(deployUrl, token);
6117
6253
  if (!apps.some((app2) => app2.appId === appId)) {
6118
6254
  process.stderr.write(
6119
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).
@@ -6227,7 +6363,6 @@ var whoami_default = defineDeepspaceCommand({
6227
6363
  if (!args.json) {
6228
6364
  console.log(`Name: ${payload.name ?? "(none)"}`);
6229
6365
  console.log(`Email: ${payload.email ?? "(none)"}`);
6230
- console.log(`UserID: ${payload.sub ?? "(none)"}`);
6231
6366
  console.log(`Type: ${accountType}`);
6232
6367
  if (payload.iss) console.log(`Issuer: ${payload.iss}`);
6233
6368
  console.log(`Dashboard: ${DASHBOARD_URL}`);
@@ -6259,7 +6394,7 @@ var apps_default = defineDeepspaceCommand({
6259
6394
  },
6260
6395
  async run({ args }) {
6261
6396
  const token = await ensureToken();
6262
- const { apps } = await apiFetch(DEPLOY_URL5, token, "/api/apps");
6397
+ const apps = await listApps(DEPLOY_URL5, token);
6263
6398
  if (!args.json) {
6264
6399
  if (!apps.length) {
6265
6400
  console.log("No apps yet. Create one with `npx create-deepspace <name>` and `deepspace deploy`.");
@@ -6267,16 +6402,18 @@ var apps_default = defineDeepspaceCommand({
6267
6402
  const rows = [...apps].sort(
6268
6403
  (left, right) => Number(right.status !== "undeployed") - Number(left.status !== "undeployed")
6269
6404
  );
6405
+ const roleOf = (a) => a.role ?? "owner";
6406
+ const roleWidth = Math.max(4, ...rows.map((a) => roleOf(a).length));
6270
6407
  const nameWidth = Math.max(4, ...rows.map((a) => (a.name ?? "\u2014").length));
6271
6408
  const statusWidth = Math.max(6, ...rows.map((a) => a.status.length));
6272
6409
  const idWidth = Math.max(6, ...rows.map((a) => a.appId.length));
6273
6410
  console.log(
6274
- `${"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`
6275
6412
  );
6276
6413
  for (const a of rows) {
6277
- const url = a.url ?? "(not deployed)";
6414
+ const url = liveAppUrl(a) ?? "(not deployed)";
6278
6415
  console.log(
6279
- `${(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}`
6280
6417
  );
6281
6418
  }
6282
6419
  }
@@ -7989,11 +8126,16 @@ var CONTENT_TYPES = {
7989
8126
  ".css": "text/css",
7990
8127
  ".csv": "text/csv",
7991
8128
  ".gif": "image/gif",
8129
+ ".htm": "text/html",
8130
+ ".html": "text/html",
7992
8131
  ".ico": "image/x-icon",
7993
8132
  ".jpeg": "image/jpeg",
7994
8133
  ".jpg": "image/jpeg",
8134
+ ".js": "text/javascript",
7995
8135
  ".json": "application/json",
7996
8136
  ".md": "text/markdown",
8137
+ ".mjs": "text/javascript",
8138
+ ".svg": "image/svg+xml",
7997
8139
  ".mp3": "audio/mpeg",
7998
8140
  ".mp4": "video/mp4",
7999
8141
  ".otf": "font/otf",
@@ -8039,7 +8181,10 @@ async function request(baseUrl, token, path, init = {}) {
8039
8181
  throw new ApiError(
8040
8182
  describeFilesFailure(res.status, text2),
8041
8183
  res.status,
8042
- code ?? (res.status === 413 ? "file_too_large" : res.status >= 500 ? "server_error" : "http_error"),
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"),
8043
8188
  path
8044
8189
  );
8045
8190
  }
@@ -8072,31 +8217,96 @@ Content-Type: ${contentType}\r
8072
8217
  contentType: `multipart/form-data; boundary=${boundary}`
8073
8218
  };
8074
8219
  }
8075
- async function uploadAppFile(baseUrl, token, appId, localPath, key) {
8220
+ async function uploadAppFile(baseUrl, token, appId, localPath, key, onProgress) {
8076
8221
  const path = filesPath(appId, `/upload?key=${encodeKeyPath(key)}`);
8077
8222
  const size = statSync(localPath).size;
8078
8223
  if (size > MAX_APP_FILE_BYTES) {
8079
- throw new ApiError(oversizeMessage(size), 0, "file_too_large", path);
8224
+ throw new ApiError(oversizeMessage(size), 0, "too_large", path);
8080
8225
  }
8081
- const { blob, contentType } = await multipartBody(
8082
- localPath,
8083
- basename3(localPath),
8084
- contentTypeFor(localPath)
8085
- );
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);
8086
8234
  const res = await request(baseUrl, token, path, {
8087
8235
  method: "POST",
8088
8236
  headers: { "Content-Type": contentType },
8089
8237
  body: blob
8090
8238
  });
8091
8239
  const body = await readJson(res, path);
8092
- return { key: body.key, name: body.name, size };
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
+ }
8093
8300
  }
8094
8301
  async function listAppFiles(baseUrl, token, appId, query) {
8095
8302
  const path = filesPath(appId, `?${query}`);
8096
8303
  return readJson(await request(baseUrl, token, path), path);
8097
8304
  }
8098
8305
  async function deleteAppFile(baseUrl, token, appId, key) {
8099
- await request(baseUrl, token, filesPath(appId, `/${encodeKeyPath(key)}`), { method: "DELETE" });
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 };
8100
8310
  }
8101
8311
  async function downloadAppFile(baseUrl, token, appId, key, destination) {
8102
8312
  const path = filesPath(appId, `/${encodeKeyPath(key)}`);
@@ -8156,7 +8366,10 @@ function servedPath(key) {
8156
8366
  return `/api/files/${encodeKeyPath(key)}?scope=app`;
8157
8367
  }
8158
8368
  var put = defineDeepspaceCommand({
8159
- meta: { name: "put", description: "Upload a file to the app\u2019s files" },
8369
+ meta: {
8370
+ name: "put",
8371
+ description: `Upload a file to the app\u2019s files (${formatBytes(MAX_APP_FILE_BYTES)} per file)`
8372
+ },
8160
8373
  args: {
8161
8374
  file: { type: "positional", description: "Local file to upload", required: true },
8162
8375
  key: { type: "string", description: "Key to store it under (default: the file name)" },
@@ -8169,12 +8382,42 @@ var put = defineDeepspaceCommand({
8169
8382
  }
8170
8383
  const key = requireKey(args.key ?? basename4(localPath));
8171
8384
  const { token, appId } = await target(args.app);
8172
- const result = await uploadAppFile(PLATFORM_URL, token, appId, localPath, key);
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
+ });
8173
8406
  if (!args.json) {
8174
- console.log(`\u2713 ${key} (${formatBytes(result.size)})`);
8407
+ const how = result.parts > 1 ? ` in ${result.parts} parts` : "";
8408
+ console.log(`\u2713 ${key} (${formatBytes(result.size)})${how}`);
8175
8409
  console.log(` Served from your app at ${servedPath(result.key)}`);
8176
8410
  }
8177
- return { data: { appId, key, storedKey: result.key, size: result.size, path: servedPath(result.key) } };
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
+ };
8178
8421
  }
8179
8422
  });
8180
8423
  var list3 = defineDeepspaceCommand({
@@ -8244,7 +8487,13 @@ var rm = defineDeepspaceCommand({
8244
8487
  async run({ args }) {
8245
8488
  const key = requireKey(String(args.key));
8246
8489
  const { token, appId } = await target(args.app);
8247
- await deleteAppFile(PLATFORM_URL, token, appId, key);
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
+ }
8248
8497
  if (!args.json) console.log(`\u2713 deleted ${key}`);
8249
8498
  return { data: { appId, key, deleted: true } };
8250
8499
  }
@@ -9795,24 +10044,36 @@ var status_default = defineCommand11({
9795
10044
  const failure = statusRemoteFailure(error);
9796
10045
  lines.push([label, failure.human]);
9797
10046
  json[key] = failure.json;
9798
- }, 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
+ }
9799
10053
  const byYou = userId !== null && rel.actor === userId;
9800
- lines.push([
9801
- "Live",
9802
- `release #${rel.seq} (${rel.kind} of ${rel.commitOid?.slice(0, 10) ?? "unknown source"}, ${timeAgo(rel.createdAt)}${byYou ? ", by you" : ""})` + (rel.url ? ` \xB7 ${rel.url}` : "")
9803
- ]);
9804
- 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 = {
9805
10056
  seq: rel.seq,
9806
10057
  kind: rel.kind,
9807
10058
  commitOid: rel.commitOid,
9808
10059
  createdAt: rel.createdAt,
9809
- url: rel.url,
9810
10060
  byYou,
9811
10061
  source: rel.source,
9812
10062
  sourceRevision: rel.sourceRevision
9813
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 };
9814
10075
  };
9815
- var reportWorkspace = reportWorkspace2, reportRemoteFailure = reportRemoteFailure2, reportRelease = reportRelease2;
10076
+ var reportWorkspace = reportWorkspace2, reportRemoteFailure = reportRemoteFailure2, reportLive = reportLive2;
9816
10077
  const api4 = repoApi(deployBaseUrl(), token, appId);
9817
10078
  const sourceResult = await observeRemote(getAppSource(deployBaseUrl(), token, appId));
9818
10079
  const source = sourceResult.ok ? sourceResult.value.source : null;
@@ -9832,10 +10093,11 @@ var status_default = defineCommand11({
9832
10093
  lines.push(["Source", "unclaimed \xB7 next normal deploy defaults to DeepSpace"]);
9833
10094
  json.source = null;
9834
10095
  }
9835
- const [refsResult, wsResult, releaseResult] = await Promise.all([
10096
+ const [refsResult, wsResult, releaseResult, appsResult] = await Promise.all([
9836
10097
  githubSource ? Promise.resolve({ ok: true, value: null }) : observeRemote(api4.getRefs()),
9837
10098
  facts.workspaceId && !githubSource ? observeRemote(api4.getWorkspace(facts.workspaceId).then((r) => r.view)) : Promise.resolve({ ok: true, value: null }),
9838
- observeRemote(api4.latestRelease().then((r) => r.release))
10099
+ observeRemote(api4.latestRelease().then((r) => r.release)),
10100
+ observeRemote(listApps(deployBaseUrl(), token))
9839
10101
  ]);
9840
10102
  if (facts.workspaceId) {
9841
10103
  if (githubSource) {
@@ -9864,12 +10126,8 @@ var status_default = defineCommand11({
9864
10126
  }
9865
10127
  }
9866
10128
  if (!releaseResult.ok) reportRemoteFailure2("Live", "live", releaseResult.error);
9867
- else if (releaseResult.value === null) {
9868
- lines.push(["Live", "never deployed"]);
9869
- json.live = null;
9870
- } else {
9871
- reportRelease2(releaseResult.value);
9872
- }
10129
+ else if (!appsResult.ok) reportRemoteFailure2("Live", "live", appsResult.error);
10130
+ else reportLive2(releaseResult.value, appsResult.value.find((a) => a.appId === appId));
9873
10131
  }
9874
10132
  spinner2?.stop("Cloud state checked.");
9875
10133
  }
@@ -9912,7 +10170,7 @@ function workspaceBranchPushRefusal(branch) {
9912
10170
  var push_default = defineDeepspaceCommand({
9913
10171
  meta: {
9914
10172
  name: "push",
9915
- 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})`
9916
10174
  },
9917
10175
  args: {
9918
10176
  branch: {
@@ -10493,6 +10751,27 @@ var clone_default = defineDeepspaceCommand({
10493
10751
 
10494
10752
  // src/cli/commands/releases.ts
10495
10753
  import * as p21 from "@clack/prompts";
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
10496
10775
  var DEPLOY_URL12 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
10497
10776
  var releases_default = defineDeepspaceCommand({
10498
10777
  meta: {
@@ -10535,11 +10814,12 @@ var releases_default = defineDeepspaceCommand({
10535
10814
  if (releases.length === 0) {
10536
10815
  p21.log.info("No releases yet \u2014 the history starts with the next `deepspace deploy`.");
10537
10816
  } else {
10817
+ const actors = await actorLabels(token, appId);
10538
10818
  for (const r of releases) {
10539
10819
  const source = r.commitOid ? `commit ${r.commitOid.slice(0, 10)}` : "(no source recorded)";
10540
10820
  const rollback = r.rollbackAvailable ? "rollback available" : "rollback unavailable";
10541
10821
  console.log(
10542
- `#${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}`
10543
10823
  );
10544
10824
  }
10545
10825
  p21.log.info(
@@ -10662,10 +10942,20 @@ Re-run with --allow-do-deletion to accept the data loss.`,
10662
10942
  "invalid_response"
10663
10943
  );
10664
10944
  }
10665
- 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
+ );
10666
10953
  const doClassGuard = result.doClassGuard === "unverified" ? "unverified" : void 0;
10667
10954
  if (!args.json) {
10668
- 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
+ }
10669
10959
  if (result.bundleRetained === false) {
10670
10960
  p22.log.warn(
10671
10961
  "The rollback is live, but its bundle was concurrently retired and is not available for a future rollback."
@@ -10689,6 +10979,7 @@ Re-run with --allow-do-deletion to accept the data loss.`,
10689
10979
  url: result.url ?? null,
10690
10980
  versionId: result.versionId ?? null,
10691
10981
  wranglerEnv: wranglerEnv ?? null,
10982
+ ...edgePropagating ? { edgePropagating: true } : {},
10692
10983
  ...doClassGuard ? { doClassGuard } : {}
10693
10984
  }
10694
10985
  };
@@ -11573,27 +11864,6 @@ var syncWorkspaceCommand = defineDeepspaceCommand({
11573
11864
 
11574
11865
  // src/cli/commands/workspace/list.ts
11575
11866
  import * as p28 from "@clack/prompts";
11576
-
11577
- // src/cli/lib/actor-labels.ts
11578
- var API_URL9 = process.env.DEEPSPACE_API_URL ?? PLATFORM_URLS.api;
11579
- async function actorLabels(token, appId) {
11580
- const labels = /* @__PURE__ */ new Map();
11581
- try {
11582
- const { sub, email } = decodeJwtPayload(token);
11583
- if (sub && email) labels.set(sub, `${email} (you)`);
11584
- } catch {
11585
- }
11586
- try {
11587
- const { collaborators } = await apiFetch(API_URL9, token, `/api/app-collaborators/${encodeURIComponent(appId)}`);
11588
- for (const c of collaborators) {
11589
- if (!labels.has(c.userId)) labels.set(c.userId, c.emailDisplay);
11590
- }
11591
- } catch {
11592
- }
11593
- return labels;
11594
- }
11595
-
11596
- // src/cli/commands/workspace/list.ts
11597
11867
  function withWorkspaceOverlaps(views, overlapsById) {
11598
11868
  return views.map((view) => ({
11599
11869
  ...view,
@@ -13363,7 +13633,7 @@ function commandTree() {
13363
13633
  return main.subCommands ?? {};
13364
13634
  }
13365
13635
  function unknownCommandMessage(unknown) {
13366
- 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." : "");
13367
13637
  }
13368
13638
  function assertKnownCommandPath(argv) {
13369
13639
  const unknown = findUnknownCommand(argv, commandTree());
@@ -13372,7 +13642,7 @@ function assertKnownCommandPath(argv) {
13372
13642
  `${unknownCommandMessage(unknown)}
13373
13643
  Run \`${unknown.helpPath.join(" ")} --help\` for the command list.`
13374
13644
  );
13375
- printAction({ cwd: process.cwd(), argv: unknown.suggestion });
13645
+ if (unknown.executable) printAction({ cwd: process.cwd(), argv: unknown.suggestion });
13376
13646
  process.exit(1);
13377
13647
  }
13378
13648
  var rawArgs = process.argv.slice(2);
@@ -13394,7 +13664,7 @@ if (rawArgs.includes("--json") && !wantsHelp) {
13394
13664
  ok: false,
13395
13665
  code: "unknown_command",
13396
13666
  error: unknownCommandMessage(unknown),
13397
- action: { cwd: process.cwd(), argv: unknown.suggestion }
13667
+ ...unknown.executable ? { action: { cwd: process.cwd(), argv: unknown.suggestion } } : {}
13398
13668
  })
13399
13669
  );
13400
13670
  process.exit(1);
@@ -13403,16 +13673,20 @@ if (rawArgs.includes("--json") && !wantsHelp) {
13403
13673
  stopActiveSpinner();
13404
13674
  const cittyCode = err instanceof Error && err.name === "CLIError" ? err.code : void 0;
13405
13675
  const code = cittyCode === "EARG" ? "missing_argument" : cittyCode === "E_UNKNOWN_COMMAND" ? "unknown_command" : cittyCode === "E_NO_COMMAND" ? "no_command" : errorCode(err);
13406
- const message = (err instanceof Error ? err.message : String(err)).replace(
13407
- // eslint-disable-next-line no-control-regex
13408
- /\u001b\[[0-9;]*m/g,
13409
- ""
13410
- );
13676
+ const message = stripAnsi(err instanceof Error ? err.message : String(err));
13411
13677
  console.log(JSON.stringify({ ok: false, error: message, ...code ? { code } : {} }));
13412
13678
  process.exit(1);
13413
13679
  });
13414
13680
  } else {
13415
13681
  assertKnownCommandPath(rawArgs);
13416
- 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
+ });
13417
13691
  }
13418
13692
  //# sourceMappingURL=cli.js.map