deepspace 0.23.0 → 0.23.2

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
@@ -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 existsSync27 } from "fs";
12
- import { dirname as dirname12, join as join25 } from "path";
11
+ import { readFileSync as readFileSync27, existsSync as existsSync28 } from "fs";
12
+ import { dirname as dirname12, join as join28 } from "path";
13
13
  import { fileURLToPath as fileURLToPath3 } from "url";
14
14
 
15
15
  // src/cli/lib/spinner.ts
@@ -215,6 +215,8 @@ var CliExit = class extends Error {
215
215
  function errorCode(err) {
216
216
  if (err instanceof ApiError || err instanceof InputError) return err.code;
217
217
  if (err instanceof Error && err.name === "GitError") return err.code ?? "git_error";
218
+ if (err instanceof Error && err.name === "WranglerConfigError")
219
+ return err.code ?? "invalid_config";
218
220
  return void 0;
219
221
  }
220
222
  var API_ERROR_HINTS = {
@@ -428,7 +430,7 @@ function splitTokenPath(token, table) {
428
430
  }
429
431
  return parts;
430
432
  }
431
- var DESTRUCTIVE_VERBS = /* @__PURE__ */ new Set(["rm", "delete", "remove", "drop", "clear", "undeploy", "kill"]);
433
+ var DESTRUCTIVE_VERBS = /* @__PURE__ */ new Set(["rm", "delete", "remove", "drop", "clear", "undeploy", "kill", "logout"]);
432
434
  function editDistance(a, b) {
433
435
  const left = a.toLowerCase();
434
436
  const right = b.toLowerCase();
@@ -473,11 +475,12 @@ function findUnknownCommand(argv, rootCommands) {
473
475
  const split = splitTokenPath(token, table);
474
476
  const relativeSuggestion = split ?? closestCommandPath(token, table);
475
477
  if (!relativeSuggestion) return null;
478
+ const guessed = relativeSuggestion.at(-1) ?? "";
476
479
  return {
477
480
  attemptedPath: ["deepspace", ...accepted, token],
478
481
  helpPath: ["deepspace", ...accepted],
479
482
  suggestion: ["deepspace", ...accepted, ...relativeSuggestion],
480
- executable: !DESTRUCTIVE_VERBS.has(relativeSuggestion.at(-1) ?? ""),
483
+ executable: split !== null && !DESTRUCTIVE_VERBS.has(guessed),
481
484
  ...split ? { quotedToken: true } : {}
482
485
  };
483
486
  }
@@ -834,7 +837,7 @@ async function provisionProfile(jwt) {
834
837
  }
835
838
  }
836
839
  function sleep(ms) {
837
- return new Promise((resolve21) => setTimeout(resolve21, ms));
840
+ return new Promise((resolve22) => setTimeout(resolve22, ms));
838
841
  }
839
842
  function base64url(bytes) {
840
843
  return bytes.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
@@ -906,9 +909,66 @@ var logout_default = defineDeepspaceCommand({
906
909
  });
907
910
 
908
911
  // src/cli/lib/app-identity.ts
909
- import { existsSync as existsSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
910
- import { join as join2, resolve as resolve2 } from "path";
911
- import { parse as parseToml } from "smol-toml";
912
+ import { existsSync as existsSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
913
+ import { join as join3, resolve as resolve2 } from "path";
914
+
915
+ // src/cli/lib/wrangler-env.ts
916
+ import { existsSync as existsSync3, readFileSync as readFileSync5, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
917
+ import { join as join2 } from "path";
918
+ import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
919
+
920
+ // src/server/rooms/app-name.ts
921
+ var APP_NAME_RULES = {
922
+ /** ^[a-z0-9](-?[a-z0-9])+$ — RFC 1035 host label, no consecutive dashes. */
923
+ pattern: /^[a-z0-9](?:-?[a-z0-9])+$/,
924
+ minLength: 2,
925
+ maxLength: 63
926
+ };
927
+ function validateAppName(raw) {
928
+ if (typeof raw !== "string") {
929
+ return { valid: false, reason: "App name must be a string" };
930
+ }
931
+ const name = raw.trim();
932
+ if (!name) {
933
+ return { valid: false, reason: "App name is required" };
934
+ }
935
+ if (name.length < APP_NAME_RULES.minLength) {
936
+ return { valid: false, reason: `App name "${name}" is too short (min ${APP_NAME_RULES.minLength} chars)` };
937
+ }
938
+ if (name.length > APP_NAME_RULES.maxLength) {
939
+ return { valid: false, reason: `App name "${name}" is too long (max ${APP_NAME_RULES.maxLength} chars)` };
940
+ }
941
+ if (!APP_NAME_RULES.pattern.test(name)) {
942
+ return {
943
+ valid: false,
944
+ reason: `App name "${name}" is invalid. Use lowercase letters, digits, and single hyphens only (must start + end with alphanumeric). Examples: my-app, search-v2, unison-search.`
945
+ };
946
+ }
947
+ return { valid: true, name };
948
+ }
949
+ function resolveAppName(raw) {
950
+ const strict = validateAppName(raw);
951
+ if (strict.valid) return { ok: true, name: strict.name };
952
+ if (typeof raw !== "string") return { ok: false, reason: strict.reason };
953
+ const trimmed = raw.trim();
954
+ if (!trimmed) return { ok: false, reason: "App name is required" };
955
+ const sanitized = sanitizeAppName(trimmed);
956
+ const sanitizedStrict = validateAppName(sanitized);
957
+ if (!sanitizedStrict.valid) {
958
+ return {
959
+ ok: false,
960
+ reason: `App name "${trimmed}" can't be sanitized into a valid name (${sanitizedStrict.reason}). Pick something matching: lowercase letters, digits, single hyphens, ${APP_NAME_RULES.minLength}-${APP_NAME_RULES.maxLength} chars.`
961
+ };
962
+ }
963
+ return {
964
+ ok: true,
965
+ name: sanitized,
966
+ warning: `App name "${trimmed}" doesn't conform to deploy rules \u2014 using "${sanitized}" instead. Update wrangler.toml's \`name\` to silence this warning.`
967
+ };
968
+ }
969
+ function sanitizeAppName(raw) {
970
+ return raw.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "");
971
+ }
912
972
 
913
973
  // src/server/utils/registry-client.ts
914
974
  var APP_ID_RE = /^app_[0-9A-HJKMNP-TV-Z]{26}$/;
@@ -937,29 +997,198 @@ function mintUlid(now = Date.now()) {
937
997
  return `${ts}${rs}`.slice(0, 26);
938
998
  }
939
999
 
940
- // src/cli/lib/app-identity.ts
941
- function readWranglerIdentityConfig(cwd) {
942
- const wranglerPath = join2(resolve2(cwd), "wrangler.toml");
943
- if (!existsSync3(wranglerPath)) return null;
1000
+ // src/cli/lib/wrangler-env.ts
1001
+ var NON_INHERITABLE_ENV_KEYS = /* @__PURE__ */ new Set([
1002
+ "define",
1003
+ "vars",
1004
+ "durable_objects",
1005
+ "kv_namespaces",
1006
+ "r2_buckets",
1007
+ "ai_search_namespaces",
1008
+ "ai_search",
1009
+ "ratelimits",
1010
+ "vectorize",
1011
+ "services",
1012
+ "queues",
1013
+ "workflows",
1014
+ "tail_consumers",
1015
+ "secrets",
1016
+ "secrets_store_secrets"
1017
+ ]);
1018
+ var generatedConfigSeq = 0;
1019
+ function resolveAppNameForEnv(config, envName) {
1020
+ if (!envName) {
1021
+ return resolveAppName(config.name);
1022
+ }
1023
+ const envBlock = config.env?.[envName];
1024
+ if (!envBlock) {
1025
+ return {
1026
+ ok: false,
1027
+ code: "missing_env_block",
1028
+ reason: `wrangler.toml: no [env.${envName}] block found. Add one with a distinct \`name\` to deploy this environment.`
1029
+ };
1030
+ }
1031
+ if (typeof envBlock.name !== "string" || envBlock.name.trim() === "") {
1032
+ return {
1033
+ ok: false,
1034
+ reason: `wrangler.toml: [env.${envName}].name is missing. Set a distinct app name (e.g. "${typeof config.name === "string" ? config.name : "myapp"}-${envName}") so this environment doesn't overwrite the default.`
1035
+ };
1036
+ }
1037
+ if (envBlock.name === config.name) {
1038
+ return {
1039
+ ok: false,
1040
+ reason: `wrangler.toml: [env.${envName}].name "${envBlock.name}" is the same as the top-level name. Pick a distinct name (e.g. "${config.name}-${envName}") so deploys don't collide.`
1041
+ };
1042
+ }
1043
+ return resolveAppName(envBlock.name);
1044
+ }
1045
+ function devVarsPathFor(appDir) {
1046
+ return join2(appDir, ".dev.vars");
1047
+ }
1048
+ function prepareWranglerEnvConfig(appDir, envName) {
1049
+ if (!envName) return { cleanup() {
1050
+ } };
1051
+ const config = readWranglerConfig(appDir);
1052
+ const resolved = resolveAppNameForEnv(config, envName);
1053
+ if (!resolved.ok) throw new Error(resolved.reason);
1054
+ const flattened = flattenWranglerEnvConfig(config, envName);
1055
+ const configPath = join2(
1056
+ appDir,
1057
+ `.wrangler.deepspace.${safeGeneratedConfigSegment(envName)}.${process.pid}.${++generatedConfigSeq}.toml`
1058
+ );
1059
+ writeFileSync2(
1060
+ configPath,
1061
+ [
1062
+ "# Generated by DeepSpace for this CLI run.",
1063
+ "# Do not edit; wrangler.toml remains the source of truth.",
1064
+ stringifyToml(flattened)
1065
+ ].join("\n").replace(/\s*$/u, "\n")
1066
+ );
1067
+ return {
1068
+ configPath,
1069
+ cleanup() {
1070
+ rmSync2(configPath, { force: true });
1071
+ }
1072
+ };
1073
+ }
1074
+ function wranglerViteEnv(baseEnv, prepared, extraEnv = {}) {
1075
+ const env = { ...baseEnv, ...extraEnv };
1076
+ if (prepared.configPath) {
1077
+ delete env.CLOUDFLARE_ENV;
1078
+ env.CLOUDFLARE_VITE_WRANGLER_CONFIG_PATH = prepared.configPath;
1079
+ }
1080
+ return env;
1081
+ }
1082
+ function flattenWranglerEnvConfig(config, envName) {
1083
+ const envRoot = asTomlRecord(config.env);
1084
+ const envBlock = asTomlRecord(envRoot?.[envName]);
1085
+ if (!envBlock) {
1086
+ throw new Error(`wrangler.toml: no [env.${envName}] block found.`);
1087
+ }
1088
+ const flattened = {};
1089
+ for (const [key, value] of Object.entries(config)) {
1090
+ if (key === "env" || NON_INHERITABLE_ENV_KEYS.has(key)) continue;
1091
+ flattened[key] = value;
1092
+ }
1093
+ for (const [key, value] of Object.entries(envBlock)) {
1094
+ flattened[key] = value;
1095
+ }
1096
+ if (!("durable_objects" in envBlock) && config.durable_objects) {
1097
+ flattened.durable_objects = config.durable_objects;
1098
+ }
1099
+ const envVars = asTomlRecord(envBlock.vars) ?? {};
1100
+ if (!("APP_NAME" in envVars) && typeof envBlock.name === "string") {
1101
+ flattened.vars = { ...envVars, APP_NAME: envBlock.name };
1102
+ }
1103
+ return flattened;
1104
+ }
1105
+ function asTomlRecord(value) {
1106
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
1107
+ }
1108
+ function safeGeneratedConfigSegment(value) {
1109
+ return value.replace(/[^A-Za-z0-9._-]/gu, "_");
1110
+ }
1111
+ var WranglerConfigError = class extends Error {
1112
+ constructor(path, message, code = "invalid_config", cause) {
1113
+ super(message);
1114
+ this.path = path;
1115
+ this.code = code;
1116
+ this.cause = cause;
1117
+ this.name = "WranglerConfigError";
1118
+ }
1119
+ };
1120
+ function causeDetail(err) {
1121
+ return err instanceof Error ? err.message : String(err);
1122
+ }
1123
+ function readWranglerConfigFile(path) {
1124
+ let raw;
944
1125
  try {
945
- return parseToml(readFileSync5(wranglerPath, "utf-8"));
1126
+ raw = readFileSync5(path, "utf-8");
946
1127
  } catch (err) {
947
- throw new InputError(
948
- `Could not parse ${wranglerPath}: ${err instanceof Error ? err.message : String(err)}`,
949
- "invalid_config"
1128
+ const missing = err?.code === "ENOENT";
1129
+ throw new WranglerConfigError(
1130
+ path,
1131
+ missing ? noWranglerConfigMessage(path) : `wrangler.toml: could not read ${path}: ${causeDetail(err)}`,
1132
+ missing ? "not_in_app_repo" : "invalid_config",
1133
+ err
950
1134
  );
951
1135
  }
1136
+ let config;
1137
+ try {
1138
+ config = parseToml(raw);
1139
+ } catch (err) {
1140
+ throw new WranglerConfigError(
1141
+ path,
1142
+ `wrangler.toml: malformed TOML at ${path}: ${causeDetail(err)}`,
1143
+ "invalid_config",
1144
+ err
1145
+ );
1146
+ }
1147
+ assertOneSectionPerAppId(config, path);
1148
+ return config;
1149
+ }
1150
+ function readWranglerConfig(appDir) {
1151
+ return readWranglerConfigFile(join2(appDir, "wrangler.toml"));
1152
+ }
1153
+ function readAppIdVar(config, envName) {
1154
+ return (envName ? config.env?.[envName]?.vars : config.vars)?.DEEPSPACE_APP_ID;
1155
+ }
1156
+ function assertOneSectionPerAppId(config, path) {
1157
+ const sections = /* @__PURE__ */ new Map();
1158
+ const record = (section, value) => {
1159
+ if (typeof value !== "string" || !APP_ID_RE.test(value)) return;
1160
+ sections.set(value, [...sections.get(value) ?? [], section]);
1161
+ };
1162
+ record("[vars]", config.vars?.DEEPSPACE_APP_ID);
1163
+ for (const envName of Object.keys(config.env ?? {})) {
1164
+ record(`[env.${envName}.vars]`, config.env?.[envName]?.vars?.DEEPSPACE_APP_ID);
1165
+ }
1166
+ for (const [appId, where] of sections) {
1167
+ if (where.length < 2) continue;
1168
+ throw new WranglerConfigError(
1169
+ path,
1170
+ `wrangler.toml: ${where.join(" and ")} ${where.length === 2 ? "both" : "all"} set DEEPSPACE_APP_ID = "${appId}" in ${path}. One id under several sections points every environment's data, secrets, and billing at the same app while the deploys go to different places \u2014 each environment is its own app \u2014 run \`deepspace app init --env <name>\` to mint one for it.`,
1171
+ "duplicate_app_id"
1172
+ );
1173
+ }
1174
+ }
1175
+ function noWranglerConfigMessage(where) {
1176
+ return `No wrangler.toml at ${where}. Are you in a DeepSpace app directory? (Scaffold one with \`npm create deepspace\`, or cd into an existing app.)`;
1177
+ }
1178
+ function hasWranglerConfig(appDir) {
1179
+ return existsSync3(join2(appDir, "wrangler.toml"));
952
1180
  }
1181
+
1182
+ // src/cli/lib/app-identity.ts
953
1183
  function readAppId(cwd = process.cwd(), wranglerEnv) {
954
- const cfg = readWranglerIdentityConfig(cwd);
955
- if (!cfg) return null;
956
- const vars = wranglerEnv ? cfg.env?.[wranglerEnv]?.vars : cfg.vars;
957
- const id = vars?.DEEPSPACE_APP_ID;
1184
+ const appDir = resolve2(cwd);
1185
+ if (!hasWranglerConfig(appDir)) return null;
1186
+ const id = readAppIdVar(readWranglerConfig(appDir), wranglerEnv);
958
1187
  return typeof id === "string" && APP_ID_RE.test(id) ? id : null;
959
1188
  }
960
1189
  function writeAppId(cwd, appId, opts = {}) {
961
- const wranglerPath = join2(resolve2(cwd), "wrangler.toml");
962
- if (!existsSync3(wranglerPath)) {
1190
+ const wranglerPath = join3(resolve2(cwd), "wrangler.toml");
1191
+ if (!existsSync4(wranglerPath)) {
963
1192
  throw new Error(`No wrangler.toml in ${resolve2(cwd)}`);
964
1193
  }
965
1194
  const existing = readAppId(cwd, opts.wranglerEnv);
@@ -969,7 +1198,7 @@ function writeAppId(cwd, appId, opts = {}) {
969
1198
  `wrangler.toml already carries ${existing}. The app id is immutable \u2014 use --new-id only to fork this repo as a separate app.`
970
1199
  );
971
1200
  }
972
- let src = readFileSync5(wranglerPath, "utf-8");
1201
+ let src = readFileSync6(wranglerPath, "utf-8");
973
1202
  const header = opts.wranglerEnv ? `[env.${opts.wranglerEnv}.vars]` : "[vars]";
974
1203
  const line = `DEEPSPACE_APP_ID = "${appId}"`;
975
1204
  const headerRe = new RegExp(`^\\${header.replace(/\./g, "\\.")}[ \\t]*$`, "m");
@@ -991,7 +1220,7 @@ ${header}
991
1220
  ${line}
992
1221
  `;
993
1222
  }
994
- writeFileSync2(wranglerPath, src);
1223
+ writeFileSync3(wranglerPath, src);
995
1224
  }
996
1225
 
997
1226
  // src/cli/commands/dev.ts
@@ -999,26 +1228,18 @@ import { resolve as resolve8, basename as basename2, join as join11 } from "path
999
1228
  import spawn2 from "cross-spawn";
1000
1229
 
1001
1230
  // src/cli/lib/app-context.ts
1002
- import { existsSync as existsSync4, readFileSync as readFileSync6, readdirSync } from "fs";
1003
- import { dirname as dirname3, join as join3, resolve as resolve3 } from "path";
1004
- import { parse as parseToml2 } from "smol-toml";
1231
+ import { existsSync as existsSync5, readdirSync } from "fs";
1232
+ import { dirname as dirname3, join as join4, resolve as resolve3 } from "path";
1005
1233
  function detectAppName(cwd = process.cwd()) {
1006
- const wranglerPath = join3(resolve3(cwd), "wrangler.toml");
1007
- if (!existsSync4(wranglerPath)) return null;
1008
- try {
1009
- const config = parseToml2(readFileSync6(wranglerPath, "utf-8"));
1010
- return typeof config.name === "string" && config.name.length > 0 ? config.name : null;
1011
- } catch (error) {
1012
- throw new InputError(
1013
- `Could not parse ${wranglerPath}: ${error instanceof Error ? error.message : String(error)}`,
1014
- "invalid_config"
1015
- );
1016
- }
1234
+ const appDir = resolve3(cwd);
1235
+ if (!existsSync5(join4(appDir, "wrangler.toml"))) return null;
1236
+ const { name } = readWranglerConfig(appDir);
1237
+ return typeof name === "string" && name.length > 0 ? name : null;
1017
1238
  }
1018
1239
  function findAppDir(start = process.cwd()) {
1019
1240
  let dir = resolve3(start);
1020
1241
  for (; ; ) {
1021
- if (existsSync4(join3(dir, "wrangler.toml"))) return dir;
1242
+ if (existsSync5(join4(dir, "wrangler.toml"))) return dir;
1022
1243
  const parent = dirname3(dir);
1023
1244
  if (parent === dir) return null;
1024
1245
  dir = parent;
@@ -1026,7 +1247,7 @@ function findAppDir(start = process.cwd()) {
1026
1247
  }
1027
1248
  function findChildApps(dir) {
1028
1249
  try {
1029
- return readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && existsSync4(join3(dir, entry.name, "wrangler.toml"))).map((entry) => entry.name).sort();
1250
+ return readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && existsSync5(join4(dir, entry.name, "wrangler.toml"))).map((entry) => entry.name).sort();
1030
1251
  } catch {
1031
1252
  return [];
1032
1253
  }
@@ -1034,19 +1255,19 @@ function findChildApps(dir) {
1034
1255
 
1035
1256
  // src/cli/lib/launch-config.ts
1036
1257
  import {
1037
- existsSync as existsSync5,
1258
+ existsSync as existsSync6,
1038
1259
  readFileSync as readFileSync8,
1039
1260
  realpathSync as realpathSync2,
1040
- writeFileSync as writeFileSync3,
1261
+ writeFileSync as writeFileSync4,
1041
1262
  mkdirSync as mkdirSync4,
1042
1263
  renameSync,
1043
1264
  unlinkSync
1044
1265
  } from "fs";
1045
- import { join as join5, resolve as resolve5, dirname as dirname5, basename, isAbsolute as isAbsolute2, sep } from "path";
1266
+ import { join as join6, resolve as resolve5, dirname as dirname5, basename, isAbsolute as isAbsolute2, sep } from "path";
1046
1267
 
1047
1268
  // src/cli/lib/git/repository.ts
1048
1269
  import { appendFileSync, mkdirSync as mkdirSync3, readFileSync as readFileSync7 } from "fs";
1049
- import { dirname as dirname4, join as join4, resolve as resolve4 } from "path";
1270
+ import { dirname as dirname4, join as join5, resolve as resolve4 } from "path";
1050
1271
 
1051
1272
  // src/cli/lib/git/process.ts
1052
1273
  import { sync as spawnSync } from "cross-spawn";
@@ -1250,7 +1471,7 @@ function listWorktrees(cwd) {
1250
1471
  }
1251
1472
  function ensureLocalExclude(cwd, pattern) {
1252
1473
  const commonDir = resolve4(cwd, gitLine(cwd, ["rev-parse", "--git-common-dir"]));
1253
- const excludePath = join4(commonDir, "info", "exclude");
1474
+ const excludePath = join5(commonDir, "info", "exclude");
1254
1475
  let current = "";
1255
1476
  try {
1256
1477
  current = readFileSync7(excludePath, "utf-8");
@@ -1272,7 +1493,7 @@ function ensureLocalExclude(cwd, pattern) {
1272
1493
  function writeLaunchFile(launchPath, config) {
1273
1494
  mkdirSync4(dirname5(launchPath), { recursive: true });
1274
1495
  const tmpPath = `${launchPath}.${process.pid}.tmp`;
1275
- writeFileSync3(tmpPath, JSON.stringify(config, null, 2) + "\n");
1496
+ writeFileSync4(tmpPath, JSON.stringify(config, null, 2) + "\n");
1276
1497
  try {
1277
1498
  renameSync(tmpPath, launchPath);
1278
1499
  } catch (err) {
@@ -1285,7 +1506,7 @@ function writeLaunchFile(launchPath, config) {
1285
1506
  }
1286
1507
  function readLaunchFile(launchPath) {
1287
1508
  const empty = { version: "0.0.1", configurations: [] };
1288
- if (!existsSync5(launchPath)) return empty;
1509
+ if (!existsSync6(launchPath)) return empty;
1289
1510
  const raw = readFileSync8(launchPath, "utf-8").trim();
1290
1511
  if (!raw) return empty;
1291
1512
  const parsed = JSON.parse(raw);
@@ -1293,7 +1514,7 @@ function readLaunchFile(launchPath) {
1293
1514
  return parsed;
1294
1515
  }
1295
1516
  function writeLaunchConfigIfMissing(appDir, appName, port, opts = {}) {
1296
- const launchPath = join5(appDir, ".claude", "launch.json");
1517
+ const launchPath = join6(appDir, ".claude", "launch.json");
1297
1518
  try {
1298
1519
  const config = readLaunchFile(launchPath);
1299
1520
  if (!config) return;
@@ -1334,7 +1555,7 @@ function writeLaunchConfigIfMissing(appDir, appName, port, opts = {}) {
1334
1555
  }
1335
1556
  function resolveAppLaunchPort(appDir) {
1336
1557
  try {
1337
- const config = readLaunchFile(join5(appDir, ".claude", "launch.json"));
1558
+ const config = readLaunchFile(join6(appDir, ".claude", "launch.json"));
1338
1559
  if (!config) return null;
1339
1560
  const appName = detectAppName(appDir) ?? basename(appDir);
1340
1561
  const entry = config.configurations.find((c) => c?.name === appName) ?? (config.configurations.length === 1 ? config.configurations[0] : void 0);
@@ -1388,15 +1609,15 @@ function deriveWorktreePort(worktreeKey) {
1388
1609
  return WORKTREE_PORT_MIN + hash % WORKTREE_PORT_RANGE;
1389
1610
  }
1390
1611
  function upsertWorktreeLaunchConfig(mainRepoRoot, worktreeName, appDir, opts) {
1391
- const launchPath = join5(mainRepoRoot, ".claude", "launch.json");
1612
+ const launchPath = join6(mainRepoRoot, ".claude", "launch.json");
1392
1613
  const entryName = WORKTREE_ENTRY_PREFIX + worktreeName;
1393
1614
  try {
1394
1615
  const config = readLaunchFile(launchPath);
1395
1616
  if (!config) return null;
1396
- const worktreesRoot = join5(mainRepoRoot, ".claude", "worktrees") + sep;
1617
+ const worktreesRoot = join6(mainRepoRoot, ".claude", "worktrees") + sep;
1397
1618
  config.configurations = config.configurations.filter((c) => {
1398
1619
  if (c?.name === entryName) return false;
1399
- const isStaleWorktreeEntry = typeof c?.name === "string" && c.name.startsWith(WORKTREE_ENTRY_PREFIX) && typeof c.cwd === "string" && isAbsolute2(c.cwd) && c.cwd.startsWith(worktreesRoot) && !existsSync5(c.cwd);
1620
+ const isStaleWorktreeEntry = typeof c?.name === "string" && c.name.startsWith(WORKTREE_ENTRY_PREFIX) && typeof c.cwd === "string" && isAbsolute2(c.cwd) && c.cwd.startsWith(worktreesRoot) && !existsSync6(c.cwd);
1400
1621
  return !isStaleWorktreeEntry;
1401
1622
  });
1402
1623
  let port = opts.port;
@@ -1431,7 +1652,7 @@ function resolveWorktreePort(appDir) {
1431
1652
  const claude = detectClaudeWorktree(appDir);
1432
1653
  if (claude) {
1433
1654
  try {
1434
- const config = readLaunchFile(join5(claude.mainRepoRoot, ".claude", "launch.json"));
1655
+ const config = readLaunchFile(join6(claude.mainRepoRoot, ".claude", "launch.json"));
1435
1656
  const entry = config?.configurations.find(
1436
1657
  (c) => c?.name === WORKTREE_ENTRY_PREFIX + claude.worktreeName
1437
1658
  );
@@ -1478,209 +1699,18 @@ async function fetchAppIdentityToken(deployUrl, callerJwt, appId) {
1478
1699
  {
1479
1700
  method: "POST",
1480
1701
  headers: { Authorization: `Bearer ${callerJwt}` }
1481
- }
1482
- );
1483
- if (response.status === 404) return null;
1484
- if (!response.ok) {
1485
- const detail = await response.text().catch(() => "");
1486
- throw new Error(`Failed to fetch APP_IDENTITY_TOKEN (${response.status}): ${detail}`);
1487
- }
1488
- const body = await response.json();
1489
- if (!body.token) {
1490
- throw new Error(`Deploy worker returned no token: ${body.error ?? "unknown error"}`);
1491
- }
1492
- return body.token;
1493
- }
1494
-
1495
- // src/cli/lib/wrangler-env.ts
1496
- import { existsSync as existsSync6, readFileSync as readFileSync9, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
1497
- import { join as join6 } from "path";
1498
- import { parse as parseToml3, stringify as stringifyToml } from "smol-toml";
1499
-
1500
- // src/server/rooms/app-name.ts
1501
- var APP_NAME_RULES = {
1502
- /** ^[a-z0-9](-?[a-z0-9])+$ — RFC 1035 host label, no consecutive dashes. */
1503
- pattern: /^[a-z0-9](?:-?[a-z0-9])+$/,
1504
- minLength: 2,
1505
- maxLength: 63
1506
- };
1507
- function validateAppName(raw) {
1508
- if (typeof raw !== "string") {
1509
- return { valid: false, reason: "App name must be a string" };
1510
- }
1511
- const name = raw.trim();
1512
- if (!name) {
1513
- return { valid: false, reason: "App name is required" };
1514
- }
1515
- if (name.length < APP_NAME_RULES.minLength) {
1516
- return { valid: false, reason: `App name "${name}" is too short (min ${APP_NAME_RULES.minLength} chars)` };
1517
- }
1518
- if (name.length > APP_NAME_RULES.maxLength) {
1519
- return { valid: false, reason: `App name "${name}" is too long (max ${APP_NAME_RULES.maxLength} chars)` };
1520
- }
1521
- if (!APP_NAME_RULES.pattern.test(name)) {
1522
- return {
1523
- valid: false,
1524
- reason: `App name "${name}" is invalid. Use lowercase letters, digits, and single hyphens only (must start + end with alphanumeric). Examples: my-app, search-v2, unison-search.`
1525
- };
1526
- }
1527
- return { valid: true, name };
1528
- }
1529
- function resolveAppName(raw) {
1530
- const strict = validateAppName(raw);
1531
- if (strict.valid) return { ok: true, name: strict.name };
1532
- if (typeof raw !== "string") return { ok: false, reason: strict.reason };
1533
- const trimmed = raw.trim();
1534
- if (!trimmed) return { ok: false, reason: "App name is required" };
1535
- const sanitized = sanitizeAppName(trimmed);
1536
- const sanitizedStrict = validateAppName(sanitized);
1537
- if (!sanitizedStrict.valid) {
1538
- return {
1539
- ok: false,
1540
- reason: `App name "${trimmed}" can't be sanitized into a valid name (${sanitizedStrict.reason}). Pick something matching: lowercase letters, digits, single hyphens, ${APP_NAME_RULES.minLength}-${APP_NAME_RULES.maxLength} chars.`
1541
- };
1542
- }
1543
- return {
1544
- ok: true,
1545
- name: sanitized,
1546
- warning: `App name "${trimmed}" doesn't conform to deploy rules \u2014 using "${sanitized}" instead. Update wrangler.toml's \`name\` to silence this warning.`
1547
- };
1548
- }
1549
- function sanitizeAppName(raw) {
1550
- return raw.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "");
1551
- }
1552
-
1553
- // src/cli/lib/wrangler-env.ts
1554
- var NON_INHERITABLE_ENV_KEYS = /* @__PURE__ */ new Set([
1555
- "define",
1556
- "vars",
1557
- "durable_objects",
1558
- "kv_namespaces",
1559
- "r2_buckets",
1560
- "ai_search_namespaces",
1561
- "ai_search",
1562
- "ratelimits",
1563
- "vectorize",
1564
- "services",
1565
- "queues",
1566
- "workflows",
1567
- "tail_consumers",
1568
- "secrets",
1569
- "secrets_store_secrets"
1570
- ]);
1571
- var generatedConfigSeq = 0;
1572
- function resolveAppNameForEnv(config, envName) {
1573
- if (!envName) {
1574
- return resolveAppName(config.name);
1575
- }
1576
- const envBlock = config.env?.[envName];
1577
- if (!envBlock) {
1578
- return {
1579
- ok: false,
1580
- code: "missing_env_block",
1581
- reason: `wrangler.toml: no [env.${envName}] block found. Add one with a distinct \`name\` to deploy this environment.`
1582
- };
1583
- }
1584
- if (typeof envBlock.name !== "string" || envBlock.name.trim() === "") {
1585
- return {
1586
- ok: false,
1587
- reason: `wrangler.toml: [env.${envName}].name is missing. Set a distinct app name (e.g. "${typeof config.name === "string" ? config.name : "myapp"}-${envName}") so this environment doesn't overwrite the default.`
1588
- };
1589
- }
1590
- if (envBlock.name === config.name) {
1591
- return {
1592
- ok: false,
1593
- reason: `wrangler.toml: [env.${envName}].name "${envBlock.name}" is the same as the top-level name. Pick a distinct name (e.g. "${config.name}-${envName}") so deploys don't collide.`
1594
- };
1595
- }
1596
- return resolveAppName(envBlock.name);
1597
- }
1598
- function devVarsPathFor(appDir) {
1599
- return join6(appDir, ".dev.vars");
1600
- }
1601
- function prepareWranglerEnvConfig(appDir, envName) {
1602
- if (!envName) return { cleanup() {
1603
- } };
1604
- const config = readWranglerConfig(appDir);
1605
- const resolved = resolveAppNameForEnv(config, envName);
1606
- if (!resolved.ok) throw new Error(resolved.reason);
1607
- const flattened = flattenWranglerEnvConfig(config, envName);
1608
- const configPath = join6(
1609
- appDir,
1610
- `.wrangler.deepspace.${safeGeneratedConfigSegment(envName)}.${process.pid}.${++generatedConfigSeq}.toml`
1611
- );
1612
- writeFileSync4(
1613
- configPath,
1614
- [
1615
- "# Generated by DeepSpace for this CLI run.",
1616
- "# Do not edit; wrangler.toml remains the source of truth.",
1617
- stringifyToml(flattened)
1618
- ].join("\n").replace(/\s*$/u, "\n")
1619
- );
1620
- return {
1621
- configPath,
1622
- cleanup() {
1623
- rmSync2(configPath, { force: true });
1624
- }
1625
- };
1626
- }
1627
- function wranglerViteEnv(baseEnv, prepared, extraEnv = {}) {
1628
- const env = { ...baseEnv, ...extraEnv };
1629
- if (prepared.configPath) {
1630
- delete env.CLOUDFLARE_ENV;
1631
- env.CLOUDFLARE_VITE_WRANGLER_CONFIG_PATH = prepared.configPath;
1632
- }
1633
- return env;
1634
- }
1635
- function flattenWranglerEnvConfig(config, envName) {
1636
- const envRoot = asTomlRecord(config.env);
1637
- const envBlock = asTomlRecord(envRoot?.[envName]);
1638
- if (!envBlock) {
1639
- throw new Error(`wrangler.toml: no [env.${envName}] block found.`);
1640
- }
1641
- const flattened = {};
1642
- for (const [key, value] of Object.entries(config)) {
1643
- if (key === "env" || NON_INHERITABLE_ENV_KEYS.has(key)) continue;
1644
- flattened[key] = value;
1645
- }
1646
- for (const [key, value] of Object.entries(envBlock)) {
1647
- flattened[key] = value;
1648
- }
1649
- if (!("durable_objects" in envBlock) && config.durable_objects) {
1650
- flattened.durable_objects = config.durable_objects;
1651
- }
1652
- const envVars = asTomlRecord(envBlock.vars) ?? {};
1653
- if (!("APP_NAME" in envVars) && typeof envBlock.name === "string") {
1654
- flattened.vars = { ...envVars, APP_NAME: envBlock.name };
1655
- }
1656
- return flattened;
1657
- }
1658
- function asTomlRecord(value) {
1659
- return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
1660
- }
1661
- function safeGeneratedConfigSegment(value) {
1662
- return value.replace(/[^A-Za-z0-9._-]/gu, "_");
1663
- }
1664
- var WranglerConfigError = class extends Error {
1665
- constructor(path, cause) {
1666
- const detail = cause instanceof Error ? cause.message : String(cause);
1667
- super(`wrangler.toml: malformed TOML at ${path}: ${detail}`);
1668
- this.path = path;
1669
- this.cause = cause;
1670
- this.name = "WranglerConfigError";
1702
+ }
1703
+ );
1704
+ if (response.status === 404) return null;
1705
+ if (!response.ok) {
1706
+ const detail = await response.text().catch(() => "");
1707
+ throw new Error(`Failed to fetch APP_IDENTITY_TOKEN (${response.status}): ${detail}`);
1671
1708
  }
1672
- };
1673
- function readWranglerConfig(appDir) {
1674
- const path = join6(appDir, "wrangler.toml");
1675
- const raw = readFileSync9(path, "utf-8");
1676
- try {
1677
- return parseToml3(raw);
1678
- } catch (err) {
1679
- throw new WranglerConfigError(path, err);
1709
+ const body = await response.json();
1710
+ if (!body.token) {
1711
+ throw new Error(`Deploy worker returned no token: ${body.error ?? "unknown error"}`);
1680
1712
  }
1681
- }
1682
- function hasWranglerConfig(appDir) {
1683
- return existsSync6(join6(appDir, "wrangler.toml"));
1713
+ return body.token;
1684
1714
  }
1685
1715
 
1686
1716
  // src/cli/lib/dev-vars.ts
@@ -1722,15 +1752,15 @@ async function writeDevVars(appDir, ownerId, callerJwt, wranglerEnv, opts = {})
1722
1752
  }
1723
1753
 
1724
1754
  // src/cli/lib/install-status.ts
1725
- import { existsSync as existsSync8, readFileSync as readFileSync11, realpathSync as realpathSync3 } from "fs";
1755
+ import { existsSync as existsSync8, readFileSync as readFileSync10, realpathSync as realpathSync3 } from "fs";
1726
1756
  import { dirname as dirname6, join as join8, resolve as resolve6 } from "path";
1727
1757
 
1728
1758
  // src/cli/lib/package-manager.ts
1729
- import { existsSync as existsSync7, readFileSync as readFileSync10 } from "fs";
1759
+ import { existsSync as existsSync7, readFileSync as readFileSync9 } from "fs";
1730
1760
  import { join as join7 } from "path";
1731
1761
  function detectPackageManager(appDir) {
1732
1762
  try {
1733
- const pkg2 = JSON.parse(readFileSync10(join7(appDir, "package.json"), "utf-8"));
1763
+ const pkg2 = JSON.parse(readFileSync9(join7(appDir, "package.json"), "utf-8"));
1734
1764
  const name = typeof pkg2.packageManager === "string" ? pkg2.packageManager.split("@")[0] : "";
1735
1765
  if (name === "npm" || name === "pnpm" || name === "yarn" || name === "bun") return name;
1736
1766
  } catch {
@@ -1768,7 +1798,7 @@ function ensureInstallReady(appDir) {
1768
1798
  const donePath = join8(sentinelDir, "install.done");
1769
1799
  const logPath = join8(sentinelDir, "install.log");
1770
1800
  if (existsSync8(errPath)) {
1771
- const detail = readFileSync11(errPath, "utf-8").trimEnd();
1801
+ const detail = readFileSync10(errPath, "utf-8").trimEnd();
1772
1802
  const log29 = existsSync8(logPath) ? ` Full log: ${logPath}.` : "";
1773
1803
  throw new Refusal(
1774
1804
  `Dependency install failed: ${detail}.${log29} Run \`npm install\` (or \`bun install\`) manually, then retry.`,
@@ -1824,7 +1854,7 @@ function canonicalPath2(path) {
1824
1854
  function installWorkerAlive(sentinelDir) {
1825
1855
  const pidPath = join8(sentinelDir, "install.pid");
1826
1856
  if (!existsSync8(pidPath)) return false;
1827
- const pid = Number(readFileSync11(pidPath, "utf-8").trim());
1857
+ const pid = Number(readFileSync10(pidPath, "utf-8").trim());
1828
1858
  if (!Number.isSafeInteger(pid) || pid <= 0) return false;
1829
1859
  try {
1830
1860
  process.kill(pid, 0);
@@ -2180,11 +2210,7 @@ async function secretsApi(deployUrl, token, path, init = {}) {
2180
2210
  });
2181
2211
  const body = await res.json().catch(() => ({}));
2182
2212
  if (!res.ok) {
2183
- const err = new Error(body.error ?? `Secrets request failed (${res.status})`);
2184
- err.status = res.status;
2185
- err.code = body.code;
2186
- err.apiPath = path;
2187
- throw err;
2213
+ throw new ApiError(body.error ?? `Secrets request failed (${res.status})`, res.status, body.code, path);
2188
2214
  }
2189
2215
  return body;
2190
2216
  }
@@ -2447,6 +2473,23 @@ function lintSchema(schema) {
2447
2473
  }
2448
2474
  return warnings;
2449
2475
  }
2476
+ function lintSchemas(schemas) {
2477
+ return [...schemas.flatMap((schema) => lintSchema(schema)), ...lintSchemaSet(schemas)];
2478
+ }
2479
+ function lintSchemaSet(schemas) {
2480
+ const warnings = [];
2481
+ if (!schemas.some((schema) => schema.name === "team_members")) {
2482
+ for (const schema of schemas) {
2483
+ const teamRoles = Object.entries(schema.permissions).filter(([, p35]) => [p35.read, p35.update, p35.delete].includes("team")).map(([role]) => role);
2484
+ if (teamRoles.length > 0) {
2485
+ warnings.push(
2486
+ `[${schema.name}] role(s) ${teamRoles.join(", ")} use the 'team' permission level, but no 'team_members' collection is registered. 'team' resolves membership from team_members (teamId, userId, status), so without it those roles are denied everything \u2014 silently. Register a team_members schema or use another level.`
2487
+ );
2488
+ }
2489
+ }
2490
+ }
2491
+ return warnings;
2492
+ }
2450
2493
 
2451
2494
  // src/cli/lib/schema-lint.ts
2452
2495
  async function lintProjectSchemas(appDir) {
@@ -2475,15 +2518,20 @@ async function lintProjectSchemas(appDir) {
2475
2518
  return null;
2476
2519
  }
2477
2520
  if (!Array.isArray(schemas)) return null;
2478
- const findings = [];
2479
- for (const schema of schemas) {
2480
- if (!schema || typeof schema !== "object") continue;
2521
+ const wellFormed = schemas.filter((schema) => {
2522
+ if (!schema || typeof schema !== "object") return false;
2481
2523
  try {
2482
- findings.push(...lintSchema(schema));
2524
+ lintSchema(schema);
2525
+ return true;
2483
2526
  } catch {
2527
+ return false;
2484
2528
  }
2529
+ });
2530
+ try {
2531
+ return lintSchemas(wellFormed);
2532
+ } catch {
2533
+ return null;
2485
2534
  }
2486
- return findings;
2487
2535
  }
2488
2536
  var MAX_PRINTED_FINDINGS = 5;
2489
2537
  function formatSchemaLintFindings(findings) {
@@ -2503,11 +2551,11 @@ import { createServer, Socket } from "net";
2503
2551
  var DEFAULT_PORT = 5173;
2504
2552
  function isPortListening(port, host, timeoutMs = 500) {
2505
2553
  const target2 = host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host;
2506
- return new Promise((resolve21) => {
2554
+ return new Promise((resolve22) => {
2507
2555
  const socket = new Socket();
2508
2556
  const done = (listening) => {
2509
2557
  socket.destroy();
2510
- resolve21(listening);
2558
+ resolve22(listening);
2511
2559
  };
2512
2560
  socket.setTimeout(timeoutMs);
2513
2561
  socket.once("connect", () => done(true));
@@ -2520,7 +2568,7 @@ async function waitForPortListening(port, host, timeoutMs, signal) {
2520
2568
  const deadline = Date.now() + timeoutMs;
2521
2569
  while (Date.now() < deadline && !signal?.aborted) {
2522
2570
  if (await isPortListening(port, host)) return true;
2523
- await new Promise((resolve21) => setTimeout(resolve21, 250));
2571
+ await new Promise((resolve22) => setTimeout(resolve22, 250));
2524
2572
  }
2525
2573
  return false;
2526
2574
  }
@@ -2738,7 +2786,7 @@ Free it with \`${killCmd}\`, or start on another port: \`deepspace dev start --p
2738
2786
 
2739
2787
  // src/cli/commands/kill.ts
2740
2788
  import { spawnSync as spawnSync2 } from "child_process";
2741
- import { readFileSync as readFileSync12, readdirSync as readdirSync3, readlinkSync } from "fs";
2789
+ import { readFileSync as readFileSync11, readdirSync as readdirSync3, readlinkSync } from "fs";
2742
2790
  import { setTimeout as wait } from "timers/promises";
2743
2791
  var SIGTERM_GRACE_MS = 1500;
2744
2792
  var IS_WIN = process.platform === "win32";
@@ -2889,7 +2937,7 @@ function listenerPidsViaProc(port) {
2889
2937
  for (const f of ["/proc/net/tcp", "/proc/net/tcp6"]) {
2890
2938
  let data;
2891
2939
  try {
2892
- data = readFileSync12(f, "utf-8");
2940
+ data = readFileSync11(f, "utf-8");
2893
2941
  } catch {
2894
2942
  continue;
2895
2943
  }
@@ -3027,7 +3075,8 @@ function isAlive(pid) {
3027
3075
  }
3028
3076
 
3029
3077
  // src/cli/commands/test.ts
3030
- import { resolve as resolve9 } from "path";
3078
+ import { readdirSync as readdirSync4 } from "fs";
3079
+ import { join as join13, relative, resolve as resolve9, sep as sep2 } from "path";
3031
3080
  import { sync as spawnSync4 } from "cross-spawn";
3032
3081
 
3033
3082
  // src/cli/lib/playwright.ts
@@ -3056,10 +3105,10 @@ function ensurePlaywright(appDir) {
3056
3105
  }
3057
3106
 
3058
3107
  // src/cli/lib/test-account-service.ts
3059
- import { readFileSync as readFileSync14 } from "fs";
3108
+ import { readFileSync as readFileSync13 } from "fs";
3060
3109
 
3061
3110
  // src/testing/accounts.ts
3062
- import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync13 } from "fs";
3111
+ import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync12 } from "fs";
3063
3112
  import { homedir as homedir2 } from "os";
3064
3113
  import { join as join12 } from "path";
3065
3114
  var TEST_ACCOUNTS_PATH = join12(homedir2(), ".deepspace", "test-accounts.json");
@@ -3129,7 +3178,7 @@ function removeTestAccounts(ids, emails = [], scope = currentTestAccountScope())
3129
3178
  function readTestAccountStore() {
3130
3179
  if (!existsSync11(TEST_ACCOUNTS_PATH)) return { version: 2, scopes: {} };
3131
3180
  try {
3132
- const raw = JSON.parse(readFileSync13(TEST_ACCOUNTS_PATH, "utf-8"));
3181
+ const raw = JSON.parse(readFileSync12(TEST_ACCOUNTS_PATH, "utf-8"));
3133
3182
  if (Array.isArray(raw)) return { version: 2, scopes: {} };
3134
3183
  if (!raw || typeof raw !== "object") {
3135
3184
  return { version: 2, scopes: {} };
@@ -3160,7 +3209,7 @@ function saveTestAccountStore(store) {
3160
3209
  var SESSION_COOKIE2 = "__Secure-better-auth.session_token";
3161
3210
  var AUTH_URL4 = process.env.DEEPSPACE_AUTH_URL ?? PLATFORM_URLS.auth;
3162
3211
  function sessionCookie() {
3163
- const token = readFileSync14(SESSION_PATH, "utf-8").trim();
3212
+ const token = readFileSync13(SESSION_PATH, "utf-8").trim();
3164
3213
  return `${SESSION_COOKIE2}=${encodeURIComponent(token)}`;
3165
3214
  }
3166
3215
  async function fetchRemoteTestAccounts() {
@@ -3208,6 +3257,7 @@ async function recoverRemoteTestAccount(id) {
3208
3257
  email: data.email,
3209
3258
  userId: data.userId,
3210
3259
  label: data.label ?? null,
3260
+ name: typeof data.name === "string" ? data.name : null,
3211
3261
  createdAt: data.createdAt ?? Date.now(),
3212
3262
  password: data.password
3213
3263
  };
@@ -3231,6 +3281,28 @@ async function syncTestAccountStore() {
3231
3281
  // src/cli/commands/test.ts
3232
3282
  var DEPLOY_URL2 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
3233
3283
  var PLAYWRIGHT_OUTPUT_DIR = ".deepspace/test-results";
3284
+ var DEFAULT_SUITE_SPECS = ["tests/smoke.spec.ts", "tests/api.spec.ts"];
3285
+ function specsSkippedByDefaultSuite(appDir) {
3286
+ const found = [];
3287
+ const walk = (dir) => {
3288
+ let entries;
3289
+ try {
3290
+ entries = readdirSync4(dir, { withFileTypes: true });
3291
+ } catch {
3292
+ return;
3293
+ }
3294
+ for (const entry of entries) {
3295
+ if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
3296
+ const full = join13(dir, entry.name);
3297
+ if (entry.isDirectory()) walk(full);
3298
+ else if (entry.name.endsWith(".spec.ts")) {
3299
+ found.push(relative(appDir, full).split(sep2).join("/"));
3300
+ }
3301
+ }
3302
+ };
3303
+ walk(join13(appDir, "tests"));
3304
+ return found.filter((spec) => !DEFAULT_SUITE_SPECS.includes(spec)).sort();
3305
+ }
3234
3306
  function playwrightTestArgs(testFiles, flags = {}) {
3235
3307
  return [
3236
3308
  "playwright",
@@ -3250,7 +3322,7 @@ var test_default = defineDeepspaceCommand({
3250
3322
  name: "test",
3251
3323
  description: "Run tests for your DeepSpace app"
3252
3324
  },
3253
- jsonDescription: "Stream test output; emit the final JSON result on the last line",
3325
+ jsonDescription: "Stream test output on stderr; stdout is the single-line JSON result",
3254
3326
  args: {
3255
3327
  suite: {
3256
3328
  type: "positional",
@@ -3285,6 +3357,7 @@ var test_default = defineDeepspaceCommand({
3285
3357
  }
3286
3358
  },
3287
3359
  async run({ args }) {
3360
+ suiteOutputToStderr = args.json === true;
3288
3361
  const say = (line) => {
3289
3362
  if (!args.json) console.log(line);
3290
3363
  };
@@ -3350,6 +3423,7 @@ var test_default = defineDeepspaceCommand({
3350
3423
  ensurePlaywright(appDir);
3351
3424
  }
3352
3425
  let exitCode = 0;
3426
+ let skippedSpecs = [];
3353
3427
  switch (suite) {
3354
3428
  case "smoke":
3355
3429
  exitCode = runPlaywright(appDir, ["tests/smoke.spec.ts"], port, wranglerEnv, forwarded);
@@ -3371,13 +3445,13 @@ var test_default = defineDeepspaceCommand({
3371
3445
  if (exitCode === 0) exitCode = runPlaywright(appDir, [], port, wranglerEnv, forwarded);
3372
3446
  break;
3373
3447
  case "default":
3374
- exitCode = runPlaywright(
3375
- appDir,
3376
- ["tests/smoke.spec.ts", "tests/api.spec.ts"],
3377
- port,
3378
- wranglerEnv,
3379
- forwarded
3380
- );
3448
+ skippedSpecs = specsSkippedByDefaultSuite(appDir);
3449
+ exitCode = runPlaywright(appDir, [...DEFAULT_SUITE_SPECS], port, wranglerEnv, forwarded);
3450
+ if (skippedSpecs.length > 0) {
3451
+ say(
3452
+ `skipped ${skippedSpecs.length} spec file(s) not in smoke+api (${skippedSpecs.join(", ")}) \u2014 run 'deepspace test run all'`
3453
+ );
3454
+ }
3381
3455
  break;
3382
3456
  default:
3383
3457
  if (suite.endsWith(".spec.ts")) {
@@ -3392,12 +3466,13 @@ Available: smoke, api, e2e, unit, all`,
3392
3466
  }
3393
3467
  if (exitCode !== 0) {
3394
3468
  throw new Refusal(`Test suite '${suite}' failed (exit ${exitCode}).`, "tests_failed", {
3395
- extra: { suite, port, exitCode }
3469
+ extra: { suite, port, exitCode, skippedSpecs }
3396
3470
  });
3397
3471
  }
3398
- return { data: { suite, port, appDir, wranglerEnv: wranglerEnv ?? null } };
3472
+ return { data: { suite, port, appDir, wranglerEnv: wranglerEnv ?? null, skippedSpecs } };
3399
3473
  }
3400
3474
  });
3475
+ var suiteOutputToStderr = false;
3401
3476
  function runSuite(appDir, argv, wranglerEnv, extraEnv = {}) {
3402
3477
  let wranglerConfig;
3403
3478
  try {
@@ -3409,7 +3484,9 @@ function runSuite(appDir, argv, wranglerEnv, extraEnv = {}) {
3409
3484
  try {
3410
3485
  const result = spawnSync4("npx", argv, {
3411
3486
  cwd: appDir,
3412
- stdio: "inherit",
3487
+ // Under --json the child's stdout is routed to our stderr: the live
3488
+ // suite output stays visible and stdout stays a single JSON line.
3489
+ stdio: suiteOutputToStderr ? ["inherit", 2, 2] : "inherit",
3413
3490
  env: wranglerViteEnv(process.env, wranglerConfig, extraEnv)
3414
3491
  });
3415
3492
  return result.status ?? 1;
@@ -3829,7 +3906,7 @@ var recover = defineDeepspaceCommand({
3829
3906
  for (const target2 of targets) {
3830
3907
  try {
3831
3908
  const account = await recoverRemoteTestAccount(target2.id);
3832
- const name = localNames.get(account.email);
3909
+ const name = account.name ?? localNames.get(account.email);
3833
3910
  upsertTestAccount({
3834
3911
  id: account.id,
3835
3912
  email: account.email,
@@ -3880,7 +3957,7 @@ var test_accounts_default = defineCommand2({
3880
3957
  // src/cli/commands/deploy.ts
3881
3958
  import * as p12 from "@clack/prompts";
3882
3959
  import { defineCommand as defineCommand3 } from "citty";
3883
- import { resolve as resolve13 } from "path";
3960
+ import { join as join19, resolve as resolve14 } from "path";
3884
3961
 
3885
3962
  // src/cli/lib/edge-propagation.ts
3886
3963
  import { request as httpRequest } from "http";
@@ -3945,14 +4022,14 @@ async function servesRelease(probe, releaseStamp) {
3945
4022
  }
3946
4023
  function getOnFreshConnection(url, timeoutMs = 5e3) {
3947
4024
  const send = url.protocol === "http:" ? httpRequest : httpsRequest;
3948
- return new Promise((resolve21) => {
4025
+ return new Promise((resolve22) => {
3949
4026
  const request2 = send(
3950
4027
  url,
3951
4028
  { method: "GET", agent: false, timeout: timeoutMs, headers: { "cache-control": "no-cache" } },
3952
4029
  (response) => {
3953
4030
  if (response.statusCode !== 200) {
3954
4031
  response.resume();
3955
- resolve21(null);
4032
+ resolve22(null);
3956
4033
  return;
3957
4034
  }
3958
4035
  let body = "";
@@ -3960,20 +4037,20 @@ function getOnFreshConnection(url, timeoutMs = 5e3) {
3960
4037
  response.on("data", (chunk) => {
3961
4038
  if (body.length < 4096) body += chunk;
3962
4039
  });
3963
- response.on("end", () => resolve21(body));
3964
- response.on("error", () => resolve21(null));
4040
+ response.on("end", () => resolve22(body));
4041
+ response.on("error", () => resolve22(null));
3965
4042
  }
3966
4043
  );
3967
4044
  request2.on("timeout", () => {
3968
4045
  request2.destroy();
3969
- resolve21(null);
4046
+ resolve22(null);
3970
4047
  });
3971
- request2.on("error", () => resolve21(null));
4048
+ request2.on("error", () => resolve22(null));
3972
4049
  request2.end();
3973
4050
  });
3974
4051
  }
3975
4052
  function sleep2(ms) {
3976
- return new Promise((resolve21) => setTimeout(resolve21, ms));
4053
+ return new Promise((resolve22) => setTimeout(resolve22, ms));
3977
4054
  }
3978
4055
 
3979
4056
  // src/shared/app-files.ts
@@ -4038,36 +4115,49 @@ function describeFilesFailure(status3, body) {
4038
4115
  import * as p6 from "@clack/prompts";
4039
4116
  import { execSync as execSync2 } from "child_process";
4040
4117
  import { createHash as createHash2 } from "crypto";
4041
- import { existsSync as existsSync14, lstatSync as lstatSync2, readFileSync as readFileSync17, readdirSync as readdirSync4, unlinkSync as unlinkSync2 } from "fs";
4042
- import { dirname as dirname7, join as join15, resolve as resolve11 } from "path";
4118
+ import { existsSync as existsSync15, readFileSync as readFileSync16, readdirSync as readdirSync6 } from "fs";
4119
+ import { dirname as dirname7, join as join17, resolve as resolve12 } from "path";
4120
+
4121
+ // src/build/plugin.ts
4122
+ import { existsSync as existsSync12, lstatSync, readdirSync as readdirSync5, unlinkSync as unlinkSync2 } from "fs";
4123
+ import { join as join14, resolve as resolve11 } from "path";
4124
+ function removeBuildDevVars(workerDir) {
4125
+ const path = join14(workerDir, ".dev.vars");
4126
+ if (!existsSync12(path)) return false;
4127
+ if (!lstatSync(path).isFile()) {
4128
+ throw new Error(`Refusing unsafe build secret path: ${path}`);
4129
+ }
4130
+ unlinkSync2(path);
4131
+ return true;
4132
+ }
4043
4133
 
4044
4134
  // src/documentation/deploy.ts
4045
- import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
4046
- import { join as join13 } from "path";
4135
+ import { existsSync as existsSync13, readFileSync as readFileSync14 } from "fs";
4136
+ import { join as join15 } from "path";
4047
4137
  function readDocumentationDeployManifest(appDir, clientDir) {
4048
- if (!existsSync12(join13(appDir, "documentation.json"))) return void 0;
4049
- const manifestPath = join13(clientDir, "_documentation", "manifest.json");
4050
- if (!existsSync12(manifestPath)) {
4138
+ if (!existsSync13(join15(appDir, "documentation.json"))) return void 0;
4139
+ const manifestPath = join15(clientDir, "_documentation", "manifest.json");
4140
+ if (!existsSync13(manifestPath)) {
4051
4141
  throw new Error(
4052
4142
  "documentation.json exists but the build produced no documentation manifest. Ensure vite.config.ts includes deepSpaceDocumentation() from deepspace/documentation."
4053
4143
  );
4054
4144
  }
4055
- return JSON.parse(readFileSync15(manifestPath, "utf8"));
4145
+ return JSON.parse(readFileSync14(manifestPath, "utf8"));
4056
4146
  }
4057
4147
 
4058
4148
  // src/cli/commands/update/app-migrations.ts
4059
4149
  import {
4060
4150
  closeSync,
4061
4151
  constants,
4062
- existsSync as existsSync13,
4152
+ existsSync as existsSync14,
4063
4153
  fstatSync,
4064
4154
  ftruncateSync,
4065
- lstatSync,
4155
+ lstatSync as lstatSync2,
4066
4156
  openSync,
4067
- readFileSync as readFileSync16,
4157
+ readFileSync as readFileSync15,
4068
4158
  writeSync
4069
4159
  } from "fs";
4070
- import { join as join14 } from "path";
4160
+ import { join as join16 } from "path";
4071
4161
 
4072
4162
  // src/shared/protocol/app-migrations.ts
4073
4163
  var APP_MIGRATION_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/;
@@ -4119,8 +4209,8 @@ var APP_MIGRATION_DEFINITIONS = SOURCE_MIGRATIONS.map(
4119
4209
  ({ id, description }) => ({ id, description })
4120
4210
  );
4121
4211
  function readAppliedAppMigrations(appDir) {
4122
- const path = join14(appDir, APP_MIGRATIONS_MANIFEST);
4123
- if (!existsSync13(path)) return [];
4212
+ const path = join16(appDir, APP_MIGRATIONS_MANIFEST);
4213
+ if (!existsSync14(path)) return [];
4124
4214
  const source = readRegularFile(path);
4125
4215
  let parsed;
4126
4216
  try {
@@ -4146,10 +4236,10 @@ function planAppMigrations(appDir) {
4146
4236
  const edits = [];
4147
4237
  for (const repoPath of tracked) {
4148
4238
  if (!isSourcePath(repoPath, appPrefix)) continue;
4149
- const absolutePath = join14(repo, repoPath);
4239
+ const absolutePath = join16(repo, repoPath);
4150
4240
  let stat;
4151
4241
  try {
4152
- stat = lstatSync(absolutePath);
4242
+ stat = lstatSync2(absolutePath);
4153
4243
  } catch {
4154
4244
  continue;
4155
4245
  }
@@ -4183,8 +4273,8 @@ function planAppMigrations(appDir) {
4183
4273
  });
4184
4274
  if (pending.length > 0 && blockers.length === 0) {
4185
4275
  const manifestPath = `${appPrefix}${APP_MIGRATIONS_MANIFEST}`;
4186
- const absoluteManifestPath = join14(repo, manifestPath);
4187
- const before = existsSync13(absoluteManifestPath) ? readRegularFile(absoluteManifestPath) : null;
4276
+ const absoluteManifestPath = join16(repo, manifestPath);
4277
+ const before = existsSync14(absoluteManifestPath) ? readRegularFile(absoluteManifestPath) : null;
4188
4278
  const after = `${JSON.stringify([...appliedIds, ...pending.map(({ id }) => id)], null, 2)}
4189
4279
  `;
4190
4280
  if (before !== after) edits.push({ path: manifestPath, before, after });
@@ -4199,13 +4289,13 @@ function planAppMigrations(appDir) {
4199
4289
  function applyAppMigrationPlan(appDir, plan) {
4200
4290
  const repo = repoToplevel(appDir);
4201
4291
  for (const edit of plan.edits) {
4202
- const absolutePath = join14(repo, edit.path);
4203
- const current = existsSync13(absolutePath) ? readRegularFile(absolutePath) : null;
4292
+ const absolutePath = join16(repo, edit.path);
4293
+ const current = existsSync14(absolutePath) ? readRegularFile(absolutePath) : null;
4204
4294
  if (current !== edit.before) {
4205
4295
  throw new Error(`Migration input changed after planning: ${edit.path}`);
4206
4296
  }
4207
4297
  }
4208
- for (const edit of plan.edits) writeRegularFile(join14(repo, edit.path), edit.before, edit.after);
4298
+ for (const edit of plan.edits) writeRegularFile(join16(repo, edit.path), edit.before, edit.after);
4209
4299
  }
4210
4300
  function normalizeEol(source) {
4211
4301
  return source.replaceAll("\r\n", "\n");
@@ -4214,18 +4304,18 @@ function restoreEol(source, checkout) {
4214
4304
  return checkout.includes("\r\n") ? source.replaceAll("\n", "\r\n") : source;
4215
4305
  }
4216
4306
  function readRegularFile(path) {
4217
- const before = lstatSync(path);
4307
+ const before = lstatSync2(path);
4218
4308
  if (!before.isFile() || before.isSymbolicLink()) {
4219
4309
  throw new Error(`Migration input must be a regular file, not a symlink: ${path}`);
4220
4310
  }
4221
4311
  const fd = openSync(path, constants.O_RDONLY);
4222
4312
  try {
4223
4313
  const opened = fstatSync(fd);
4224
- const after = lstatSync(path);
4314
+ const after = lstatSync2(path);
4225
4315
  if (!opened.isFile() || opened.dev !== after.dev || opened.ino !== after.ino) {
4226
4316
  throw new Error(`Migration input changed while opening: ${path}`);
4227
4317
  }
4228
- return readFileSync16(fd, "utf-8");
4318
+ return readFileSync15(fd, "utf-8");
4229
4319
  } finally {
4230
4320
  closeSync(fd);
4231
4321
  }
@@ -4240,18 +4330,18 @@ function writeRegularFile(path, before, after) {
4240
4330
  }
4241
4331
  return;
4242
4332
  }
4243
- const pathStat = lstatSync(path);
4333
+ const pathStat = lstatSync2(path);
4244
4334
  if (!pathStat.isFile() || pathStat.isSymbolicLink()) {
4245
4335
  throw new Error(`Migration input must be a regular file, not a symlink: ${path}`);
4246
4336
  }
4247
4337
  const fd = openSync(path, constants.O_RDWR);
4248
4338
  try {
4249
4339
  const opened = fstatSync(fd);
4250
- const currentPath = lstatSync(path);
4340
+ const currentPath = lstatSync2(path);
4251
4341
  if (!opened.isFile() || opened.dev !== currentPath.dev || opened.ino !== currentPath.ino) {
4252
4342
  throw new Error(`Migration input changed while opening: ${path}`);
4253
4343
  }
4254
- if (readFileSync16(fd, "utf-8") !== before) {
4344
+ if (readFileSync15(fd, "utf-8") !== before) {
4255
4345
  throw new Error(`Migration input changed after planning: ${path}`);
4256
4346
  }
4257
4347
  ftruncateSync(fd, 0);
@@ -4578,16 +4668,6 @@ function findWorkerOwnedNotFoundBlockers(source, file) {
4578
4668
 
4579
4669
  // src/cli/commands/deploy/build.ts
4580
4670
  var RESERVED_RUN_WORKER_FIRST2 = new Set(RESERVED_RUN_WORKER_FIRST);
4581
- function removeBuildDevVars(workerDir) {
4582
- const path = join15(workerDir, ".dev.vars");
4583
- if (!existsSync14(path)) return false;
4584
- const stat = lstatSync2(path);
4585
- if (!stat.isFile() || stat.isSymbolicLink()) {
4586
- throw new Error(`Refusing unsafe build secret path: ${path}`);
4587
- }
4588
- unlinkSync2(path);
4589
- return true;
4590
- }
4591
4671
  function declaredCompatibilityDate(config) {
4592
4672
  const declared = config.compatibility_date;
4593
4673
  return typeof declared === "string" && /^\d{4}-\d{2}-\d{2}$/.test(declared) ? declared : null;
@@ -4624,19 +4704,19 @@ async function buildDeployBundle(options) {
4624
4704
  preparedWranglerConfig?.cleanup();
4625
4705
  }
4626
4706
  spinner2.stop(`Built (${Math.round((Date.now() - buildStarted) / 1e3)}s)`);
4627
- const deployConfigPath = join15(appDir, ".wrangler", "deploy", "config.json");
4628
- if (!existsSync14(deployConfigPath)) {
4707
+ const deployConfigPath = join17(appDir, ".wrangler", "deploy", "config.json");
4708
+ if (!existsSync15(deployConfigPath)) {
4629
4709
  output.die(
4630
4710
  "Build output config not found at .wrangler/deploy/config.json",
4631
4711
  "build_output_missing"
4632
4712
  );
4633
4713
  }
4634
- const deployConfig = JSON.parse(readFileSync17(deployConfigPath, "utf-8"));
4635
- const outputWranglerPath = resolve11(dirname7(deployConfigPath), deployConfig.configPath);
4636
- if (!existsSync14(outputWranglerPath)) {
4714
+ const deployConfig = JSON.parse(readFileSync16(deployConfigPath, "utf-8"));
4715
+ const outputWranglerPath = resolve12(dirname7(deployConfigPath), deployConfig.configPath);
4716
+ if (!existsSync15(outputWranglerPath)) {
4637
4717
  output.die(`Output wrangler.json not found at ${outputWranglerPath}`, "build_output_missing");
4638
4718
  }
4639
- const outputConfig = JSON.parse(readFileSync17(outputWranglerPath, "utf-8"));
4719
+ const outputConfig = JSON.parse(readFileSync16(outputWranglerPath, "utf-8"));
4640
4720
  if (outputConfig.name && outputConfig.name !== appName) {
4641
4721
  const envHint = envName ? ` (--env ${envName})` : "";
4642
4722
  output.die(
@@ -4650,12 +4730,12 @@ async function buildDeployBundle(options) {
4650
4730
  } catch (error) {
4651
4731
  output.die(errorMessage(error), "build_output_unsafe");
4652
4732
  }
4653
- const workerBundlePath = join15(workerDir, outputConfig.main);
4654
- const clientDir = outputConfig.assets?.directory ? resolve11(workerDir, outputConfig.assets.directory) : null;
4655
- if (!existsSync14(workerBundlePath)) {
4733
+ const workerBundlePath = join17(workerDir, outputConfig.main);
4734
+ const clientDir = outputConfig.assets?.directory ? resolve12(workerDir, outputConfig.assets.directory) : null;
4735
+ if (!existsSync15(workerBundlePath)) {
4656
4736
  output.die(`Worker bundle not found at ${workerBundlePath}`, "build_output_missing");
4657
4737
  }
4658
- if (!clientDir || !existsSync14(clientDir)) {
4738
+ if (!clientDir || !existsSync15(clientDir)) {
4659
4739
  return output.die(`Client assets not found at ${clientDir}`, "build_output_missing");
4660
4740
  }
4661
4741
  try {
@@ -4670,7 +4750,7 @@ async function buildDeployBundle(options) {
4670
4750
  spinner2.stop(`Collected ${assets.length} assets`);
4671
4751
  const appIdRefusal = clientAppIdRefusal(assets, appId, envName);
4672
4752
  if (appIdRefusal) output.die(appIdRefusal.message, appIdRefusal.code);
4673
- const workerJs = readFileSync17(workerBundlePath, "utf-8");
4753
+ const workerJs = readFileSync16(workerBundlePath, "utf-8");
4674
4754
  const doBindings = outputConfig.durable_objects?.bindings;
4675
4755
  const sqliteClasses = new Set(
4676
4756
  outputConfig.migrations?.flatMap((migration) => migration.new_sqlite_classes ?? []) ?? []
@@ -4753,7 +4833,7 @@ function clientAppIdRefusal(assets, appId, envName) {
4753
4833
  let unsubstituted;
4754
4834
  for (const asset of assets) {
4755
4835
  if (!CLIENT_CODE_ASSET.test(asset.path)) continue;
4756
- const source = readFileSync17(asset.sourcePath, "utf-8");
4836
+ const source = readFileSync16(asset.sourcePath, "utf-8");
4757
4837
  if (unsubstituted === void 0 && source.includes(APP_ID_DEFINE)) unsubstituted = asset.path;
4758
4838
  for (const found of source.matchAll(APP_ID_LITERAL)) {
4759
4839
  if (found[0] !== appId && !foreign.has(found[0])) foreign.set(found[0], asset.path);
@@ -4780,14 +4860,14 @@ var LARGE_MEDIA_HOME = "Large media belongs in your app files (`deepspace app fi
4780
4860
  function collectAssets(dir) {
4781
4861
  const assets = [];
4782
4862
  const walk = (currentDir, prefix) => {
4783
- for (const entry of readdirSync4(currentDir, { withFileTypes: true })) {
4863
+ for (const entry of readdirSync6(currentDir, { withFileTypes: true })) {
4784
4864
  if (isDeployAssetControlFile(entry.name)) continue;
4785
- const fullPath = join15(currentDir, entry.name);
4865
+ const fullPath = join17(currentDir, entry.name);
4786
4866
  const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
4787
4867
  if (entry.isDirectory()) {
4788
4868
  walk(fullPath, relativePath);
4789
4869
  } else {
4790
- const bytes = readFileSync17(fullPath);
4870
+ const bytes = readFileSync16(fullPath);
4791
4871
  assets.push({
4792
4872
  path: `/${relativePath}`,
4793
4873
  hash: createHash2("sha256").update(bytes).digest("hex"),
@@ -4806,9 +4886,9 @@ function readDeployAssetConfig(clientDir) {
4806
4886
  ["_headers", "_headers"],
4807
4887
  ["_redirects", "_redirects"]
4808
4888
  ]) {
4809
- const filePath = join15(clientDir, file);
4810
- if (!existsSync14(filePath)) continue;
4811
- const content = readFileSync17(filePath, "utf8");
4889
+ const filePath = join17(clientDir, file);
4890
+ if (!existsSync15(filePath)) continue;
4891
+ const content = readFileSync16(filePath, "utf8");
4812
4892
  if (content.trim()) config[key] = content;
4813
4893
  }
4814
4894
  return config;
@@ -4822,8 +4902,8 @@ function errorMessage(error) {
4822
4902
 
4823
4903
  // src/cli/commands/deploy/commerce.ts
4824
4904
  import * as p7 from "@clack/prompts";
4825
- import { existsSync as existsSync15 } from "fs";
4826
- import { join as join16 } from "path";
4905
+ import { existsSync as existsSync16 } from "fs";
4906
+ import { join as join18 } from "path";
4827
4907
  import { pathToFileURL as pathToFileURL2 } from "url";
4828
4908
 
4829
4909
  // src/shared/api-error.ts
@@ -4875,10 +4955,10 @@ function errorMessage2(error) {
4875
4955
  return error instanceof Error ? error.message : String(error);
4876
4956
  }
4877
4957
  async function loadDeclaration(appDir, sourceFile, outputFile, exportName) {
4878
- const sourcePath = join16(appDir, "src", sourceFile);
4879
- if (!existsSync15(sourcePath)) return void 0;
4958
+ const sourcePath = join18(appDir, "src", sourceFile);
4959
+ if (!existsSync16(sourcePath)) return void 0;
4880
4960
  const esbuild = await import("esbuild");
4881
- const outputPath = join16(appDir, ".wrangler", "deploy", outputFile);
4961
+ const outputPath = join18(appDir, ".wrangler", "deploy", outputFile);
4882
4962
  await esbuild.build({
4883
4963
  entryPoints: [sourcePath],
4884
4964
  bundle: true,
@@ -4971,7 +5051,7 @@ Stripe will keep charging them until you cancel. Cancel at the end of their curr
4971
5051
  }
4972
5052
  }
4973
5053
  async function syncSubscriptionPlans(appDir, appName, token, nonInteractive) {
4974
- if (!existsSync15(join16(appDir, "src", "subscriptions.ts"))) return;
5054
+ if (!existsSync16(join18(appDir, "src", "subscriptions.ts"))) return;
4975
5055
  let plans;
4976
5056
  try {
4977
5057
  plans = await loadDeclaration(
@@ -5026,7 +5106,7 @@ async function syncSubscriptionPlans(appDir, appName, token, nonInteractive) {
5026
5106
  }
5027
5107
  }
5028
5108
  async function syncOneTimeProducts(appDir, appName, token) {
5029
- if (!existsSync15(join16(appDir, "src", "products.ts"))) return;
5109
+ if (!existsSync16(join18(appDir, "src", "products.ts"))) return;
5030
5110
  let products;
5031
5111
  try {
5032
5112
  products = await loadDeclaration(
@@ -5128,7 +5208,7 @@ function createDeployOutput(json) {
5128
5208
 
5129
5209
  // src/cli/commands/deploy/request.ts
5130
5210
  import * as p10 from "@clack/prompts";
5131
- import { readFileSync as readFileSync18 } from "fs";
5211
+ import { readFileSync as readFileSync17 } from "fs";
5132
5212
 
5133
5213
  // src/cli/commands/deploy/repository.ts
5134
5214
  import * as p9 from "@clack/prompts";
@@ -5319,9 +5399,9 @@ function repoApi(deployUrl, token, appId) {
5319
5399
  }
5320
5400
 
5321
5401
  // src/cli/lib/vc-remote.ts
5322
- import { existsSync as existsSync16 } from "fs";
5402
+ import { existsSync as existsSync17 } from "fs";
5323
5403
  import { tmpdir } from "os";
5324
- import { isAbsolute as isAbsolute3, relative, resolve as resolve12 } from "path";
5404
+ import { isAbsolute as isAbsolute3, relative as relative2, resolve as resolve13 } from "path";
5325
5405
  function spaceRemoteName(environment = DEEPSPACE_ENV) {
5326
5406
  if (environment === "staging") return "space-staging";
5327
5407
  if (environment === "production") return "space";
@@ -5367,14 +5447,14 @@ function entryIsTransient(cwd) {
5367
5447
  const entry = process.argv[1];
5368
5448
  if (!entry) return true;
5369
5449
  if (/[\\/]_npx[\\/]/.test(entry) || entry.startsWith(tmpdir())) return true;
5370
- const rel = relative(resolve12(cwd), resolve12(entry));
5450
+ const rel = relative2(resolve13(cwd), resolve13(entry));
5371
5451
  return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel);
5372
5452
  }
5373
5453
  function helperEntryUsable(command) {
5374
5454
  const match = /^!(?:DEEPSPACE_ENV=(production|staging) )?'([^']*)' '([^']*)' git-credential$/.exec(command);
5375
5455
  const expectedEnvironment = DEEPSPACE_ENV === "production" ? void 0 : DEEPSPACE_ENV;
5376
5456
  if (!match || match[1] !== expectedEnvironment) return false;
5377
- return existsSync16(match[2]) && existsSync16(match[3]);
5457
+ return existsSync17(match[2]) && existsSync17(match[3]);
5378
5458
  }
5379
5459
  function credentialHelperCommand(environment = DEEPSPACE_ENV) {
5380
5460
  const entry = process.argv[1];
@@ -5509,12 +5589,6 @@ function classifyPushTransportFailure(error, cwd) {
5509
5589
  error: `A first push to this new app hit your plan's active-app quota. Use \`deepspace app list\` to choose an app to undeploy, or upgrade your plan, then retry.`
5510
5590
  };
5511
5591
  }
5512
- if (/(?:HTTP |error: )422\b/i.test(message)) {
5513
- return {
5514
- code: "source_managed_by_github",
5515
- error: "This app uses GitHub source. Manage commits with normal Git/GitHub; `deepspace deploy` ships the local working tree without changing Git."
5516
- };
5517
- }
5518
5592
  if (/(?:HTTP |error: )429\b/i.test(message)) {
5519
5593
  return {
5520
5594
  code: "rate_limited",
@@ -5524,6 +5598,12 @@ function classifyPushTransportFailure(error, cwd) {
5524
5598
  if (/(?:HTTP |error: )413\b/i.test(message)) {
5525
5599
  return { code: "push_too_large", error: pushTooLargeMessage(cwd) };
5526
5600
  }
5601
+ if (/(?:HTTP |error: )422\b/i.test(message)) {
5602
+ return {
5603
+ code: "source_managed_by_github",
5604
+ error: "This app's source is managed by GitHub, so pushes to the DeepSpace repo are refused. Push to the GitHub repository instead \u2014 `deepspace app status` names it."
5605
+ };
5606
+ }
5527
5607
  return null;
5528
5608
  }
5529
5609
  function pushToSpace(cwd, token, refspec, opts = {}) {
@@ -5895,7 +5975,7 @@ async function pushWithTransientRetry(push) {
5895
5975
  const message = errorMessage3(error);
5896
5976
  const transient = /(?:HTTP |error: )(?:429|503)\b/i.test(message);
5897
5977
  if (transient && attempt < backoffMs.length && Date.now() + backoffMs[attempt] < deadline) {
5898
- await new Promise((resolve21) => setTimeout(resolve21, backoffMs[attempt]));
5978
+ await new Promise((resolve22) => setTimeout(resolve22, backoffMs[attempt]));
5899
5979
  continue;
5900
5980
  }
5901
5981
  throw error;
@@ -5924,6 +6004,12 @@ function detachedHeadRefusal() {
5924
6004
  }
5925
6005
 
5926
6006
  // src/cli/commands/deploy/request.ts
6007
+ function renameRefusalMessage(rename2) {
6008
+ return `This deploy renames the app: ${rename2.fromHost} \u2192 ${rename2.toHost}. Confirmation needs an interactive terminal \u2014 re-run with --rename to approve the rename, or \`deepspace app init --new-id\` if you meant a separate app.`;
6009
+ }
6010
+ function renamePromptMessage(rename2) {
6011
+ return `This deploy renames the app: ${rename2.fromHost} \u2192 ${rename2.toHost}. The URL changes and the old one stops serving right away; data, secrets, and collaborators travel with it \u2014 the display name in \`src/constants.ts\` (\`APP_NAME\`) does not, update it yourself. (Meant a separate app? Run \`deepspace app init --new-id\` instead.) Rename?`;
6012
+ }
5927
6013
  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.";
5928
6014
  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.";
5929
6015
  var REQUIRED_ASSET_TRANSPORT = "content-addressed-v1";
@@ -6043,16 +6129,11 @@ async function deployBuiltBundle(options) {
6043
6129
  let body = await response.json().catch(() => ({}));
6044
6130
  if (response.status === 409 && body.code === "rename_required" && !confirmRename) {
6045
6131
  spinner2.stop("Rename confirmation needed");
6132
+ const rename2 = { fromHost: body.fromHost ?? "", toHost: body.toHost ?? "" };
6046
6133
  if (output.nonInteractive) {
6047
- await bail(
6048
- `This deploy renames the app: ${body.fromHost} \u2192 ${body.toHost}. Confirmation needs an interactive terminal \u2014 re-run with --rename to approve the rename, or \`deepspace app init --new-id\` if you meant a separate app.`,
6049
- null,
6050
- "rename_required"
6051
- );
6134
+ await bail(renameRefusalMessage(rename2), null, "rename_required");
6052
6135
  }
6053
- const confirmed = await p10.confirm({
6054
- message: `This deploy renames the app: ${body.fromHost} \u2192 ${body.toHost}. The URL changes and the old one stops serving right away; data, secrets, and collaborators travel with it. (Meant a separate app? Run \`deepspace app init --new-id\` instead.) Rename?`
6055
- });
6136
+ const confirmed = await p10.confirm({ message: renamePromptMessage(rename2) });
6056
6137
  if (p10.isCancel(confirmed) || !confirmed) await bail("Deploy cancelled.", null);
6057
6138
  confirmRename = true;
6058
6139
  spinner2.start(`Deploying ${appName}...`);
@@ -6179,7 +6260,7 @@ async function uploadDeployAssets(options) {
6179
6260
  // against the plan before it touches storage, and a stream body
6180
6261
  // would be sent chunked with no length at all; a retry also needs a
6181
6262
  // body it can send again.
6182
- body: readFileSync18(asset.sourcePath)
6263
+ body: readFileSync17(asset.sourcePath)
6183
6264
  })
6184
6265
  );
6185
6266
  if (!response.ok) {
@@ -6260,7 +6341,7 @@ async function postWithRetry(url, makeInit, {
6260
6341
  lastError = error;
6261
6342
  }
6262
6343
  if (attempt < attempts) {
6263
- await new Promise((resolve21) => setTimeout(resolve21, Math.min(8e3, 500 * 2 ** (attempt - 1))));
6344
+ await new Promise((resolve22) => setTimeout(resolve22, Math.min(8e3, 500 * 2 ** (attempt - 1))));
6264
6345
  }
6265
6346
  }
6266
6347
  if (lastResponse) return lastResponse;
@@ -6395,180 +6476,213 @@ var deploy_default = defineCommand3({
6395
6476
  },
6396
6477
  async run({ args }) {
6397
6478
  const output = createDeployOutput(args.json === true);
6398
- preflightNodeVersion("deploy");
6399
- const appDir = resolve13(args.dir ?? ".");
6400
- const selectorRefusal = blankSelectorRefusal(args);
6401
- if (selectorRefusal) output.die(selectorRefusal.error, selectorRefusal.code);
6402
- const envName = typeof args.env === "string" && args.env.trim() ? args.env.trim() : void 0;
6403
- if (!hasWranglerConfig(appDir)) {
6404
- output.die("No wrangler.toml found. Are you in a DeepSpace app directory?", "not_in_app_repo");
6405
- }
6406
6479
  try {
6407
- ensureInstallReady(appDir);
6480
+ await runDeploy(args, output);
6408
6481
  } catch (error) {
6409
- const refusal = error;
6410
- output.die(refusal.message ?? String(error), refusal.code ?? "deps_missing", {
6411
- action: refusal.action
6412
- });
6482
+ if (error instanceof CliExit) throw error;
6483
+ output.die(formatCliError(error), errorCode(error) ?? "deploy_failed");
6413
6484
  }
6414
- p12.intro("Deploying DeepSpace app");
6415
- output.showIntro();
6416
- const wranglerConfig = readWranglerConfig(appDir);
6417
- const nameResult = resolveAppNameForEnv(wranglerConfig, envName);
6418
- if (!nameResult.ok) return output.die(nameResult.reason, nameResult.code ?? "invalid_app_name");
6419
- const declaredName = envName ? wranglerConfig.env?.[envName]?.name : wranglerConfig.name;
6420
- if (declaredName !== nameResult.name) {
6421
- const location = envName ? `[env.${envName}].name` : "`name`";
6422
- output.die(
6423
- `wrangler.toml: ${location} "${declaredName}" is not in canonical form. Update it to "${nameResult.name}" and re-run.`,
6424
- "invalid_app_name"
6425
- );
6485
+ }
6486
+ });
6487
+ async function runDeploy(args, output) {
6488
+ preflightNodeVersion("deploy");
6489
+ const appDir = resolve14(args.dir ?? ".");
6490
+ const selectorRefusal = blankSelectorRefusal(args);
6491
+ if (selectorRefusal) output.die(selectorRefusal.error, selectorRefusal.code);
6492
+ const envName = typeof args.env === "string" && args.env.trim() ? args.env.trim() : void 0;
6493
+ if (!hasWranglerConfig(appDir)) {
6494
+ output.die(noWranglerConfigMessage(join19(appDir, "wrangler.toml")), "not_in_app_repo");
6495
+ }
6496
+ try {
6497
+ ensureInstallReady(appDir);
6498
+ } catch (error) {
6499
+ const refusal = error;
6500
+ output.die(refusal.message ?? String(error), refusal.code ?? "deps_missing", {
6501
+ action: refusal.action
6502
+ });
6503
+ }
6504
+ p12.intro("Deploying DeepSpace app");
6505
+ output.showIntro();
6506
+ const wranglerConfig = readWranglerConfig(appDir);
6507
+ const nameResult = resolveAppNameForEnv(wranglerConfig, envName);
6508
+ if (!nameResult.ok) return output.die(nameResult.reason, nameResult.code ?? "invalid_app_name");
6509
+ const declaredName = envName ? wranglerConfig.env?.[envName]?.name : wranglerConfig.name;
6510
+ if (declaredName !== nameResult.name) {
6511
+ const location = envName ? `[env.${envName}].name` : "`name`";
6512
+ output.die(
6513
+ `wrangler.toml: ${location} "${declaredName}" is not in canonical form. Update it to "${nameResult.name}" and re-run.`,
6514
+ "invalid_app_name"
6515
+ );
6516
+ }
6517
+ const appName = nameResult.name;
6518
+ const appId = readAppId(appDir, envName);
6519
+ p12.log.info(envName ? `App: ${appName} (env: ${envName})` : `App: ${appName}`);
6520
+ const { token, ownerId } = await authenticate(appDir, output);
6521
+ if (!appId) {
6522
+ return output.die(
6523
+ `No app id in wrangler.toml${envName ? ` for [env.${envName}]` : ""}. Run \`deepspace app init${envName ? ` --env ${envName}` : ""}\` to register this app, then re-deploy.`,
6524
+ "app_not_initialized",
6525
+ {
6526
+ action: {
6527
+ cwd: appDir,
6528
+ argv: ["deepspace", "app", "init", ...envName ? ["--env", envName] : []]
6529
+ },
6530
+ actionRequired: true
6531
+ }
6532
+ );
6533
+ }
6534
+ p12.log.info(`Id: ${appId}`);
6535
+ const sourceState = await getAppSource(DEPLOY_URL3, token, appId);
6536
+ if (!sourceState.registered) {
6537
+ output.die(
6538
+ `${appId} is not registered. If this repo's id came from an older SDK's scaffold, run \`deepspace app init --new-id\` to register it as a fresh app; a brand-new app dir registers with \`deepspace app init\`.`,
6539
+ "app_not_registered"
6540
+ );
6541
+ }
6542
+ const secretsCache = await loadDeploySecrets({
6543
+ deployUrl: DEPLOY_URL3,
6544
+ appDir,
6545
+ appId,
6546
+ envName,
6547
+ ownerId,
6548
+ token,
6549
+ output
6550
+ });
6551
+ const ownerJwtRefusal = ownerJwtMissingRefusal(sourceState);
6552
+ if (ownerJwtRefusal) output.die(ownerJwtRefusal.error, ownerJwtRefusal.code);
6553
+ let confirmRename = args.rename === true;
6554
+ const rename2 = pendingRename(sourceState.registeredHost, appName);
6555
+ if (rename2 && !confirmRename) {
6556
+ if (output.nonInteractive) {
6557
+ output.die(renameRefusalMessage(rename2), "rename_required");
6426
6558
  }
6427
- const appName = nameResult.name;
6428
- const appId = readAppId(appDir, envName);
6429
- p12.log.info(envName ? `App: ${appName} (env: ${envName})` : `App: ${appName}`);
6430
- const { token, ownerId } = await authenticate(appDir, output);
6431
- if (!appId) {
6432
- return output.die(
6433
- `No app id in wrangler.toml${envName ? ` for [env.${envName}]` : ""}. Run \`deepspace app init${envName ? ` --env ${envName}` : ""}\` to register this app, then re-deploy.`,
6434
- "app_not_initialized",
6435
- {
6436
- action: {
6437
- cwd: appDir,
6438
- argv: ["deepspace", "app", "init", ...envName ? ["--env", envName] : []]
6439
- },
6440
- actionRequired: true
6441
- }
6559
+ const confirmed = await p12.confirm({ message: renamePromptMessage(rename2) });
6560
+ if (p12.isCancel(confirmed) || !confirmed) output.die("Deploy cancelled.", "rename_declined");
6561
+ confirmRename = true;
6562
+ }
6563
+ const repositoryPreflight = preflightDeployRepository({
6564
+ appDir,
6565
+ push: args.push !== false,
6566
+ source: sourceState.source
6567
+ });
6568
+ if (repositoryPreflight) {
6569
+ output.die(repositoryPreflight.error, repositoryPreflight.code);
6570
+ }
6571
+ const spinner2 = createSpinner();
6572
+ const bundle = await buildDeployBundle({
6573
+ appDir,
6574
+ appName,
6575
+ appId,
6576
+ envName,
6577
+ output,
6578
+ spinner: spinner2
6579
+ });
6580
+ const oversized = oversizedAssetRefusal(bundle.assets);
6581
+ if (oversized) {
6582
+ spinner2.stop("Deploy failed");
6583
+ output.die(oversized, "assets_too_large");
6584
+ }
6585
+ const repository = await syncDeployRepository({
6586
+ deployUrl: DEPLOY_URL3,
6587
+ appDir,
6588
+ appId,
6589
+ token,
6590
+ push: args.push !== false,
6591
+ ignoreStale: Boolean(args["ignore-stale"]),
6592
+ output,
6593
+ sourceState
6594
+ });
6595
+ const secrets = prepareDeploySecrets({
6596
+ cache: secretsCache,
6597
+ customBindings: bundle.customBindings,
6598
+ doManifest: bundle.doManifest,
6599
+ output
6600
+ });
6601
+ const body = await deployBuiltBundle({
6602
+ deployUrl: DEPLOY_URL3,
6603
+ appDir,
6604
+ appId,
6605
+ appName,
6606
+ token,
6607
+ rename: confirmRename,
6608
+ claimReleased: args["claim-released"] === true,
6609
+ ignoreStale: Boolean(args["ignore-stale"]),
6610
+ bundle,
6611
+ secrets,
6612
+ repository,
6613
+ output,
6614
+ spinner: spinner2
6615
+ });
6616
+ let serving = "unverifiable";
6617
+ const edgeWaitStarted = Date.now();
6618
+ if (body.url) {
6619
+ spinner2.message("Waiting for the edge to serve this release...");
6620
+ serving = await waitForLiveRelease(body.url, body.releaseStamp, 9e4);
6621
+ if (serving !== "confirmed") {
6622
+ spinner2.stop(
6623
+ serving === "unconfirmed" ? "Deployed \u2014 the edge is still rolling this release out" : "Deployed \u2014 serving could not be verified from here"
6442
6624
  );
6443
- }
6444
- p12.log.info(`Id: ${appId}`);
6445
- const secretsCache = await loadDeploySecrets({
6446
- deployUrl: DEPLOY_URL3,
6447
- appDir,
6448
- appId,
6449
- envName,
6450
- ownerId,
6451
- token,
6452
- output
6453
- });
6454
- const sourceState = await getAppSource(DEPLOY_URL3, token, appId);
6455
- if (!sourceState.registered) {
6456
- output.die(
6457
- `${appId} is not registered. If this repo's id came from an older SDK's scaffold, run \`deepspace app init --new-id\` to register it as a fresh app; a brand-new app dir registers with \`deepspace app init\`.`,
6458
- "app_not_registered"
6625
+ p12.log.warn(
6626
+ serving === "unconfirmed" ? "Some requests still get the previous release. Cloudflare rolls a version out per edge machine; it usually settles within a couple of minutes. Wait before asserting against it." : "This release carries no serving stamp (older platform, or a resumed deploy), so the CLI cannot tell which release the edge answers with."
6459
6627
  );
6628
+ p12.log.info(`URL: ${body.url}`);
6629
+ await syncCommerce(appDir, appId, token, output.nonInteractive);
6630
+ if (output.json) {
6631
+ return output.emitJson({
6632
+ ok: true,
6633
+ appId,
6634
+ appName,
6635
+ url: body.url,
6636
+ releaseId: body.releaseId ?? null,
6637
+ bundleRetained: body.bundleRetained ?? null,
6638
+ serving,
6639
+ recoverable: repository.recoverable,
6640
+ ...staleBaseGuardFields(body)
6641
+ });
6642
+ }
6643
+ p12.outro("Done");
6644
+ return;
6460
6645
  }
6461
- const repositoryPreflight = preflightDeployRepository({
6462
- appDir,
6463
- push: args.push !== false,
6464
- source: sourceState.source
6465
- });
6466
- if (repositoryPreflight) {
6467
- output.die(repositoryPreflight.error, repositoryPreflight.code);
6468
- }
6469
- const spinner2 = createSpinner();
6470
- const bundle = await buildDeployBundle({
6471
- appDir,
6472
- appName,
6473
- appId,
6474
- envName,
6475
- output,
6476
- spinner: spinner2
6477
- });
6478
- const oversized = oversizedAssetRefusal(bundle.assets);
6479
- if (oversized) {
6480
- spinner2.stop("Deploy failed");
6481
- output.die(oversized, "assets_too_large");
6482
- }
6483
- const repository = await syncDeployRepository({
6484
- deployUrl: DEPLOY_URL3,
6485
- appDir,
6486
- appId,
6487
- token,
6488
- push: args.push !== false,
6489
- ignoreStale: Boolean(args["ignore-stale"]),
6490
- output,
6491
- sourceState
6492
- });
6493
- const secrets = prepareDeploySecrets({
6494
- cache: secretsCache,
6495
- customBindings: bundle.customBindings,
6496
- doManifest: bundle.doManifest,
6497
- output
6498
- });
6499
- const body = await deployBuiltBundle({
6500
- deployUrl: DEPLOY_URL3,
6501
- appDir,
6646
+ }
6647
+ spinner2.stop(
6648
+ `Deployed! (edge confirmed in ${Math.round((Date.now() - edgeWaitStarted) / 1e3)}s)`
6649
+ );
6650
+ p12.log.success(`Live at: ${body.url}`);
6651
+ await syncCommerce(appDir, appId, token, output.nonInteractive);
6652
+ if (output.json) {
6653
+ return output.emitJson({
6654
+ ok: true,
6502
6655
  appId,
6503
6656
  appName,
6504
- token,
6505
- rename: args.rename === true,
6506
- claimReleased: args["claim-released"] === true,
6507
- ignoreStale: Boolean(args["ignore-stale"]),
6508
- bundle,
6509
- secrets,
6510
- repository,
6511
- output,
6512
- spinner: spinner2
6657
+ url: body.url ?? null,
6658
+ // Always present, so a caller branches on one field instead of
6659
+ // inferring success from an absent one.
6660
+ serving,
6661
+ releaseId: body.releaseId ?? null,
6662
+ bundleRetained: body.bundleRetained ?? null,
6663
+ recoverable: repository.recoverable,
6664
+ ...staleBaseGuardFields(body)
6513
6665
  });
6514
- let serving = "unverifiable";
6515
- const edgeWaitStarted = Date.now();
6516
- if (body.url) {
6517
- spinner2.message("Waiting for the edge to serve this release...");
6518
- serving = await waitForLiveRelease(body.url, body.releaseStamp, 9e4);
6519
- if (serving !== "confirmed") {
6520
- spinner2.stop(
6521
- serving === "unconfirmed" ? "Deployed \u2014 the edge is still rolling this release out" : "Deployed \u2014 serving could not be verified from here"
6522
- );
6523
- p12.log.warn(
6524
- serving === "unconfirmed" ? "Some requests still get the previous release. Cloudflare rolls a version out per edge machine; it usually settles within a couple of minutes. Wait before asserting against it." : "This release carries no serving stamp (older platform, or a resumed deploy), so the CLI cannot tell which release the edge answers with."
6525
- );
6526
- p12.log.info(`URL: ${body.url}`);
6527
- await syncCommerce(appDir, appId, token, output.nonInteractive);
6528
- if (output.json) {
6529
- return output.emitJson({
6530
- ok: true,
6531
- appId,
6532
- appName,
6533
- url: body.url,
6534
- releaseId: body.releaseId ?? null,
6535
- bundleRetained: body.bundleRetained ?? null,
6536
- serving,
6537
- recoverable: repository.recoverable,
6538
- ...staleBaseGuardFields(body)
6539
- });
6540
- }
6541
- p12.outro("Done");
6542
- return;
6543
- }
6544
- }
6545
- spinner2.stop(
6546
- `Deployed! (edge confirmed in ${Math.round((Date.now() - edgeWaitStarted) / 1e3)}s)`
6547
- );
6548
- p12.log.success(`Live at: ${body.url}`);
6549
- await syncCommerce(appDir, appId, token, output.nonInteractive);
6550
- if (output.json) {
6551
- return output.emitJson({
6552
- ok: true,
6553
- appId,
6554
- appName,
6555
- url: body.url ?? null,
6556
- // Always present, so a caller branches on one field instead of
6557
- // inferring success from an absent one.
6558
- serving,
6559
- releaseId: body.releaseId ?? null,
6560
- bundleRetained: body.bundleRetained ?? null,
6561
- recoverable: repository.recoverable,
6562
- ...staleBaseGuardFields(body)
6563
- });
6564
- }
6565
- p12.outro("Done");
6566
6666
  }
6567
- });
6667
+ p12.outro("Done");
6668
+ }
6568
6669
  async function syncCommerce(appDir, appId, token, nonInteractive) {
6569
6670
  await syncSubscriptionPlans(appDir, appId, token, nonInteractive);
6570
6671
  await syncOneTimeProducts(appDir, appId, token);
6571
6672
  }
6673
+ function ownerJwtMissingRefusal(state) {
6674
+ if (state.onBehalf?.ownerJwtLive !== false) return null;
6675
+ return {
6676
+ code: "owner_jwt_missing",
6677
+ error: "Cannot preserve the existing secrets: this app has no live deployment carrying an APP_OWNER_JWT. Ask the owner to redeploy."
6678
+ };
6679
+ }
6680
+ function pendingRename(registeredHost, appName) {
6681
+ if (!registeredHost) return null;
6682
+ const [label, ...domain] = registeredHost.split(".");
6683
+ if (label === appName) return null;
6684
+ return { fromHost: registeredHost, toHost: [appName, ...domain].join(".") };
6685
+ }
6572
6686
  function blankSelectorRefusal(args) {
6573
6687
  if (args.env !== void 0 && !String(args.env).trim()) {
6574
6688
  return {
@@ -6629,15 +6743,18 @@ function requireAppIdArg(explicit) {
6629
6743
  function liveAppUrl(app2) {
6630
6744
  return app2.status === "active" && app2.deployedAt !== null ? app2.url : null;
6631
6745
  }
6632
- async function listApps(deployUrl, token) {
6633
- const { apps } = await apiFetch(deployUrl, token, "/api/apps");
6634
- if (!Array.isArray(apps)) {
6746
+ async function listAppsPage(deployUrl, token) {
6747
+ const body = await apiFetch(deployUrl, token, "/api/apps");
6748
+ if (!Array.isArray(body.apps)) {
6635
6749
  throw new InputError(
6636
6750
  `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?).`,
6637
6751
  "invalid_response"
6638
6752
  );
6639
6753
  }
6640
- return apps;
6754
+ return { apps: body.apps, pendingTransfers: body.pendingTransfers ?? [] };
6755
+ }
6756
+ async function listApps(deployUrl, token) {
6757
+ return (await listAppsPage(deployUrl, token)).apps;
6641
6758
  }
6642
6759
  function matchAppSelector(apps, selector) {
6643
6760
  const byId = apps.find((app2) => app2.appId === selector);
@@ -6759,6 +6876,11 @@ var undeploy_default = defineDeepspaceCommand({
6759
6876
  type: "string",
6760
6877
  description: "wrangler.toml [env.<name>] block whose deployed app to remove (e.g. --env staging). Ignored if a positional name is given.",
6761
6878
  required: false
6879
+ },
6880
+ yes: {
6881
+ type: "boolean",
6882
+ description: "Skip the interactive confirmation. (Scripts and --json callers are never prompted; the command itself is their consent.)",
6883
+ default: false
6762
6884
  }
6763
6885
  },
6764
6886
  async run({ args }) {
@@ -6784,7 +6906,18 @@ var undeploy_default = defineDeepspaceCommand({
6784
6906
  "not_in_app_repo"
6785
6907
  );
6786
6908
  }
6787
- if (!args.json) p13.intro(`Undeploying ${appId}`);
6909
+ if (!args.json && !args.yes && process.stdin.isTTY) {
6910
+ p13.intro(`Undeploying ${appId}`);
6911
+ const confirmed = await p13.confirm({
6912
+ message: `Take ${appId} offline now? Its URL stops serving immediately; data, secrets, and the registration stay (the name is reserved for you for 30 days).`,
6913
+ initialValue: false
6914
+ });
6915
+ if (p13.isCancel(confirmed) || !confirmed) {
6916
+ throw new Refusal("Undeploy cancelled.", "undeploy_declined");
6917
+ }
6918
+ } else if (!args.json) {
6919
+ p13.intro(`Undeploying ${appId}`);
6920
+ }
6788
6921
  const s = args.json ? null : createSpinner();
6789
6922
  s?.start("Removing...");
6790
6923
  const res = await fetch(`${DEPLOY_URL4}/api/deploy/${appId}`, {
@@ -6879,17 +7012,17 @@ var apps_default = defineDeepspaceCommand({
6879
7012
  },
6880
7013
  async run({ args }) {
6881
7014
  const token = await ensureToken();
6882
- const apps = await listApps(DEPLOY_URL5, token);
7015
+ const { apps, pendingTransfers } = await listAppsPage(DEPLOY_URL5, token);
6883
7016
  if (!args.json) {
6884
- if (!apps.length) {
7017
+ if (!apps.length && !pendingTransfers.length) {
6885
7018
  console.log("No apps yet. Create one with `npx create-deepspace <name>` and `deepspace deploy`.");
6886
- } else {
7019
+ } else if (apps.length) {
6887
7020
  const rows = [...apps].sort(
6888
7021
  (left, right) => Number(right.status !== "undeployed") - Number(left.status !== "undeployed")
6889
7022
  );
6890
7023
  const roleOf = (a) => a.role ?? "owner";
6891
7024
  const roleWidth = Math.max(4, ...rows.map((a) => roleOf(a).length));
6892
- const nameWidth = Math.max(4, ...rows.map((a) => (a.name ?? "\u2014").length));
7025
+ const nameWidth = Math.max(4, ...rows.map((a) => nameCell(a).length));
6893
7026
  const statusWidth = Math.max(6, ...rows.map((a) => a.status.length));
6894
7027
  const idWidth = Math.max(6, ...rows.map((a) => a.appId.length));
6895
7028
  console.log(
@@ -6898,14 +7031,36 @@ var apps_default = defineDeepspaceCommand({
6898
7031
  for (const a of rows) {
6899
7032
  const url = liveAppUrl(a) ?? "(not deployed)";
6900
7033
  console.log(
6901
- `${(a.name ?? "\u2014").padEnd(nameWidth)} ${a.status.padEnd(statusWidth)} ${roleOf(a).padEnd(roleWidth)} ${a.appId.padEnd(idWidth)} ${url}`
7034
+ `${nameCell(a).padEnd(nameWidth)} ${a.status.padEnd(statusWidth)} ${roleOf(a).padEnd(roleWidth)} ${a.appId.padEnd(idWidth)} ${url}`
6902
7035
  );
6903
7036
  }
7037
+ const reserved = rows.filter((a) => a.reservedName && a.reservedUntil);
7038
+ if (reserved.length) {
7039
+ console.log("\nReserved names return to the open pool when they expire:");
7040
+ for (const a of reserved) {
7041
+ console.log(
7042
+ ` ${a.reservedName} \u2014 held for you until ${a.reservedUntil}${a.name ? ` (${a.appId} now serves ${a.name})` : ""}`
7043
+ );
7044
+ }
7045
+ }
7046
+ }
7047
+ if (pendingTransfers.length) {
7048
+ console.log("\nTransfer offers waiting for you:");
7049
+ for (const offer2 of pendingTransfers) {
7050
+ console.log(` ${offer2.appId} \u2014 expires ${offer2.expiresAt}`);
7051
+ }
7052
+ console.log(
7053
+ " Accept with `deepspace app transfer accept --app <APP ID>`; `deepspace app transfer status --app <APP ID>` names the offerer."
7054
+ );
6904
7055
  }
6905
7056
  }
6906
- return { data: { apps } };
7057
+ return { data: { apps, pendingTransfers } };
6907
7058
  }
6908
7059
  });
7060
+ function nameCell(app2) {
7061
+ if (app2.name) return app2.name;
7062
+ return app2.reservedName ? `${app2.reservedName} (reserved)` : "\u2014";
7063
+ }
6909
7064
 
6910
7065
  // src/cli/commands/logs.ts
6911
7066
  import { defineCommand as defineCommand4 } from "citty";
@@ -7289,37 +7444,37 @@ var usage_default = defineDeepspaceCommand({
7289
7444
  });
7290
7445
 
7291
7446
  // src/cli/commands/add.ts
7292
- import { existsSync as existsSync18, readFileSync as readFileSync20, realpathSync as realpathSync4 } from "fs";
7293
- import { dirname as dirname9, join as join18, resolve as resolve15 } from "path";
7447
+ import { existsSync as existsSync19, readFileSync as readFileSync19, realpathSync as realpathSync4 } from "fs";
7448
+ import { dirname as dirname9, join as join21, resolve as resolve16 } from "path";
7294
7449
  import { pathToFileURL as pathToFileURL3 } from "url";
7295
7450
 
7296
7451
  // src/cli/commands/feature-installer.ts
7297
7452
  import {
7298
7453
  closeSync as closeSync2,
7299
7454
  copyFileSync,
7300
- existsSync as existsSync17,
7455
+ existsSync as existsSync18,
7301
7456
  fstatSync as fstatSync2,
7302
7457
  mkdirSync as mkdirSync6,
7303
7458
  mkdtempSync,
7304
7459
  openSync as openSync2,
7305
- readFileSync as readFileSync19,
7460
+ readFileSync as readFileSync18,
7306
7461
  readSync,
7307
- readdirSync as readdirSync5,
7462
+ readdirSync as readdirSync7,
7308
7463
  rmSync as rmSync4,
7309
7464
  writeFileSync as writeFileSync5
7310
7465
  } from "fs";
7311
7466
  import { spawnSync as spawnSync6 } from "child_process";
7312
7467
  import { tmpdir as tmpdir2 } from "os";
7313
- import { dirname as dirname8, isAbsolute as isAbsolute4, join as join17, resolve as resolve14 } from "path";
7468
+ import { dirname as dirname8, isAbsolute as isAbsolute4, join as join20, resolve as resolve15 } from "path";
7314
7469
  import { fileURLToPath } from "url";
7315
7470
  var NAV_MARKER = "// \u2500\u2500 Features add nav items below this line \u2500\u2500";
7316
7471
  function featurePackageRoot(moduleUrl = import.meta.url) {
7317
7472
  let dir = dirname8(fileURLToPath(moduleUrl));
7318
7473
  for (; ; ) {
7319
- const packagePath = join17(dir, "package.json");
7320
- if (existsSync17(packagePath)) {
7474
+ const packagePath = join20(dir, "package.json");
7475
+ if (existsSync18(packagePath)) {
7321
7476
  try {
7322
- const pkg2 = JSON.parse(readFileSync19(packagePath, "utf-8"));
7477
+ const pkg2 = JSON.parse(readFileSync18(packagePath, "utf-8"));
7323
7478
  if (pkg2.name === "deepspace") return dir;
7324
7479
  } catch {
7325
7480
  }
@@ -7330,13 +7485,13 @@ function featurePackageRoot(moduleUrl = import.meta.url) {
7330
7485
  }
7331
7486
  }
7332
7487
  function hasFeatureCatalog(packageRoot = featurePackageRoot()) {
7333
- return existsSync17(join17(packageRoot, "features"));
7488
+ return existsSync18(join20(packageRoot, "features"));
7334
7489
  }
7335
7490
  function readFeatureConfig(id, packageRoot = featurePackageRoot()) {
7336
- const configPath = join17(packageRoot, "features", id, "feature.json");
7337
- if (!existsSync17(configPath)) return null;
7491
+ const configPath = join20(packageRoot, "features", id, "feature.json");
7492
+ if (!existsSync18(configPath)) return null;
7338
7493
  try {
7339
- const config = JSON.parse(readFileSync19(configPath, "utf-8"));
7494
+ const config = JSON.parse(readFileSync18(configPath, "utf-8"));
7340
7495
  if (typeof config.id !== "string" || typeof config.name !== "string" || !Array.isArray(config.files)) {
7341
7496
  return null;
7342
7497
  }
@@ -7346,9 +7501,9 @@ function readFeatureConfig(id, packageRoot = featurePackageRoot()) {
7346
7501
  }
7347
7502
  }
7348
7503
  function listFeatureConfigs(packageRoot = featurePackageRoot()) {
7349
- const featuresDir = join17(packageRoot, "features");
7350
- if (!existsSync17(featuresDir)) return [];
7351
- return readdirSync5(featuresDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => readFeatureConfig(entry.name, packageRoot)).filter((config) => config !== null && !config.internal).sort((a, b) => a.id.localeCompare(b.id));
7504
+ const featuresDir = join20(packageRoot, "features");
7505
+ if (!existsSync18(featuresDir)) return [];
7506
+ return readdirSync7(featuresDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => readFeatureConfig(entry.name, packageRoot)).filter((config) => config !== null && !config.internal).sort((a, b) => a.id.localeCompare(b.id));
7352
7507
  }
7353
7508
  function featureCatalog(packageRoot = featurePackageRoot()) {
7354
7509
  return listFeatureConfigs(packageRoot).map((config) => ({
@@ -7385,7 +7540,7 @@ function suggestFeature(featureId, packageRoot = featurePackageRoot()) {
7385
7540
  return closest !== null && closestDistance <= threshold ? closest : null;
7386
7541
  }
7387
7542
  function resolveTargetDir(raw) {
7388
- return isAbsolute4(raw) ? raw : resolve14(raw);
7543
+ return isAbsolute4(raw) ? raw : resolve15(raw);
7389
7544
  }
7390
7545
  function resolveFeatureDestPath(fileDest, route) {
7391
7546
  const pages = "src/pages/";
@@ -7401,7 +7556,7 @@ function copyFile(src, dest) {
7401
7556
  function copyFileWithImportShift(src, dest, levels) {
7402
7557
  mkdirSync6(dirname8(dest), { recursive: true });
7403
7558
  const prefix = "../".repeat(levels);
7404
- const shifted = readFileSync19(src, "utf-8").replace(
7559
+ const shifted = readFileSync18(src, "utf-8").replace(
7405
7560
  /(from\s+|import\s+|import\()(['"])(\.\.\/)/g,
7406
7561
  `$1$2${prefix}$3`
7407
7562
  );
@@ -7414,12 +7569,12 @@ function installFiles(packageRoot, config, targetDir, emit) {
7414
7569
  const preexisting = [];
7415
7570
  const missing = [];
7416
7571
  for (const file of config.files) {
7417
- const srcPath = join17(packageRoot, "features", config.id, file.src);
7572
+ const srcPath = join20(packageRoot, "features", config.id, file.src);
7418
7573
  const resolvedDest = resolveFeatureDestPath(file.dest, config.route);
7419
- const destPath = join17(targetDir, resolvedDest);
7420
- const existed = existsSync17(destPath);
7574
+ const destPath = join20(targetDir, resolvedDest);
7575
+ const existed = existsSync18(destPath);
7421
7576
  if (existed) preexisting.push(resolvedDest);
7422
- if (!existsSync17(srcPath)) {
7577
+ if (!existsSync18(srcPath)) {
7423
7578
  emit(` Warning: source not found: ${file.src}`, "stderr");
7424
7579
  if (!existed) missing.push(resolvedDest);
7425
7580
  continue;
@@ -7456,11 +7611,11 @@ function unresolvedSchema(schema, reason, emit) {
7456
7611
  }
7457
7612
  function integrateSchema(config, targetDir, emit) {
7458
7613
  if (!config.schema) return [];
7459
- const schemasPath = join17(targetDir, "src", "schemas.ts");
7460
- if (!existsSync17(schemasPath)) {
7614
+ const schemasPath = join20(targetDir, "src", "schemas.ts");
7615
+ if (!existsSync18(schemasPath)) {
7461
7616
  return [unresolvedSchema(config.schema, "src/schemas.ts was not found.", emit)];
7462
7617
  }
7463
- let content = readFileSync19(schemasPath, "utf-8");
7618
+ let content = readFileSync18(schemasPath, "utf-8");
7464
7619
  const { exportName, importPath, spreadOperator } = config.schema;
7465
7620
  const importPattern = new RegExp(`^import\\s+\\{[^}]*\\b${exportName}\\b`, "m");
7466
7621
  if (importPattern.test(content)) {
@@ -7513,11 +7668,11 @@ function unresolvedActions(actions, reason, emit) {
7513
7668
  }
7514
7669
  function integrateActions(config, targetDir, emit) {
7515
7670
  if (!config.actions?.length) return [];
7516
- const actionsPath = join17(targetDir, "src", "actions", "index.ts");
7517
- if (!existsSync17(actionsPath)) {
7671
+ const actionsPath = join20(targetDir, "src", "actions", "index.ts");
7672
+ if (!existsSync18(actionsPath)) {
7518
7673
  return [unresolvedActions(config.actions, "src/actions/index.ts was not found.", emit)];
7519
7674
  }
7520
- let content = readFileSync19(actionsPath, "utf-8");
7675
+ let content = readFileSync18(actionsPath, "utf-8");
7521
7676
  const registryPattern = /export const actions:\s*Record<string,\s*ActionHandler(?:<[^>]*>)?>\s*=\s*\{/;
7522
7677
  if (!registryPattern.test(content)) {
7523
7678
  const reason = "src/actions/index.ts has an unexpected structure.";
@@ -7563,8 +7718,8 @@ ${registryEntry}`);
7563
7718
  function integrateCode(config, targetDir, emit) {
7564
7719
  const unresolved = [];
7565
7720
  for (const insertion of config.code ?? []) {
7566
- const targetPath = join17(targetDir, insertion.file);
7567
- if (!existsSync17(targetPath)) {
7721
+ const targetPath = join20(targetDir, insertion.file);
7722
+ if (!existsSync18(targetPath)) {
7568
7723
  unresolved.push({
7569
7724
  area: "code",
7570
7725
  reason: `${insertion.file} was not found.`,
@@ -7572,7 +7727,7 @@ function integrateCode(config, targetDir, emit) {
7572
7727
  });
7573
7728
  continue;
7574
7729
  }
7575
- const original = readFileSync19(targetPath, "utf-8");
7730
+ const original = readFileSync18(targetPath, "utf-8");
7576
7731
  if (original.includes(insertion.insert.trim())) {
7577
7732
  emit(` Integration already present: ${insertion.file}`);
7578
7733
  continue;
@@ -7609,8 +7764,8 @@ ${content.slice(
7609
7764
  }
7610
7765
  function integrateIgnore(config, targetDir) {
7611
7766
  if (!config.ignore?.length) return;
7612
- const targetPath = join17(targetDir, ".gitignore");
7613
- const current = existsSync17(targetPath) ? readFileSync19(targetPath, "utf-8") : "";
7767
+ const targetPath = join20(targetDir, ".gitignore");
7768
+ const current = existsSync18(targetPath) ? readFileSync18(targetPath, "utf-8") : "";
7614
7769
  const present = new Set(current.split(/\r?\n/).map((line) => line.trim()));
7615
7770
  const missing = config.ignore.filter((pattern) => !present.has(pattern));
7616
7771
  if (missing.length === 0) return;
@@ -7622,15 +7777,15 @@ function integrateWrangler(config, targetDir, emit) {
7622
7777
  const runWorkerFirst = config.wrangler?.runWorkerFirst === true;
7623
7778
  const rateLimits = config.wrangler?.rateLimits ?? [];
7624
7779
  if (!runWorkerFirst && rateLimits.length === 0) return [];
7625
- const targetPath = join17(targetDir, "wrangler.toml");
7780
+ const targetPath = join20(targetDir, "wrangler.toml");
7626
7781
  const steps = [
7627
7782
  ...runWorkerFirst ? ["Set assets.run_worker_first = true in wrangler.toml."] : [],
7628
7783
  ...rateLimits.map((binding) => `Add the ${binding.name} rate-limit binding to wrangler.toml.`)
7629
7784
  ];
7630
- if (!existsSync17(targetPath)) {
7785
+ if (!existsSync18(targetPath)) {
7631
7786
  return [{ area: "wrangler", reason: "wrangler.toml was not found.", steps }];
7632
7787
  }
7633
- let source = readFileSync19(targetPath, "utf-8");
7788
+ let source = readFileSync18(targetPath, "utf-8");
7634
7789
  if (runWorkerFirst && !/^\[assets\]\s*$/m.test(source)) {
7635
7790
  return [{ area: "wrangler", reason: "wrangler.toml has no [assets] section.", steps }];
7636
7791
  }
@@ -7706,8 +7861,8 @@ function rateLimitBindingBlock(scope, binding) {
7706
7861
  }
7707
7862
  function integrateCss(packageRoot, config, targetDir, emit) {
7708
7863
  if (!config.css?.length) return [];
7709
- const stylesPath = join17(targetDir, "src", "styles.css");
7710
- if (!existsSync17(stylesPath)) {
7864
+ const stylesPath = join20(targetDir, "src", "styles.css");
7865
+ if (!existsSync18(stylesPath)) {
7711
7866
  emit(" Warning: Cannot integrate CSS \u2014 src/styles.css not found");
7712
7867
  return [
7713
7868
  {
@@ -7719,12 +7874,12 @@ function integrateCss(packageRoot, config, targetDir, emit) {
7719
7874
  }
7720
7875
  ];
7721
7876
  }
7722
- let styles = readFileSync19(stylesPath, "utf-8");
7877
+ let styles = readFileSync18(stylesPath, "utf-8");
7723
7878
  let integrated = 0;
7724
7879
  const unresolved = [];
7725
7880
  for (const cssFile of config.css) {
7726
- const srcPath = join17(packageRoot, "features", config.id, cssFile);
7727
- if (!existsSync17(srcPath)) {
7881
+ const srcPath = join20(packageRoot, "features", config.id, cssFile);
7882
+ if (!existsSync18(srcPath)) {
7728
7883
  emit(` Warning: CSS source not found: ${cssFile}`, "stderr");
7729
7884
  unresolved.push({
7730
7885
  area: "css",
@@ -7733,7 +7888,7 @@ function integrateCss(packageRoot, config, targetDir, emit) {
7733
7888
  });
7734
7889
  continue;
7735
7890
  }
7736
- const css = readFileSync19(srcPath, "utf-8");
7891
+ const css = readFileSync18(srcPath, "utf-8");
7737
7892
  const fingerprint = css.match(/\.([\w-]+)\s*\{|@keyframes\s+([\w-]+)/);
7738
7893
  const marker = fingerprint?.[1] ?? fingerprint?.[2];
7739
7894
  if (marker && styles.includes(marker)) {
@@ -7767,11 +7922,11 @@ function routeRolesSuffix(route) {
7767
7922
  }
7768
7923
  function integrateRoute(config, targetDir, emit) {
7769
7924
  if (!config.route) return [];
7770
- const navPath = join17(targetDir, "src", "nav.ts");
7771
- if (!existsSync17(navPath)) {
7925
+ const navPath = join20(targetDir, "src", "nav.ts");
7926
+ if (!existsSync18(navPath)) {
7772
7927
  return [unresolvedRoute(config.route, config, "src/nav.ts was not found.", emit)];
7773
7928
  }
7774
- let content = readFileSync19(navPath, "utf-8");
7929
+ let content = readFileSync18(navPath, "utf-8");
7775
7930
  const routePath = config.route.path;
7776
7931
  if (content.includes(`'${routePath}'`)) {
7777
7932
  emit(` Nav already present: ${routePath}`);
@@ -7818,10 +7973,10 @@ function runPackageInstall(packageManager, targetDir, outputVisible) {
7818
7973
  const child = spawnSync6(packageManager, ["install"], { ...options, stdio: "inherit" });
7819
7974
  return { error: child.error, status: child.status };
7820
7975
  }
7821
- const captureDir = mkdtempSync(join17(tmpdir2(), "deepspace-add-install-"));
7976
+ const captureDir = mkdtempSync(join20(tmpdir2(), "deepspace-add-install-"));
7822
7977
  let outputFd;
7823
7978
  try {
7824
- outputFd = openSync2(join17(captureDir, "output.log"), "w+");
7979
+ outputFd = openSync2(join20(captureDir, "output.log"), "w+");
7825
7980
  const child = spawnSync6(packageManager, ["install"], {
7826
7981
  ...options,
7827
7982
  stdio: ["ignore", outputFd, outputFd]
@@ -7845,14 +8000,14 @@ function integrateDependencies(config, targetDir, runInstall, outputVisible, emi
7845
8000
  if (declarations.every(({ values }) => !values || Object.keys(values).length === 0)) {
7846
8001
  return { added: [], installRan: false, packageManager };
7847
8002
  }
7848
- const packagePath = join17(targetDir, "package.json");
7849
- if (!existsSync17(packagePath)) {
8003
+ const packagePath = join20(targetDir, "package.json");
8004
+ if (!existsSync18(packagePath)) {
7850
8005
  emit(" Could not patch package.json (file not found)");
7851
8006
  return { added: [], installRan: false, packageManager };
7852
8007
  }
7853
8008
  let pkg2;
7854
8009
  try {
7855
- pkg2 = JSON.parse(readFileSync19(packagePath, "utf-8"));
8010
+ pkg2 = JSON.parse(readFileSync18(packagePath, "utf-8"));
7856
8011
  } catch (error) {
7857
8012
  emit(
7858
8013
  ` Could not patch package.json (parse error: ${error instanceof Error ? error.message : String(error)})`
@@ -7912,7 +8067,7 @@ ${child.diagnosticTail}` : "";
7912
8067
  }
7913
8068
  function dependencySnapshot(targetDir) {
7914
8069
  try {
7915
- const pkg2 = JSON.parse(readFileSync19(join17(targetDir, "package.json"), "utf-8"));
8070
+ const pkg2 = JSON.parse(readFileSync18(join20(targetDir, "package.json"), "utf-8"));
7916
8071
  return {
7917
8072
  dependencies: pkg2.dependencies ?? {},
7918
8073
  devDependencies: pkg2.devDependencies ?? {}
@@ -7924,7 +8079,7 @@ function dependencySnapshot(targetDir) {
7924
8079
  function installedNavFeatures(packageRoot, targetDir) {
7925
8080
  return listFeatureConfigs(packageRoot).filter((feature) => feature.category === "nav" && feature.files.length > 0).filter((feature) => {
7926
8081
  const signature = resolveFeatureDestPath(feature.files[0].dest, feature.route);
7927
- return existsSync17(join17(targetDir, signature));
8082
+ return existsSync18(join20(targetDir, signature));
7928
8083
  }).map((feature) => feature.id);
7929
8084
  }
7930
8085
  function warnNavConflicts(packageRoot, config, targetDir, emit) {
@@ -8157,11 +8312,11 @@ function renderFeatureInfo(config) {
8157
8312
  return lines.join("\n");
8158
8313
  }
8159
8314
  function isDeepSpaceApp(dir) {
8160
- if (existsSync18(join18(dir, "wrangler.toml"))) return true;
8161
- const packagePath = join18(dir, "package.json");
8162
- if (!existsSync18(packagePath)) return false;
8315
+ if (existsSync19(join21(dir, "wrangler.toml"))) return true;
8316
+ const packagePath = join21(dir, "package.json");
8317
+ if (!existsSync19(packagePath)) return false;
8163
8318
  try {
8164
- const pkg2 = JSON.parse(readFileSync20(packagePath, "utf-8"));
8319
+ const pkg2 = JSON.parse(readFileSync19(packagePath, "utf-8"));
8165
8320
  return Boolean(pkg2.dependencies?.deepspace || pkg2.devDependencies?.deepspace);
8166
8321
  } catch {
8167
8322
  return false;
@@ -8170,8 +8325,8 @@ function isDeepSpaceApp(dir) {
8170
8325
  function installedPackageRoot(fromDir) {
8171
8326
  let dir = fromDir;
8172
8327
  for (; ; ) {
8173
- const candidate = join18(dir, "node_modules", "deepspace");
8174
- if (existsSync18(join18(candidate, "package.json"))) return candidate;
8328
+ const candidate = join21(dir, "node_modules", "deepspace");
8329
+ if (existsSync19(join21(candidate, "package.json"))) return candidate;
8175
8330
  const parent = dirname9(dir);
8176
8331
  if (parent === dir) return null;
8177
8332
  dir = parent;
@@ -8188,9 +8343,9 @@ async function targetInstaller(appDir) {
8188
8343
  if (realpathSync4(installedRoot) === realpathSync4(featurePackageRoot())) {
8189
8344
  return { installFeature };
8190
8345
  }
8191
- const entry = join18(installedRoot, "dist", "feature-installer.js");
8192
- const catalog = join18(installedRoot, "features");
8193
- if (!existsSync18(entry) || !existsSync18(catalog)) {
8346
+ const entry = join21(installedRoot, "dist", "feature-installer.js");
8347
+ const catalog = join21(installedRoot, "features");
8348
+ if (!existsSync19(entry) || !existsSync19(catalog)) {
8194
8349
  throw new Refusal(
8195
8350
  `The app's installed deepspace package does not contain the typed feature installer.
8196
8351
  Expected: ${entry}
@@ -8282,8 +8437,8 @@ var add_default = defineDeepspaceCommand({
8282
8437
  }
8283
8438
  return { data: { features: featureCatalog(root) } };
8284
8439
  }
8285
- const appDir = resolve15(args.dir ?? ".");
8286
- if (!existsSync18(appDir)) {
8440
+ const appDir = resolve16(args.dir ?? ".");
8441
+ if (!existsSync19(appDir)) {
8287
8442
  throw new Refusal(
8288
8443
  `Target directory not found: ${appDir}
8289
8444
  Create a new app first: deepspace app create <name>`,
@@ -8363,11 +8518,11 @@ function fmtDate(iso) {
8363
8518
  if (!iso) return "\u2014";
8364
8519
  return iso.slice(0, 10);
8365
8520
  }
8366
- async function confirm4(message) {
8521
+ async function confirm6(message) {
8367
8522
  process.stdout.write(`${message} [y/N] `);
8368
- const answer = await new Promise((resolve21) => {
8523
+ const answer = await new Promise((resolve22) => {
8369
8524
  process.stdin.setEncoding("utf-8");
8370
- process.stdin.once("data", (data) => resolve21(data.toString().trim().toLowerCase()));
8525
+ process.stdin.once("data", (data) => resolve22(data.toString().trim().toLowerCase()));
8371
8526
  });
8372
8527
  process.stdin.pause();
8373
8528
  return answer === "y" || answer === "yes";
@@ -8442,7 +8597,7 @@ var buy = defineDeepspaceCommand({
8442
8597
  "confirmation_required"
8443
8598
  );
8444
8599
  }
8445
- const ok2 = await confirm4(
8600
+ const ok2 = await confirm6(
8446
8601
  `Buy ${domain} for ${price}/yr (auto-renews; non-refundable), attach to ${appId}? Provisioning takes ${expectedTime}.`
8447
8602
  );
8448
8603
  if (!ok2) {
@@ -8612,7 +8767,7 @@ var detach = defineDeepspaceCommand({
8612
8767
  "confirmation_required"
8613
8768
  );
8614
8769
  }
8615
- const ok2 = await confirm4(
8770
+ const ok2 = await confirm6(
8616
8771
  `Detach ${domain}? Routing will stop; the domain stays registered (auto-renew is unchanged).`
8617
8772
  );
8618
8773
  if (!ok2) {
@@ -8658,8 +8813,8 @@ var domain_default = defineCommand5({
8658
8813
 
8659
8814
  // src/cli/commands/app-files.ts
8660
8815
  import { defineCommand as defineCommand6 } from "citty";
8661
- import { basename as basename4, resolve as resolve16 } from "path";
8662
- import { existsSync as existsSync19, statSync as statSync2 } from "fs";
8816
+ import { basename as basename4, resolve as resolve17 } from "path";
8817
+ import { existsSync as existsSync20, statSync as statSync2 } from "fs";
8663
8818
 
8664
8819
  // src/cli/lib/app-files-api.ts
8665
8820
  import { createWriteStream, openAsBlob, statSync } from "fs";
@@ -8937,8 +9092,8 @@ var put = defineDeepspaceCommand({
8937
9092
  app: { type: "string", description: "App id or live name (defaults to ./wrangler.toml)" }
8938
9093
  },
8939
9094
  async run({ args }) {
8940
- const localPath = resolve16(String(args.file));
8941
- if (!existsSync19(localPath) || !statSync2(localPath).isFile()) {
9095
+ const localPath = resolve17(String(args.file));
9096
+ if (!existsSync20(localPath) || !statSync2(localPath).isFile()) {
8942
9097
  throw new Refusal(`No such file: ${args.file}`, "file_not_found");
8943
9098
  }
8944
9099
  const key = requireKey(args.key ?? basename4(localPath));
@@ -9038,7 +9193,7 @@ var get = defineDeepspaceCommand({
9038
9193
  },
9039
9194
  async run({ args }) {
9040
9195
  const key = requireKey(String(args.key));
9041
- const destination = resolve16(String(args.out ?? basename4(key)));
9196
+ const destination = resolve17(String(args.out ?? basename4(key)));
9042
9197
  const { token, appId } = await target(args.app);
9043
9198
  const result = await downloadAppFile(PLATFORM_URL, token, appId, key, destination);
9044
9199
  if (!args.json) console.log(`\u2713 ${key} \u2192 ${destination} (${formatBytes(result.bytes)})`);
@@ -9118,7 +9273,10 @@ var list4 = defineDeepspaceCommand({
9118
9273
  }
9119
9274
  });
9120
9275
  var add = defineDeepspaceCommand({
9121
- meta: { name: "add", description: "Authorize someone to deploy your app" },
9276
+ meta: {
9277
+ name: "add",
9278
+ description: "Authorize someone to deploy your app \u2014 a collaborator can also read and change every app secret (plaintext); they cannot undeploy or transfer"
9279
+ },
9122
9280
  args: {
9123
9281
  email: {
9124
9282
  type: "positional",
@@ -9328,7 +9486,11 @@ var offer = defineDeepspaceCommand({
9328
9486
  if (replaced) console.log(`\u25B2 Replaced the pending offer to ${pendingTo}.`);
9329
9487
  console.log(
9330
9488
  `\u2713 Offered ${app2} to ${email} (expires ${expiresAt}).
9331
- They accept with: deepspace app transfer accept --app ${app2}`
9489
+ They accept with: deepspace app transfer accept --app ${app2}
9490
+ (it also shows in their \`deepspace app list\`)
9491
+ \u25B2 On acceptance you lose ALL access to ${app2} \u2014 you are not kept on as a
9492
+ collaborator. Only the new owner can add you back. Collaborators you added
9493
+ stay on the app for the new owner.`
9332
9494
  );
9333
9495
  }
9334
9496
  return {
@@ -9355,7 +9517,9 @@ var status2 = defineDeepspaceCommand({
9355
9517
  return { data: { app: app2, transfer: null } };
9356
9518
  }
9357
9519
  if (!args.json) {
9358
- console.log(`Pending: ${app2} \u2192 ${transfer.toEmailDisplay} (expires ${transfer.expiresAt}).`);
9520
+ console.log(
9521
+ `Pending: ${app2} \u2014 from ${transfer.fromEmailDisplay} to ${transfer.toEmailDisplay} (expires ${transfer.expiresAt}).`
9522
+ );
9359
9523
  }
9360
9524
  return { data: { app: app2, transfer } };
9361
9525
  }
@@ -9375,15 +9539,32 @@ var accept2 = defineDeepspaceCommand({
9375
9539
  const msg = err instanceof Error ? err.message : String(err);
9376
9540
  throw new Refusal(msg, msg.startsWith("No app id") ? "not_in_app_repo" : "invalid_app");
9377
9541
  }
9378
- await apiFetch(DEPLOY_URL10, token, `/api/apps/${encodeURIComponent(app2)}/transfer/accept`, {
9542
+ const { replayed, acceptedAt, inheritedCollaborators } = await apiFetch(DEPLOY_URL10, token, `/api/apps/${encodeURIComponent(app2)}/transfer/accept`, {
9379
9543
  method: "POST"
9380
9544
  });
9381
9545
  if (!args.json) {
9382
9546
  console.log(
9383
- `\u2713 You now own ${app2}. Run \`deepspace app init\` in a fresh clone (or set DEEPSPACE_APP_ID = "${app2}" in wrangler.toml) and deploy.`
9547
+ replayed ? `\u2022 ${app2} is already yours${acceptedAt ? ` (since ${acceptedAt})` : ""} \u2014 no pending offer was accepted. Run \`deepspace app list\` to see any other offers.` : `\u2713 You now own ${app2}. Run \`deepspace app init\` in a fresh clone (or set DEEPSPACE_APP_ID = "${app2}" in wrangler.toml) and deploy.`
9384
9548
  );
9549
+ if (!replayed && inheritedCollaborators === void 0) {
9550
+ console.log(
9551
+ `\u25B2 Could not list collaborators the previous owner may have left on the app \u2014 review with \`deepspace app collaborators list --app ${app2}\`.`
9552
+ );
9553
+ } else if (!replayed && inheritedCollaborators && inheritedCollaborators.length > 0) {
9554
+ console.log(
9555
+ `\u25B2 ${inheritedCollaborators.length} collaborator(s) added by the previous owner stay on the app with deploy and secrets access \u2014 review with \`deepspace app collaborators list --app ${app2}\`.`
9556
+ );
9557
+ }
9385
9558
  }
9386
- return { data: { app: app2, accepted: true } };
9559
+ return {
9560
+ data: {
9561
+ app: app2,
9562
+ accepted: !replayed,
9563
+ replayed: replayed === true,
9564
+ ...acceptedAt ? { acceptedAt } : {},
9565
+ ...inheritedCollaborators === void 0 ? {} : { inheritedCollaborators }
9566
+ }
9567
+ };
9387
9568
  }
9388
9569
  });
9389
9570
  var cancel4 = defineDeepspaceCommand({
@@ -9408,7 +9589,7 @@ var transfer_default = defineCommand8({
9408
9589
  import { defineCommand as defineCommand9 } from "citty";
9409
9590
 
9410
9591
  // src/cli/commands/_invoke-impl.ts
9411
- import { readFileSync as readFileSync21 } from "fs";
9592
+ import { readFileSync as readFileSync20 } from "fs";
9412
9593
  import * as p15 from "@clack/prompts";
9413
9594
  var API_URL7 = process.env.DEEPSPACE_API_URL ?? PLATFORM_URLS.api;
9414
9595
  var DEFAULT_TIMEOUT_MS = 12e4;
@@ -9449,7 +9630,7 @@ function resolveBody(opts) {
9449
9630
  if (opts.body != null) {
9450
9631
  raw = opts.body;
9451
9632
  } else if (opts.bodyFile != null) {
9452
- raw = opts.bodyFile === "-" ? readFileSync21(0, "utf-8") : readFileSync21(opts.bodyFile, "utf-8");
9633
+ raw = opts.bodyFile === "-" ? readFileSync20(0, "utf-8") : readFileSync20(opts.bodyFile, "utf-8");
9453
9634
  } else {
9454
9635
  return "{}";
9455
9636
  }
@@ -9743,8 +9924,8 @@ var integrations_default = defineCommand9({
9743
9924
 
9744
9925
  // src/cli/commands/feedback.ts
9745
9926
  import * as p16 from "@clack/prompts";
9746
- import { existsSync as existsSync20, readFileSync as readFileSync22 } from "fs";
9747
- import { dirname as dirname10, join as join19 } from "path";
9927
+ import { existsSync as existsSync21, readFileSync as readFileSync21 } from "fs";
9928
+ import { dirname as dirname10, join as join22 } from "path";
9748
9929
  import { fileURLToPath as fileURLToPath2 } from "url";
9749
9930
  import { release } from "os";
9750
9931
  var API_URL8 = process.env.DEEPSPACE_API_URL ?? PLATFORM_URLS.api;
@@ -9767,12 +9948,12 @@ function readCliVersion() {
9767
9948
  try {
9768
9949
  const here = dirname10(fileURLToPath2(import.meta.url));
9769
9950
  const candidates = [
9770
- join19(here, "..", "package.json"),
9771
- join19(here, "..", "..", "..", "package.json")
9951
+ join22(here, "..", "package.json"),
9952
+ join22(here, "..", "..", "..", "package.json")
9772
9953
  ];
9773
9954
  for (const path of candidates) {
9774
- if (!existsSync20(path)) continue;
9775
- const pkg2 = JSON.parse(readFileSync22(path, "utf-8"));
9955
+ if (!existsSync21(path)) continue;
9956
+ const pkg2 = JSON.parse(readFileSync21(path, "utf-8"));
9776
9957
  if (pkg2.name === "deepspace" && pkg2.version) return pkg2.version;
9777
9958
  }
9778
9959
  } catch {
@@ -9968,7 +10149,7 @@ Context: deepspace ${payload.cliVersion} \xB7 ${payload.nodeVersion} \xB7 ${payl
9968
10149
 
9969
10150
  // src/cli/commands/secrets.ts
9970
10151
  import { defineCommand as defineCommand10 } from "citty";
9971
- import { readFileSync as readFileSync23 } from "fs";
10152
+ import { readFileSync as readFileSync22 } from "fs";
9972
10153
 
9973
10154
  // src/cli/lib/citty-args.ts
9974
10155
  function dedupePositionals(bound, rest) {
@@ -10070,6 +10251,7 @@ function warnIfDebugRoutesEnabled(secrets) {
10070
10251
  return true;
10071
10252
  }
10072
10253
  var APPLY_HINT = "Run `deepspace deploy` to apply \u2014 secrets take effect at deploy time.";
10254
+ var APPLIES_AT_DEPLOY = { appliesAtDeploy: true };
10073
10255
  var LOCAL_DEV_HINT = "Running `deepspace dev start`? Restart it to load the change locally.";
10074
10256
  var list6 = defineCommand10({
10075
10257
  meta: { name: "list", description: "List masked secrets in a config" },
@@ -10136,7 +10318,7 @@ var set = defineCommand10({
10136
10318
  const n = Object.keys(secrets).length;
10137
10319
  ok(
10138
10320
  args.json === true,
10139
- { appId: t.appId, config: t.configName, set: Object.keys(secrets) },
10321
+ { appId: t.appId, config: t.configName, set: Object.keys(secrets), ...APPLIES_AT_DEPLOY },
10140
10322
  () => {
10141
10323
  console.log(`Set ${n} secret${n === 1 ? "" : "s"} in ${t.configName}.`);
10142
10324
  if (!warnIfDebugRoutesEnabled(secrets)) {
@@ -10209,7 +10391,7 @@ var del2 = defineCommand10({
10209
10391
  }
10210
10392
  }
10211
10393
  const note3 = absent > 0 ? ` (${absent} already absent)` : "";
10212
- ok(args.json === true, { appId: t.appId, config: t.configName, deleted, absent }, () => {
10394
+ ok(args.json === true, { appId: t.appId, config: t.configName, deleted, absent, ...APPLIES_AT_DEPLOY }, () => {
10213
10395
  console.log(
10214
10396
  `Deleted ${deleted} secret${deleted === 1 ? "" : "s"} from ${t.configName}.${note3}`
10215
10397
  );
@@ -10238,7 +10420,7 @@ var upload = defineCommand10({
10238
10420
  async run({ args }) {
10239
10421
  try {
10240
10422
  const t = await resolveTarget(args);
10241
- const content = args.file === "-" ? readFileSync23(0, "utf-8") : readFileSync23(args.file, "utf-8");
10423
+ const content = args.file === "-" ? readFileSync22(0, "utf-8") : readFileSync22(args.file, "utf-8");
10242
10424
  const secrets = parseSecretsUpload(content);
10243
10425
  if (Object.keys(secrets).length === 0)
10244
10426
  throw new InputError("No secrets found in the input.", "empty_input");
@@ -10246,7 +10428,7 @@ var upload = defineCommand10({
10246
10428
  const uploaded = Object.keys(secrets);
10247
10429
  ok(
10248
10430
  args.json === true,
10249
- { appId: t.appId, config: t.configName, uploaded, replaced: args.replace === true },
10431
+ { appId: t.appId, config: t.configName, uploaded, replaced: args.replace === true, ...APPLIES_AT_DEPLOY },
10250
10432
  () => {
10251
10433
  console.log(`Uploaded ${uploaded.length} secrets to ${t.configName}.`);
10252
10434
  if (!warnIfDebugRoutesEnabled(secrets)) console.log(APPLY_HINT);
@@ -10421,12 +10603,19 @@ var secrets_default = defineCommand10({
10421
10603
  });
10422
10604
 
10423
10605
  // src/cli/commands/init.ts
10424
- import { existsSync as existsSync21 } from "fs";
10425
- import { join as join20 } from "path";
10606
+ import { existsSync as existsSync22 } from "fs";
10607
+ import { join as join23 } from "path";
10426
10608
  var DEPLOY_URL12 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
10609
+ function wranglerConfigUncommitted(appDir) {
10610
+ if (!existsSync22(join23(appDir, ".git"))) return false;
10611
+ const dryRun = runGit(appDir, ["commit", "--dry-run", "-m", "x", "--", "wrangler.toml"], {
10612
+ allowFail: true
10613
+ });
10614
+ return dryRun.status === 0;
10615
+ }
10427
10616
  function commitScaffoldIfUnborn(appDir, token) {
10428
10617
  try {
10429
- if (!existsSync21(join20(appDir, ".git"))) return false;
10618
+ if (!existsSync22(join23(appDir, ".git"))) return false;
10430
10619
  if (runGit(appDir, ["rev-parse", "--verify", "HEAD"], { allowFail: true }).status === 0) {
10431
10620
  return false;
10432
10621
  }
@@ -10503,13 +10692,13 @@ var init_default = defineDeepspaceCommand({
10503
10692
  writeAppId(appDir, appId, { wranglerEnv: envName, force: Boolean(args["new-id"]) });
10504
10693
  const committedScaffold = commitScaffoldIfUnborn(appDir, token);
10505
10694
  if (!args.json) {
10695
+ const envSuffix = envName ? ` (env: ${envName})` : "";
10696
+ const commitNote = committedScaffold ? " \u2014 initial scaffold commit created." : " \u2014 commit wrangler.toml.";
10506
10697
  if (existing) {
10507
- console.log(`Forked: ${existing} \u2192 ${appId}${envName ? ` (env: ${envName})` : ""}`);
10698
+ console.log(`Forked: ${existing} \u2192 ${appId}${envSuffix}${commitNote}`);
10508
10699
  console.log("Registered as a NEW app under your account; the original is untouched.");
10509
10700
  } else {
10510
- console.log(
10511
- `Registered ${appId}${envName ? ` (env: ${envName})` : ""} to your account` + (committedScaffold ? " \u2014 initial scaffold commit created." : " \u2014 commit wrangler.toml.")
10512
- );
10701
+ console.log(`Registered ${appId}${envSuffix} to your account${commitNote}`);
10513
10702
  console.log("The first deploy claims the `name` subdomain.");
10514
10703
  }
10515
10704
  console.log(`App dir: ${appDir}`);
@@ -10522,7 +10711,18 @@ var init_default = defineDeepspaceCommand({
10522
10711
  committedScaffold,
10523
10712
  appDir,
10524
10713
  env: envName ?? null
10525
- }
10714
+ },
10715
+ // The one follow-up when the id was written into an existing history:
10716
+ // rendered as the `Next:` line and carried in `--json`, so a machine
10717
+ // caller does not have to infer it from `committedScaffold: false`.
10718
+ // Offered only when the file is actually uncommitted — `git commit` on
10719
+ // a clean file fails, and an action must be executable as handed over.
10720
+ ...!committedScaffold && wranglerConfigUncommitted(appDir) ? {
10721
+ action: {
10722
+ cwd: appDir,
10723
+ argv: ["git", "commit", "-m", existing ? `Fork as ${appId}` : `Register ${appId}`, "--", "wrangler.toml"]
10724
+ }
10725
+ } : {}
10526
10726
  };
10527
10727
  }
10528
10728
  });
@@ -10530,7 +10730,7 @@ var init_default = defineDeepspaceCommand({
10530
10730
  // src/cli/commands/status.ts
10531
10731
  import { defineCommand as defineCommand11 } from "citty";
10532
10732
  import * as p17 from "@clack/prompts";
10533
- import { existsSync as existsSync22, readFileSync as readFileSync24 } from "fs";
10733
+ import { existsSync as existsSync23, readFileSync as readFileSync23 } from "fs";
10534
10734
  function timeAgo(iso) {
10535
10735
  const ms = Date.now() - Date.parse(iso);
10536
10736
  if (!Number.isFinite(ms) || ms < 0) return iso;
@@ -10600,22 +10800,21 @@ var status_default = defineCommand11({
10600
10800
  "DEEPSPACE_API_URL",
10601
10801
  "DEEPSPACE_PLATFORM_URL"
10602
10802
  ].filter((k) => process.env[k]);
10803
+ const urls = effectivePlatformUrls();
10804
+ lines.push([
10805
+ "Env",
10806
+ [DEEPSPACE_ENV, ...urlOverrides.map((k) => `${k} set`)].join(" \xB7 ")
10807
+ ]);
10808
+ json.env = DEEPSPACE_ENV;
10809
+ json.services = urls;
10603
10810
  if (DEEPSPACE_ENV !== "production" || urlOverrides.length > 0) {
10604
- const urls = effectivePlatformUrls();
10605
- const detail = [
10606
- DEEPSPACE_ENV !== "production" ? DEEPSPACE_ENV : null,
10607
- ...urlOverrides.map((k) => `${k} set`)
10608
- ].filter(Boolean).join(" \xB7 ");
10609
- lines.push(["Env", detail]);
10610
10811
  lines.push([
10611
10812
  "Services",
10612
10813
  `auth ${urls.auth} \xB7 api ${urls.api} \xB7 platform ${urls.platform} \xB7 deploy ${urls.deploy}`
10613
10814
  ]);
10614
- json.env = DEEPSPACE_ENV;
10615
- json.services = urls;
10616
10815
  if (urlOverrides.length > 0) json.urlOverrides = urlOverrides;
10617
10816
  }
10618
- facts.loggedIn = existsSync22(SESSION_PATH);
10817
+ facts.loggedIn = existsSync23(SESSION_PATH);
10619
10818
  let userId = null;
10620
10819
  if (!facts.loggedIn) {
10621
10820
  lines.push(["Session", "not logged in"]);
@@ -10624,7 +10823,7 @@ var status_default = defineCommand11({
10624
10823
  let who = "logged in";
10625
10824
  try {
10626
10825
  const payload = decodeJwtPayload(
10627
- readFileSync24(TOKEN_PATH, "utf-8").trim()
10826
+ readFileSync23(TOKEN_PATH, "utf-8").trim()
10628
10827
  );
10629
10828
  if (payload.email) who = payload.email;
10630
10829
  userId = payload.sub ?? null;
@@ -10868,11 +11067,7 @@ var push_default = defineDeepspaceCommand({
10868
11067
  const force = args.force === true;
10869
11068
  const appDir = findAppDir();
10870
11069
  try {
10871
- if (!appDir)
10872
- throw new Refusal(
10873
- "No wrangler.toml found \u2014 run from inside an app directory.",
10874
- "not_in_app_repo"
10875
- );
11070
+ if (!appDir) throw new Refusal(noWranglerConfigMessage(process.cwd()), "not_in_app_repo");
10876
11071
  assertSyncableRepo(appDir);
10877
11072
  if (branchArg !== void 0) {
10878
11073
  const b = branchArg.trim();
@@ -10895,9 +11090,11 @@ var push_default = defineDeepspaceCommand({
10895
11090
  "unknown_branch"
10896
11091
  );
10897
11092
  }
11093
+ const uninitialized = hasWranglerConfig(appDir) && readAppId(appDir) === null;
10898
11094
  throw new Refusal(
10899
- `Branch "${branch}" has no commits yet \u2014 commit first, then push.`,
10900
- "no_commits"
11095
+ uninitialized ? `Branch "${branch}" has no commits yet, and wrangler.toml still holds the \`__APP_ID__\` placeholder \u2014 run \`deepspace app init\` first: it registers the app and makes the initial commit (committing first would put the unregistered placeholder in history).` : `Branch "${branch}" has no commits yet \u2014 commit first, then push.`,
11096
+ "no_commits",
11097
+ uninitialized ? { action: { cwd: appDir, argv: ["deepspace", "app", "init"] } } : {}
10901
11098
  );
10902
11099
  }
10903
11100
  const spinner2 = args.json ? null : createSpinner();
@@ -10961,6 +11158,13 @@ var push_default = defineDeepspaceCommand({
10961
11158
  "app_not_registered"
10962
11159
  );
10963
11160
  }
11161
+ if (sourceState.source?.provider === "github") {
11162
+ throw new Refusal(
11163
+ `This app uses GitHub source (${sourceState.source.repository}). Manage commits with normal Git/GitHub; \`deepspace deploy\` ships the local working tree without changing Git.`,
11164
+ "source_managed_by_github",
11165
+ { extra: { appId, repository: sourceState.source.repository } }
11166
+ );
11167
+ }
10964
11168
  const pullRecoveryCwd = selectedBranchCheckout(appDir, branch) ?? appDir;
10965
11169
  ensureSpaceRemote(appDir, appId);
10966
11170
  if (force) {
@@ -11100,11 +11304,7 @@ var pull_default = defineDeepspaceCommand({
11100
11304
  const branchArg = args.branch === void 0 ? void 0 : String(args.branch);
11101
11305
  const appArg = typeof args.app === "string" ? args.app : void 0;
11102
11306
  const appDir = findAppDir();
11103
- if (!appDir)
11104
- throw new Refusal(
11105
- "No wrangler.toml found \u2014 run from inside an app directory.",
11106
- "not_in_app_repo"
11107
- );
11307
+ if (!appDir) throw new Refusal(noWranglerConfigMessage(process.cwd()), "not_in_app_repo");
11108
11308
  if (branchArg !== void 0) {
11109
11309
  const b = branchArg.trim();
11110
11310
  if (!b) throw new Refusal("--branch was given an empty branch name.", "invalid_branch");
@@ -11132,9 +11332,9 @@ var pull_default = defineDeepspaceCommand({
11132
11332
  const token = await ensureToken();
11133
11333
  const deployUrl = deployBaseUrl();
11134
11334
  const appId = await resolveAppTarget(deployUrl, token, appArg);
11135
- ensureSpaceRemote(appDir, appId);
11136
11335
  spinner2?.message(`Checking the cloud ${branch} ref\u2026`);
11137
11336
  const remote = await repoApi(deployUrl, token, appId).getRefs();
11337
+ ensureSpaceRemote(appDir, appId);
11138
11338
  if (!remote) {
11139
11339
  spinner2?.stop("No cloud repo yet.");
11140
11340
  const pushCommand2 = targetedVcCommand("push", appId, branch);
@@ -11322,18 +11522,18 @@ var pull_default = defineDeepspaceCommand({
11322
11522
 
11323
11523
  // src/cli/commands/clone.ts
11324
11524
  import * as p20 from "@clack/prompts";
11325
- import { existsSync as existsSync23, readdirSync as readdirSync6, rmSync as rmSync5 } from "fs";
11326
- import { resolve as resolve17, basename as basename5, join as join21 } from "path";
11525
+ import { existsSync as existsSync24, readdirSync as readdirSync8, rmSync as rmSync5 } from "fs";
11526
+ import { resolve as resolve18, basename as basename5, join as join24 } from "path";
11327
11527
  function cleanFailedCloneDir(dir) {
11328
11528
  let entries;
11329
11529
  try {
11330
- entries = readdirSync6(dir);
11530
+ entries = readdirSync8(dir);
11331
11531
  } catch {
11332
- return existsSync23(dir) ? ["<unreadable>"] : [];
11532
+ return existsSync24(dir) ? ["<unreadable>"] : [];
11333
11533
  }
11334
11534
  if (entries.length === 1 && entries[0] === ".git") {
11335
11535
  try {
11336
- rmSync5(join21(dir, ".git"), { recursive: true, force: true });
11536
+ rmSync5(join24(dir, ".git"), { recursive: true, force: true });
11337
11537
  return [];
11338
11538
  } catch {
11339
11539
  return entries;
@@ -11342,10 +11542,10 @@ function cleanFailedCloneDir(dir) {
11342
11542
  return entries;
11343
11543
  }
11344
11544
  function validateCloneTarget(dir) {
11345
- if (!existsSync23(dir)) return false;
11545
+ if (!existsSync24(dir)) return false;
11346
11546
  let entries;
11347
11547
  try {
11348
- entries = readdirSync6(dir);
11548
+ entries = readdirSync8(dir);
11349
11549
  } catch (error) {
11350
11550
  if (error.code === "ENOTDIR") {
11351
11551
  throw new Refusal(`${dir} already exists and is not a directory.`, "dir_exists");
@@ -11377,7 +11577,7 @@ var clone_default = defineDeepspaceCommand({
11377
11577
  async run({ args }) {
11378
11578
  assertAppTargetResolvable(String(args.app));
11379
11579
  const selector = String(args.app).trim();
11380
- const dir = resolve17(String(args.dir ?? selector));
11580
+ const dir = resolve18(String(args.dir ?? selector));
11381
11581
  const dirPreexisted = validateCloneTarget(dir);
11382
11582
  const spinner2 = args.json ? null : createSpinner();
11383
11583
  spinner2?.start(`Resolving ${selector}\u2026`);
@@ -11550,7 +11750,7 @@ function pickPreviousRelease(releases) {
11550
11750
  var rollback_default = defineDeepspaceCommand({
11551
11751
  meta: {
11552
11752
  name: "rollback",
11553
- description: "Re-deploy a prior release's exact bundle (no rebuild)"
11753
+ description: "Re-deploy a prior release's exact bundle (no rebuild); the live secrets are kept, not the release's"
11554
11754
  },
11555
11755
  args: {
11556
11756
  release: {
@@ -11685,7 +11885,7 @@ import { defineCommand as defineCommand12 } from "citty";
11685
11885
 
11686
11886
  // src/cli/commands/workspace/new.ts
11687
11887
  import * as p25 from "@clack/prompts";
11688
- import { isAbsolute as isAbsolute6, resolve as resolve18 } from "path";
11888
+ import { isAbsolute as isAbsolute6, resolve as resolve19 } from "path";
11689
11889
 
11690
11890
  // src/cli/commands/workspace/analysis.ts
11691
11891
  import * as p23 from "@clack/prompts";
@@ -11819,25 +12019,25 @@ function printOverlaps(overlaps, json) {
11819
12019
  // src/cli/commands/workspace/local.ts
11820
12020
  import * as p24 from "@clack/prompts";
11821
12021
  import {
11822
- existsSync as existsSync24,
12022
+ existsSync as existsSync25,
11823
12023
  mkdirSync as mkdirSync7,
11824
- readFileSync as readFileSync25,
12024
+ readFileSync as readFileSync24,
11825
12025
  realpathSync as realpathSync6,
11826
12026
  renameSync as renameSync2,
11827
12027
  statSync as statSync3,
11828
12028
  writeFileSync as writeFileSync6
11829
12029
  } from "fs";
11830
- import { dirname as dirname11, isAbsolute as isAbsolute5, join as join22, relative as relative2, sep as sep2 } from "path";
12030
+ import { dirname as dirname11, isAbsolute as isAbsolute5, join as join25, relative as relative3, sep as sep3 } from "path";
11831
12031
  var WORKSPACE_MARKER = "deepspace-workspace.json";
11832
12032
  function excludeWorktreeDir(appDir, dir) {
11833
12033
  try {
11834
12034
  ensureLocalExclude(appDir, ".deepspace/");
11835
12035
  const realDir = realpathSync6(dir);
11836
- const rel = relative2(realpathSync6(repoToplevel(appDir)), realDir);
11837
- if (!rel || rel === ".." || rel.startsWith(`..${sep2}`) || isAbsolute5(rel)) return;
11838
- const pattern = rel.split(sep2).join("/");
12036
+ const rel = relative3(realpathSync6(repoToplevel(appDir)), realDir);
12037
+ if (!rel || rel === ".." || rel.startsWith(`..${sep3}`) || isAbsolute5(rel)) return;
12038
+ const pattern = rel.split(sep3).join("/");
11839
12039
  if (pattern === ".deepspace" || pattern.startsWith(".deepspace/")) return;
11840
- const relApp = relative2(realpathSync6(appDir), realDir).split(sep2).join("/");
12040
+ const relApp = relative3(realpathSync6(appDir), realDir).split(sep3).join("/");
11841
12041
  if (relApp === ".deepspace" || relApp.startsWith(".deepspace/")) return;
11842
12042
  ensureLocalExclude(appDir, `/${pattern}/`);
11843
12043
  } catch {
@@ -11848,7 +12048,7 @@ function worktreeMarkerPath(worktreeRoot) {
11848
12048
  const result = runGit(worktreeRoot, ["rev-parse", "--absolute-git-dir"], { allowFail: true });
11849
12049
  if (result.status !== 0) return null;
11850
12050
  const gitDir = result.stdout.toString("utf-8").trim();
11851
- return isAbsolute5(gitDir) ? join22(gitDir, WORKSPACE_MARKER) : null;
12051
+ return isAbsolute5(gitDir) ? join25(gitDir, WORKSPACE_MARKER) : null;
11852
12052
  } catch {
11853
12053
  return null;
11854
12054
  }
@@ -11884,7 +12084,7 @@ function isManagedWorkspaceWorktree(worktreeRoot, workspaceId) {
11884
12084
  const markerPath = worktreeMarkerPath(worktreeRoot);
11885
12085
  if (!markerPath) return false;
11886
12086
  try {
11887
- const marker = JSON.parse(readFileSync25(markerPath, "utf-8"));
12087
+ const marker = JSON.parse(readFileSync24(markerPath, "utf-8"));
11888
12088
  return marker.version === 1 && marker.workspaceId === workspaceId && marker.mode === "managed-linked";
11889
12089
  } catch {
11890
12090
  return false;
@@ -11893,11 +12093,11 @@ function isManagedWorkspaceWorktree(worktreeRoot, workspaceId) {
11893
12093
  function defaultWorkspaceRoot(appDir, workspaceId) {
11894
12094
  const primary = listWorktrees(appDir)[0];
11895
12095
  if (!primary) throw new Error("Git did not report a primary worktree");
11896
- return join22(primary.path, ".deepspace", "ws", workspaceId.slice(3).toLowerCase());
12096
+ return join25(primary.path, ".deepspace", "ws", workspaceId.slice(3).toLowerCase());
11897
12097
  }
11898
12098
  function appDirInWorktree(appDir, worktreeRoot) {
11899
- const appRelative = relative2(realpathSync6(repoToplevel(appDir)), realpathSync6(appDir));
11900
- return appRelative ? join22(worktreeRoot, appRelative) : worktreeRoot;
12099
+ const appRelative = relative3(realpathSync6(repoToplevel(appDir)), realpathSync6(appDir));
12100
+ return appRelative ? join25(worktreeRoot, appRelative) : worktreeRoot;
11901
12101
  }
11902
12102
  function primaryAppDir(appDir) {
11903
12103
  const primary = listWorktrees(appDir)[0];
@@ -11932,7 +12132,7 @@ function rematerializeWorkspaceWorktree(appDir, worktreeRoot, branch, workspaceI
11932
12132
  function inOwnLinkedWorktree(appDir, id) {
11933
12133
  if (workspaceIdFromBranch(currentBranch(appDir)) !== id) return false;
11934
12134
  try {
11935
- return statSync3(join22(repoToplevel(appDir), ".git")).isFile();
12135
+ return statSync3(join25(repoToplevel(appDir), ".git")).isFile();
11936
12136
  } catch {
11937
12137
  return false;
11938
12138
  }
@@ -12057,7 +12257,7 @@ function cleanupWorkspaceLocal(appDir, id, trunkBranch, options = {}) {
12057
12257
  }
12058
12258
  runGit(mainDir, ["worktree", "prune"], { allowFail: true });
12059
12259
  out.worktreeDir = null;
12060
- const leftover = existsSync24(linked.path);
12260
+ const leftover = existsSync25(linked.path);
12061
12261
  if (leftover) out.leftoverDir = linked.path;
12062
12262
  else out.worktreeRemoved = linked.path;
12063
12263
  deleteWsBranch(mainDir, branch, expectedBranchOid, out);
@@ -12137,7 +12337,7 @@ async function resolveTarget2(appArg) {
12137
12337
  const appDir = findAppDir();
12138
12338
  if (!appDir) {
12139
12339
  throw new Refusal(
12140
- "No wrangler.toml found \u2014 run from inside an app directory (or use -a <app> for list, and for drop with an explicit ws_ id).",
12340
+ `${noWranglerConfigMessage(process.cwd())} (Or use -a <app> for list, and for drop with an explicit ws_ id.)`,
12141
12341
  "not_in_app_repo"
12142
12342
  );
12143
12343
  }
@@ -12317,7 +12517,7 @@ var newWorkspaceCommand = defineDeepspaceCommand({
12317
12517
  idempotencyKey: mintIdempotencyKey()
12318
12518
  });
12319
12519
  const dirInput = typeof args.dir === "string" ? args.dir.trim() : "";
12320
- const worktreeRoot = dirInput ? isAbsolute6(dirInput) ? dirInput : resolve18(appDir, dirInput) : defaultWorkspaceRoot(appDir, id);
12520
+ const worktreeRoot = dirInput ? isAbsolute6(dirInput) ? dirInput : resolve19(appDir, dirInput) : defaultWorkspaceRoot(appDir, id);
12321
12521
  const dir = appDirInWorktree(appDir, worktreeRoot);
12322
12522
  try {
12323
12523
  materializeWorkspaceWorktree(appDir, worktreeRoot, branch, baseOid, id);
@@ -12355,14 +12555,14 @@ var newWorkspaceCommand = defineDeepspaceCommand({
12355
12555
 
12356
12556
  // src/cli/commands/workspace/attach.ts
12357
12557
  import * as p26 from "@clack/prompts";
12358
- import { existsSync as existsSync25, readdirSync as readdirSync7, realpathSync as realpathSync7, rmSync as rmSync6 } from "fs";
12359
- import { isAbsolute as isAbsolute7, resolve as resolve19 } from "path";
12558
+ import { existsSync as existsSync26, readdirSync as readdirSync9, realpathSync as realpathSync7, rmSync as rmSync6 } from "fs";
12559
+ import { isAbsolute as isAbsolute7, resolve as resolve20 } from "path";
12360
12560
  function samePath(a, b) {
12361
12561
  const real = (p35) => {
12362
12562
  try {
12363
12563
  return realpathSync7(p35);
12364
12564
  } catch {
12365
- return resolve19(p35);
12565
+ return resolve20(p35);
12366
12566
  }
12367
12567
  };
12368
12568
  return real(a) === real(b);
@@ -12375,10 +12575,10 @@ function finishedWorkspaceMessage(id, view) {
12375
12575
  return `Workspace ${id} is ${workspace.status} \u2014 its ref is retained briefly (fetch ${workspace.ref} to inspect), but attach only works on active workspaces.`;
12376
12576
  }
12377
12577
  function cleanFailedFreshAttachDir(dir) {
12378
- if (!existsSync25(dir)) return [];
12578
+ if (!existsSync26(dir)) return [];
12379
12579
  let entries;
12380
12580
  try {
12381
- entries = readdirSync7(dir);
12581
+ entries = readdirSync9(dir);
12382
12582
  } catch {
12383
12583
  return ["<unreadable>"];
12384
12584
  }
@@ -12452,12 +12652,12 @@ var attachWorkspaceCommand = defineDeepspaceCommand({
12452
12652
  );
12453
12653
  }
12454
12654
  const requestedDir = typeof args.dir === "string" ? args.dir.trim() : "";
12455
- const worktreeRoot = requestedDir ? isAbsolute7(requestedDir) ? requestedDir : resolve19(appDir, requestedDir) : defaultWorkspaceRoot(appDir, id);
12655
+ const worktreeRoot = requestedDir ? isAbsolute7(requestedDir) ? requestedDir : resolve20(appDir, requestedDir) : defaultWorkspaceRoot(appDir, id);
12456
12656
  const dir2 = appDirInWorktree(appDir, worktreeRoot);
12457
12657
  const localTip = resolveCommit(appDir, `refs/heads/${branch}`);
12458
12658
  if (localTip) {
12459
12659
  let existing = resolveWorkspaceWorktree(listWorktrees(appDir), id);
12460
- if (existing && !existsSync25(existing)) {
12660
+ if (existing && !existsSync26(existing)) {
12461
12661
  runGit(appDir, ["worktree", "prune"], { allowFail: true });
12462
12662
  existing = null;
12463
12663
  }
@@ -12571,11 +12771,11 @@ var attachWorkspaceCommand = defineDeepspaceCommand({
12571
12771
  extra: { status: view.workspace.status }
12572
12772
  });
12573
12773
  }
12574
- const dir = resolve19(
12774
+ const dir = resolve20(
12575
12775
  process.cwd(),
12576
12776
  (typeof args.dir === "string" ? args.dir.trim() : "") || `ws-${id.slice(3).toLowerCase()}`
12577
12777
  );
12578
- if (existsSync25(dir)) {
12778
+ if (existsSync26(dir)) {
12579
12779
  spinner2?.stop("Target exists.");
12580
12780
  throw new Refusal(`${dir} already exists \u2014 pass a different directory.`, "dir_exists");
12581
12781
  }
@@ -12912,13 +13112,13 @@ import * as p30 from "@clack/prompts";
12912
13112
 
12913
13113
  // src/cli/lib/validation.ts
12914
13114
  import { sync as spawnSync7 } from "cross-spawn";
12915
- import { readFileSync as readFileSync26 } from "fs";
12916
- import { join as join23 } from "path";
13115
+ import { readFileSync as readFileSync25 } from "fs";
13116
+ import { join as join26 } from "path";
12917
13117
  var MAX_SUMMARY_CHARS = 4e3;
12918
13118
  var MAX_OUTPUT_BUFFER = 64 * 1024 * 1024;
12919
13119
  function configuredCommand(appDir) {
12920
13120
  try {
12921
- const pkg2 = JSON.parse(readFileSync26(join23(appDir, "package.json"), "utf-8"));
13121
+ const pkg2 = JSON.parse(readFileSync25(join26(appDir, "package.json"), "utf-8"));
12922
13122
  if (pkg2.scripts?.validate) return `${detectPackageManager(appDir)} run --silent validate`;
12923
13123
  } catch {
12924
13124
  }
@@ -13994,13 +14194,13 @@ function sameRefs(left, right) {
13994
14194
 
13995
14195
  // src/cli/commands/update.ts
13996
14196
  import * as p34 from "@clack/prompts";
13997
- import { existsSync as existsSync26, readFileSync as readFileSync27, writeFileSync as writeFileSync7 } from "fs";
13998
- import { join as join24, resolve as resolve20 } from "path";
14197
+ import { existsSync as existsSync27, readFileSync as readFileSync26, writeFileSync as writeFileSync7 } from "fs";
14198
+ import { join as join27, resolve as resolve21 } from "path";
13999
14199
 
14000
14200
  // package.json
14001
14201
  var package_default = {
14002
14202
  name: "deepspace",
14003
- version: "0.23.0",
14203
+ version: "0.23.2",
14004
14204
  engines: {
14005
14205
  node: ">=22.15.0 <23 || >=24 <25 || >=26 <27"
14006
14206
  },
@@ -14175,24 +14375,25 @@ function gapIsTooWide(from, to) {
14175
14375
  return from.major === 0 && from.minor < OLDEST_SUPPORTED_MINOR;
14176
14376
  }
14177
14377
  function readAppSdkVersion(appDir) {
14178
- const installed = join24(appDir, "node_modules", "deepspace", "package.json");
14179
- if (existsSync26(installed)) {
14180
- const parsed2 = JSON.parse(readFileSync27(installed, "utf8"));
14378
+ const installed = join27(appDir, "node_modules", "deepspace", "package.json");
14379
+ if (existsSync27(installed)) {
14380
+ const parsed2 = JSON.parse(readFileSync26(installed, "utf8"));
14181
14381
  if (typeof parsed2.version === "string") return parsed2.version;
14182
14382
  }
14183
- const manifest = join24(appDir, "package.json");
14184
- if (!existsSync26(manifest)) return null;
14185
- const parsed = JSON.parse(readFileSync27(manifest, "utf8"));
14383
+ const manifest = join27(appDir, "package.json");
14384
+ if (!existsSync27(manifest)) return null;
14385
+ const parsed = JSON.parse(readFileSync26(manifest, "utf8"));
14186
14386
  return parsed.dependencies?.deepspace ?? null;
14187
14387
  }
14188
14388
  function pinSdkVersion(appDir, version) {
14189
- const path = join24(appDir, "package.json");
14190
- const source = readFileSync27(path, "utf8");
14389
+ const path = join27(appDir, "package.json");
14390
+ const source = readFileSync26(path, "utf8");
14191
14391
  const manifest = JSON.parse(source);
14192
14392
  const dependencies = manifest.dependencies;
14193
14393
  if (!dependencies || typeof dependencies !== "object" || Array.isArray(dependencies)) return false;
14194
14394
  let changed = false;
14195
- if (Object.prototype.hasOwnProperty.call(dependencies, "deepspace") && typeof dependencies.deepspace === "string" && dependencies.deepspace !== `^${version}`) {
14395
+ const registrySpec = (spec) => !/^(file|link|workspace|git\+|github|https?):/.test(spec) && !spec.includes("/");
14396
+ if (Object.prototype.hasOwnProperty.call(dependencies, "deepspace") && typeof dependencies.deepspace === "string" && registrySpec(dependencies.deepspace) && dependencies.deepspace !== `^${version}`) {
14196
14397
  dependencies.deepspace = `^${version}`;
14197
14398
  changed = true;
14198
14399
  }
@@ -14213,9 +14414,9 @@ function buildPreviewSecretsUpgradeInstruction(appDir) {
14213
14414
  "vite.config.mts",
14214
14415
  "vite.config.js",
14215
14416
  "vite.config.mjs"
14216
- ].find((candidate) => existsSync26(join24(appDir, candidate)));
14417
+ ].find((candidate) => existsSync27(join27(appDir, candidate)));
14217
14418
  if (!configName) return null;
14218
- const source = readFileSync27(join24(appDir, configName), "utf8");
14419
+ const source = readFileSync26(join27(appDir, configName), "utf8");
14219
14420
  if (source.includes("deepspace-remove-build-preview-secrets") || source.includes("deepspaceBuild(") || source.includes("'deepspace/build'") || source.includes('"deepspace/build"')) {
14220
14421
  return null;
14221
14422
  }
@@ -14223,9 +14424,9 @@ function buildPreviewSecretsUpgradeInstruction(appDir) {
14223
14424
  }
14224
14425
  function usersSchemaVisibilityUpgradeInstruction(appDir) {
14225
14426
  const schemaName = "src/schemas/users-schema.ts";
14226
- const path = join24(appDir, schemaName);
14227
- if (!existsSync26(path)) return null;
14228
- const source = readFileSync27(path, "utf8");
14427
+ const path = join27(appDir, schemaName);
14428
+ if (!existsSync27(path)) return null;
14429
+ const source = readFileSync26(path, "utf8");
14229
14430
  if (!/\bmember\s*:\s*\{[^}]*\bread\s*:\s*true\b/s.test(source)) return null;
14230
14431
  return `${schemaName}: review the member user-row visibility change at ${USERS_SCHEMA_VISIBILITY_GUIDE}; app update left this app-owned schema unchanged.`;
14231
14432
  }
@@ -14285,12 +14486,9 @@ var update_default = defineDeepspaceCommand({
14285
14486
  }
14286
14487
  },
14287
14488
  async run({ args }) {
14288
- const appDir = resolve20(".");
14489
+ const appDir = resolve21(".");
14289
14490
  if (!hasWranglerConfig(appDir)) {
14290
- throw new Refusal(
14291
- "No wrangler.toml found \u2014 run this from inside a DeepSpace app directory.",
14292
- "not_in_app_repo"
14293
- );
14491
+ throw new Refusal(noWranglerConfigMessage(join27(appDir, "wrangler.toml")), "not_in_app_repo");
14294
14492
  }
14295
14493
  assertSyncableRepo(appDir);
14296
14494
  const currentRaw = readAppSdkVersion(appDir);
@@ -14376,8 +14574,8 @@ var update_default = defineDeepspaceCommand({
14376
14574
 
14377
14575
  // src/cli/cli.ts
14378
14576
  var __dirname = dirname12(fileURLToPath3(import.meta.url));
14379
- var pkg = JSON.parse(readFileSync28(join25(__dirname, "..", "package.json"), "utf-8"));
14380
- if (process.platform === "linux" && !process.env.SSL_CERT_FILE && existsSync27("/etc/ssl/certs/ca-certificates.crt")) {
14577
+ var pkg = JSON.parse(readFileSync27(join28(__dirname, "..", "package.json"), "utf-8"));
14578
+ if (process.platform === "linux" && !process.env.SSL_CERT_FILE && existsSync28("/etc/ssl/certs/ca-certificates.crt")) {
14381
14579
  process.env.SSL_CERT_FILE = "/etc/ssl/certs/ca-certificates.crt";
14382
14580
  }
14383
14581
  var create2 = defineCommand14({