deepspace 0.20.0 → 0.22.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/{chunk-A7UFIOWO.js → chunk-7TDBFEB7.js} +7 -2
  3. package/dist/chunk-7TDBFEB7.js.map +1 -0
  4. package/dist/cli.js +288 -120
  5. package/dist/cli.js.map +1 -1
  6. package/dist/documentation-client-core.d.ts +3 -2
  7. package/dist/documentation-client-core.js +1 -1
  8. package/dist/documentation-default-renderer.cjs +48 -48
  9. package/dist/documentation-react.d.ts +2 -1
  10. package/dist/documentation-react.js +1 -1
  11. package/dist/documentation-runtime.js +7 -7
  12. package/dist/documentation-server-core.d.ts +3 -2
  13. package/dist/documentation-server-core.js +1 -1
  14. package/dist/documentation.d.ts +40 -12
  15. package/dist/documentation.js +40 -7
  16. package/dist/documentation.js.map +1 -1
  17. package/dist/index.d.ts +225 -21
  18. package/dist/index.js +239 -100
  19. package/dist/index.js.map +1 -1
  20. package/dist/{public-CFAj1hL2.d.ts → public-DenQ1TsZ.d.ts} +36 -11
  21. package/dist/server.d.ts +32 -4
  22. package/dist/server.js +177 -43
  23. package/dist/server.js.map +1 -1
  24. package/dist/testing-mcp.d.ts +69 -0
  25. package/dist/testing-mcp.js +99 -0
  26. package/dist/testing-mcp.js.map +1 -0
  27. package/dist/testing.d.ts +17 -20
  28. package/dist/testing.js +47 -25
  29. package/dist/testing.js.map +1 -1
  30. package/dist/worker.d.ts +57 -5
  31. package/dist/worker.js +262 -62
  32. package/dist/worker.js.map +1 -1
  33. package/features/ai-chat/feature.json +2 -2
  34. package/features/ai-chat/src/AiChatPage.tsx +56 -21
  35. package/features/ai-chat/src/ChatPanel.tsx +33 -10
  36. package/features/ai-chat/src/__tests__/ChatPanel.lifecycle.test.tsx +156 -0
  37. package/features/cron/src/CronLogPage.tsx +107 -69
  38. package/features/file-attachments/feature.json +2 -1
  39. package/features/file-manager/feature.json +2 -1
  40. package/features/file-manager/src/FileManagerPage.tsx +227 -194
  41. package/features/integration-test/src/IntegrationTestPage.tsx +54 -34
  42. package/package.json +5 -1
  43. package/dist/chunk-A7UFIOWO.js.map +0 -1
package/dist/cli.js CHANGED
@@ -8,8 +8,8 @@ import {
8
8
  renderUsage
9
9
  } from "citty";
10
10
  import { sync as spawnSync8 } from "cross-spawn";
11
- import { readFileSync as readFileSync28, existsSync as existsSync26 } from "fs";
12
- import { dirname as dirname12, join as join24 } from "path";
11
+ import { readFileSync as readFileSync28, existsSync as existsSync27 } from "fs";
12
+ import { dirname as dirname12, join as join25 } from "path";
13
13
  import { fileURLToPath as fileURLToPath3 } from "url";
14
14
 
15
15
  // src/cli/lib/spinner.ts
@@ -530,7 +530,7 @@ import { readFileSync as readFileSync2, existsSync, mkdirSync } from "fs";
530
530
  import { join } from "path";
531
531
  import { homedir } from "os";
532
532
 
