deepspace 0.12.0 → 0.14.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,133 @@ 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
+ var RELEASE_STAMP_PATH = "/.well-known/deepspace/release.json";
3802
+ async function waitForLiveRelease(url, releaseStamp, timeoutMs) {
3803
+ return releaseStamp ? waitForReleaseStamp(url, releaseStamp, timeoutMs) : waitForEdgePropagation(url, timeoutMs);
3804
+ }
3805
+ async function waitForReleaseStamp(url, releaseStamp, timeoutMs) {
3806
+ const deadline = Date.now() + timeoutMs;
3807
+ let attempt = 0;
3808
+ let streak = 0;
3809
+ while (Date.now() < deadline) {
3810
+ attempt++;
3811
+ try {
3812
+ const probe = new URL(RELEASE_STAMP_PATH, url);
3813
+ probe.searchParams.set("probe", `${Date.now()}-${attempt}`);
3814
+ const response = await fetch(probe, { redirect: "manual" });
3815
+ const body = await response.json().catch(() => null);
3816
+ if (response.ok && body?.release === releaseStamp) {
3817
+ streak++;
3818
+ if (streak >= 3) return true;
3819
+ await new Promise((resolve19) => setTimeout(resolve19, 1500));
3820
+ continue;
3821
+ }
3822
+ streak = 0;
3823
+ } catch {
3824
+ streak = 0;
3825
+ }
3826
+ const waitMs = Math.min(8e3, 1e3 * 2 ** (attempt - 1));
3827
+ await new Promise((resolve19) => setTimeout(resolve19, waitMs));
3828
+ }
3829
+ return false;
3830
+ }
3831
+ async function waitForEdgePropagation(url, timeoutMs) {
3832
+ const deadline = Date.now() + timeoutMs;
3833
+ let attempt = 0;
3834
+ let streak = 0;
3835
+ while (Date.now() < deadline) {
3836
+ attempt++;
3837
+ try {
3838
+ const response = await fetch(url, { redirect: "manual" });
3839
+ const body = await response.text();
3840
+ if (!NOT_SERVING.some((marker) => body.includes(marker))) {
3841
+ streak++;
3842
+ if (streak >= 3) return true;
3843
+ await new Promise((resolve19) => setTimeout(resolve19, 1500));
3844
+ continue;
3845
+ }
3846
+ streak = 0;
3847
+ } catch {
3848
+ streak = 0;
3849
+ }
3850
+ const waitMs = Math.min(8e3, 1e3 * 2 ** (attempt - 1));
3851
+ await new Promise((resolve19) => setTimeout(resolve19, waitMs));
3852
+ }
3853
+ return false;
3854
+ }
3855
+
3856
+ // src/shared/app-files.ts
3857
+ var MAX_APP_FILE_BYTES = 1024 * 1024 * 1024;
3858
+ var MAX_UPLOAD_REQUEST_BYTES = 25 * 1024 * 1024;
3859
+ var UPLOAD_PART_BYTES = 20 * 1024 * 1024;
3860
+ var MAX_UPLOAD_PARTS = Math.ceil(MAX_APP_FILE_BYTES / UPLOAD_PART_BYTES);
3861
+ var MAX_BASE64_UPLOAD_BYTES = Math.floor(MAX_UPLOAD_REQUEST_BYTES * 3 / 4);
3862
+ function planUploadParts(totalBytes, partSize) {
3863
+ const parts = [];
3864
+ for (let start = 0; start < totalBytes; start += partSize) {
3865
+ parts.push({
3866
+ partNumber: parts.length + 1,
3867
+ start,
3868
+ end: Math.min(start + partSize, totalBytes)
3869
+ });
3870
+ }
3871
+ return parts;
3872
+ }
3873
+ var APP_STORAGE_LIMIT_BYTES = {
3874
+ test: 0,
3875
+ free: 128 * 1024 * 1024,
3876
+ starter: 512 * 1024 * 1024,
3877
+ premium: 2 * 1024 * 1024 * 1024,
3878
+ admin: 10 * 1024 * 1024 * 1024
3879
+ };
3880
+ var MAX_DEPLOY_ASSET_FILE_BYTES = 25 * 1024 * 1024;
3881
+ var DANGEROUS_MIME_TYPES = /* @__PURE__ */ new Set([
3882
+ "text/html",
3883
+ "application/xhtml+xml",
3884
+ "image/svg+xml",
3885
+ "application/xml",
3886
+ "text/xml",
3887
+ "application/javascript",
3888
+ "text/javascript",
3889
+ "application/ecmascript",
3890
+ "text/ecmascript"
3891
+ ]);
3892
+ function activeContentMessage(mimeType) {
3893
+ 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.`;
3894
+ }
3895
+ function formatBytes(bytes) {
3896
+ if (bytes >= 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GiB`;
3897
+ if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
3898
+ if (bytes >= 1024) return `${Math.round(bytes / 1024)} KiB`;
3899
+ return `${bytes} B`;
3900
+ }
3901
+ function encodeKeyPath(key) {
3902
+ return key.split("/").map(encodeURIComponent).join("/");
3903
+ }
3904
+ function oversizeMessage(bytes, limit = MAX_APP_FILE_BYTES) {
3905
+ return `That file is ${formatBytes(bytes)}; the limit is ${formatBytes(limit)} per file.`;
3906
+ }
3907
+ function describeFilesFailure(status3, body) {
3908
+ const trimmed = body.trim();
3909
+ if (trimmed.startsWith("{")) {
3910
+ try {
3911
+ const parsed = JSON.parse(trimmed);
3912
+ if (parsed.error) return parsed.error;
3913
+ } catch {
3914
+ }
3915
+ }
3916
+ if (status3 === 413) {
3917
+ 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.`;
3918
+ }
3919
+ if (status3 === 411) return "The upload did not declare its length.";
3920
+ if (status3 === 401 || status3 === 403) return "Not authorized to write these files.";
3921
+ if (status3 === 415) return "That file type is not accepted.";
3922
+ if (status3 >= 500) return `The files service failed (HTTP ${status3}). Retry shortly.`;
3923
+ return `Upload failed (HTTP ${status3}).`;
3924
+ }
3925
+
3728
3926
  // src/cli/commands/deploy/build.ts
3729
3927
  import * as p6 from "@clack/prompts";
3730
3928
  import { execSync as execSync2 } from "child_process";
@@ -4044,7 +4242,14 @@ var RESERVED_RUN_WORKER_FIRST = /* @__PURE__ */ new Set([
4044
4242
  "/_documentation",
4045
4243
  "/_documentation/*",
4046
4244
  "/_documentation-root",
4047
- "/_documentation-root/*"
4245
+ "/_documentation-root/*",
4246
+ // Agent-protocol paths: the worker must see them so they cannot fall through
4247
+ // to the SPA shell (templates/base/src/server/http-routes.ts reserves them).
4248
+ "/llms.txt",
4249
+ "/llms-full.txt",
4250
+ "/.well-known/mcp",
4251
+ "/.well-known/mcp.json",
4252
+ "/.well-known/mcp/*"
4048
4253
  ]);
4049
4254
  async function buildDeployBundle(options) {
4050
4255
  const { appDir, appName, envName, sharedDevVarsCache, output, spinner: spinner2 } = options;
@@ -4178,6 +4383,12 @@ function resolveDeployRunWorkerFirst(config) {
4178
4383
  const routes = extractRunWorkerFirst(config);
4179
4384
  return routes;
4180
4385
  }
4386
+ function oversizedAssetRefusal(assets) {
4387
+ const tooBig = assets.find((asset) => asset.size > MAX_DEPLOY_ASSET_FILE_BYTES);
4388
+ if (!tooBig) return null;
4389
+ return `${tooBig.path} is ${formatBytes(tooBig.size)}; the per-file limit for deploy assets is ${formatBytes(MAX_DEPLOY_ASSET_FILE_BYTES)}. ${LARGE_MEDIA_HOME}`;
4390
+ }
4391
+ 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
4392
  function collectAssets(dir) {
4182
4393
  const assets = [];
4183
4394
  const walk = (currentDir, prefix) => {
@@ -4670,37 +4881,6 @@ function repoApi(deployUrl, token, appId) {
4670
4881
  };
4671
4882
  }
4672
4883
 
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
4884
  // src/cli/lib/vc-remote.ts
4705
4885
  import { existsSync as existsSync17 } from "fs";
4706
4886
  import { tmpdir } from "os";
@@ -4913,10 +5093,11 @@ function pushToSpace(cwd, token, refspec, opts = {}) {
4913
5093
  var OVERSIZED_PUSH_RE = /too large|exceeds|\bLFS\b/i;
4914
5094
  var SERVER_OBJECT_CAP_BYTES = 20 * 1024 * 1024;
4915
5095
  var SERVER_PACK_CAP_BYTES = 32 * 1024 * 1024;
5096
+ var PUSH_CEILINGS = `${formatBytes(SERVER_OBJECT_CAP_BYTES)} per file, ${formatBytes(SERVER_PACK_CAP_BYTES)} per push`;
4916
5097
  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
5098
  function removeFromHistoryAdvice(path) {
4918
5099
  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\`).`;
5100
+ 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
5101
  }
4921
5102
  function fixForObjects(objects) {
4922
5103
  if (objects.length === 0) return `${OVERSIZED_PUSH_FIX} ${removeFromHistoryAdvice(null)}`;
@@ -5402,7 +5583,7 @@ async function deployBuiltBundle(options) {
5402
5583
  );
5403
5584
  }
5404
5585
  if (body.onBehalfOfOwner) {
5405
- p10.log.warn(`Deployed on behalf of owner ${body.onBehalfOfOwner}`);
5586
+ p10.log.warn("This release is attributed to the app's owner, not to your account.");
5406
5587
  }
5407
5588
  if (body.staleBaseGuard === "skipped") {
5408
5589
  p10.log.warn(
@@ -5483,10 +5664,11 @@ async function uploadDeployAssets(options) {
5483
5664
  p10.log.info(`Assets: ${byHash.size} file(s), all already uploaded`);
5484
5665
  return;
5485
5666
  }
5486
- const missingBytes = missing.reduce((total, asset) => total + asset.size, 0);
5667
+ const missingBytes = missing.reduce((total2, asset) => total2 + asset.size, 0);
5487
5668
  p10.log.info(
5488
5669
  `Assets: ${byHash.size} file(s), uploading ${missing.length} (${Math.ceil(missingBytes / 1024)} KiB); the rest are already stored`
5489
5670
  );
5671
+ const total = missing.length;
5490
5672
  let uploaded = 0;
5491
5673
  const next = () => missing.pop();
5492
5674
  const worker = async () => {
@@ -5515,7 +5697,7 @@ async function uploadDeployAssets(options) {
5515
5697
  );
5516
5698
  }
5517
5699
  uploaded++;
5518
- spinner2.message(`Uploading assets \u2014 ${uploaded}/${uploaded + missing.length}...`);
5700
+ spinner2.message(`Uploading assets \u2014 ${uploaded}/${total}...`);
5519
5701
  }
5520
5702
  };
5521
5703
  try {
@@ -5746,7 +5928,11 @@ var DEPLOY_URL3 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
5746
5928
  var deploy_default = defineCommand3({
5747
5929
  meta: {
5748
5930
  name: "deploy",
5749
- description: "Build and deploy your DeepSpace app"
5931
+ // The per-file ceiling belongs in --help: it was previously discoverable
5932
+ // only by hitting it, after the build and the push had already run. The
5933
+ // per-deploy total is deliberately absent — it is env-configurable, so any
5934
+ // number stated here would be wrong for some environment.
5935
+ description: `Build and deploy your DeepSpace app (assets: ${formatBytes(MAX_DEPLOY_ASSET_FILE_BYTES)} per file)`
5750
5936
  },
5751
5937
  args: {
5752
5938
  dir: {
@@ -5842,6 +6028,20 @@ var deploy_default = defineCommand3({
5842
6028
  output
5843
6029
  });
5844
6030
  const sourceState = await getAppSource(DEPLOY_URL3, token, appId);
6031
+ const spinner2 = createSpinner();
6032
+ const bundle = await buildDeployBundle({
6033
+ appDir,
6034
+ appName,
6035
+ envName,
6036
+ sharedDevVarsCache: secretsCache.linked !== null,
6037
+ output,
6038
+ spinner: spinner2
6039
+ });
6040
+ const oversized = oversizedAssetRefusal(bundle.assets);
6041
+ if (oversized) {
6042
+ spinner2.stop("Deploy failed");
6043
+ output.die(oversized, "assets_too_large");
6044
+ }
5845
6045
  const repository = await syncDeployRepository({
5846
6046
  deployUrl: DEPLOY_URL3,
5847
6047
  appDir,
@@ -5852,15 +6052,6 @@ var deploy_default = defineCommand3({
5852
6052
  output,
5853
6053
  sourceState
5854
6054
  });
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
6055
  const secrets = prepareDeploySecrets({
5865
6056
  appDir,
5866
6057
  envName,
@@ -5886,7 +6077,7 @@ var deploy_default = defineCommand3({
5886
6077
  });
5887
6078
  if (body.url) {
5888
6079
  spinner2.message("Waiting for edge propagation...");
5889
- const ready = await waitForAssetsReady(body.url, 9e4);
6080
+ const ready = await waitForLiveRelease(body.url, body.releaseStamp, 9e4);
5890
6081
  if (!ready) {
5891
6082
  spinner2.stop("Deployed (edge propagation still in progress after 90s)");
5892
6083
  p12.log.warn(
@@ -5951,30 +6142,6 @@ function blankSelectorRefusal(args) {
5951
6142
  function staleBaseGuardFields(body) {
5952
6143
  return body.staleBaseGuard === "skipped" ? { staleBaseGuard: "skipped" } : {};
5953
6144
  }
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
6145
  function errorMessage6(error) {
5979
6146
  return error instanceof Error ? error.message : String(error);
5980
6147
  }
@@ -6014,6 +6181,19 @@ function requireAppIdArg(explicit) {
6014
6181
  }
6015
6182
  return id;
6016
6183
  }
6184
+ function liveAppUrl(app2) {
6185
+ return app2.status === "active" && app2.deployedAt !== null ? app2.url : null;
6186
+ }
6187
+ async function listApps(deployUrl, token) {
6188
+ const { apps } = await apiFetch(deployUrl, token, "/api/apps");
6189
+ if (!Array.isArray(apps)) {
6190
+ throw new InputError(
6191
+ `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?).`,
6192
+ "invalid_response"
6193
+ );
6194
+ }
6195
+ return apps;
6196
+ }
6017
6197
  function matchAppSelector(apps, selector) {
6018
6198
  const byId = apps.find((app2) => app2.appId === selector);
6019
6199
  if (byId) return byId.appId;
@@ -6023,14 +6203,7 @@ function matchAppSelector(apps, selector) {
6023
6203
  }
6024
6204
  async function resolveAppSelector(deployUrl, token, selector) {
6025
6205
  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);
6206
+ const id = matchAppSelector(await listApps(deployUrl, token), selector);
6034
6207
  if (id) return id;
6035
6208
  const shared = await resolveAppIdByName(deployUrl, token, selector);
6036
6209
  if (shared) return shared;
@@ -6113,7 +6286,7 @@ function assertAppTargetResolvable(appArg, options = {}) {
6113
6286
  async function warnIfPhantomApp(deployUrl, token, appId, explicit) {
6114
6287
  if (!explicit || !STRICT_APP_ID_RE.test(explicit.trim())) return;
6115
6288
  try {
6116
- const { apps } = await apiFetch(deployUrl, token, "/api/apps");
6289
+ const apps = await listApps(deployUrl, token);
6117
6290
  if (!apps.some((app2) => app2.appId === appId)) {
6118
6291
  process.stderr.write(
6119
6292
  `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 +6400,6 @@ var whoami_default = defineDeepspaceCommand({
6227
6400
  if (!args.json) {
6228
6401
  console.log(`Name: ${payload.name ?? "(none)"}`);
6229
6402
  console.log(`Email: ${payload.email ?? "(none)"}`);
6230
- console.log(`UserID: ${payload.sub ?? "(none)"}`);
6231
6403
  console.log(`Type: ${accountType}`);
6232
6404
  if (payload.iss) console.log(`Issuer: ${payload.iss}`);
6233
6405
  console.log(`Dashboard: ${DASHBOARD_URL}`);
@@ -6259,7 +6431,7 @@ var apps_default = defineDeepspaceCommand({
6259
6431
  },
6260
6432
  async run({ args }) {
6261
6433
  const token = await ensureToken();
6262
- const { apps } = await apiFetch(DEPLOY_URL5, token, "/api/apps");
6434
+ const apps = await listApps(DEPLOY_URL5, token);
6263
6435
  if (!args.json) {
6264
6436
  if (!apps.length) {
6265
6437
  console.log("No apps yet. Create one with `npx create-deepspace <name>` and `deepspace deploy`.");
@@ -6267,16 +6439,18 @@ var apps_default = defineDeepspaceCommand({
6267
6439
  const rows = [...apps].sort(
6268
6440
  (left, right) => Number(right.status !== "undeployed") - Number(left.status !== "undeployed")
6269
6441
  );
6442
+ const roleOf = (a) => a.role ?? "owner";
6443
+ const roleWidth = Math.max(4, ...rows.map((a) => roleOf(a).length));
6270
6444
  const nameWidth = Math.max(4, ...rows.map((a) => (a.name ?? "\u2014").length));
6271
6445
  const statusWidth = Math.max(6, ...rows.map((a) => a.status.length));
6272
6446
  const idWidth = Math.max(6, ...rows.map((a) => a.appId.length));
6273
6447
  console.log(
6274
- `${"NAME".padEnd(nameWidth)} ${"STATUS".padEnd(statusWidth)} ${"APP ID".padEnd(idWidth)} URL`
6448
+ `${"NAME".padEnd(nameWidth)} ${"STATUS".padEnd(statusWidth)} ${"ROLE".padEnd(roleWidth)} ${"APP ID".padEnd(idWidth)} URL`
6275
6449
  );
6276
6450
  for (const a of rows) {
6277
- const url = a.url ?? "(not deployed)";
6451
+ const url = liveAppUrl(a) ?? "(not deployed)";
6278
6452
  console.log(
6279
- `${(a.name ?? "\u2014").padEnd(nameWidth)} ${a.status.padEnd(statusWidth)} ${a.appId.padEnd(idWidth)} ${url}`
6453
+ `${(a.name ?? "\u2014").padEnd(nameWidth)} ${a.status.padEnd(statusWidth)} ${roleOf(a).padEnd(roleWidth)} ${a.appId.padEnd(idWidth)} ${url}`
6280
6454
  );
6281
6455
  }
6282
6456
  }
@@ -7989,11 +8163,16 @@ var CONTENT_TYPES = {
7989
8163
  ".css": "text/css",
7990
8164
  ".csv": "text/csv",
7991
8165
  ".gif": "image/gif",
8166
+ ".htm": "text/html",
8167
+ ".html": "text/html",
7992
8168
  ".ico": "image/x-icon",
7993
8169
  ".jpeg": "image/jpeg",
7994
8170
  ".jpg": "image/jpeg",
8171
+ ".js": "text/javascript",
7995
8172
  ".json": "application/json",
7996
8173
  ".md": "text/markdown",
8174
+ ".mjs": "text/javascript",
8175
+ ".svg": "image/svg+xml",
7997
8176
  ".mp3": "audio/mpeg",
7998
8177
  ".mp4": "video/mp4",
7999
8178
  ".otf": "font/otf",
@@ -8039,7 +8218,10 @@ async function request(baseUrl, token, path, init = {}) {
8039
8218
  throw new ApiError(
8040
8219
  describeFilesFailure(res.status, text2),
8041
8220
  res.status,
8042
- code ?? (res.status === 413 ? "file_too_large" : res.status >= 500 ? "server_error" : "http_error"),
8221
+ // Same slug the worker emits for the same condition — the edge cutting an
8222
+ // oversized request short is not a different failure from the worker
8223
+ // refusing it, and two codes made it look like one.
8224
+ code ?? (res.status === 413 ? "too_large" : res.status >= 500 ? "server_error" : "http_error"),
8043
8225
  path
8044
8226
  );
8045
8227
  }
@@ -8072,31 +8254,96 @@ Content-Type: ${contentType}\r
8072
8254
  contentType: `multipart/form-data; boundary=${boundary}`
8073
8255
  };
8074
8256
  }
8075
- async function uploadAppFile(baseUrl, token, appId, localPath, key) {
8257
+ async function uploadAppFile(baseUrl, token, appId, localPath, key, onProgress) {
8076
8258
  const path = filesPath(appId, `/upload?key=${encodeKeyPath(key)}`);
8077
8259
  const size = statSync(localPath).size;
8078
8260
  if (size > MAX_APP_FILE_BYTES) {
8079
- throw new ApiError(oversizeMessage(size), 0, "file_too_large", path);
8261
+ throw new ApiError(oversizeMessage(size), 0, "too_large", path);
8080
8262
  }
8081
- const { blob, contentType } = await multipartBody(
8082
- localPath,
8083
- basename3(localPath),
8084
- contentTypeFor(localPath)
8085
- );
8263
+ const declaredType = contentTypeFor(localPath);
8264
+ if (DANGEROUS_MIME_TYPES.has(declaredType)) {
8265
+ throw new ApiError(activeContentMessage(declaredType), 0, "active_content", path);
8266
+ }
8267
+ if (size > UPLOAD_PART_BYTES) {
8268
+ return uploadInParts(baseUrl, token, appId, localPath, key, size, onProgress);
8269
+ }
8270
+ const { blob, contentType } = await multipartBody(localPath, basename3(localPath), declaredType);
8086
8271
  const res = await request(baseUrl, token, path, {
8087
8272
  method: "POST",
8088
8273
  headers: { "Content-Type": contentType },
8089
8274
  body: blob
8090
8275
  });
8091
8276
  const body = await readJson(res, path);
8092
- return { key: body.key, name: body.name, size };
8277
+ onProgress?.(size, size, 1, 1);
8278
+ return { key: body.key, name: body.name, size, parts: 1 };
8279
+ }
8280
+ function isRetryable(err) {
8281
+ if (!(err instanceof ApiError)) return false;
8282
+ return err.status === 0 || err.status === 429 || err.status >= 500;
8283
+ }
8284
+ async function uploadInParts(baseUrl, token, appId, localPath, key, size, onProgress) {
8285
+ const contentType = contentTypeFor(localPath);
8286
+ const initPath = filesPath(appId, `/multipart?key=${encodeKeyPath(key)}`);
8287
+ const session = await readJson(
8288
+ await request(baseUrl, token, initPath, {
8289
+ method: "POST",
8290
+ headers: { "Content-Type": "application/json" },
8291
+ body: JSON.stringify({ name: basename3(localPath), mimeType: contentType, size })
8292
+ }),
8293
+ initPath
8294
+ );
8295
+ const query = `uploadKey=${encodeKeyPath(session.uploadKey)}&uploadId=${encodeURIComponent(session.uploadId)}`;
8296
+ if (!Number.isSafeInteger(session.partSize) || session.partSize < 1) {
8297
+ throw new ApiError(
8298
+ `The platform advertised an unusable part size (${session.partSize}).`,
8299
+ 0,
8300
+ "invalid_response",
8301
+ initPath
8302
+ );
8303
+ }
8304
+ const plan = planUploadParts(size, session.partSize);
8305
+ const file = await openAsBlob(localPath, { type: contentType });
8306
+ const uploaded = [];
8307
+ try {
8308
+ for (const part of plan) {
8309
+ const partPath = filesPath(appId, `/multipart/part?${query}&partNumber=${part.partNumber}`);
8310
+ const send = () => request(baseUrl, token, partPath, {
8311
+ method: "PUT",
8312
+ body: file.slice(part.start, part.end, contentType)
8313
+ });
8314
+ const res = await send().catch(async (err) => {
8315
+ if (!isRetryable(err)) throw err;
8316
+ return send();
8317
+ });
8318
+ uploaded.push(await readJson(res, partPath));
8319
+ onProgress?.(part.end, size, part.partNumber, plan.length);
8320
+ }
8321
+ const completePath = filesPath(appId, `/multipart/complete?${query}`);
8322
+ const body = await readJson(
8323
+ await request(baseUrl, token, completePath, {
8324
+ method: "POST",
8325
+ headers: { "Content-Type": "application/json" },
8326
+ body: JSON.stringify({ parts: uploaded })
8327
+ }),
8328
+ completePath
8329
+ );
8330
+ return { key: body.key, name: body.name, size, parts: plan.length };
8331
+ } catch (err) {
8332
+ await request(baseUrl, token, filesPath(appId, `/multipart?${query}`), {
8333
+ method: "DELETE"
8334
+ }).catch(() => void 0);
8335
+ throw err;
8336
+ }
8093
8337
  }
8094
8338
  async function listAppFiles(baseUrl, token, appId, query) {
8095
8339
  const path = filesPath(appId, `?${query}`);
8096
8340
  return readJson(await request(baseUrl, token, path), path);
8097
8341
  }
8098
8342
  async function deleteAppFile(baseUrl, token, appId, key) {
8099
- await request(baseUrl, token, filesPath(appId, `/${encodeKeyPath(key)}`), { method: "DELETE" });
8343
+ const path = filesPath(appId, `/${encodeKeyPath(key)}`);
8344
+ const res = await request(baseUrl, token, path, { method: "DELETE" });
8345
+ const body = await readJson(res, path);
8346
+ return { existed: body.existed !== false };
8100
8347
  }
8101
8348
  async function downloadAppFile(baseUrl, token, appId, key, destination) {
8102
8349
  const path = filesPath(appId, `/${encodeKeyPath(key)}`);
@@ -8156,7 +8403,10 @@ function servedPath(key) {
8156
8403
  return `/api/files/${encodeKeyPath(key)}?scope=app`;
8157
8404
  }
8158
8405
  var put = defineDeepspaceCommand({
8159
- meta: { name: "put", description: "Upload a file to the app\u2019s files" },
8406
+ meta: {
8407
+ name: "put",
8408
+ description: `Upload a file to the app\u2019s files (${formatBytes(MAX_APP_FILE_BYTES)} per file)`
8409
+ },
8160
8410
  args: {
8161
8411
  file: { type: "positional", description: "Local file to upload", required: true },
8162
8412
  key: { type: "string", description: "Key to store it under (default: the file name)" },
@@ -8169,12 +8419,42 @@ var put = defineDeepspaceCommand({
8169
8419
  }
8170
8420
  const key = requireKey(args.key ?? basename4(localPath));
8171
8421
  const { token, appId } = await target(args.app);
8172
- const result = await uploadAppFile(PLATFORM_URL, token, appId, localPath, key);
8422
+ const spinner2 = args.json ? null : createSpinner();
8423
+ let started = false;
8424
+ const result = await uploadAppFile(
8425
+ PLATFORM_URL,
8426
+ token,
8427
+ appId,
8428
+ localPath,
8429
+ key,
8430
+ (sent, total, part, parts) => {
8431
+ if (parts === 1 || !spinner2) return;
8432
+ if (!started) {
8433
+ spinner2.start(`Uploading ${key} in ${parts} parts`);
8434
+ started = true;
8435
+ }
8436
+ spinner2.message(
8437
+ `${key} \u2014 part ${part}/${parts} (${formatBytes(sent)} of ${formatBytes(total)})`
8438
+ );
8439
+ }
8440
+ ).finally(() => {
8441
+ if (started) spinner2?.stop();
8442
+ });
8173
8443
  if (!args.json) {
8174
- console.log(`\u2713 ${key} (${formatBytes(result.size)})`);
8444
+ const how = result.parts > 1 ? ` in ${result.parts} parts` : "";
8445
+ console.log(`\u2713 ${key} (${formatBytes(result.size)})${how}`);
8175
8446
  console.log(` Served from your app at ${servedPath(result.key)}`);
8176
8447
  }
8177
- return { data: { appId, key, storedKey: result.key, size: result.size, path: servedPath(result.key) } };
8448
+ return {
8449
+ data: {
8450
+ appId,
8451
+ key,
8452
+ storedKey: result.key,
8453
+ size: result.size,
8454
+ parts: result.parts,
8455
+ path: servedPath(result.key)
8456
+ }
8457
+ };
8178
8458
  }
8179
8459
  });
8180
8460
  var list3 = defineDeepspaceCommand({
@@ -8215,8 +8495,14 @@ var list3 = defineDeepspaceCommand({
8215
8495
  const more = limit < MAX_LIST_LIMIT ? "raise --limit or narrow with --prefix" : "narrow with --prefix";
8216
8496
  if (result.truncated) console.log(`(truncated at ${limit} \u2014 ${more})`);
8217
8497
  }
8498
+ if (result.storage) {
8499
+ const { usedBytes, limitBytes } = result.storage;
8500
+ console.log(
8501
+ limitBytes === void 0 ? `Storage: ${formatBytes(usedBytes)} used (limit unavailable right now)` : `Storage: ${formatBytes(usedBytes)} of ${formatBytes(limitBytes)} used`
8502
+ );
8503
+ }
8218
8504
  }
8219
- return { data: { appId, files, truncated: result.truncated } };
8505
+ return { data: { appId, files, truncated: result.truncated, storage: result.storage ?? null } };
8220
8506
  }
8221
8507
  });
8222
8508
  var get = defineDeepspaceCommand({
@@ -8244,7 +8530,13 @@ var rm = defineDeepspaceCommand({
8244
8530
  async run({ args }) {
8245
8531
  const key = requireKey(String(args.key));
8246
8532
  const { token, appId } = await target(args.app);
8247
- await deleteAppFile(PLATFORM_URL, token, appId, key);
8533
+ const { existed } = await deleteAppFile(PLATFORM_URL, token, appId, key);
8534
+ if (!existed) {
8535
+ throw new Refusal(
8536
+ `No file at ${key} \u2014 nothing was deleted. Check the key with \`deepspace app files list\`.`,
8537
+ "file_not_found"
8538
+ );
8539
+ }
8248
8540
  if (!args.json) console.log(`\u2713 deleted ${key}`);
8249
8541
  return { data: { appId, key, deleted: true } };
8250
8542
  }
@@ -8338,7 +8630,7 @@ var add = defineDeepspaceCommand({
8338
8630
  }
8339
8631
  if (!args.json) {
8340
8632
  console.log(
8341
- `\u2713 Invite sent to ${res.email} (expires ${expires}). They become a collaborator when they sign in.`
8633
+ `\u2713 Invite sent to ${res.email} (expires ${expires}). They accept it from the emailed link, or by signing in and running \`deepspace app collaborators accept ${app2}\`.`
8342
8634
  );
8343
8635
  }
8344
8636
  return { data: { ...res } };
@@ -8407,9 +8699,56 @@ var cancel3 = defineDeepspaceCommand({
8407
8699
  return { data: { cancelled: found } };
8408
8700
  }
8409
8701
  });
8702
+ var invites = defineDeepspaceCommand({
8703
+ meta: { name: "invites", description: "List invites waiting for you" },
8704
+ async run({ args }) {
8705
+ const token = await ensureToken();
8706
+ const { invites: invites2 } = await api2(
8707
+ token,
8708
+ "/api/app-collaborators/invites/mine"
8709
+ );
8710
+ if (!args.json) {
8711
+ if (!invites2.length) {
8712
+ console.log("No pending invites for your email.");
8713
+ } else {
8714
+ for (const invite of invites2) {
8715
+ console.log(
8716
+ `${invite.appId} invited by ${invite.invitedByName} (expires ${new Date(invite.expiresAt).toLocaleDateString()})`
8717
+ );
8718
+ }
8719
+ console.log("Accept one with `deepspace app collaborators accept <app-id>`.");
8720
+ }
8721
+ }
8722
+ return { data: { invites: invites2 } };
8723
+ }
8724
+ });
8725
+ var accept = defineDeepspaceCommand({
8726
+ meta: { name: "accept", description: "Accept an invite to collaborate on an app" },
8727
+ args: {
8728
+ app: { type: "positional", description: "App id (see `collaborators invites`)", required: true }
8729
+ },
8730
+ async run({ args }) {
8731
+ const appId = String(args.app);
8732
+ const token = await ensureToken();
8733
+ const res = await api2(
8734
+ token,
8735
+ `/api/app-collaborators/invites/${encodeURIComponent(appId)}/accept`,
8736
+ { method: "POST" }
8737
+ );
8738
+ if (!args.json) {
8739
+ console.log(
8740
+ res.alreadyMember ? `\u2022 You already have access to ${res.appId}.` : `\u2713 You can now deploy ${res.appId}.`
8741
+ );
8742
+ }
8743
+ return {
8744
+ data: { ...res },
8745
+ action: cliAction("deepspace", "pull", "--app", res.appId)
8746
+ };
8747
+ }
8748
+ });
8410
8749
  var collaborators_default = defineCommand7({
8411
8750
  meta: { name: "collaborators", description: "Manage who can deploy your app" },
8412
- subCommands: { list: list4, add, remove, cancel: cancel3 }
8751
+ subCommands: { list: list4, add, remove, cancel: cancel3, invites, accept }
8413
8752
  });
8414
8753
 
8415
8754
  // src/cli/commands/transfer.ts
@@ -8494,7 +8833,7 @@ var status2 = defineDeepspaceCommand({
8494
8833
  return { data: { app: app2, transfer } };
8495
8834
  }
8496
8835
  });
8497
- var accept = defineDeepspaceCommand({
8836
+ var accept2 = defineDeepspaceCommand({
8498
8837
  meta: { name: "accept", description: "Accept a transfer offered to you" },
8499
8838
  args: {
8500
8839
  app: { type: "string", alias: "a", description: "App id of the offered app", required: true }
@@ -8535,7 +8874,7 @@ var cancel4 = defineDeepspaceCommand({
8535
8874
  });
8536
8875
  var transfer_default = defineCommand8({
8537
8876
  meta: { name: "transfer", description: "Transfer app ownership to another user" },
8538
- subCommands: { offer, status: status2, accept, cancel: cancel4 }
8877
+ subCommands: { offer, status: status2, accept: accept2, cancel: cancel4 }
8539
8878
  });
8540
8879
 
8541
8880
  // src/cli/commands/integrations.ts
@@ -9795,24 +10134,36 @@ var status_default = defineCommand11({
9795
10134
  const failure = statusRemoteFailure(error);
9796
10135
  lines.push([label, failure.human]);
9797
10136
  json[key] = failure.json;
9798
- }, reportRelease2 = function(rel) {
10137
+ }, reportLive2 = function(rel, entry) {
10138
+ if (rel === null) {
10139
+ lines.push(["Live", "never deployed"]);
10140
+ json.live = null;
10141
+ return;
10142
+ }
9799
10143
  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 = {
10144
+ const release2 = `release #${rel.seq} (${rel.kind} of ${rel.commitOid?.slice(0, 10) ?? "unknown source"}, ${timeAgo(rel.createdAt)}${byYou ? ", by you" : ""})`;
10145
+ const details = {
9805
10146
  seq: rel.seq,
9806
10147
  kind: rel.kind,
9807
10148
  commitOid: rel.commitOid,
9808
10149
  createdAt: rel.createdAt,
9809
- url: rel.url,
9810
10150
  byYou,
9811
10151
  source: rel.source,
9812
10152
  sourceRevision: rel.sourceRevision
9813
10153
  };
10154
+ if (!entry) {
10155
+ lines.push(["Live", `${release2} \xB7 serving state not visible from your account`]);
10156
+ json.live = { ...details, serving: null, url: null };
10157
+ return;
10158
+ }
10159
+ const url = liveAppUrl(entry);
10160
+ lines.push([
10161
+ "Live",
10162
+ url ? `${release2} \xB7 ${url}` : `not serving \u2014 ${entry.status}, last ${release2}`
10163
+ ]);
10164
+ json.live = { ...details, serving: url !== null, url };
9814
10165
  };
9815
- var reportWorkspace = reportWorkspace2, reportRemoteFailure = reportRemoteFailure2, reportRelease = reportRelease2;
10166
+ var reportWorkspace = reportWorkspace2, reportRemoteFailure = reportRemoteFailure2, reportLive = reportLive2;
9816
10167
  const api4 = repoApi(deployBaseUrl(), token, appId);
9817
10168
  const sourceResult = await observeRemote(getAppSource(deployBaseUrl(), token, appId));
9818
10169
  const source = sourceResult.ok ? sourceResult.value.source : null;
@@ -9832,10 +10183,11 @@ var status_default = defineCommand11({
9832
10183
  lines.push(["Source", "unclaimed \xB7 next normal deploy defaults to DeepSpace"]);
9833
10184
  json.source = null;
9834
10185
  }
9835
- const [refsResult, wsResult, releaseResult] = await Promise.all([
10186
+ const [refsResult, wsResult, releaseResult, appsResult] = await Promise.all([
9836
10187
  githubSource ? Promise.resolve({ ok: true, value: null }) : observeRemote(api4.getRefs()),
9837
10188
  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))
10189
+ observeRemote(api4.latestRelease().then((r) => r.release)),
10190
+ observeRemote(listApps(deployBaseUrl(), token))
9839
10191
  ]);
9840
10192
  if (facts.workspaceId) {
9841
10193
  if (githubSource) {
@@ -9864,12 +10216,8 @@ var status_default = defineCommand11({
9864
10216
  }
9865
10217
  }
9866
10218
  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
- }
10219
+ else if (!appsResult.ok) reportRemoteFailure2("Live", "live", appsResult.error);
10220
+ else reportLive2(releaseResult.value, appsResult.value.find((a) => a.appId === appId));
9873
10221
  }
9874
10222
  spinner2?.stop("Cloud state checked.");
9875
10223
  }
@@ -9912,7 +10260,7 @@ function workspaceBranchPushRefusal(branch) {
9912
10260
  var push_default = defineDeepspaceCommand({
9913
10261
  meta: {
9914
10262
  name: "push",
9915
- description: "Push local git commits to the app's cloud repo"
10263
+ description: `Push local git commits to the app's cloud repo (${PUSH_CEILINGS})`
9916
10264
  },
9917
10265
  args: {
9918
10266
  branch: {
@@ -10493,6 +10841,27 @@ var clone_default = defineDeepspaceCommand({
10493
10841
 
10494
10842
  // src/cli/commands/releases.ts
10495
10843
  import * as p21 from "@clack/prompts";
10844
+
10845
+ // src/cli/lib/actor-labels.ts
10846
+ var API_URL9 = process.env.DEEPSPACE_API_URL ?? PLATFORM_URLS.api;
10847
+ async function actorLabels(token, appId) {
10848
+ const labels = /* @__PURE__ */ new Map();
10849
+ try {
10850
+ const { sub, email } = decodeJwtPayload(token);
10851
+ if (sub && email) labels.set(sub, `${email} (you)`);
10852
+ } catch {
10853
+ }
10854
+ try {
10855
+ const { collaborators } = await apiFetch(API_URL9, token, `/api/app-collaborators/${encodeURIComponent(appId)}`);
10856
+ for (const c of collaborators) {
10857
+ if (!labels.has(c.userId)) labels.set(c.userId, c.emailDisplay);
10858
+ }
10859
+ } catch {
10860
+ }
10861
+ return labels;
10862
+ }
10863
+
10864
+ // src/cli/commands/releases.ts
10496
10865
  var DEPLOY_URL12 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
10497
10866
  var releases_default = defineDeepspaceCommand({
10498
10867
  meta: {
@@ -10535,11 +10904,12 @@ var releases_default = defineDeepspaceCommand({
10535
10904
  if (releases.length === 0) {
10536
10905
  p21.log.info("No releases yet \u2014 the history starts with the next `deepspace deploy`.");
10537
10906
  } else {
10907
+ const actors = await actorLabels(token, appId);
10538
10908
  for (const r of releases) {
10539
10909
  const source = r.commitOid ? `commit ${r.commitOid.slice(0, 10)}` : "(no source recorded)";
10540
10910
  const rollback = r.rollbackAvailable ? "rollback available" : "rollback unavailable";
10541
10911
  console.log(
10542
- `#${r.seq} ${r.id} ${r.kind.padEnd(8)} ${r.createdAt} ${r.actor} ${source} ${rollback}`
10912
+ `#${r.seq} ${r.id} ${r.kind.padEnd(8)} ${r.createdAt} ${actors.get(r.actor) ?? r.actor} ${source} ${rollback}`
10543
10913
  );
10544
10914
  }
10545
10915
  p21.log.info(
@@ -10662,10 +11032,20 @@ Re-run with --allow-do-deletion to accept the data loss.`,
10662
11032
  "invalid_response"
10663
11033
  );
10664
11034
  }
10665
- spinner2?.stop(`Rolled back to ${result.rolledBackTo ?? releaseId}.`);
11035
+ let edgePropagating = false;
11036
+ if (result.url) {
11037
+ spinner2?.message("Waiting for edge propagation...");
11038
+ edgePropagating = !await waitForLiveRelease(result.url, result.releaseStamp, 9e4);
11039
+ }
11040
+ spinner2?.stop(
11041
+ edgePropagating ? `Rolled back to ${result.rolledBackTo ?? releaseId} (edge propagation still in progress after 90s).` : `Rolled back to ${result.rolledBackTo ?? releaseId}.`
11042
+ );
10666
11043
  const doClassGuard = result.doClassGuard === "unverified" ? "unverified" : void 0;
10667
11044
  if (!args.json) {
10668
- if (result.url) p22.log.success(`Live at: ${result.url}`);
11045
+ if (result.url) {
11046
+ if (edgePropagating) p22.log.warn(`URL: ${result.url} (accepted; serving not yet verified)`);
11047
+ else p22.log.success(`Live at: ${result.url}`);
11048
+ }
10669
11049
  if (result.bundleRetained === false) {
10670
11050
  p22.log.warn(
10671
11051
  "The rollback is live, but its bundle was concurrently retired and is not available for a future rollback."
@@ -10689,6 +11069,7 @@ Re-run with --allow-do-deletion to accept the data loss.`,
10689
11069
  url: result.url ?? null,
10690
11070
  versionId: result.versionId ?? null,
10691
11071
  wranglerEnv: wranglerEnv ?? null,
11072
+ ...edgePropagating ? { edgePropagating: true } : {},
10692
11073
  ...doClassGuard ? { doClassGuard } : {}
10693
11074
  }
10694
11075
  };
@@ -11573,27 +11954,6 @@ var syncWorkspaceCommand = defineDeepspaceCommand({
11573
11954
 
11574
11955
  // src/cli/commands/workspace/list.ts
11575
11956
  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
11957
  function withWorkspaceOverlaps(views, overlapsById) {
11598
11958
  return views.map((view) => ({
11599
11959
  ...view,
@@ -13363,7 +13723,7 @@ function commandTree() {
13363
13723
  return main.subCommands ?? {};
13364
13724
  }
13365
13725
  function unknownCommandMessage(unknown) {
13366
- return `Unknown command: ${unknown.attemptedPath.join(" ")} \u2014 did you mean \`${unknown.suggestion.join(" ")}\`?`;
13726
+ 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
13727
  }
13368
13728
  function assertKnownCommandPath(argv) {
13369
13729
  const unknown = findUnknownCommand(argv, commandTree());
@@ -13372,7 +13732,7 @@ function assertKnownCommandPath(argv) {
13372
13732
  `${unknownCommandMessage(unknown)}
13373
13733
  Run \`${unknown.helpPath.join(" ")} --help\` for the command list.`
13374
13734
  );
13375
- printAction({ cwd: process.cwd(), argv: unknown.suggestion });
13735
+ if (unknown.executable) printAction({ cwd: process.cwd(), argv: unknown.suggestion });
13376
13736
  process.exit(1);
13377
13737
  }
13378
13738
  var rawArgs = process.argv.slice(2);
@@ -13394,7 +13754,7 @@ if (rawArgs.includes("--json") && !wantsHelp) {
13394
13754
  ok: false,
13395
13755
  code: "unknown_command",
13396
13756
  error: unknownCommandMessage(unknown),
13397
- action: { cwd: process.cwd(), argv: unknown.suggestion }
13757
+ ...unknown.executable ? { action: { cwd: process.cwd(), argv: unknown.suggestion } } : {}
13398
13758
  })
13399
13759
  );
13400
13760
  process.exit(1);
@@ -13403,16 +13763,20 @@ if (rawArgs.includes("--json") && !wantsHelp) {
13403
13763
  stopActiveSpinner();
13404
13764
  const cittyCode = err instanceof Error && err.name === "CLIError" ? err.code : void 0;
13405
13765
  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
- );
13766
+ const message = stripAnsi(err instanceof Error ? err.message : String(err));
13411
13767
  console.log(JSON.stringify({ ok: false, error: message, ...code ? { code } : {} }));
13412
13768
  process.exit(1);
13413
13769
  });
13414
13770
  } else {
13415
13771
  assertKnownCommandPath(rawArgs);
13416
- runMain(wrapCommandErrors(main));
13772
+ runMain(wrapCommandErrors(main), {
13773
+ showUsage: async (cmd, parent) => {
13774
+ try {
13775
+ console.log(forTerminal(await renderUsage(cmd, parent)) + "\n");
13776
+ } catch (error) {
13777
+ console.error(error);
13778
+ }
13779
+ }
13780
+ });
13417
13781
  }
13418
13782
  //# sourceMappingURL=cli.js.map