533
- // src/cli/jwt.ts
533
+ // src/shared/jwt.ts
534
534
  function decodeJwtPayload(token) {
535
535
  const payloadSegment = token.split(".")[1];
536
536
  if (!payloadSegment) {
@@ -913,8 +913,6 @@ import { parse as parseToml } from "smol-toml";
913
913
  // src/server/utils/registry-client.ts
914
914
  var APP_ID_RE = /^app_[0-9A-HJKMNP-TV-Z]{26}$/;
915
915
  var STRICT_APP_ID_RE = APP_ID_RE;
916
-
917
- // src/cli/lib/app-identity.ts
918
916
  var CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
919
917
  function mintUlid(now = Date.now()) {
920
918
  let ts = "";
@@ -938,9 +936,8 @@ function mintUlid(now = Date.now()) {
938
936
  }
939
937
  return `${ts}${rs}`.slice(0, 26);
940
938
  }
941
- function mintAppId(now = Date.now()) {
942
- return `app_${mintUlid(now)}`;
943
- }
939
+
940
+ // src/cli/lib/app-identity.ts
944
941
  function readWranglerIdentityConfig(cwd) {
945
942
  const wranglerPath = join2(resolve2(cwd), "wrangler.toml");
946
943
  if (!existsSync3(wranglerPath)) return null;
@@ -996,6 +993,18 @@ ${line}
996
993
  }
997
994
  writeFileSync2(wranglerPath, src);
998
995
  }
996
+ var APP_ID_PLACEHOLDER = "__APP_ID__";
997
+ function stampAppIdInSources(cwd, appId, previousAppId) {
998
+ const constantsPath = join2(resolve2(cwd), "src", "constants.ts");
999
+ if (!existsSync3(constantsPath)) return false;
1000
+ const needles = [APP_ID_PLACEHOLDER, ...previousAppId ? [previousAppId] : []];
1001
+ const content = readFileSync5(constantsPath, "utf-8");
1002
+ let next = content;
1003
+ for (const needle of needles) next = next.replaceAll(needle, appId);
1004
+ if (next === content) return false;
1005
+ writeFileSync2(constantsPath, next);
1006
+ return true;
1007
+ }
999
1008
 
1000
1009
  // src/cli/commands/dev.ts
1001
1010
  import { resolve as resolve8, basename as basename2, join as join11 } from "path";
@@ -2677,11 +2686,15 @@ Free it with \`${killCmd}\`, or start on another port: \`deepspace dev start --p
2677
2686
  }
2678
2687
  const url = `http://${host === DEFAULT_DEV_HOST ? "localhost" : host}:${port}`;
2679
2688
  say("Starting dev server...\n");
2680
- const vite = spawn2("npx", ["vite", "--port", String(port), "--strictPort", "--host", host], {
2681
- cwd: appDir,
2682
- stdio: "inherit",
2683
- env: wranglerViteEnv(process.env, wranglerConfig, { DEEPSPACE_PORT: String(port) })
2684
- });
2689
+ const vite = spawn2(
2690
+ "npx",
2691
+ ["vite", "--port", String(port), "--strictPort", "--host", host, "--clearScreen", "false"],
2692
+ {
2693
+ cwd: appDir,
2694
+ stdio: "inherit",
2695
+ env: wranglerViteEnv(process.env, wranglerConfig, { DEEPSPACE_PORT: String(port) })
2696
+ }
2697
+ );
2685
2698
  const readiness = new AbortController();
2686
2699
  if (args.json) {
2687
2700
  void waitForPortListening(port, host, READINESS_TIMEOUT_MS, readiness.signal).then((ready) => {
@@ -3195,7 +3208,7 @@ async function syncTestAccountStore() {
3195
3208
  // src/cli/commands/test.ts
3196
3209
  var DEPLOY_URL2 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
3197
3210
  var PLAYWRIGHT_OUTPUT_DIR = ".deepspace/test-results";
3198
- function playwrightTestArgs(testFiles) {
3211
+ function playwrightTestArgs(testFiles, flags = {}) {
3199
3212
  return [
3200
3213
  "playwright",
3201
3214
  "test",
@@ -3203,6 +3216,9 @@ function playwrightTestArgs(testFiles) {
3203
3216
  "tests/playwright.config.ts",
3204
3217
  "--output",
3205
3218
  PLAYWRIGHT_OUTPUT_DIR,
3219
+ ...flags.grep ? ["--grep", flags.grep] : [],
3220
+ ...flags.project ? ["--project", flags.project] : [],
3221
+ ...flags.headed ? ["--headed"] : [],
3206
3222
  ...testFiles
3207
3223
  ];
3208
3224
  }
@@ -3223,6 +3239,21 @@ var test_default = defineDeepspaceCommand({
3223
3239
  description: `Port for vite/webServer (default ${DEFAULT_PORT}, or $DEEPSPACE_PORT)`,
3224
3240
  required: false
3225
3241
  },
3242
+ grep: {
3243
+ type: "string",
3244
+ description: "Only run Playwright tests matching this pattern (forwarded to --grep)",
3245
+ required: false
3246
+ },
3247
+ project: {
3248
+ type: "string",
3249
+ description: "Playwright project to run (forwarded to --project)",
3250
+ required: false
3251
+ },
3252
+ headed: {
3253
+ type: "boolean",
3254
+ description: "Run Playwright browsers headed (forwarded to --headed)",
3255
+ default: false
3256
+ },
3226
3257
  env: {
3227
3258
  type: "string",
3228
3259
  alias: "e",
@@ -3237,6 +3268,11 @@ var test_default = defineDeepspaceCommand({
3237
3268
  preflightNodeVersion("test run");
3238
3269
  const suite = args.suite ?? "default";
3239
3270
  const wranglerEnv = typeof args.env === "string" && args.env.trim() ? args.env.trim() : void 0;
3271
+ const forwarded = {
3272
+ grep: typeof args.grep === "string" && args.grep ? args.grep : void 0,
3273
+ project: typeof args.project === "string" && args.project ? args.project : void 0,
3274
+ headed: Boolean(args.headed)
3275
+ };
3240
3276
  const start = resolve9(".");
3241
3277
  const appDir = findAppDir(start);
3242
3278
  if (!appDir) {
@@ -3293,32 +3329,36 @@ var test_default = defineDeepspaceCommand({
3293
3329
  let exitCode = 0;
3294
3330
  switch (suite) {
3295
3331
  case "smoke":
3296
- exitCode = runPlaywright(appDir, ["tests/smoke.spec.ts"], port, wranglerEnv);
3332
+ exitCode = runPlaywright(appDir, ["tests/smoke.spec.ts"], port, wranglerEnv, forwarded);
3297
3333
  break;
3298
3334
  case "api":
3299
- exitCode = runPlaywright(appDir, ["tests/api.spec.ts"], port, wranglerEnv);
3335
+ exitCode = runPlaywright(appDir, ["tests/api.spec.ts"], port, wranglerEnv, forwarded);
3300
3336
  break;
3301
3337
  case "e2e":
3302
- exitCode = runPlaywright(appDir, [], port, wranglerEnv);
3338
+ exitCode = runPlaywright(appDir, [], port, wranglerEnv, forwarded);
3303
3339
  break;
3304
3340
  case "unit":
3341
+ if (forwarded.grep || forwarded.project || forwarded.headed) {
3342
+ console.error("note: --grep/--project/--headed apply to Playwright suites and are ignored by `unit`");
3343
+ }
3305
3344
  exitCode = runVitest(appDir);
3306
3345
  break;
3307
3346
  case "all":
3308
3347
  exitCode = runVitest(appDir);
3309
- if (exitCode === 0) exitCode = runPlaywright(appDir, [], port, wranglerEnv);
3348
+ if (exitCode === 0) exitCode = runPlaywright(appDir, [], port, wranglerEnv, forwarded);
3310
3349
  break;
3311
3350
  case "default":
3312
3351
  exitCode = runPlaywright(
3313
3352
  appDir,
3314
3353
  ["tests/smoke.spec.ts", "tests/api.spec.ts"],
3315
3354
  port,
3316
- wranglerEnv
3355
+ wranglerEnv,
3356
+ forwarded
3317
3357
  );
3318
3358
  break;
3319
3359
  default:
3320
3360
  if (suite.endsWith(".spec.ts")) {
3321
- exitCode = runPlaywright(appDir, [suite], port, wranglerEnv);
3361
+ exitCode = runPlaywright(appDir, [suite], port, wranglerEnv, forwarded);
3322
3362
  } else {
3323
3363
  throw new Refusal(
3324
3364
  `Unknown test suite: ${suite}
@@ -3335,7 +3375,7 @@ Available: smoke, api, e2e, unit, all`,
3335
3375
  return { data: { suite, port, appDir, wranglerEnv: wranglerEnv ?? null } };
3336
3376
  }
3337
3377
  });
3338
- function runPlaywright(appDir, testFiles, port, wranglerEnv) {
3378
+ function runPlaywright(appDir, testFiles, port, wranglerEnv, flags) {
3339
3379
  let wranglerConfig;
3340
3380
  try {
3341
3381
  wranglerConfig = prepareWranglerEnvConfig(appDir, wranglerEnv);
@@ -3344,7 +3384,7 @@ function runPlaywright(appDir, testFiles, port, wranglerEnv) {
3344
3384
  return 1;
3345
3385
  }
3346
3386
  try {
3347
- const result = spawnSync4("npx", playwrightTestArgs(testFiles), {
3387
+ const result = spawnSync4("npx", playwrightTestArgs(testFiles, flags), {
3348
3388
  cwd: appDir,
3349
3389
  stdio: "inherit",
3350
3390
  env: wranglerViteEnv(process.env, wranglerConfig, {
@@ -3453,7 +3493,7 @@ var create = defineDeepspaceCommand({
3453
3493
  await ensureToken();
3454
3494
  const email = args.email;
3455
3495
  const password = args.password;
3456
- const name = args.name;
3496
+ const name = args.name ?? email.split("@")[0];
3457
3497
  const label = args.label;
3458
3498
  let account;
3459
3499
  try {
@@ -3477,6 +3517,7 @@ var create = defineDeepspaceCommand({
3477
3517
  console.log(`Created test account:`);
3478
3518
  console.log(` ID: ${account.id}`);
3479
3519
  console.log(` Email: ${account.email}`);
3520
+ console.log(` Selector: ${name}`);
3480
3521
  console.log(` Password: ${password}`);
3481
3522
  console.log(` UserID: ${account.userId}`);
3482
3523
  if (account.label) console.log(` Label: ${account.label}`);
@@ -3487,6 +3528,7 @@ Saved to ${TEST_ACCOUNTS_PATH}`);
3487
3528
  data: {
3488
3529
  id: account.id,
3489
3530
  email: account.email,
3531
+ name,
3490
3532
  // The password is already echoed on the human path and stored 0600 in
3491
3533
  // ACCOUNTS_PATH — withholding it from --json would only force scripts
3492
3534
  // to parse the file themselves.
@@ -3504,6 +3546,18 @@ var list = defineDeepspaceCommand({
3504
3546
  name: "list",
3505
3547
  description: "List your test accounts"
3506
3548
  },
3549
+ args: {
3550
+ usable: {
3551
+ type: "boolean",
3552
+ description: "Only accounts with locally saved credentials (usable by the users() fixture)",
3553
+ default: false
3554
+ },
3555
+ reveal: {
3556
+ type: "boolean",
3557
+ description: "Print locally saved passwords instead of masking them",
3558
+ default: false
3559
+ }
3560
+ },
3507
3561
  async run({ args }) {
3508
3562
  await ensureToken();
3509
3563
  let remote;
@@ -3512,42 +3566,47 @@ var list = defineDeepspaceCommand({
3512
3566
  } catch (err) {
3513
3567
  throw new Refusal(`Failed: ${err.message}`, "test_accounts_list_failed");
3514
3568
  }
3515
- const local = loadAllTestAccounts();
3516
- const localByEmail = new Map(local.map((a) => [a.email, a]));
3517
- if (remote.length === 0) {
3569
+ const localByEmail = new Map(loadAllTestAccounts().map((a) => [a.email, a]));
3570
+ const accounts = remote.map((a) => {
3571
+ const stored = localByEmail.get(a.email);
3572
+ return {
3573
+ id: a.id,
3574
+ email: a.email,
3575
+ userId: a.userId,
3576
+ label: a.label,
3577
+ createdAt: a.createdAt,
3578
+ name: stored?.name ?? null,
3579
+ password: stored?.password ?? null,
3580
+ usableByFixture: Boolean(stored?.password)
3581
+ };
3582
+ }).filter((a) => !args.usable || a.usableByFixture);
3583
+ if (accounts.length === 0) {
3518
3584
  if (!args.json) {
3519
3585
  console.log(
3520
- "No test accounts. Create one with: deepspace test accounts create --email <email> --password <password>"
3586
+ remote.length > 0 ? `No test accounts with locally saved credentials (${remote.length} remote-only). Recreate them with: deepspace test accounts create --email <email> --password <password>` : "No test accounts. Create one with: deepspace test accounts create --email <email> --password <password>"
3521
3587
  );
3522
3588
  }
3523
3589
  return { data: { accounts: [], count: 0, limit: 10 } };
3524
3590
  }
3525
3591
  if (!args.json) {
3526
- console.log(`Test accounts (${remote.length}/10):
3592
+ console.log(`Test accounts (${remote.length}/10${args.usable ? ", usable only" : ""}):
3527
3593
  `);
3528
- for (const a of remote) {
3529
- const stored = localByEmail.get(a.email);
3594
+ for (const a of accounts) {
3530
3595
  const date = new Date(a.createdAt).toLocaleDateString();
3531
3596
  console.log(` ${a.email}${a.label ? ` (${a.label})` : ""}`);
3532
3597
  console.log(` ID: ${a.id} UserID: ${a.userId} Created: ${date}`);
3533
- if (stored?.password) {
3534
- console.log(` Password: ${stored.password}`);
3598
+ console.log(` Selector: ${a.name ?? "(none)"}`);
3599
+ if (a.usableByFixture) {
3600
+ console.log(` Password: ${args.reveal ? a.password : "(saved locally)"}`);
3535
3601
  } else {
3536
- console.log(` Password: (not saved locally)`);
3602
+ console.log(` Password: (not saved locally \u2014 not usable by the users() fixture)`);
3537
3603
  }
3538
3604
  }
3539
3605
  }
3540
3606
  return {
3541
3607
  data: {
3542
- accounts: remote.map((a) => ({
3543
- id: a.id,
3544
- email: a.email,
3545
- userId: a.userId,
3546
- label: a.label,
3547
- createdAt: a.createdAt,
3548
- password: localByEmail.get(a.email)?.password ?? null
3549
- })),
3550
- count: remote.length,
3608
+ accounts,
3609
+ count: accounts.length,
3551
3610
  limit: 10
3552
3611
  }
3553
3612
  };
@@ -4417,6 +4476,7 @@ async function buildDeployBundle(options) {
4417
4476
  if (lintFindings && lintFindings.length > 0) {
4418
4477
  p6.log.warn(formatSchemaLintFindings(lintFindings).join("\n"));
4419
4478
  }
4479
+ const buildStarted = Date.now();
4420
4480
  spinner2.start("Building...");
4421
4481
  let preparedWranglerConfig;
4422
4482
  try {
@@ -4439,7 +4499,7 @@ async function buildDeployBundle(options) {
4439
4499
  } finally {
4440
4500
  preparedWranglerConfig?.cleanup();
4441
4501
  }
4442
- spinner2.stop("Built");
4502
+ spinner2.stop(`Built (${Math.round((Date.now() - buildStarted) / 1e3)}s)`);
4443
4503
  const deployConfigPath = join15(appDir, ".wrangler", "deploy", "config.json");
4444
4504
  if (!existsSync14(deployConfigPath)) {
4445
4505
  output.die(
@@ -4601,6 +4661,51 @@ import * as p7 from "@clack/prompts";
4601
4661
  import { existsSync as existsSync15 } from "fs";
4602
4662
  import { join as join16 } from "path";
4603
4663
  import { pathToFileURL as pathToFileURL2 } from "url";
4664
+
4665
+ // src/shared/api-error.ts
4666
+ var SLUG_RE = /^[a-z0-9]+(?:[_.-][a-z0-9]+)*$/;
4667
+ var HUMAN_MESSAGES = {
4668
+ owner_connect_not_ready: "This app isn't ready to accept payments yet \u2014 the app owner needs to finish payment setup.",
4669
+ invalid_json: "The request body was not valid JSON.",
4670
+ invalid_body: "The request body is invalid.",
4671
+ invalid_product_id: "Invalid product id.",
4672
+ duplicate_product_id: "Duplicate product id.",
4673
+ unknown_product: "Unknown product.",
4674
+ too_many_products: "Too many products declared.",
4675
+ invalid_amount: "Invalid amount.",
4676
+ invalid_name: "Invalid product name.",
4677
+ invalid_description: "Invalid product description.",
4678
+ unsupported_currency: "Unsupported currency.",
4679
+ unknown_app: "Unknown app.",
4680
+ not_app_owner: "Only the app owner (or a collaborator) can do this.",
4681
+ invalid_return_url: "Invalid return URL.",
4682
+ invalid_cancel_url: "Invalid cancel URL.",
4683
+ unknown_plan: "Unknown subscription plan.",
4684
+ unknown_price: "No price is configured for that billing interval.",
4685
+ no_subscription: "No subscription found for this account."
4686
+ };
4687
+ function normalizeApiError(status3, body) {
4688
+ const payload = body !== null && typeof body === "object" ? body : {};
4689
+ const rawError = typeof payload.error === "string" ? payload.error : void 0;
4690
+ const message = typeof payload.message === "string" ? payload.message : void 0;
4691
+ const humanError = rawError !== void 0 && !SLUG_RE.test(rawError) ? rawError : void 0;
4692
+ const code = rawError !== void 0 && SLUG_RE.test(rawError) ? rawError : typeof payload.code === "string" && SLUG_RE.test(payload.code) ? payload.code : void 0;
4693
+ const details = {};
4694
+ for (const [key, value] of Object.entries(payload)) {
4695
+ if (key === "success" || key === "error" || key === "message" || key === "issues" || key === "code")
4696
+ continue;
4697
+ details[key] = value;
4698
+ }
4699
+ return {
4700
+ error: message ?? humanError ?? (code !== void 0 ? HUMAN_MESSAGES[code] : void 0) ?? rawError ?? `Request failed (${status3})`,
4701
+ ...code !== void 0 ? { code } : {},
4702
+ status: status3,
4703
+ ...Object.keys(details).length > 0 ? { details } : {},
4704
+ ...Array.isArray(payload.issues) ? { issues: payload.issues } : {}
4705
+ };
4706
+ }
4707
+
4708
+ // src/cli/commands/deploy/commerce.ts
4604
4709
  var API_URL2 = process.env.DEEPSPACE_API_URL ?? PLATFORM_URLS.api;
4605
4710
  function errorMessage2(error) {
4606
4711
  return error instanceof Error ? error.message : String(error);
@@ -4674,7 +4779,7 @@ Stripe will keep charging them until you cancel. Cancel at the end of their curr
4674
4779
  });
4675
4780
  const body = await response.json().catch(() => ({}));
4676
4781
  if (!response.ok || !body.success) {
4677
- lastError = body.error ?? `HTTP ${response.status}`;
4782
+ lastError = normalizeApiError(response.status, body).error;
4678
4783
  spinner2.stop(`Cancel failed for ${change.slug}: ${lastError}`);
4679
4784
  stopped = true;
4680
4785
  break;
@@ -4732,7 +4837,7 @@ async function syncSubscriptionPlans(appDir, appName, token, nonInteractive) {
4732
4837
  if (!response.ok || !body.ok) {
4733
4838
  spinner2.stop("Plan sync skipped");
4734
4839
  p7.log.warn(
4735
- `Plan sync failed: ${body.error ?? `HTTP ${response.status}`}${body.hint ? ` \u2014 ${body.hint}` : ""}`
4840
+ `Plan sync failed: ${normalizeApiError(response.status, body).error}${body.hint ? ` \u2014 ${body.hint}` : ""}`
4736
4841
  );
4737
4842
  return;
4738
4843
  }
@@ -4790,7 +4895,7 @@ async function syncOneTimeProducts(appDir, appName, token) {
4790
4895
  }
4791
4896
  spinner2?.stop("Product sync skipped");
4792
4897
  p7.log.warn(
4793
- `Product sync failed: ${body.error ?? `HTTP ${response.status}`}${body.details ? ` \u2014 ${JSON.stringify(body.details)}` : ""}`
4898
+ `Product sync failed: ${normalizeApiError(response.status, body).error}${body.details ? ` \u2014 ${JSON.stringify(body.details)}` : ""}`
4794
4899
  );
4795
4900
  } catch (error) {
4796
4901
  spinner2?.stop("Product sync skipped");
@@ -5522,7 +5627,7 @@ async function syncDeployRepository(options) {
5522
5627
  p9.log.info(`Workspace ${workspaceBranchId} is synced at ${tip.slice(0, 10)}.`);
5523
5628
  } else if (secretFiles.length > 0) {
5524
5629
  p9.log.warn(
5525
- `Skipping cloud-repo push \u2014 the branch tracks secret file(s): ${secretFiles.join(", ")}. Untrack with \`git rm --cached ${secretFiles[0]}\` (add to .gitignore), re-commit, then \`deepspace push\`. Deploy continues; this release's source isn't recoverable until then.`
5630
+ `Skipping cloud-repo push \u2014 the branch tracks secret file(s): ${secretFiles.join(", ")}. Untrack with \`git rm --cached ${secretFiles[0]}\` (add to .gitignore), re-commit, then \`deepspace push\`. Without the push this deploy carries no source lineage: if the live release has one, the server refuses it as stale (--ignore-stale replaces it anyway), and this release's source isn't recoverable until the push works.`
5526
5631
  );
5527
5632
  } else {
5528
5633
  const pushResult = await pushWithTransientRetry(
@@ -5658,7 +5763,7 @@ function detachedHeadRefusal() {
5658
5763
  var CLOUDFLARE_DEPLOY_ERROR_HINT = "Deployment reached the DeepSpace deploy worker, but Cloudflare's deployment control-plane API failed. This is often caused by a Cloudflare Dashboard/API incident or Workers for Platforms entitlement outage. Already deployed apps should keep serving; wait for Cloudflare to recover and retry.";
5659
5764
  var DEPLOY_SERVICE_LIMIT_HINT = "The DeepSpace deploy service hit a resource limit processing this deploy (the deploy worker was terminated before it could answer). This is a DeepSpace-side failure, not a Cloudflare incident \u2014 your app was not changed. Retry; if it keeps happening, report the app id and asset count.";
5660
5765
  var REQUIRED_ASSET_TRANSPORT = "content-addressed-v1";
5661
- var UPLOAD_CONCURRENCY = 4;
5766
+ var UPLOAD_CONCURRENCY = 16;
5662
5767
  async function deployBuiltBundle(options) {
5663
5768
  const {
5664
5769
  deployUrl,
@@ -5747,15 +5852,19 @@ async function deployBuiltBundle(options) {
5747
5852
  form.append("deployKey", repository.deployKey);
5748
5853
  return form;
5749
5854
  };
5750
- const postCommit = async () => postWithRetry(
5751
- `${deployUrl}/api/deploy/${appId}`,
5752
- () => ({
5753
- method: "POST",
5754
- headers: { Authorization: `Bearer ${token}` },
5755
- body: makeForm()
5756
- }),
5757
- { retryServerErrors: false }
5758
- );
5855
+ let commitStarted = Date.now();
5856
+ const postCommit = async () => {
5857
+ commitStarted = Date.now();
5858
+ return postWithRetry(
5859
+ `${deployUrl}/api/deploy/${appId}`,
5860
+ () => ({
5861
+ method: "POST",
5862
+ headers: { Authorization: `Bearer ${token}` },
5863
+ body: makeForm()
5864
+ }),
5865
+ { retryServerErrors: false, attempts: 2, timeoutMs: 24e4 }
5866
+ );
5867
+ };
5759
5868
  let response;
5760
5869
  try {
5761
5870
  response = await postCommit();
@@ -5802,6 +5911,7 @@ async function deployBuiltBundle(options) {
5802
5911
  body.code
5803
5912
  );
5804
5913
  }
5914
+ p10.log.info(`Platform commit: ${Math.round((Date.now() - commitStarted) / 1e3)}s`);
5805
5915
  if (body.onBehalfOfOwner) {
5806
5916
  p10.log.warn("This release is attributed to the app's owner, not to your account.");
5807
5917
  }
@@ -5920,8 +6030,12 @@ async function uploadDeployAssets(options) {
5920
6030
  spinner2.message(`Uploading assets \u2014 ${uploaded}/${total}...`);
5921
6031
  }
5922
6032
  };
6033
+ const uploadStarted = Date.now();
5923
6034
  try {
5924
6035
  await Promise.all(Array.from({ length: Math.min(UPLOAD_CONCURRENCY, missing.length) }, worker));
6036
+ p10.log.info(
6037
+ `Uploaded ${total} file(s) in ${Math.round((Date.now() - uploadStarted) / 1e3)}s`
6038
+ );
5925
6039
  } catch (error) {
5926
6040
  spinner2.stop("Deploy failed");
5927
6041
  if (error instanceof AssetUploadError) {
@@ -5967,13 +6081,14 @@ function errorMessage4(error) {
5967
6081
  }
5968
6082
  async function postWithRetry(url, makeInit, {
5969
6083
  attempts = 4,
5970
- retryServerErrors = true
6084
+ retryServerErrors = true,
6085
+ timeoutMs = 6e4
5971
6086
  } = {}) {
5972
6087
  let lastResponse;
5973
6088
  let lastError;
5974
6089
  for (let attempt = 1; attempt <= attempts; attempt++) {
5975
6090
  try {
5976
- const response = await fetch(url, makeInit());
6091
+ const response = await fetch(url, { ...makeInit(), signal: AbortSignal.timeout(timeoutMs) });
5977
6092
  const transient = retryServerErrors && (response.status >= 500 || response.status === 408 || response.status === 429);
5978
6093
  if (!transient) return response;
5979
6094
  lastResponse = response;
@@ -6146,13 +6261,14 @@ var deploy_default = defineCommand3({
6146
6261
  );
6147
6262
  }
6148
6263
  const appName = nameResult.name;
6149
- let appId = readAppId(appDir, envName);
6264
+ const appId = readAppId(appDir, envName);
6150
6265
  p12.log.info(envName ? `App: ${appName} (env: ${envName})` : `App: ${appName}`);
6151
6266
  const { token, ownerId } = await authenticate(appDir, output);
6152
6267
  if (!appId) {
6153
- appId = mintAppId();
6154
- writeAppId(appDir, appId, { wranglerEnv: envName });
6155
- p12.log.info(`Minted app id ${appId} \u2014 commit wrangler.toml.`);
6268
+ return output.die(
6269
+ `No app id in wrangler.toml${envName ? ` for [env.${envName}]` : ""}. Run \`deepspace app init${envName ? ` --env ${envName}` : ""}\` to register this app, then re-deploy.`,
6270
+ "app_not_initialized"
6271
+ );
6156
6272
  }
6157
6273
  p12.log.info(`Id: ${appId}`);
6158
6274
  const secretsCache = await loadDeploySecrets({
@@ -6218,6 +6334,7 @@ var deploy_default = defineCommand3({
6218
6334
  spinner: spinner2
6219
6335
  });
6220
6336
  let serving = "unverifiable";
6337
+ const edgeWaitStarted = Date.now();
6221
6338
  if (body.url) {
6222
6339
  spinner2.message("Waiting for the edge to serve this release...");
6223
6340
  serving = await waitForLiveRelease(body.url, body.releaseStamp, 9e4);
@@ -6247,7 +6364,9 @@ var deploy_default = defineCommand3({
6247
6364
  return;
6248
6365
  }
6249
6366
  }
6250
- spinner2.stop("Deployed!");
6367
+ spinner2.stop(
6368
+ `Deployed! (edge confirmed in ${Math.round((Date.now() - edgeWaitStarted) / 1e3)}s)`
6369
+ );
6251
6370
  p12.log.success(`Live at: ${body.url}`);
6252
6371
  await syncCommerce(appDir, appId, token, output.nonInteractive);
6253
6372
  if (output.json) {
@@ -6508,7 +6627,10 @@ var undeploy_default = defineDeepspaceCommand({
6508
6627
  }
6509
6628
  );
6510
6629
  }
6511
- throw new Refusal(body.error ?? `Undeploy error (${res.status})`, "undeploy_failed");
6630
+ throw new Refusal(
6631
+ body.error ?? `Undeploy error (${res.status})`,
6632
+ body.code ?? "undeploy_failed"
6633
+ );
6512
6634
  }
6513
6635
  s?.stop("Removed");
6514
6636
  const hosts = body.releasedHosts ?? [];
@@ -9205,7 +9327,9 @@ async function runList(args) {
9205
9327
  for (const ep of endpoints) {
9206
9328
  const pad = ep.endpoint.padEnd(widest);
9207
9329
  const cost = formatCurrency(ep.billing.baseCost, ep.billing.currency);
9208
- console.log(` ${pad} ${ep.billing.model.padEnd(12)} ${cost}`);
9330
+ const oauth = ep.requiresOAuth ? " [oauth]" : "";
9331
+ console.log(` ${pad} ${ep.billing.model.padEnd(12)} ${cost}${oauth}`);
9332
+ if (ep.description) console.log(` ${ep.description}`);
9209
9333
  }
9210
9334
  console.log();
9211
9335
  }
@@ -9234,9 +9358,15 @@ async function runInfo(args) {
9234
9358
  }
9235
9359
  if (!args.json) {
9236
9360
  console.log(`${integration}/${endpoint}`);
9361
+ if (info2.description) console.log(` ${info2.description}`);
9237
9362
  console.log(
9238
9363
  ` billing: ${formatCurrency(info2.billing.baseCost, info2.billing.currency)} ${billingUnit(info2.billing.model)}`
9239
9364
  );
9365
+ if (info2.requiresOAuth) {
9366
+ console.log(
9367
+ " Requires OAuth: without a connected account, a call succeeds with data { requiresOAuth: true, authUrl } \u2014 open the authUrl, then retry."
9368
+ );
9369
+ }
9240
9370
  console.log();
9241
9371
  console.log("Input schema:");
9242
9372
  console.log(
@@ -9245,6 +9375,11 @@ async function runInfo(args) {
9245
9375
  console.log();
9246
9376
  console.log("Example body:");
9247
9377
  console.log(info2.example ? JSON.stringify(info2.example, null, 2) : " (no example available)");
9378
+ if (info2.outputSchema) {
9379
+ console.log();
9380
+ console.log("Output schema:");
9381
+ console.log(JSON.stringify(info2.outputSchema, null, 2));
9382
+ }
9248
9383
  }
9249
9384
  return { data: info2 };
9250
9385
  }
@@ -9325,17 +9460,19 @@ async function runInvoke(args) {
9325
9460
  }
9326
9461
  return { data: payload };
9327
9462
  }
9328
- const errMsg = payload.error ?? `Request failed (${res.status})`;
9329
- const lines = [`${integration}/${endpoint} (${res.status}, ${elapsed}ms): ${errMsg}`];
9330
- if (Array.isArray(payload.issues)) {
9331
- for (const issue of payload.issues) {
9463
+ const norm = normalizeApiError(res.status, payload);
9464
+ const lines = [`${integration}/${endpoint} (${res.status}, ${elapsed}ms): ${norm.error}`];
9465
+ if (norm.issues) {
9466
+ for (const issue of norm.issues) {
9332
9467
  const path = issue.path?.length ? issue.path.join(".") : "(root)";
9333
9468
  lines.push(` - ${path}: ${issue.message}`);
9334
9469
  }
9335
9470
  }
9336
- const code = typeof payload.code === "string" ? payload.code : "integration_call_failed";
9471
+ const code = norm.code ?? "integration_call_failed";
9337
9472
  const rest = { ...payload };
9338
9473
  delete rest.error;
9474
+ delete rest.message;
9475
+ delete rest.code;
9339
9476
  throw new Refusal(lines.join("\n"), code, {
9340
9477
  ...res.status === 401 ? { action: cliAction("deepspace", "auth", "login") } : {},
9341
9478
  extra: rest
@@ -10106,10 +10243,29 @@ var secrets_default = defineCommand10({
10106
10243
  });
10107
10244
 
10108
10245
  // src/cli/commands/init.ts
10246
+ import { existsSync as existsSync21 } from "fs";
10247
+ import { join as join20 } from "path";
10248
+ var DEPLOY_URL12 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
10249
+ function commitScaffoldIfUnborn(appDir, token) {
10250
+ try {
10251
+ if (!existsSync21(join20(appDir, ".git"))) return false;
10252
+ if (runGit(appDir, ["rev-parse", "--verify", "HEAD"], { allowFail: true }).status === 0) {
10253
+ return false;
10254
+ }
10255
+ ensureGitIdentity(appDir, token);
10256
+ if (runGit(appDir, ["add", "-A"], { allowFail: true }).status !== 0) return false;
10257
+ const commit = runGit(appDir, ["commit", "-m", "Initial DeepSpace scaffold", "--no-verify"], {
10258
+ allowFail: true
10259
+ });
10260
+ return commit.status === 0;
10261
+ } catch {
10262
+ return false;
10263
+ }
10264
+ }
10109
10265
  var init_default = defineDeepspaceCommand({
10110
10266
  meta: {
10111
10267
  name: "init",
10112
- description: "Mint this app\u2019s immutable DEEPSPACE_APP_ID into wrangler.toml"
10268
+ description: "Register this app: mint its immutable DEEPSPACE_APP_ID into wrangler.toml"
10113
10269
  },
10114
10270
  args: {
10115
10271
  "new-id": {
@@ -10143,23 +10299,31 @@ var init_default = defineDeepspaceCommand({
10143
10299
  data: { status: "already_initialized", appId: existing, appDir, env: envName ?? null }
10144
10300
  };
10145
10301
  }
10146
- const appId = mintAppId();
10302
+ const token = await ensureToken();
10303
+ const { appId } = await apiFetch(DEPLOY_URL12, token, "/api/apps/mint", {
10304
+ method: "POST"
10305
+ });
10147
10306
  writeAppId(appDir, appId, { wranglerEnv: envName, force: Boolean(args["new-id"]) });
10307
+ if (!envName) stampAppIdInSources(appDir, appId, existing);
10308
+ const committedScaffold = commitScaffoldIfUnborn(appDir, token);
10148
10309
  if (!args.json) {
10149
10310
  if (existing) {
10150
10311
  console.log(`Forked: ${existing} \u2192 ${appId}${envName ? ` (env: ${envName})` : ""}`);
10151
- console.log("The next deploy registers this as a NEW app; the original is untouched.");
10312
+ console.log("Registered as a NEW app under your account; the original is untouched.");
10152
10313
  } else {
10153
- console.log(`Minted ${appId}${envName ? ` (env: ${envName})` : ""} \u2014 commit wrangler.toml.`);
10154
- console.log("The first deploy registers it and claims the `name` subdomain.");
10314
+ console.log(
10315
+ `Registered ${appId}${envName ? ` (env: ${envName})` : ""} to your account` + (committedScaffold ? " \u2014 initial scaffold commit created." : " \u2014 commit wrangler.toml.")
10316
+ );
10317
+ console.log("The first deploy claims the `name` subdomain.");
10155
10318
  }
10156
10319
  console.log(`App dir: ${appDir}`);
10157
10320
  }
10158
10321
  return {
10159
10322
  data: {
10160
- status: existing ? "forked" : "minted",
10323
+ status: existing ? "forked" : "registered",
10161
10324
  appId,
10162
10325
  ...existing ? { previousAppId: existing } : {},
10326
+ committedScaffold,
10163
10327
  appDir,
10164
10328
  env: envName ?? null
10165
10329
  }
@@ -10170,7 +10334,7 @@ var init_default = defineDeepspaceCommand({
10170
10334
  // src/cli/commands/status.ts
10171
10335
  import { defineCommand as defineCommand11 } from "citty";
10172
10336
  import * as p17 from "@clack/prompts";
10173
- import { existsSync as existsSync21, readFileSync as readFileSync24 } from "fs";
10337
+ import { existsSync as existsSync22, readFileSync as readFileSync24 } from "fs";
10174
10338
  function timeAgo(iso) {
10175
10339
  const ms = Date.now() - Date.parse(iso);
10176
10340
  if (!Number.isFinite(ms) || ms < 0) return iso;
@@ -10255,7 +10419,7 @@ var status_default = defineCommand11({
10255
10419
  json.services = urls;
10256
10420
  if (urlOverrides.length > 0) json.urlOverrides = urlOverrides;
10257
10421
  }
10258
- facts.loggedIn = existsSync21(SESSION_PATH);
10422
+ facts.loggedIn = existsSync22(SESSION_PATH);
10259
10423
  let userId = null;
10260
10424
  if (!facts.loggedIn) {
10261
10425
  lines.push(["Session", "not logged in"]);
@@ -10944,18 +11108,18 @@ var pull_default = defineDeepspaceCommand({
10944
11108
 
10945
11109
  // src/cli/commands/clone.ts
10946
11110
  import * as p20 from "@clack/prompts";
10947
- import { existsSync as existsSync22, readdirSync as readdirSync6, rmSync as rmSync5 } from "fs";
10948
- import { resolve as resolve17, basename as basename5, join as join20 } from "path";
11111
+ import { existsSync as existsSync23, readdirSync as readdirSync6, rmSync as rmSync5 } from "fs";
11112
+ import { resolve as resolve17, basename as basename5, join as join21 } from "path";
10949
11113
  function cleanFailedCloneDir(dir) {
10950
11114
  let entries;
10951
11115
  try {
10952
11116
  entries = readdirSync6(dir);
10953
11117
  } catch {
10954
- return existsSync22(dir) ? ["<unreadable>"] : [];
11118
+ return existsSync23(dir) ? ["<unreadable>"] : [];
10955
11119
  }
10956
11120
  if (entries.length === 1 && entries[0] === ".git") {
10957
11121
  try {
10958
- rmSync5(join20(dir, ".git"), { recursive: true, force: true });
11122
+ rmSync5(join21(dir, ".git"), { recursive: true, force: true });
10959
11123
  return [];
10960
11124
  } catch {
10961
11125
  return entries;
@@ -10964,7 +11128,7 @@ function cleanFailedCloneDir(dir) {
10964
11128
  return entries;
10965
11129
  }
10966
11130
  function validateCloneTarget(dir) {
10967
- if (!existsSync22(dir)) return false;
11131
+ if (!existsSync23(dir)) return false;
10968
11132
  let entries;
10969
11133
  try {
10970
11134
  entries = readdirSync6(dir);
@@ -11087,7 +11251,7 @@ async function actorLabels(token, appId) {
11087
11251
  }
11088
11252
 
11089
11253
  // src/cli/commands/releases.ts
11090
- var DEPLOY_URL12 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
11254
+ var DEPLOY_URL13 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
11091
11255
  var releases_default = defineDeepspaceCommand({
11092
11256
  meta: {
11093
11257
  name: "releases",
@@ -11122,8 +11286,8 @@ var releases_default = defineDeepspaceCommand({
11122
11286
  const spinner2 = args.json ? null : createSpinner();
11123
11287
  spinner2?.start("Loading release history\u2026");
11124
11288
  const token = await ensureToken();
11125
- const appId = await resolveAppTarget(DEPLOY_URL12, token, appArg, { wranglerEnv: envArg });
11126
- const { releases } = await repoApi(DEPLOY_URL12, token, appId).listReleases(limit);
11289
+ const appId = await resolveAppTarget(DEPLOY_URL13, token, appArg, { wranglerEnv: envArg });
11290
+ const { releases } = await repoApi(DEPLOY_URL13, token, appId).listReleases(limit);
11127
11291
  spinner2?.stop(`Loaded ${releases.length} ${releases.length === 1 ? "release" : "releases"}.`);
11128
11292
  if (!args.json) {
11129
11293
  if (releases.length === 0) {
@@ -11156,7 +11320,7 @@ Retry in a moment, or re-run with --allow-do-deletion to accept the risk.`,
11156
11320
  "do_guard_unavailable"
11157
11321
  );
11158
11322
  }
11159
- var DEPLOY_URL13 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
11323
+ var DEPLOY_URL14 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
11160
11324
  function pickPreviousRelease(releases) {
11161
11325
  if (releases.length === 0) {
11162
11326
  return { error: "No releases recorded yet \u2014 nothing to roll back to.", code: "no_releases" };
@@ -11218,8 +11382,8 @@ var rollback_default = defineDeepspaceCommand({
11218
11382
  const spinner2 = args.json ? null : createSpinner();
11219
11383
  spinner2?.start("Preparing rollback\u2026");
11220
11384
  const token = await ensureToken();
11221
- const appId = await resolveAppTarget(DEPLOY_URL13, token, appArg, { wranglerEnv: envArg });
11222
- const api4 = repoApi(DEPLOY_URL13, token, appId);
11385
+ const appId = await resolveAppTarget(DEPLOY_URL14, token, appArg, { wranglerEnv: envArg });
11386
+ const api4 = repoApi(DEPLOY_URL14, token, appId);
11223
11387
  let releaseId = args.release ? String(args.release).trim() : "";
11224
11388
  if (!releaseId) {
11225
11389
  const { releases } = await api4.listReleases(2);
@@ -11230,7 +11394,7 @@ var rollback_default = defineDeepspaceCommand({
11230
11394
  spinner2?.message(`Rolling back to ${releaseId}\u2026`);
11231
11395
  let result;
11232
11396
  try {
11233
- result = await apiFetch(DEPLOY_URL13, token, `/api/deploy/${appId}/rollback`, {
11397
+ result = await apiFetch(DEPLOY_URL14, token, `/api/deploy/${appId}/rollback`, {
11234
11398
  method: "POST",
11235
11399
  body: JSON.stringify({
11236
11400
  releaseId,
@@ -11441,7 +11605,7 @@ function printOverlaps(overlaps, json) {
11441
11605
  // src/cli/commands/workspace/local.ts
11442
11606
  import * as p24 from "@clack/prompts";
11443
11607
  import {
11444
- existsSync as existsSync23,
11608
+ existsSync as existsSync24,
11445
11609
  mkdirSync as mkdirSync7,
11446
11610
  readFileSync as readFileSync25,
11447
11611
  realpathSync as realpathSync6,
@@ -11449,7 +11613,7 @@ import {
11449
11613
  statSync as statSync3,
11450
11614
  writeFileSync as writeFileSync7
11451
11615
  } from "fs";
11452
- import { dirname as dirname11, isAbsolute as isAbsolute5, join as join21, relative as relative2, sep as sep2 } from "path";
11616
+ import { dirname as dirname11, isAbsolute as isAbsolute5, join as join22, relative as relative2, sep as sep2 } from "path";
11453
11617
  var WORKSPACE_MARKER = "deepspace-workspace.json";
11454
11618
  function excludeWorktreeDir(appDir, dir) {
11455
11619
  try {
@@ -11470,7 +11634,7 @@ function worktreeMarkerPath(worktreeRoot) {
11470
11634
  const result = runGit(worktreeRoot, ["rev-parse", "--absolute-git-dir"], { allowFail: true });
11471
11635
  if (result.status !== 0) return null;
11472
11636
  const gitDir = result.stdout.toString("utf-8").trim();
11473
- return isAbsolute5(gitDir) ? join21(gitDir, WORKSPACE_MARKER) : null;
11637
+ return isAbsolute5(gitDir) ? join22(gitDir, WORKSPACE_MARKER) : null;
11474
11638
  } catch {
11475
11639
  return null;
11476
11640
  }
@@ -11515,11 +11679,11 @@ function isManagedWorkspaceWorktree(worktreeRoot, workspaceId) {
11515
11679
  function defaultWorkspaceRoot(appDir, workspaceId) {
11516
11680
  const primary = listWorktrees(appDir)[0];
11517
11681
  if (!primary) throw new Error("Git did not report a primary worktree");
11518
- return join21(primary.path, ".deepspace", "ws", workspaceId.slice(3).toLowerCase());
11682
+ return join22(primary.path, ".deepspace", "ws", workspaceId.slice(3).toLowerCase());
11519
11683
  }
11520
11684
  function appDirInWorktree(appDir, worktreeRoot) {
11521
11685
  const appRelative = relative2(realpathSync6(repoToplevel(appDir)), realpathSync6(appDir));
11522
- return appRelative ? join21(worktreeRoot, appRelative) : worktreeRoot;
11686
+ return appRelative ? join22(worktreeRoot, appRelative) : worktreeRoot;
11523
11687
  }
11524
11688
  function primaryAppDir(appDir) {
11525
11689
  const primary = listWorktrees(appDir)[0];
@@ -11554,7 +11718,7 @@ function rematerializeWorkspaceWorktree(appDir, worktreeRoot, branch, workspaceI
11554
11718
  function inOwnLinkedWorktree(appDir, id) {
11555
11719
  if (workspaceIdFromBranch(currentBranch(appDir)) !== id) return false;
11556
11720
  try {
11557
- return statSync3(join21(repoToplevel(appDir), ".git")).isFile();
11721
+ return statSync3(join22(repoToplevel(appDir), ".git")).isFile();
11558
11722
  } catch {
11559
11723
  return false;
11560
11724
  }
@@ -11679,7 +11843,7 @@ function cleanupWorkspaceLocal(appDir, id, trunkBranch, options = {}) {
11679
11843
  }
11680
11844
  runGit(mainDir, ["worktree", "prune"], { allowFail: true });
11681
11845
  out.worktreeDir = null;
11682
- const leftover = existsSync23(linked.path);
11846
+ const leftover = existsSync24(linked.path);
11683
11847
  if (leftover) out.leftoverDir = linked.path;
11684
11848
  else out.worktreeRemoved = linked.path;
11685
11849
  deleteWsBranch(mainDir, branch, expectedBranchOid, out);
@@ -11977,7 +12141,7 @@ var newWorkspaceCommand = defineDeepspaceCommand({
11977
12141
 
11978
12142
  // src/cli/commands/workspace/attach.ts
11979
12143
  import * as p26 from "@clack/prompts";
11980
- import { existsSync as existsSync24, readdirSync as readdirSync7, realpathSync as realpathSync7, rmSync as rmSync6 } from "fs";
12144
+ import { existsSync as existsSync25, readdirSync as readdirSync7, realpathSync as realpathSync7, rmSync as rmSync6 } from "fs";
11981
12145
  import { isAbsolute as isAbsolute7, resolve as resolve19 } from "path";
11982
12146
  function samePath(a, b) {
11983
12147
  const real = (p35) => {
@@ -11997,7 +12161,7 @@ function finishedWorkspaceMessage(id, view) {
11997
12161
  return `Workspace ${id} is ${workspace.status} \u2014 its ref is retained briefly (fetch ${workspace.ref} to inspect), but attach only works on active workspaces.`;
11998
12162
  }
11999
12163
  function cleanFailedFreshAttachDir(dir) {
12000
- if (!existsSync24(dir)) return [];
12164
+ if (!existsSync25(dir)) return [];
12001
12165
  let entries;
12002
12166
  try {
12003
12167
  entries = readdirSync7(dir);
@@ -12079,7 +12243,7 @@ var attachWorkspaceCommand = defineDeepspaceCommand({
12079
12243
  const localTip = resolveCommit(appDir, `refs/heads/${branch}`);
12080
12244
  if (localTip) {
12081
12245
  let existing = resolveWorkspaceWorktree(listWorktrees(appDir), id);
12082
- if (existing && !existsSync24(existing)) {
12246
+ if (existing && !existsSync25(existing)) {
12083
12247
  runGit(appDir, ["worktree", "prune"], { allowFail: true });
12084
12248
  existing = null;
12085
12249
  }
@@ -12197,7 +12361,7 @@ var attachWorkspaceCommand = defineDeepspaceCommand({
12197
12361
  process.cwd(),
12198
12362
  (typeof args.dir === "string" ? args.dir.trim() : "") || `ws-${id.slice(3).toLowerCase()}`
12199
12363
  );
12200
- if (existsSync24(dir)) {
12364
+ if (existsSync25(dir)) {
12201
12365
  spinner2?.stop("Target exists.");
12202
12366
  throw new Refusal(`${dir} already exists \u2014 pass a different directory.`, "dir_exists");
12203
12367
  }
@@ -12535,12 +12699,12 @@ import * as p30 from "@clack/prompts";
12535
12699
  // src/cli/lib/validation.ts
12536
12700
  import { sync as spawnSync7 } from "cross-spawn";
12537
12701
  import { readFileSync as readFileSync26 } from "fs";
12538
- import { join as join22 } from "path";
12702
+ import { join as join23 } from "path";
12539
12703
  var MAX_SUMMARY_CHARS = 4e3;
12540
12704
  var MAX_OUTPUT_BUFFER = 64 * 1024 * 1024;
12541
12705
  function configuredCommand(appDir) {
12542
12706
  try {
12543
- const pkg2 = JSON.parse(readFileSync26(join22(appDir, "package.json"), "utf-8"));
12707
+ const pkg2 = JSON.parse(readFileSync26(join23(appDir, "package.json"), "utf-8"));
12544
12708
  if (pkg2.scripts?.validate) return `${detectPackageManager(appDir)} run --silent validate`;
12545
12709
  } catch {
12546
12710
  }
@@ -12581,7 +12745,7 @@ function conflictMarkerFiles(diffCheckOutput) {
12581
12745
  function pullAfterLandAction(primaryDir, intoBranch, landedOid) {
12582
12746
  if (currentBranch(primaryDir) !== intoBranch) return null;
12583
12747
  if (resolveCommit(primaryDir, "HEAD") === landedOid) return null;
12584
- return { cwd: primaryDir, argv: ["deepspace", "pull"] };
12748
+ return executableAction({ cwd: primaryDir, argv: ["deepspace", "pull"] });
12585
12749
  }
12586
12750
  function landResumeArgv(args) {
12587
12751
  const argv = ["deepspace", "workspace", "land"];
@@ -12862,6 +13026,7 @@ var landWorkspaceCommand = defineDeepspaceCommand({
12862
13026
  }
12863
13027
  spinner2?.stop(`Landed ${id} into ${intoBranch} at ${landedOid.slice(0, 10)}.`);
12864
13028
  const primaryDir = primaryAppDir(appDir);
13029
+ const action = pullAfterLandAction(primaryDir, intoBranch, landedOid);
12865
13030
  const cleanup = keepWorktree ? null : cleanupWorkspaceLocal(appDir, id, intoBranch, { expectedBranchOid: landedOid });
12866
13031
  const inOwnWorktree = !cleanup && inOwnLinkedWorktree(appDir, id);
12867
13032
  const retainedWorktree = cleanup?.worktreeRetained ?? (keepWorktree || inOwnWorktree ? appDir : null);
@@ -12895,7 +13060,6 @@ var landWorkspaceCommand = defineDeepspaceCommand({
12895
13060
  { actionRequired: true, action: action2, extra: data }
12896
13061
  );
12897
13062
  }
12898
- const action = pullAfterLandAction(primaryDir, intoBranch, landedOid);
12899
13063
  return { data, ...action ? { action } : {} };
12900
13064
  }
12901
13065
  });
@@ -13616,13 +13780,13 @@ function sameRefs(left, right) {
13616
13780
 
13617
13781
  // src/cli/commands/update.ts
13618
13782
  import * as p34 from "@clack/prompts";
13619
- import { existsSync as existsSync25, readFileSync as readFileSync27, writeFileSync as writeFileSync8 } from "fs";
13620
- import { join as join23, resolve as resolve20 } from "path";
13783
+ import { existsSync as existsSync26, readFileSync as readFileSync27, writeFileSync as writeFileSync8 } from "fs";
13784
+ import { join as join24, resolve as resolve20 } from "path";
13621
13785
 
13622
13786
  // package.json
13623
13787
  var package_default = {
13624
13788
  name: "deepspace",
13625
- version: "0.20.0",
13789
+ version: "0.22.0",
13626
13790
  engines: {
13627
13791
  node: ">=22.15.0 <23 || >=24 <25 || >=26 <27"
13628
13792
  },
@@ -13655,6 +13819,10 @@ var package_default = {
13655
13819
  types: "./dist/testing.d.ts",
13656
13820
  import: "./dist/testing.js"
13657
13821
  },
13822
+ "./testing/mcp": {
13823
+ types: "./dist/testing-mcp.d.ts",
13824
+ import: "./dist/testing-mcp.js"
13825
+ },
13658
13826
  "./documentation": {
13659
13827
  types: "./dist/documentation.d.ts",
13660
13828
  import: "./dist/documentation.js"
@@ -13789,18 +13957,18 @@ function gapIsTooWide(from, to) {
13789
13957
  return from.major === 0 && from.minor < OLDEST_SUPPORTED_MINOR;
13790
13958
  }
13791
13959
  function readAppSdkVersion(appDir) {
13792
- const installed = join23(appDir, "node_modules", "deepspace", "package.json");
13793
- if (existsSync25(installed)) {
13960
+ const installed = join24(appDir, "node_modules", "deepspace", "package.json");
13961
+ if (existsSync26(installed)) {
13794
13962
  const parsed2 = JSON.parse(readFileSync27(installed, "utf8"));
13795
13963
  if (typeof parsed2.version === "string") return parsed2.version;
13796
13964
  }
13797
- const manifest = join23(appDir, "package.json");
13798
- if (!existsSync25(manifest)) return null;
13965
+ const manifest = join24(appDir, "package.json");
13966
+ if (!existsSync26(manifest)) return null;
13799
13967
  const parsed = JSON.parse(readFileSync27(manifest, "utf8"));
13800
13968
  return parsed.dependencies?.deepspace ?? null;
13801
13969
  }
13802
13970
  function pinSdkVersion(appDir, version) {
13803
- const path = join23(appDir, "package.json");
13971
+ const path = join24(appDir, "package.json");
13804
13972
  const source = readFileSync27(path, "utf8");
13805
13973
  const manifest = JSON.parse(source);
13806
13974
  const dependencies = manifest.dependencies;
@@ -13827,16 +13995,16 @@ function buildPreviewSecretsUpgradeInstruction(appDir) {
13827
13995
  "vite.config.mts",
13828
13996
  "vite.config.js",
13829
13997
  "vite.config.mjs"
13830
- ].find((candidate) => existsSync25(join23(appDir, candidate)));
13998
+ ].find((candidate) => existsSync26(join24(appDir, candidate)));
13831
13999
  if (!configName) return null;
13832
- const source = readFileSync27(join23(appDir, configName), "utf8");
14000
+ const source = readFileSync27(join24(appDir, configName), "utf8");
13833
14001
  if (source.includes("deepspace-remove-build-preview-secrets")) return null;
13834
14002
  return `${configName}: add the build-preview secret cleanup from ${BUILD_PREVIEW_SECRETS_GUIDE}; app update left this app-owned file unchanged.`;
13835
14003
  }
13836
14004
  function usersSchemaVisibilityUpgradeInstruction(appDir) {
13837
14005
  const schemaName = "src/schemas/users-schema.ts";
13838
- const path = join23(appDir, schemaName);
13839
- if (!existsSync25(path)) return null;
14006
+ const path = join24(appDir, schemaName);
14007
+ if (!existsSync26(path)) return null;
13840
14008
  const source = readFileSync27(path, "utf8");
13841
14009
  if (!/\bmember\s*:\s*\{[^}]*\bread\s*:\s*true\b/s.test(source)) return null;
13842
14010
  return `${schemaName}: review the member user-row visibility change at ${USERS_SCHEMA_VISIBILITY_GUIDE}; app update left this app-owned schema unchanged.`;
@@ -13988,8 +14156,8 @@ var update_default = defineDeepspaceCommand({
13988
14156
 
13989
14157
  // src/cli/cli.ts
13990
14158
  var __dirname = dirname12(fileURLToPath3(import.meta.url));
13991
- var pkg = JSON.parse(readFileSync28(join24(__dirname, "..", "package.json"), "utf-8"));
13992
- if (process.platform === "linux" && !process.env.SSL_CERT_FILE && existsSync26("/etc/ssl/certs/ca-certificates.crt")) {
14159
+ var pkg = JSON.parse(readFileSync28(join25(__dirname, "..", "package.json"), "utf-8"));
14160
+ if (process.platform === "linux" && !process.env.SSL_CERT_FILE && existsSync27("/etc/ssl/certs/ca-certificates.crt")) {
13993
14161
  process.env.SSL_CERT_FILE = "/etc/ssl/certs/ca-certificates.crt";
13994
14162
  }
13995
14163
  var create2 = defineCommand14({