deepspace 0.23.0 → 0.23.1

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
1134
+ );
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
950
1145
  );
951
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.)`;
952
1177
  }
1178
+ function hasWranglerConfig(appDir) {
1179
+ return existsSync3(join2(appDir, "wrangler.toml"));
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
  );
@@ -1492,197 +1713,6 @@ async function fetchAppIdentityToken(deployUrl, callerJwt, appId) {
1492
1713
  return body.token;
1493
1714
  }
1494
1715
 
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";
1671
- }
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);
1680
- }
1681
- }
1682
- function hasWranglerConfig(appDir) {
1683
- return existsSync6(join6(appDir, "wrangler.toml"));
1684
- }
1685
-
1686
1716
  // src/cli/lib/dev-vars.ts
1687
1717
  var HEADER = [
1688
1718
  "# Generated by deepspace \u2014 do not edit; this file is rewritten on every",
@@ -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",
@@ -3350,6 +3422,7 @@ var test_default = defineDeepspaceCommand({
3350
3422
  ensurePlaywright(appDir);
3351
3423
  }
3352
3424
  let exitCode = 0;
3425
+ let skippedSpecs = [];
3353
3426
  switch (suite) {
3354
3427
  case "smoke":
3355
3428
  exitCode = runPlaywright(appDir, ["tests/smoke.spec.ts"], port, wranglerEnv, forwarded);
@@ -3371,13 +3444,13 @@ var test_default = defineDeepspaceCommand({
3371
3444
  if (exitCode === 0) exitCode = runPlaywright(appDir, [], port, wranglerEnv, forwarded);
3372
3445
  break;
3373
3446
  case "default":
3374
- exitCode = runPlaywright(
3375
- appDir,
3376
- ["tests/smoke.spec.ts", "tests/api.spec.ts"],
3377
- port,
3378
- wranglerEnv,
3379
- forwarded
3380
- );
3447
+ skippedSpecs = specsSkippedByDefaultSuite(appDir);
3448
+ exitCode = runPlaywright(appDir, [...DEFAULT_SUITE_SPECS], port, wranglerEnv, forwarded);
3449
+ if (skippedSpecs.length > 0) {
3450
+ say(
3451
+ `skipped ${skippedSpecs.length} spec file(s) not in smoke+api (${skippedSpecs.join(", ")}) \u2014 run 'deepspace test run all'`
3452
+ );
3453
+ }
3381
3454
  break;
3382
3455
  default:
3383
3456
  if (suite.endsWith(".spec.ts")) {
@@ -3392,10 +3465,10 @@ Available: smoke, api, e2e, unit, all`,
3392
3465
  }
3393
3466
  if (exitCode !== 0) {
3394
3467
  throw new Refusal(`Test suite '${suite}' failed (exit ${exitCode}).`, "tests_failed", {
3395
- extra: { suite, port, exitCode }
3468
+ extra: { suite, port, exitCode, skippedSpecs }
3396
3469
  });
3397
3470
  }
3398
- return { data: { suite, port, appDir, wranglerEnv: wranglerEnv ?? null } };
3471
+ return { data: { suite, port, appDir, wranglerEnv: wranglerEnv ?? null, skippedSpecs } };
3399
3472
  }
3400
3473
  });
3401
3474
  function runSuite(appDir, argv, wranglerEnv, extraEnv = {}) {
@@ -3829,7 +3902,7 @@ var recover = defineDeepspaceCommand({
3829
3902
  for (const target2 of targets) {
3830
3903
  try {
3831
3904
  const account = await recoverRemoteTestAccount(target2.id);
3832
- const name = localNames.get(account.email);
3905
+ const name = account.name ?? localNames.get(account.email);
3833
3906
  upsertTestAccount({
3834
3907
  id: account.id,
3835
3908
  email: account.email,
@@ -3880,7 +3953,7 @@ var test_accounts_default = defineCommand2({
3880
3953
  // src/cli/commands/deploy.ts
3881
3954
  import * as p12 from "@clack/prompts";
3882
3955
  import { defineCommand as defineCommand3 } from "citty";
3883
- import { resolve as resolve13 } from "path";
3956
+ import { join as join19, resolve as resolve14 } from "path";
3884
3957
 
3885
3958
  // src/cli/lib/edge-propagation.ts
3886
3959
  import { request as httpRequest } from "http";
@@ -3945,14 +4018,14 @@ async function servesRelease(probe, releaseStamp) {
3945
4018
  }
3946
4019
  function getOnFreshConnection(url, timeoutMs = 5e3) {
3947
4020
  const send = url.protocol === "http:" ? httpRequest : httpsRequest;
3948
- return new Promise((resolve21) => {
4021
+ return new Promise((resolve22) => {
3949
4022
  const request2 = send(
3950
4023
  url,
3951
4024
  { method: "GET", agent: false, timeout: timeoutMs, headers: { "cache-control": "no-cache" } },
3952
4025
  (response) => {
3953
4026
  if (response.statusCode !== 200) {
3954
4027
  response.resume();
3955
- resolve21(null);
4028
+ resolve22(null);
3956
4029
  return;
3957
4030
  }
3958
4031
  let body = "";
@@ -3960,20 +4033,20 @@ function getOnFreshConnection(url, timeoutMs = 5e3) {
3960
4033
  response.on("data", (chunk) => {
3961
4034
  if (body.length < 4096) body += chunk;
3962
4035
  });
3963
- response.on("end", () => resolve21(body));
3964
- response.on("error", () => resolve21(null));
4036
+ response.on("end", () => resolve22(body));
4037
+ response.on("error", () => resolve22(null));
3965
4038
  }
3966
4039
  );
3967
4040
  request2.on("timeout", () => {
3968
4041
  request2.destroy();
3969
- resolve21(null);
4042
+ resolve22(null);
3970
4043
  });
3971
- request2.on("error", () => resolve21(null));
4044
+ request2.on("error", () => resolve22(null));
3972
4045
  request2.end();
3973
4046
  });
3974
4047
  }
3975
4048
  function sleep2(ms) {
3976
- return new Promise((resolve21) => setTimeout(resolve21, ms));
4049
+ return new Promise((resolve22) => setTimeout(resolve22, ms));
3977
4050
  }
3978
4051
 
3979
4052
  // src/shared/app-files.ts
@@ -4038,36 +4111,49 @@ function describeFilesFailure(status3, body) {
4038
4111
  import * as p6 from "@clack/prompts";
4039
4112
  import { execSync as execSync2 } from "child_process";
4040
4113
  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";
4114
+ import { existsSync as existsSync15, readFileSync as readFileSync16, readdirSync as readdirSync6 } from "fs";
4115
+ import { dirname as dirname7, join as join17, resolve as resolve12 } from "path";
4116
+
4117
+ // src/build/plugin.ts
4118
+ import { existsSync as existsSync12, lstatSync, readdirSync as readdirSync5, unlinkSync as unlinkSync2 } from "fs";
4119
+ import { join as join14, resolve as resolve11 } from "path";
4120
+ function removeBuildDevVars(workerDir) {
4121
+ const path = join14(workerDir, ".dev.vars");
4122
+ if (!existsSync12(path)) return false;
4123
+ if (!lstatSync(path).isFile()) {
4124
+ throw new Error(`Refusing unsafe build secret path: ${path}`);
4125
+ }
4126
+ unlinkSync2(path);
4127
+ return true;
4128
+ }
4043
4129
 
4044
4130
  // src/documentation/deploy.ts
4045
- import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
4046
- import { join as join13 } from "path";
4131
+ import { existsSync as existsSync13, readFileSync as readFileSync14 } from "fs";
4132
+ import { join as join15 } from "path";
4047
4133
  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)) {
4134
+ if (!existsSync13(join15(appDir, "documentation.json"))) return void 0;
4135
+ const manifestPath = join15(clientDir, "_documentation", "manifest.json");
4136
+ if (!existsSync13(manifestPath)) {
4051
4137
  throw new Error(
4052
4138
  "documentation.json exists but the build produced no documentation manifest. Ensure vite.config.ts includes deepSpaceDocumentation() from deepspace/documentation."
4053
4139
  );
4054
4140
  }
4055
- return JSON.parse(readFileSync15(manifestPath, "utf8"));
4141
+ return JSON.parse(readFileSync14(manifestPath, "utf8"));
4056
4142
  }
4057
4143
 
4058
4144
  // src/cli/commands/update/app-migrations.ts
4059
4145
  import {
4060
4146
  closeSync,
4061
4147
  constants,
4062
- existsSync as existsSync13,
4148
+ existsSync as existsSync14,
4063
4149
  fstatSync,
4064
4150
  ftruncateSync,
4065
- lstatSync,
4151
+ lstatSync as lstatSync2,
4066
4152
  openSync,
4067
- readFileSync as readFileSync16,
4153
+ readFileSync as readFileSync15,
4068
4154
  writeSync
4069
4155
  } from "fs";
4070
- import { join as join14 } from "path";
4156
+ import { join as join16 } from "path";
4071
4157
 
4072
4158
  // src/shared/protocol/app-migrations.ts
4073
4159
  var APP_MIGRATION_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/;
@@ -4119,8 +4205,8 @@ var APP_MIGRATION_DEFINITIONS = SOURCE_MIGRATIONS.map(
4119
4205
  ({ id, description }) => ({ id, description })
4120
4206
  );
4121
4207
  function readAppliedAppMigrations(appDir) {
4122
- const path = join14(appDir, APP_MIGRATIONS_MANIFEST);
4123
- if (!existsSync13(path)) return [];
4208
+ const path = join16(appDir, APP_MIGRATIONS_MANIFEST);
4209
+ if (!existsSync14(path)) return [];
4124
4210
  const source = readRegularFile(path);
4125
4211
  let parsed;
4126
4212
  try {
@@ -4146,10 +4232,10 @@ function planAppMigrations(appDir) {
4146
4232
  const edits = [];
4147
4233
  for (const repoPath of tracked) {
4148
4234
  if (!isSourcePath(repoPath, appPrefix)) continue;
4149
- const absolutePath = join14(repo, repoPath);
4235
+ const absolutePath = join16(repo, repoPath);
4150
4236
  let stat;
4151
4237
  try {
4152
- stat = lstatSync(absolutePath);
4238
+ stat = lstatSync2(absolutePath);
4153
4239
  } catch {
4154
4240
  continue;
4155
4241
  }
@@ -4183,8 +4269,8 @@ function planAppMigrations(appDir) {
4183
4269
  });
4184
4270
  if (pending.length > 0 && blockers.length === 0) {
4185
4271
  const manifestPath = `${appPrefix}${APP_MIGRATIONS_MANIFEST}`;
4186
- const absoluteManifestPath = join14(repo, manifestPath);
4187
- const before = existsSync13(absoluteManifestPath) ? readRegularFile(absoluteManifestPath) : null;
4272
+ const absoluteManifestPath = join16(repo, manifestPath);
4273
+ const before = existsSync14(absoluteManifestPath) ? readRegularFile(absoluteManifestPath) : null;
4188
4274
  const after = `${JSON.stringify([...appliedIds, ...pending.map(({ id }) => id)], null, 2)}
4189
4275
  `;
4190
4276
  if (before !== after) edits.push({ path: manifestPath, before, after });
@@ -4199,13 +4285,13 @@ function planAppMigrations(appDir) {
4199
4285
  function applyAppMigrationPlan(appDir, plan) {
4200
4286
  const repo = repoToplevel(appDir);
4201
4287
  for (const edit of plan.edits) {
4202
- const absolutePath = join14(repo, edit.path);
4203
- const current = existsSync13(absolutePath) ? readRegularFile(absolutePath) : null;
4288
+ const absolutePath = join16(repo, edit.path);
4289
+ const current = existsSync14(absolutePath) ? readRegularFile(absolutePath) : null;
4204
4290
  if (current !== edit.before) {
4205
4291
  throw new Error(`Migration input changed after planning: ${edit.path}`);
4206
4292
  }
4207
4293
  }
4208
- for (const edit of plan.edits) writeRegularFile(join14(repo, edit.path), edit.before, edit.after);
4294
+ for (const edit of plan.edits) writeRegularFile(join16(repo, edit.path), edit.before, edit.after);
4209
4295
  }
4210
4296
  function normalizeEol(source) {
4211
4297
  return source.replaceAll("\r\n", "\n");
@@ -4214,18 +4300,18 @@ function restoreEol(source, checkout) {
4214
4300
  return checkout.includes("\r\n") ? source.replaceAll("\n", "\r\n") : source;
4215
4301
  }
4216
4302
  function readRegularFile(path) {
4217
- const before = lstatSync(path);
4303
+ const before = lstatSync2(path);
4218
4304
  if (!before.isFile() || before.isSymbolicLink()) {
4219
4305
  throw new Error(`Migration input must be a regular file, not a symlink: ${path}`);
4220
4306
  }
4221
4307
  const fd = openSync(path, constants.O_RDONLY);
4222
4308
  try {
4223
4309
  const opened = fstatSync(fd);
4224
- const after = lstatSync(path);
4310
+ const after = lstatSync2(path);
4225
4311
  if (!opened.isFile() || opened.dev !== after.dev || opened.ino !== after.ino) {
4226
4312
  throw new Error(`Migration input changed while opening: ${path}`);
4227
4313
  }
4228
- return readFileSync16(fd, "utf-8");
4314
+ return readFileSync15(fd, "utf-8");
4229
4315
  } finally {
4230
4316
  closeSync(fd);
4231
4317
  }
@@ -4240,18 +4326,18 @@ function writeRegularFile(path, before, after) {
4240
4326
  }
4241
4327
  return;
4242
4328
  }
4243
- const pathStat = lstatSync(path);
4329
+ const pathStat = lstatSync2(path);
4244
4330
  if (!pathStat.isFile() || pathStat.isSymbolicLink()) {
4245
4331
  throw new Error(`Migration input must be a regular file, not a symlink: ${path}`);
4246
4332
  }
4247
4333
  const fd = openSync(path, constants.O_RDWR);
4248
4334
  try {
4249
4335
  const opened = fstatSync(fd);
4250
- const currentPath = lstatSync(path);
4336
+ const currentPath = lstatSync2(path);
4251
4337
  if (!opened.isFile() || opened.dev !== currentPath.dev || opened.ino !== currentPath.ino) {
4252
4338
  throw new Error(`Migration input changed while opening: ${path}`);
4253
4339
  }
4254
- if (readFileSync16(fd, "utf-8") !== before) {
4340
+ if (readFileSync15(fd, "utf-8") !== before) {
4255
4341
  throw new Error(`Migration input changed after planning: ${path}`);
4256
4342
  }
4257
4343
  ftruncateSync(fd, 0);
@@ -4578,16 +4664,6 @@ function findWorkerOwnedNotFoundBlockers(source, file) {
4578
4664
 
4579
4665
  // src/cli/commands/deploy/build.ts
4580
4666
  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
4667
  function declaredCompatibilityDate(config) {
4592
4668
  const declared = config.compatibility_date;
4593
4669
  return typeof declared === "string" && /^\d{4}-\d{2}-\d{2}$/.test(declared) ? declared : null;
@@ -4624,19 +4700,19 @@ async function buildDeployBundle(options) {
4624
4700
  preparedWranglerConfig?.cleanup();
4625
4701
  }
4626
4702
  spinner2.stop(`Built (${Math.round((Date.now() - buildStarted) / 1e3)}s)`);
4627
- const deployConfigPath = join15(appDir, ".wrangler", "deploy", "config.json");
4628
- if (!existsSync14(deployConfigPath)) {
4703
+ const deployConfigPath = join17(appDir, ".wrangler", "deploy", "config.json");
4704
+ if (!existsSync15(deployConfigPath)) {
4629
4705
  output.die(
4630
4706
  "Build output config not found at .wrangler/deploy/config.json",
4631
4707
  "build_output_missing"
4632
4708
  );
4633
4709
  }
4634
- const deployConfig = JSON.parse(readFileSync17(deployConfigPath, "utf-8"));
4635
- const outputWranglerPath = resolve11(dirname7(deployConfigPath), deployConfig.configPath);
4636
- if (!existsSync14(outputWranglerPath)) {
4710
+ const deployConfig = JSON.parse(readFileSync16(deployConfigPath, "utf-8"));
4711
+ const outputWranglerPath = resolve12(dirname7(deployConfigPath), deployConfig.configPath);
4712
+ if (!existsSync15(outputWranglerPath)) {
4637
4713
  output.die(`Output wrangler.json not found at ${outputWranglerPath}`, "build_output_missing");
4638
4714
  }
4639
- const outputConfig = JSON.parse(readFileSync17(outputWranglerPath, "utf-8"));
4715
+ const outputConfig = JSON.parse(readFileSync16(outputWranglerPath, "utf-8"));
4640
4716
  if (outputConfig.name && outputConfig.name !== appName) {
4641
4717
  const envHint = envName ? ` (--env ${envName})` : "";
4642
4718
  output.die(
@@ -4650,12 +4726,12 @@ async function buildDeployBundle(options) {
4650
4726
  } catch (error) {
4651
4727
  output.die(errorMessage(error), "build_output_unsafe");
4652
4728
  }
4653
- const workerBundlePath = join15(workerDir, outputConfig.main);
4654
- const clientDir = outputConfig.assets?.directory ? resolve11(workerDir, outputConfig.assets.directory) : null;
4655
- if (!existsSync14(workerBundlePath)) {
4729
+ const workerBundlePath = join17(workerDir, outputConfig.main);
4730
+ const clientDir = outputConfig.assets?.directory ? resolve12(workerDir, outputConfig.assets.directory) : null;
4731
+ if (!existsSync15(workerBundlePath)) {
4656
4732
  output.die(`Worker bundle not found at ${workerBundlePath}`, "build_output_missing");
4657
4733
  }
4658
- if (!clientDir || !existsSync14(clientDir)) {
4734
+ if (!clientDir || !existsSync15(clientDir)) {
4659
4735
  return output.die(`Client assets not found at ${clientDir}`, "build_output_missing");
4660
4736
  }
4661
4737
  try {
@@ -4670,7 +4746,7 @@ async function buildDeployBundle(options) {
4670
4746
  spinner2.stop(`Collected ${assets.length} assets`);
4671
4747
  const appIdRefusal = clientAppIdRefusal(assets, appId, envName);
4672
4748
  if (appIdRefusal) output.die(appIdRefusal.message, appIdRefusal.code);
4673
- const workerJs = readFileSync17(workerBundlePath, "utf-8");
4749
+ const workerJs = readFileSync16(workerBundlePath, "utf-8");
4674
4750
  const doBindings = outputConfig.durable_objects?.bindings;
4675
4751
  const sqliteClasses = new Set(
4676
4752
  outputConfig.migrations?.flatMap((migration) => migration.new_sqlite_classes ?? []) ?? []
@@ -4753,7 +4829,7 @@ function clientAppIdRefusal(assets, appId, envName) {
4753
4829
  let unsubstituted;
4754
4830
  for (const asset of assets) {
4755
4831
  if (!CLIENT_CODE_ASSET.test(asset.path)) continue;
4756
- const source = readFileSync17(asset.sourcePath, "utf-8");
4832
+ const source = readFileSync16(asset.sourcePath, "utf-8");
4757
4833
  if (unsubstituted === void 0 && source.includes(APP_ID_DEFINE)) unsubstituted = asset.path;
4758
4834
  for (const found of source.matchAll(APP_ID_LITERAL)) {
4759
4835
  if (found[0] !== appId && !foreign.has(found[0])) foreign.set(found[0], asset.path);
@@ -4780,14 +4856,14 @@ var LARGE_MEDIA_HOME = "Large media belongs in your app files (`deepspace app fi
4780
4856
  function collectAssets(dir) {
4781
4857
  const assets = [];
4782
4858
  const walk = (currentDir, prefix) => {
4783
- for (const entry of readdirSync4(currentDir, { withFileTypes: true })) {
4859
+ for (const entry of readdirSync6(currentDir, { withFileTypes: true })) {
4784
4860
  if (isDeployAssetControlFile(entry.name)) continue;
4785
- const fullPath = join15(currentDir, entry.name);
4861
+ const fullPath = join17(currentDir, entry.name);
4786
4862
  const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
4787
4863
  if (entry.isDirectory()) {
4788
4864
  walk(fullPath, relativePath);
4789
4865
  } else {
4790
- const bytes = readFileSync17(fullPath);
4866
+ const bytes = readFileSync16(fullPath);
4791
4867
  assets.push({
4792
4868
  path: `/${relativePath}`,
4793
4869
  hash: createHash2("sha256").update(bytes).digest("hex"),
@@ -4806,9 +4882,9 @@ function readDeployAssetConfig(clientDir) {
4806
4882
  ["_headers", "_headers"],
4807
4883
  ["_redirects", "_redirects"]
4808
4884
  ]) {
4809
- const filePath = join15(clientDir, file);
4810
- if (!existsSync14(filePath)) continue;
4811
- const content = readFileSync17(filePath, "utf8");
4885
+ const filePath = join17(clientDir, file);
4886
+ if (!existsSync15(filePath)) continue;
4887
+ const content = readFileSync16(filePath, "utf8");
4812
4888
  if (content.trim()) config[key] = content;
4813
4889
  }
4814
4890
  return config;
@@ -4822,8 +4898,8 @@ function errorMessage(error) {
4822
4898
 
4823
4899
  // src/cli/commands/deploy/commerce.ts
4824
4900
  import * as p7 from "@clack/prompts";
4825
- import { existsSync as existsSync15 } from "fs";
4826
- import { join as join16 } from "path";
4901
+ import { existsSync as existsSync16 } from "fs";
4902
+ import { join as join18 } from "path";
4827
4903
  import { pathToFileURL as pathToFileURL2 } from "url";
4828
4904
 
4829
4905
  // src/shared/api-error.ts
@@ -4875,10 +4951,10 @@ function errorMessage2(error) {
4875
4951
  return error instanceof Error ? error.message : String(error);
4876
4952
  }
4877
4953
  async function loadDeclaration(appDir, sourceFile, outputFile, exportName) {
4878
- const sourcePath = join16(appDir, "src", sourceFile);
4879
- if (!existsSync15(sourcePath)) return void 0;
4954
+ const sourcePath = join18(appDir, "src", sourceFile);
4955
+ if (!existsSync16(sourcePath)) return void 0;
4880
4956
  const esbuild = await import("esbuild");
4881
- const outputPath = join16(appDir, ".wrangler", "deploy", outputFile);
4957
+ const outputPath = join18(appDir, ".wrangler", "deploy", outputFile);
4882
4958
  await esbuild.build({
4883
4959
  entryPoints: [sourcePath],
4884
4960
  bundle: true,
@@ -4971,7 +5047,7 @@ Stripe will keep charging them until you cancel. Cancel at the end of their curr
4971
5047
  }
4972
5048
  }
4973
5049
  async function syncSubscriptionPlans(appDir, appName, token, nonInteractive) {
4974
- if (!existsSync15(join16(appDir, "src", "subscriptions.ts"))) return;
5050
+ if (!existsSync16(join18(appDir, "src", "subscriptions.ts"))) return;
4975
5051
  let plans;
4976
5052
  try {
4977
5053
  plans = await loadDeclaration(
@@ -5026,7 +5102,7 @@ async function syncSubscriptionPlans(appDir, appName, token, nonInteractive) {
5026
5102
  }
5027
5103
  }
5028
5104
  async function syncOneTimeProducts(appDir, appName, token) {
5029
- if (!existsSync15(join16(appDir, "src", "products.ts"))) return;
5105
+ if (!existsSync16(join18(appDir, "src", "products.ts"))) return;
5030
5106
  let products;
5031
5107
  try {
5032
5108
  products = await loadDeclaration(
@@ -5128,7 +5204,7 @@ function createDeployOutput(json) {
5128
5204
 
5129
5205
  // src/cli/commands/deploy/request.ts
5130
5206
  import * as p10 from "@clack/prompts";
5131
- import { readFileSync as readFileSync18 } from "fs";
5207
+ import { readFileSync as readFileSync17 } from "fs";
5132
5208
 
5133
5209
  // src/cli/commands/deploy/repository.ts
5134
5210
  import * as p9 from "@clack/prompts";
@@ -5319,9 +5395,9 @@ function repoApi(deployUrl, token, appId) {
5319
5395
  }
5320
5396
 
5321
5397
  // src/cli/lib/vc-remote.ts
5322
- import { existsSync as existsSync16 } from "fs";
5398
+ import { existsSync as existsSync17 } from "fs";
5323
5399
  import { tmpdir } from "os";
5324
- import { isAbsolute as isAbsolute3, relative, resolve as resolve12 } from "path";
5400
+ import { isAbsolute as isAbsolute3, relative as relative2, resolve as resolve13 } from "path";
5325
5401
  function spaceRemoteName(environment = DEEPSPACE_ENV) {
5326
5402
  if (environment === "staging") return "space-staging";
5327
5403
  if (environment === "production") return "space";
@@ -5367,14 +5443,14 @@ function entryIsTransient(cwd) {
5367
5443
  const entry = process.argv[1];
5368
5444
  if (!entry) return true;
5369
5445
  if (/[\\/]_npx[\\/]/.test(entry) || entry.startsWith(tmpdir())) return true;
5370
- const rel = relative(resolve12(cwd), resolve12(entry));
5446
+ const rel = relative2(resolve13(cwd), resolve13(entry));
5371
5447
  return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel);
5372
5448
  }
5373
5449
  function helperEntryUsable(command) {
5374
5450
  const match = /^!(?:DEEPSPACE_ENV=(production|staging) )?'([^']*)' '([^']*)' git-credential$/.exec(command);
5375
5451
  const expectedEnvironment = DEEPSPACE_ENV === "production" ? void 0 : DEEPSPACE_ENV;
5376
5452
  if (!match || match[1] !== expectedEnvironment) return false;
5377
- return existsSync16(match[2]) && existsSync16(match[3]);
5453
+ return existsSync17(match[2]) && existsSync17(match[3]);
5378
5454
  }
5379
5455
  function credentialHelperCommand(environment = DEEPSPACE_ENV) {
5380
5456
  const entry = process.argv[1];
@@ -5509,12 +5585,6 @@ function classifyPushTransportFailure(error, cwd) {
5509
5585
  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
5586
  };
5511
5587
  }
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
5588
  if (/(?:HTTP |error: )429\b/i.test(message)) {
5519
5589
  return {
5520
5590
  code: "rate_limited",
@@ -5524,6 +5594,12 @@ function classifyPushTransportFailure(error, cwd) {
5524
5594
  if (/(?:HTTP |error: )413\b/i.test(message)) {
5525
5595
  return { code: "push_too_large", error: pushTooLargeMessage(cwd) };
5526
5596
  }
5597
+ if (/(?:HTTP |error: )422\b/i.test(message)) {
5598
+ return {
5599
+ code: "source_managed_by_github",
5600
+ 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."
5601
+ };
5602
+ }
5527
5603
  return null;
5528
5604
  }
5529
5605
  function pushToSpace(cwd, token, refspec, opts = {}) {
@@ -5895,7 +5971,7 @@ async function pushWithTransientRetry(push) {
5895
5971
  const message = errorMessage3(error);
5896
5972
  const transient = /(?:HTTP |error: )(?:429|503)\b/i.test(message);
5897
5973
  if (transient && attempt < backoffMs.length && Date.now() + backoffMs[attempt] < deadline) {
5898
- await new Promise((resolve21) => setTimeout(resolve21, backoffMs[attempt]));
5974
+ await new Promise((resolve22) => setTimeout(resolve22, backoffMs[attempt]));
5899
5975
  continue;
5900
5976
  }
5901
5977
  throw error;
@@ -6179,7 +6255,7 @@ async function uploadDeployAssets(options) {
6179
6255
  // against the plan before it touches storage, and a stream body
6180
6256
  // would be sent chunked with no length at all; a retry also needs a
6181
6257
  // body it can send again.
6182
- body: readFileSync18(asset.sourcePath)
6258
+ body: readFileSync17(asset.sourcePath)
6183
6259
  })
6184
6260
  );
6185
6261
  if (!response.ok) {
@@ -6260,7 +6336,7 @@ async function postWithRetry(url, makeInit, {
6260
6336
  lastError = error;
6261
6337
  }
6262
6338
  if (attempt < attempts) {
6263
- await new Promise((resolve21) => setTimeout(resolve21, Math.min(8e3, 500 * 2 ** (attempt - 1))));
6339
+ await new Promise((resolve22) => setTimeout(resolve22, Math.min(8e3, 500 * 2 ** (attempt - 1))));
6264
6340
  }
6265
6341
  }
6266
6342
  if (lastResponse) return lastResponse;
@@ -6396,12 +6472,12 @@ var deploy_default = defineCommand3({
6396
6472
  async run({ args }) {
6397
6473
  const output = createDeployOutput(args.json === true);
6398
6474
  preflightNodeVersion("deploy");
6399
- const appDir = resolve13(args.dir ?? ".");
6475
+ const appDir = resolve14(args.dir ?? ".");
6400
6476
  const selectorRefusal = blankSelectorRefusal(args);
6401
6477
  if (selectorRefusal) output.die(selectorRefusal.error, selectorRefusal.code);
6402
6478
  const envName = typeof args.env === "string" && args.env.trim() ? args.env.trim() : void 0;
6403
6479
  if (!hasWranglerConfig(appDir)) {
6404
- output.die("No wrangler.toml found. Are you in a DeepSpace app directory?", "not_in_app_repo");
6480
+ output.die(noWranglerConfigMessage(join19(appDir, "wrangler.toml")), "not_in_app_repo");
6405
6481
  }
6406
6482
  try {
6407
6483
  ensureInstallReady(appDir);
@@ -6442,6 +6518,13 @@ var deploy_default = defineCommand3({
6442
6518
  );
6443
6519
  }
6444
6520
  p12.log.info(`Id: ${appId}`);
6521
+ const sourceState = await getAppSource(DEPLOY_URL3, token, appId);
6522
+ if (!sourceState.registered) {
6523
+ output.die(
6524
+ `${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\`.`,
6525
+ "app_not_registered"
6526
+ );
6527
+ }
6445
6528
  const secretsCache = await loadDeploySecrets({
6446
6529
  deployUrl: DEPLOY_URL3,
6447
6530
  appDir,
@@ -6451,12 +6534,17 @@ var deploy_default = defineCommand3({
6451
6534
  token,
6452
6535
  output
6453
6536
  });
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"
6459
- );
6537
+ const ownerJwtRefusal = ownerJwtMissingRefusal(sourceState);
6538
+ if (ownerJwtRefusal) output.die(ownerJwtRefusal.error, ownerJwtRefusal.code);
6539
+ let confirmRename = args.rename === true;
6540
+ const rename2 = pendingRename(sourceState.registeredHost, appName);
6541
+ if (rename2 && !confirmRename) {
6542
+ if (output.nonInteractive) {
6543
+ output.die(renameRefusalMessage(rename2), "rename_required");
6544
+ }
6545
+ const confirmed = await p12.confirm({ message: renamePromptMessage(rename2) });
6546
+ if (p12.isCancel(confirmed) || !confirmed) output.die("Deploy cancelled.", "rename_declined");
6547
+ confirmRename = true;
6460
6548
  }
6461
6549
  const repositoryPreflight = preflightDeployRepository({
6462
6550
  appDir,
@@ -6502,7 +6590,7 @@ var deploy_default = defineCommand3({
6502
6590
  appId,
6503
6591
  appName,
6504
6592
  token,
6505
- rename: args.rename === true,
6593
+ rename: confirmRename,
6506
6594
  claimReleased: args["claim-released"] === true,
6507
6595
  ignoreStale: Boolean(args["ignore-stale"]),
6508
6596
  bundle,
@@ -6569,6 +6657,25 @@ async function syncCommerce(appDir, appId, token, nonInteractive) {
6569
6657
  await syncSubscriptionPlans(appDir, appId, token, nonInteractive);
6570
6658
  await syncOneTimeProducts(appDir, appId, token);
6571
6659
  }
6660
+ function ownerJwtMissingRefusal(state) {
6661
+ if (state.onBehalf?.ownerJwtLive !== false) return null;
6662
+ return {
6663
+ code: "owner_jwt_missing",
6664
+ error: "Cannot preserve the existing secrets: this app has no live deployment carrying an APP_OWNER_JWT. Ask the owner to redeploy."
6665
+ };
6666
+ }
6667
+ function pendingRename(registeredHost, appName) {
6668
+ if (!registeredHost) return null;
6669
+ const [label, ...domain] = registeredHost.split(".");
6670
+ if (label === appName) return null;
6671
+ return { fromHost: registeredHost, toHost: [appName, ...domain].join(".") };
6672
+ }
6673
+ function renameRefusalMessage(rename2) {
6674
+ 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.`;
6675
+ }
6676
+ function renamePromptMessage(rename2) {
6677
+ 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. (Meant a separate app? Run \`deepspace app init --new-id\` instead.) Rename?`;
6678
+ }
6572
6679
  function blankSelectorRefusal(args) {
6573
6680
  if (args.env !== void 0 && !String(args.env).trim()) {
6574
6681
  return {
@@ -6629,15 +6736,18 @@ function requireAppIdArg(explicit) {
6629
6736
  function liveAppUrl(app2) {
6630
6737
  return app2.status === "active" && app2.deployedAt !== null ? app2.url : null;
6631
6738
  }
6632
- async function listApps(deployUrl, token) {
6633
- const { apps } = await apiFetch(deployUrl, token, "/api/apps");
6634
- if (!Array.isArray(apps)) {
6739
+ async function listAppsPage(deployUrl, token) {
6740
+ const body = await apiFetch(deployUrl, token, "/api/apps");
6741
+ if (!Array.isArray(body.apps)) {
6635
6742
  throw new InputError(
6636
6743
  `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
6744
  "invalid_response"
6638
6745
  );
6639
6746
  }
6640
- return apps;
6747
+ return { apps: body.apps, pendingTransfers: body.pendingTransfers ?? [] };
6748
+ }
6749
+ async function listApps(deployUrl, token) {
6750
+ return (await listAppsPage(deployUrl, token)).apps;
6641
6751
  }
6642
6752
  function matchAppSelector(apps, selector) {
6643
6753
  const byId = apps.find((app2) => app2.appId === selector);
@@ -6879,17 +6989,17 @@ var apps_default = defineDeepspaceCommand({
6879
6989
  },
6880
6990
  async run({ args }) {
6881
6991
  const token = await ensureToken();
6882
- const apps = await listApps(DEPLOY_URL5, token);
6992
+ const { apps, pendingTransfers } = await listAppsPage(DEPLOY_URL5, token);
6883
6993
  if (!args.json) {
6884
- if (!apps.length) {
6994
+ if (!apps.length && !pendingTransfers.length) {
6885
6995
  console.log("No apps yet. Create one with `npx create-deepspace <name>` and `deepspace deploy`.");
6886
- } else {
6996
+ } else if (apps.length) {
6887
6997
  const rows = [...apps].sort(
6888
6998
  (left, right) => Number(right.status !== "undeployed") - Number(left.status !== "undeployed")
6889
6999
  );
6890
7000
  const roleOf = (a) => a.role ?? "owner";
6891
7001
  const roleWidth = Math.max(4, ...rows.map((a) => roleOf(a).length));
6892
- const nameWidth = Math.max(4, ...rows.map((a) => (a.name ?? "\u2014").length));
7002
+ const nameWidth = Math.max(4, ...rows.map((a) => nameCell(a).length));
6893
7003
  const statusWidth = Math.max(6, ...rows.map((a) => a.status.length));
6894
7004
  const idWidth = Math.max(6, ...rows.map((a) => a.appId.length));
6895
7005
  console.log(
@@ -6898,14 +7008,36 @@ var apps_default = defineDeepspaceCommand({
6898
7008
  for (const a of rows) {
6899
7009
  const url = liveAppUrl(a) ?? "(not deployed)";
6900
7010
  console.log(
6901
- `${(a.name ?? "\u2014").padEnd(nameWidth)} ${a.status.padEnd(statusWidth)} ${roleOf(a).padEnd(roleWidth)} ${a.appId.padEnd(idWidth)} ${url}`
7011
+ `${nameCell(a).padEnd(nameWidth)} ${a.status.padEnd(statusWidth)} ${roleOf(a).padEnd(roleWidth)} ${a.appId.padEnd(idWidth)} ${url}`
6902
7012
  );
6903
7013
  }
7014
+ const reserved = rows.filter((a) => a.reservedName && a.reservedUntil);
7015
+ if (reserved.length) {
7016
+ console.log("\nReserved names return to the open pool when they expire:");
7017
+ for (const a of reserved) {
7018
+ console.log(
7019
+ ` ${a.reservedName} \u2014 held for you until ${a.reservedUntil}${a.name ? ` (${a.appId} now serves ${a.name})` : ""}`
7020
+ );
7021
+ }
7022
+ }
7023
+ }
7024
+ if (pendingTransfers.length) {
7025
+ console.log("\nTransfer offers waiting for you:");
7026
+ for (const offer2 of pendingTransfers) {
7027
+ console.log(` ${offer2.appId} \u2014 expires ${offer2.expiresAt}`);
7028
+ }
7029
+ console.log(
7030
+ " Accept with `deepspace app transfer accept --app <APP ID>`; `deepspace app transfer status --app <APP ID>` names the offerer."
7031
+ );
6904
7032
  }
6905
7033
  }
6906
- return { data: { apps } };
7034
+ return { data: { apps, pendingTransfers } };
6907
7035
  }
6908
7036
  });
7037
+ function nameCell(app2) {
7038
+ if (app2.name) return app2.name;
7039
+ return app2.reservedName ? `${app2.reservedName} (reserved)` : "\u2014";
7040
+ }
6909
7041
 
6910
7042
  // src/cli/commands/logs.ts
6911
7043
  import { defineCommand as defineCommand4 } from "citty";
@@ -7289,37 +7421,37 @@ var usage_default = defineDeepspaceCommand({
7289
7421
  });
7290
7422
 
7291
7423
  // 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";
7424
+ import { existsSync as existsSync19, readFileSync as readFileSync19, realpathSync as realpathSync4 } from "fs";
7425
+ import { dirname as dirname9, join as join21, resolve as resolve16 } from "path";
7294
7426
  import { pathToFileURL as pathToFileURL3 } from "url";
7295
7427
 
7296
7428
  // src/cli/commands/feature-installer.ts
7297
7429
  import {
7298
7430
  closeSync as closeSync2,
7299
7431
  copyFileSync,
7300
- existsSync as existsSync17,
7432
+ existsSync as existsSync18,
7301
7433
  fstatSync as fstatSync2,
7302
7434
  mkdirSync as mkdirSync6,
7303
7435
  mkdtempSync,
7304
7436
  openSync as openSync2,
7305
- readFileSync as readFileSync19,
7437
+ readFileSync as readFileSync18,
7306
7438
  readSync,
7307
- readdirSync as readdirSync5,
7439
+ readdirSync as readdirSync7,
7308
7440
  rmSync as rmSync4,
7309
7441
  writeFileSync as writeFileSync5
7310
7442
  } from "fs";
7311
7443
  import { spawnSync as spawnSync6 } from "child_process";
7312
7444
  import { tmpdir as tmpdir2 } from "os";
7313
- import { dirname as dirname8, isAbsolute as isAbsolute4, join as join17, resolve as resolve14 } from "path";
7445
+ import { dirname as dirname8, isAbsolute as isAbsolute4, join as join20, resolve as resolve15 } from "path";
7314
7446
  import { fileURLToPath } from "url";
7315
7447
  var NAV_MARKER = "// \u2500\u2500 Features add nav items below this line \u2500\u2500";
7316
7448
  function featurePackageRoot(moduleUrl = import.meta.url) {
7317
7449
  let dir = dirname8(fileURLToPath(moduleUrl));
7318
7450
  for (; ; ) {
7319
- const packagePath = join17(dir, "package.json");
7320
- if (existsSync17(packagePath)) {
7451
+ const packagePath = join20(dir, "package.json");
7452
+ if (existsSync18(packagePath)) {
7321
7453
  try {
7322
- const pkg2 = JSON.parse(readFileSync19(packagePath, "utf-8"));
7454
+ const pkg2 = JSON.parse(readFileSync18(packagePath, "utf-8"));
7323
7455
  if (pkg2.name === "deepspace") return dir;
7324
7456
  } catch {
7325
7457
  }
@@ -7330,13 +7462,13 @@ function featurePackageRoot(moduleUrl = import.meta.url) {
7330
7462
  }
7331
7463
  }
7332
7464
  function hasFeatureCatalog(packageRoot = featurePackageRoot()) {
7333
- return existsSync17(join17(packageRoot, "features"));
7465
+ return existsSync18(join20(packageRoot, "features"));
7334
7466
  }
7335
7467
  function readFeatureConfig(id, packageRoot = featurePackageRoot()) {
7336
- const configPath = join17(packageRoot, "features", id, "feature.json");
7337
- if (!existsSync17(configPath)) return null;
7468
+ const configPath = join20(packageRoot, "features", id, "feature.json");
7469
+ if (!existsSync18(configPath)) return null;
7338
7470
  try {
7339
- const config = JSON.parse(readFileSync19(configPath, "utf-8"));
7471
+ const config = JSON.parse(readFileSync18(configPath, "utf-8"));
7340
7472
  if (typeof config.id !== "string" || typeof config.name !== "string" || !Array.isArray(config.files)) {
7341
7473
  return null;
7342
7474
  }
@@ -7346,9 +7478,9 @@ function readFeatureConfig(id, packageRoot = featurePackageRoot()) {
7346
7478
  }
7347
7479
  }
7348
7480
  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));
7481
+ const featuresDir = join20(packageRoot, "features");
7482
+ if (!existsSync18(featuresDir)) return [];
7483
+ 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
7484
  }
7353
7485
  function featureCatalog(packageRoot = featurePackageRoot()) {
7354
7486
  return listFeatureConfigs(packageRoot).map((config) => ({
@@ -7385,7 +7517,7 @@ function suggestFeature(featureId, packageRoot = featurePackageRoot()) {
7385
7517
  return closest !== null && closestDistance <= threshold ? closest : null;
7386
7518
  }
7387
7519
  function resolveTargetDir(raw) {
7388
- return isAbsolute4(raw) ? raw : resolve14(raw);
7520
+ return isAbsolute4(raw) ? raw : resolve15(raw);
7389
7521
  }
7390
7522
  function resolveFeatureDestPath(fileDest, route) {
7391
7523
  const pages = "src/pages/";
@@ -7401,7 +7533,7 @@ function copyFile(src, dest) {
7401
7533
  function copyFileWithImportShift(src, dest, levels) {
7402
7534
  mkdirSync6(dirname8(dest), { recursive: true });
7403
7535
  const prefix = "../".repeat(levels);
7404
- const shifted = readFileSync19(src, "utf-8").replace(
7536
+ const shifted = readFileSync18(src, "utf-8").replace(
7405
7537
  /(from\s+|import\s+|import\()(['"])(\.\.\/)/g,
7406
7538
  `$1$2${prefix}$3`
7407
7539
  );
@@ -7414,12 +7546,12 @@ function installFiles(packageRoot, config, targetDir, emit) {
7414
7546
  const preexisting = [];
7415
7547
  const missing = [];
7416
7548
  for (const file of config.files) {
7417
- const srcPath = join17(packageRoot, "features", config.id, file.src);
7549
+ const srcPath = join20(packageRoot, "features", config.id, file.src);
7418
7550
  const resolvedDest = resolveFeatureDestPath(file.dest, config.route);
7419
- const destPath = join17(targetDir, resolvedDest);
7420
- const existed = existsSync17(destPath);
7551
+ const destPath = join20(targetDir, resolvedDest);
7552
+ const existed = existsSync18(destPath);
7421
7553
  if (existed) preexisting.push(resolvedDest);
7422
- if (!existsSync17(srcPath)) {
7554
+ if (!existsSync18(srcPath)) {
7423
7555
  emit(` Warning: source not found: ${file.src}`, "stderr");
7424
7556
  if (!existed) missing.push(resolvedDest);
7425
7557
  continue;
@@ -7456,11 +7588,11 @@ function unresolvedSchema(schema, reason, emit) {
7456
7588
  }
7457
7589
  function integrateSchema(config, targetDir, emit) {
7458
7590
  if (!config.schema) return [];
7459
- const schemasPath = join17(targetDir, "src", "schemas.ts");
7460
- if (!existsSync17(schemasPath)) {
7591
+ const schemasPath = join20(targetDir, "src", "schemas.ts");
7592
+ if (!existsSync18(schemasPath)) {
7461
7593
  return [unresolvedSchema(config.schema, "src/schemas.ts was not found.", emit)];
7462
7594
  }
7463
- let content = readFileSync19(schemasPath, "utf-8");
7595
+ let content = readFileSync18(schemasPath, "utf-8");
7464
7596
  const { exportName, importPath, spreadOperator } = config.schema;
7465
7597
  const importPattern = new RegExp(`^import\\s+\\{[^}]*\\b${exportName}\\b`, "m");
7466
7598
  if (importPattern.test(content)) {
@@ -7513,11 +7645,11 @@ function unresolvedActions(actions, reason, emit) {
7513
7645
  }
7514
7646
  function integrateActions(config, targetDir, emit) {
7515
7647
  if (!config.actions?.length) return [];
7516
- const actionsPath = join17(targetDir, "src", "actions", "index.ts");
7517
- if (!existsSync17(actionsPath)) {
7648
+ const actionsPath = join20(targetDir, "src", "actions", "index.ts");
7649
+ if (!existsSync18(actionsPath)) {
7518
7650
  return [unresolvedActions(config.actions, "src/actions/index.ts was not found.", emit)];
7519
7651
  }
7520
- let content = readFileSync19(actionsPath, "utf-8");
7652
+ let content = readFileSync18(actionsPath, "utf-8");
7521
7653
  const registryPattern = /export const actions:\s*Record<string,\s*ActionHandler(?:<[^>]*>)?>\s*=\s*\{/;
7522
7654
  if (!registryPattern.test(content)) {
7523
7655
  const reason = "src/actions/index.ts has an unexpected structure.";
@@ -7563,8 +7695,8 @@ ${registryEntry}`);
7563
7695
  function integrateCode(config, targetDir, emit) {
7564
7696
  const unresolved = [];
7565
7697
  for (const insertion of config.code ?? []) {
7566
- const targetPath = join17(targetDir, insertion.file);
7567
- if (!existsSync17(targetPath)) {
7698
+ const targetPath = join20(targetDir, insertion.file);
7699
+ if (!existsSync18(targetPath)) {
7568
7700
  unresolved.push({
7569
7701
  area: "code",
7570
7702
  reason: `${insertion.file} was not found.`,
@@ -7572,7 +7704,7 @@ function integrateCode(config, targetDir, emit) {
7572
7704
  });
7573
7705
  continue;
7574
7706
  }
7575
- const original = readFileSync19(targetPath, "utf-8");
7707
+ const original = readFileSync18(targetPath, "utf-8");
7576
7708
  if (original.includes(insertion.insert.trim())) {
7577
7709
  emit(` Integration already present: ${insertion.file}`);
7578
7710
  continue;
@@ -7609,8 +7741,8 @@ ${content.slice(
7609
7741
  }
7610
7742
  function integrateIgnore(config, targetDir) {
7611
7743
  if (!config.ignore?.length) return;
7612
- const targetPath = join17(targetDir, ".gitignore");
7613
- const current = existsSync17(targetPath) ? readFileSync19(targetPath, "utf-8") : "";
7744
+ const targetPath = join20(targetDir, ".gitignore");
7745
+ const current = existsSync18(targetPath) ? readFileSync18(targetPath, "utf-8") : "";
7614
7746
  const present = new Set(current.split(/\r?\n/).map((line) => line.trim()));
7615
7747
  const missing = config.ignore.filter((pattern) => !present.has(pattern));
7616
7748
  if (missing.length === 0) return;
@@ -7622,15 +7754,15 @@ function integrateWrangler(config, targetDir, emit) {
7622
7754
  const runWorkerFirst = config.wrangler?.runWorkerFirst === true;
7623
7755
  const rateLimits = config.wrangler?.rateLimits ?? [];
7624
7756
  if (!runWorkerFirst && rateLimits.length === 0) return [];
7625
- const targetPath = join17(targetDir, "wrangler.toml");
7757
+ const targetPath = join20(targetDir, "wrangler.toml");
7626
7758
  const steps = [
7627
7759
  ...runWorkerFirst ? ["Set assets.run_worker_first = true in wrangler.toml."] : [],
7628
7760
  ...rateLimits.map((binding) => `Add the ${binding.name} rate-limit binding to wrangler.toml.`)
7629
7761
  ];
7630
- if (!existsSync17(targetPath)) {
7762
+ if (!existsSync18(targetPath)) {
7631
7763
  return [{ area: "wrangler", reason: "wrangler.toml was not found.", steps }];
7632
7764
  }
7633
- let source = readFileSync19(targetPath, "utf-8");
7765
+ let source = readFileSync18(targetPath, "utf-8");
7634
7766
  if (runWorkerFirst && !/^\[assets\]\s*$/m.test(source)) {
7635
7767
  return [{ area: "wrangler", reason: "wrangler.toml has no [assets] section.", steps }];
7636
7768
  }
@@ -7706,8 +7838,8 @@ function rateLimitBindingBlock(scope, binding) {
7706
7838
  }
7707
7839
  function integrateCss(packageRoot, config, targetDir, emit) {
7708
7840
  if (!config.css?.length) return [];
7709
- const stylesPath = join17(targetDir, "src", "styles.css");
7710
- if (!existsSync17(stylesPath)) {
7841
+ const stylesPath = join20(targetDir, "src", "styles.css");
7842
+ if (!existsSync18(stylesPath)) {
7711
7843
  emit(" Warning: Cannot integrate CSS \u2014 src/styles.css not found");
7712
7844
  return [
7713
7845
  {
@@ -7719,12 +7851,12 @@ function integrateCss(packageRoot, config, targetDir, emit) {
7719
7851
  }
7720
7852
  ];
7721
7853
  }
7722
- let styles = readFileSync19(stylesPath, "utf-8");
7854
+ let styles = readFileSync18(stylesPath, "utf-8");
7723
7855
  let integrated = 0;
7724
7856
  const unresolved = [];
7725
7857
  for (const cssFile of config.css) {
7726
- const srcPath = join17(packageRoot, "features", config.id, cssFile);
7727
- if (!existsSync17(srcPath)) {
7858
+ const srcPath = join20(packageRoot, "features", config.id, cssFile);
7859
+ if (!existsSync18(srcPath)) {
7728
7860
  emit(` Warning: CSS source not found: ${cssFile}`, "stderr");
7729
7861
  unresolved.push({
7730
7862
  area: "css",
@@ -7733,7 +7865,7 @@ function integrateCss(packageRoot, config, targetDir, emit) {
7733
7865
  });
7734
7866
  continue;
7735
7867
  }
7736
- const css = readFileSync19(srcPath, "utf-8");
7868
+ const css = readFileSync18(srcPath, "utf-8");
7737
7869
  const fingerprint = css.match(/\.([\w-]+)\s*\{|@keyframes\s+([\w-]+)/);
7738
7870
  const marker = fingerprint?.[1] ?? fingerprint?.[2];
7739
7871
  if (marker && styles.includes(marker)) {
@@ -7767,11 +7899,11 @@ function routeRolesSuffix(route) {
7767
7899
  }
7768
7900
  function integrateRoute(config, targetDir, emit) {
7769
7901
  if (!config.route) return [];
7770
- const navPath = join17(targetDir, "src", "nav.ts");
7771
- if (!existsSync17(navPath)) {
7902
+ const navPath = join20(targetDir, "src", "nav.ts");
7903
+ if (!existsSync18(navPath)) {
7772
7904
  return [unresolvedRoute(config.route, config, "src/nav.ts was not found.", emit)];
7773
7905
  }
7774
- let content = readFileSync19(navPath, "utf-8");
7906
+ let content = readFileSync18(navPath, "utf-8");
7775
7907
  const routePath = config.route.path;
7776
7908
  if (content.includes(`'${routePath}'`)) {
7777
7909
  emit(` Nav already present: ${routePath}`);
@@ -7818,10 +7950,10 @@ function runPackageInstall(packageManager, targetDir, outputVisible) {
7818
7950
  const child = spawnSync6(packageManager, ["install"], { ...options, stdio: "inherit" });
7819
7951
  return { error: child.error, status: child.status };
7820
7952
  }
7821
- const captureDir = mkdtempSync(join17(tmpdir2(), "deepspace-add-install-"));
7953
+ const captureDir = mkdtempSync(join20(tmpdir2(), "deepspace-add-install-"));
7822
7954
  let outputFd;
7823
7955
  try {
7824
- outputFd = openSync2(join17(captureDir, "output.log"), "w+");
7956
+ outputFd = openSync2(join20(captureDir, "output.log"), "w+");
7825
7957
  const child = spawnSync6(packageManager, ["install"], {
7826
7958
  ...options,
7827
7959
  stdio: ["ignore", outputFd, outputFd]
@@ -7845,14 +7977,14 @@ function integrateDependencies(config, targetDir, runInstall, outputVisible, emi
7845
7977
  if (declarations.every(({ values }) => !values || Object.keys(values).length === 0)) {
7846
7978
  return { added: [], installRan: false, packageManager };
7847
7979
  }
7848
- const packagePath = join17(targetDir, "package.json");
7849
- if (!existsSync17(packagePath)) {
7980
+ const packagePath = join20(targetDir, "package.json");
7981
+ if (!existsSync18(packagePath)) {
7850
7982
  emit(" Could not patch package.json (file not found)");
7851
7983
  return { added: [], installRan: false, packageManager };
7852
7984
  }
7853
7985
  let pkg2;
7854
7986
  try {
7855
- pkg2 = JSON.parse(readFileSync19(packagePath, "utf-8"));
7987
+ pkg2 = JSON.parse(readFileSync18(packagePath, "utf-8"));
7856
7988
  } catch (error) {
7857
7989
  emit(
7858
7990
  ` Could not patch package.json (parse error: ${error instanceof Error ? error.message : String(error)})`
@@ -7912,7 +8044,7 @@ ${child.diagnosticTail}` : "";
7912
8044
  }
7913
8045
  function dependencySnapshot(targetDir) {
7914
8046
  try {
7915
- const pkg2 = JSON.parse(readFileSync19(join17(targetDir, "package.json"), "utf-8"));
8047
+ const pkg2 = JSON.parse(readFileSync18(join20(targetDir, "package.json"), "utf-8"));
7916
8048
  return {
7917
8049
  dependencies: pkg2.dependencies ?? {},
7918
8050
  devDependencies: pkg2.devDependencies ?? {}
@@ -7924,7 +8056,7 @@ function dependencySnapshot(targetDir) {
7924
8056
  function installedNavFeatures(packageRoot, targetDir) {
7925
8057
  return listFeatureConfigs(packageRoot).filter((feature) => feature.category === "nav" && feature.files.length > 0).filter((feature) => {
7926
8058
  const signature = resolveFeatureDestPath(feature.files[0].dest, feature.route);
7927
- return existsSync17(join17(targetDir, signature));
8059
+ return existsSync18(join20(targetDir, signature));
7928
8060
  }).map((feature) => feature.id);
7929
8061
  }
7930
8062
  function warnNavConflicts(packageRoot, config, targetDir, emit) {
@@ -8157,11 +8289,11 @@ function renderFeatureInfo(config) {
8157
8289
  return lines.join("\n");
8158
8290
  }
8159
8291
  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;
8292
+ if (existsSync19(join21(dir, "wrangler.toml"))) return true;
8293
+ const packagePath = join21(dir, "package.json");
8294
+ if (!existsSync19(packagePath)) return false;
8163
8295
  try {
8164
- const pkg2 = JSON.parse(readFileSync20(packagePath, "utf-8"));
8296
+ const pkg2 = JSON.parse(readFileSync19(packagePath, "utf-8"));
8165
8297
  return Boolean(pkg2.dependencies?.deepspace || pkg2.devDependencies?.deepspace);
8166
8298
  } catch {
8167
8299
  return false;
@@ -8170,8 +8302,8 @@ function isDeepSpaceApp(dir) {
8170
8302
  function installedPackageRoot(fromDir) {
8171
8303
  let dir = fromDir;
8172
8304
  for (; ; ) {
8173
- const candidate = join18(dir, "node_modules", "deepspace");
8174
- if (existsSync18(join18(candidate, "package.json"))) return candidate;
8305
+ const candidate = join21(dir, "node_modules", "deepspace");
8306
+ if (existsSync19(join21(candidate, "package.json"))) return candidate;
8175
8307
  const parent = dirname9(dir);
8176
8308
  if (parent === dir) return null;
8177
8309
  dir = parent;
@@ -8188,9 +8320,9 @@ async function targetInstaller(appDir) {
8188
8320
  if (realpathSync4(installedRoot) === realpathSync4(featurePackageRoot())) {
8189
8321
  return { installFeature };
8190
8322
  }
8191
- const entry = join18(installedRoot, "dist", "feature-installer.js");
8192
- const catalog = join18(installedRoot, "features");
8193
- if (!existsSync18(entry) || !existsSync18(catalog)) {
8323
+ const entry = join21(installedRoot, "dist", "feature-installer.js");
8324
+ const catalog = join21(installedRoot, "features");
8325
+ if (!existsSync19(entry) || !existsSync19(catalog)) {
8194
8326
  throw new Refusal(
8195
8327
  `The app's installed deepspace package does not contain the typed feature installer.
8196
8328
  Expected: ${entry}
@@ -8282,8 +8414,8 @@ var add_default = defineDeepspaceCommand({
8282
8414
  }
8283
8415
  return { data: { features: featureCatalog(root) } };
8284
8416
  }
8285
- const appDir = resolve15(args.dir ?? ".");
8286
- if (!existsSync18(appDir)) {
8417
+ const appDir = resolve16(args.dir ?? ".");
8418
+ if (!existsSync19(appDir)) {
8287
8419
  throw new Refusal(
8288
8420
  `Target directory not found: ${appDir}
8289
8421
  Create a new app first: deepspace app create <name>`,
@@ -8363,11 +8495,11 @@ function fmtDate(iso) {
8363
8495
  if (!iso) return "\u2014";
8364
8496
  return iso.slice(0, 10);
8365
8497
  }
8366
- async function confirm4(message) {
8498
+ async function confirm5(message) {
8367
8499
  process.stdout.write(`${message} [y/N] `);
8368
- const answer = await new Promise((resolve21) => {
8500
+ const answer = await new Promise((resolve22) => {
8369
8501
  process.stdin.setEncoding("utf-8");
8370
- process.stdin.once("data", (data) => resolve21(data.toString().trim().toLowerCase()));
8502
+ process.stdin.once("data", (data) => resolve22(data.toString().trim().toLowerCase()));
8371
8503
  });
8372
8504
  process.stdin.pause();
8373
8505
  return answer === "y" || answer === "yes";
@@ -8442,7 +8574,7 @@ var buy = defineDeepspaceCommand({
8442
8574
  "confirmation_required"
8443
8575
  );
8444
8576
  }
8445
- const ok2 = await confirm4(
8577
+ const ok2 = await confirm5(
8446
8578
  `Buy ${domain} for ${price}/yr (auto-renews; non-refundable), attach to ${appId}? Provisioning takes ${expectedTime}.`
8447
8579
  );
8448
8580
  if (!ok2) {
@@ -8612,7 +8744,7 @@ var detach = defineDeepspaceCommand({
8612
8744
  "confirmation_required"
8613
8745
  );
8614
8746
  }
8615
- const ok2 = await confirm4(
8747
+ const ok2 = await confirm5(
8616
8748
  `Detach ${domain}? Routing will stop; the domain stays registered (auto-renew is unchanged).`
8617
8749
  );
8618
8750
  if (!ok2) {
@@ -8658,8 +8790,8 @@ var domain_default = defineCommand5({
8658
8790
 
8659
8791
  // src/cli/commands/app-files.ts
8660
8792
  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";
8793
+ import { basename as basename4, resolve as resolve17 } from "path";
8794
+ import { existsSync as existsSync20, statSync as statSync2 } from "fs";
8663
8795
 
8664
8796
  // src/cli/lib/app-files-api.ts
8665
8797
  import { createWriteStream, openAsBlob, statSync } from "fs";
@@ -8937,8 +9069,8 @@ var put = defineDeepspaceCommand({
8937
9069
  app: { type: "string", description: "App id or live name (defaults to ./wrangler.toml)" }
8938
9070
  },
8939
9071
  async run({ args }) {
8940
- const localPath = resolve16(String(args.file));
8941
- if (!existsSync19(localPath) || !statSync2(localPath).isFile()) {
9072
+ const localPath = resolve17(String(args.file));
9073
+ if (!existsSync20(localPath) || !statSync2(localPath).isFile()) {
8942
9074
  throw new Refusal(`No such file: ${args.file}`, "file_not_found");
8943
9075
  }
8944
9076
  const key = requireKey(args.key ?? basename4(localPath));
@@ -9038,7 +9170,7 @@ var get = defineDeepspaceCommand({
9038
9170
  },
9039
9171
  async run({ args }) {
9040
9172
  const key = requireKey(String(args.key));
9041
- const destination = resolve16(String(args.out ?? basename4(key)));
9173
+ const destination = resolve17(String(args.out ?? basename4(key)));
9042
9174
  const { token, appId } = await target(args.app);
9043
9175
  const result = await downloadAppFile(PLATFORM_URL, token, appId, key, destination);
9044
9176
  if (!args.json) console.log(`\u2713 ${key} \u2192 ${destination} (${formatBytes(result.bytes)})`);
@@ -9118,7 +9250,10 @@ var list4 = defineDeepspaceCommand({
9118
9250
  }
9119
9251
  });
9120
9252
  var add = defineDeepspaceCommand({
9121
- meta: { name: "add", description: "Authorize someone to deploy your app" },
9253
+ meta: {
9254
+ name: "add",
9255
+ description: "Authorize someone to deploy your app \u2014 a collaborator can also read and change every app secret (plaintext); they cannot undeploy or transfer"
9256
+ },
9122
9257
  args: {
9123
9258
  email: {
9124
9259
  type: "positional",
@@ -9328,7 +9463,11 @@ var offer = defineDeepspaceCommand({
9328
9463
  if (replaced) console.log(`\u25B2 Replaced the pending offer to ${pendingTo}.`);
9329
9464
  console.log(
9330
9465
  `\u2713 Offered ${app2} to ${email} (expires ${expiresAt}).
9331
- They accept with: deepspace app transfer accept --app ${app2}`
9466
+ They accept with: deepspace app transfer accept --app ${app2}
9467
+ (it also shows in their \`deepspace app list\`)
9468
+ \u25B2 On acceptance you lose ALL access to ${app2} \u2014 you are not kept on as a
9469
+ collaborator. Only the new owner can add you back. Collaborators you added
9470
+ stay on the app for the new owner.`
9332
9471
  );
9333
9472
  }
9334
9473
  return {
@@ -9355,7 +9494,9 @@ var status2 = defineDeepspaceCommand({
9355
9494
  return { data: { app: app2, transfer: null } };
9356
9495
  }
9357
9496
  if (!args.json) {
9358
- console.log(`Pending: ${app2} \u2192 ${transfer.toEmailDisplay} (expires ${transfer.expiresAt}).`);
9497
+ console.log(
9498
+ `Pending: ${app2} \u2014 from ${transfer.fromEmailDisplay} to ${transfer.toEmailDisplay} (expires ${transfer.expiresAt}).`
9499
+ );
9359
9500
  }
9360
9501
  return { data: { app: app2, transfer } };
9361
9502
  }
@@ -9375,15 +9516,32 @@ var accept2 = defineDeepspaceCommand({
9375
9516
  const msg = err instanceof Error ? err.message : String(err);
9376
9517
  throw new Refusal(msg, msg.startsWith("No app id") ? "not_in_app_repo" : "invalid_app");
9377
9518
  }
9378
- await apiFetch(DEPLOY_URL10, token, `/api/apps/${encodeURIComponent(app2)}/transfer/accept`, {
9519
+ const { replayed, acceptedAt, inheritedCollaborators } = await apiFetch(DEPLOY_URL10, token, `/api/apps/${encodeURIComponent(app2)}/transfer/accept`, {
9379
9520
  method: "POST"
9380
9521
  });
9381
9522
  if (!args.json) {
9382
9523
  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.`
9524
+ 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
9525
  );
9526
+ if (!replayed && inheritedCollaborators === void 0) {
9527
+ console.log(
9528
+ `\u25B2 Could not list collaborators the previous owner may have left on the app \u2014 review with \`deepspace app collaborators list --app ${app2}\`.`
9529
+ );
9530
+ } else if (!replayed && inheritedCollaborators && inheritedCollaborators.length > 0) {
9531
+ console.log(
9532
+ `\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}\`.`
9533
+ );
9534
+ }
9385
9535
  }
9386
- return { data: { app: app2, accepted: true } };
9536
+ return {
9537
+ data: {
9538
+ app: app2,
9539
+ accepted: !replayed,
9540
+ replayed: replayed === true,
9541
+ ...acceptedAt ? { acceptedAt } : {},
9542
+ ...inheritedCollaborators === void 0 ? {} : { inheritedCollaborators }
9543
+ }
9544
+ };
9387
9545
  }
9388
9546
  });
9389
9547
  var cancel4 = defineDeepspaceCommand({
@@ -9408,7 +9566,7 @@ var transfer_default = defineCommand8({
9408
9566
  import { defineCommand as defineCommand9 } from "citty";
9409
9567
 
9410
9568
  // src/cli/commands/_invoke-impl.ts
9411
- import { readFileSync as readFileSync21 } from "fs";
9569
+ import { readFileSync as readFileSync20 } from "fs";
9412
9570
  import * as p15 from "@clack/prompts";
9413
9571
  var API_URL7 = process.env.DEEPSPACE_API_URL ?? PLATFORM_URLS.api;
9414
9572
  var DEFAULT_TIMEOUT_MS = 12e4;
@@ -9449,7 +9607,7 @@ function resolveBody(opts) {
9449
9607
  if (opts.body != null) {
9450
9608
  raw = opts.body;
9451
9609
  } else if (opts.bodyFile != null) {
9452
- raw = opts.bodyFile === "-" ? readFileSync21(0, "utf-8") : readFileSync21(opts.bodyFile, "utf-8");
9610
+ raw = opts.bodyFile === "-" ? readFileSync20(0, "utf-8") : readFileSync20(opts.bodyFile, "utf-8");
9453
9611
  } else {
9454
9612
  return "{}";
9455
9613
  }
@@ -9743,8 +9901,8 @@ var integrations_default = defineCommand9({
9743
9901
 
9744
9902
  // src/cli/commands/feedback.ts
9745
9903
  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";
9904
+ import { existsSync as existsSync21, readFileSync as readFileSync21 } from "fs";
9905
+ import { dirname as dirname10, join as join22 } from "path";
9748
9906
  import { fileURLToPath as fileURLToPath2 } from "url";
9749
9907
  import { release } from "os";
9750
9908
  var API_URL8 = process.env.DEEPSPACE_API_URL ?? PLATFORM_URLS.api;
@@ -9767,12 +9925,12 @@ function readCliVersion() {
9767
9925
  try {
9768
9926
  const here = dirname10(fileURLToPath2(import.meta.url));
9769
9927
  const candidates = [
9770
- join19(here, "..", "package.json"),
9771
- join19(here, "..", "..", "..", "package.json")
9928
+ join22(here, "..", "package.json"),
9929
+ join22(here, "..", "..", "..", "package.json")
9772
9930
  ];
9773
9931
  for (const path of candidates) {
9774
- if (!existsSync20(path)) continue;
9775
- const pkg2 = JSON.parse(readFileSync22(path, "utf-8"));
9932
+ if (!existsSync21(path)) continue;
9933
+ const pkg2 = JSON.parse(readFileSync21(path, "utf-8"));
9776
9934
  if (pkg2.name === "deepspace" && pkg2.version) return pkg2.version;
9777
9935
  }
9778
9936
  } catch {
@@ -9968,7 +10126,7 @@ Context: deepspace ${payload.cliVersion} \xB7 ${payload.nodeVersion} \xB7 ${payl
9968
10126
 
9969
10127
  // src/cli/commands/secrets.ts
9970
10128
  import { defineCommand as defineCommand10 } from "citty";
9971
- import { readFileSync as readFileSync23 } from "fs";
10129
+ import { readFileSync as readFileSync22 } from "fs";
9972
10130
 
9973
10131
  // src/cli/lib/citty-args.ts
9974
10132
  function dedupePositionals(bound, rest) {
@@ -10070,6 +10228,7 @@ function warnIfDebugRoutesEnabled(secrets) {
10070
10228
  return true;
10071
10229
  }
10072
10230
  var APPLY_HINT = "Run `deepspace deploy` to apply \u2014 secrets take effect at deploy time.";
10231
+ var APPLIES_AT_DEPLOY = { appliesAtDeploy: true };
10073
10232
  var LOCAL_DEV_HINT = "Running `deepspace dev start`? Restart it to load the change locally.";
10074
10233
  var list6 = defineCommand10({
10075
10234
  meta: { name: "list", description: "List masked secrets in a config" },
@@ -10136,7 +10295,7 @@ var set = defineCommand10({
10136
10295
  const n = Object.keys(secrets).length;
10137
10296
  ok(
10138
10297
  args.json === true,
10139
- { appId: t.appId, config: t.configName, set: Object.keys(secrets) },
10298
+ { appId: t.appId, config: t.configName, set: Object.keys(secrets), ...APPLIES_AT_DEPLOY },
10140
10299
  () => {
10141
10300
  console.log(`Set ${n} secret${n === 1 ? "" : "s"} in ${t.configName}.`);
10142
10301
  if (!warnIfDebugRoutesEnabled(secrets)) {
@@ -10209,7 +10368,7 @@ var del2 = defineCommand10({
10209
10368
  }
10210
10369
  }
10211
10370
  const note3 = absent > 0 ? ` (${absent} already absent)` : "";
10212
- ok(args.json === true, { appId: t.appId, config: t.configName, deleted, absent }, () => {
10371
+ ok(args.json === true, { appId: t.appId, config: t.configName, deleted, absent, ...APPLIES_AT_DEPLOY }, () => {
10213
10372
  console.log(
10214
10373
  `Deleted ${deleted} secret${deleted === 1 ? "" : "s"} from ${t.configName}.${note3}`
10215
10374
  );
@@ -10238,7 +10397,7 @@ var upload = defineCommand10({
10238
10397
  async run({ args }) {
10239
10398
  try {
10240
10399
  const t = await resolveTarget(args);
10241
- const content = args.file === "-" ? readFileSync23(0, "utf-8") : readFileSync23(args.file, "utf-8");
10400
+ const content = args.file === "-" ? readFileSync22(0, "utf-8") : readFileSync22(args.file, "utf-8");
10242
10401
  const secrets = parseSecretsUpload(content);
10243
10402
  if (Object.keys(secrets).length === 0)
10244
10403
  throw new InputError("No secrets found in the input.", "empty_input");
@@ -10246,7 +10405,7 @@ var upload = defineCommand10({
10246
10405
  const uploaded = Object.keys(secrets);
10247
10406
  ok(
10248
10407
  args.json === true,
10249
- { appId: t.appId, config: t.configName, uploaded, replaced: args.replace === true },
10408
+ { appId: t.appId, config: t.configName, uploaded, replaced: args.replace === true, ...APPLIES_AT_DEPLOY },
10250
10409
  () => {
10251
10410
  console.log(`Uploaded ${uploaded.length} secrets to ${t.configName}.`);
10252
10411
  if (!warnIfDebugRoutesEnabled(secrets)) console.log(APPLY_HINT);
@@ -10421,12 +10580,19 @@ var secrets_default = defineCommand10({
10421
10580
  });
10422
10581
 
10423
10582
  // src/cli/commands/init.ts
10424
- import { existsSync as existsSync21 } from "fs";
10425
- import { join as join20 } from "path";
10583
+ import { existsSync as existsSync22 } from "fs";
10584
+ import { join as join23 } from "path";
10426
10585
  var DEPLOY_URL12 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
10586
+ function wranglerConfigUncommitted(appDir) {
10587
+ if (!existsSync22(join23(appDir, ".git"))) return false;
10588
+ const dryRun = runGit(appDir, ["commit", "--dry-run", "-m", "x", "--", "wrangler.toml"], {
10589
+ allowFail: true
10590
+ });
10591
+ return dryRun.status === 0;
10592
+ }
10427
10593
  function commitScaffoldIfUnborn(appDir, token) {
10428
10594
  try {
10429
- if (!existsSync21(join20(appDir, ".git"))) return false;
10595
+ if (!existsSync22(join23(appDir, ".git"))) return false;
10430
10596
  if (runGit(appDir, ["rev-parse", "--verify", "HEAD"], { allowFail: true }).status === 0) {
10431
10597
  return false;
10432
10598
  }
@@ -10503,13 +10669,13 @@ var init_default = defineDeepspaceCommand({
10503
10669
  writeAppId(appDir, appId, { wranglerEnv: envName, force: Boolean(args["new-id"]) });
10504
10670
  const committedScaffold = commitScaffoldIfUnborn(appDir, token);
10505
10671
  if (!args.json) {
10672
+ const envSuffix = envName ? ` (env: ${envName})` : "";
10673
+ const commitNote = committedScaffold ? " \u2014 initial scaffold commit created." : " \u2014 commit wrangler.toml.";
10506
10674
  if (existing) {
10507
- console.log(`Forked: ${existing} \u2192 ${appId}${envName ? ` (env: ${envName})` : ""}`);
10675
+ console.log(`Forked: ${existing} \u2192 ${appId}${envSuffix}${commitNote}`);
10508
10676
  console.log("Registered as a NEW app under your account; the original is untouched.");
10509
10677
  } else {
10510
- console.log(
10511
- `Registered ${appId}${envName ? ` (env: ${envName})` : ""} to your account` + (committedScaffold ? " \u2014 initial scaffold commit created." : " \u2014 commit wrangler.toml.")
10512
- );
10678
+ console.log(`Registered ${appId}${envSuffix} to your account${commitNote}`);
10513
10679
  console.log("The first deploy claims the `name` subdomain.");
10514
10680
  }
10515
10681
  console.log(`App dir: ${appDir}`);
@@ -10522,7 +10688,18 @@ var init_default = defineDeepspaceCommand({
10522
10688
  committedScaffold,
10523
10689
  appDir,
10524
10690
  env: envName ?? null
10525
- }
10691
+ },
10692
+ // The one follow-up when the id was written into an existing history:
10693
+ // rendered as the `Next:` line and carried in `--json`, so a machine
10694
+ // caller does not have to infer it from `committedScaffold: false`.
10695
+ // Offered only when the file is actually uncommitted — `git commit` on
10696
+ // a clean file fails, and an action must be executable as handed over.
10697
+ ...!committedScaffold && wranglerConfigUncommitted(appDir) ? {
10698
+ action: {
10699
+ cwd: appDir,
10700
+ argv: ["git", "commit", "-m", existing ? `Fork as ${appId}` : `Register ${appId}`, "--", "wrangler.toml"]
10701
+ }
10702
+ } : {}
10526
10703
  };
10527
10704
  }
10528
10705
  });
@@ -10530,7 +10707,7 @@ var init_default = defineDeepspaceCommand({
10530
10707
  // src/cli/commands/status.ts
10531
10708
  import { defineCommand as defineCommand11 } from "citty";
10532
10709
  import * as p17 from "@clack/prompts";
10533
- import { existsSync as existsSync22, readFileSync as readFileSync24 } from "fs";
10710
+ import { existsSync as existsSync23, readFileSync as readFileSync23 } from "fs";
10534
10711
  function timeAgo(iso) {
10535
10712
  const ms = Date.now() - Date.parse(iso);
10536
10713
  if (!Number.isFinite(ms) || ms < 0) return iso;
@@ -10615,7 +10792,7 @@ var status_default = defineCommand11({
10615
10792
  json.services = urls;
10616
10793
  if (urlOverrides.length > 0) json.urlOverrides = urlOverrides;
10617
10794
  }
10618
- facts.loggedIn = existsSync22(SESSION_PATH);
10795
+ facts.loggedIn = existsSync23(SESSION_PATH);
10619
10796
  let userId = null;
10620
10797
  if (!facts.loggedIn) {
10621
10798
  lines.push(["Session", "not logged in"]);
@@ -10624,7 +10801,7 @@ var status_default = defineCommand11({
10624
10801
  let who = "logged in";
10625
10802
  try {
10626
10803
  const payload = decodeJwtPayload(
10627
- readFileSync24(TOKEN_PATH, "utf-8").trim()
10804
+ readFileSync23(TOKEN_PATH, "utf-8").trim()
10628
10805
  );
10629
10806
  if (payload.email) who = payload.email;
10630
10807
  userId = payload.sub ?? null;
@@ -10868,11 +11045,7 @@ var push_default = defineDeepspaceCommand({
10868
11045
  const force = args.force === true;
10869
11046
  const appDir = findAppDir();
10870
11047
  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
- );
11048
+ if (!appDir) throw new Refusal(noWranglerConfigMessage(process.cwd()), "not_in_app_repo");
10876
11049
  assertSyncableRepo(appDir);
10877
11050
  if (branchArg !== void 0) {
10878
11051
  const b = branchArg.trim();
@@ -10895,9 +11068,11 @@ var push_default = defineDeepspaceCommand({
10895
11068
  "unknown_branch"
10896
11069
  );
10897
11070
  }
11071
+ const uninitialized = hasWranglerConfig(appDir) && readAppId(appDir) === null;
10898
11072
  throw new Refusal(
10899
- `Branch "${branch}" has no commits yet \u2014 commit first, then push.`,
10900
- "no_commits"
11073
+ 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.`,
11074
+ "no_commits",
11075
+ uninitialized ? { action: { cwd: appDir, argv: ["deepspace", "app", "init"] } } : {}
10901
11076
  );
10902
11077
  }
10903
11078
  const spinner2 = args.json ? null : createSpinner();
@@ -10961,6 +11136,13 @@ var push_default = defineDeepspaceCommand({
10961
11136
  "app_not_registered"
10962
11137
  );
10963
11138
  }
11139
+ if (sourceState.source?.provider === "github") {
11140
+ throw new Refusal(
11141
+ `This app uses GitHub source (${sourceState.source.repository}). Manage commits with normal Git/GitHub; \`deepspace deploy\` ships the local working tree without changing Git.`,
11142
+ "source_managed_by_github",
11143
+ { extra: { appId, repository: sourceState.source.repository } }
11144
+ );
11145
+ }
10964
11146
  const pullRecoveryCwd = selectedBranchCheckout(appDir, branch) ?? appDir;
10965
11147
  ensureSpaceRemote(appDir, appId);
10966
11148
  if (force) {
@@ -11100,11 +11282,7 @@ var pull_default = defineDeepspaceCommand({
11100
11282
  const branchArg = args.branch === void 0 ? void 0 : String(args.branch);
11101
11283
  const appArg = typeof args.app === "string" ? args.app : void 0;
11102
11284
  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
- );
11285
+ if (!appDir) throw new Refusal(noWranglerConfigMessage(process.cwd()), "not_in_app_repo");
11108
11286
  if (branchArg !== void 0) {
11109
11287
  const b = branchArg.trim();
11110
11288
  if (!b) throw new Refusal("--branch was given an empty branch name.", "invalid_branch");
@@ -11322,18 +11500,18 @@ var pull_default = defineDeepspaceCommand({
11322
11500
 
11323
11501
  // src/cli/commands/clone.ts
11324
11502
  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";
11503
+ import { existsSync as existsSync24, readdirSync as readdirSync8, rmSync as rmSync5 } from "fs";
11504
+ import { resolve as resolve18, basename as basename5, join as join24 } from "path";
11327
11505
  function cleanFailedCloneDir(dir) {
11328
11506
  let entries;
11329
11507
  try {
11330
- entries = readdirSync6(dir);
11508
+ entries = readdirSync8(dir);
11331
11509
  } catch {
11332
- return existsSync23(dir) ? ["<unreadable>"] : [];
11510
+ return existsSync24(dir) ? ["<unreadable>"] : [];
11333
11511
  }
11334
11512
  if (entries.length === 1 && entries[0] === ".git") {
11335
11513
  try {
11336
- rmSync5(join21(dir, ".git"), { recursive: true, force: true });
11514
+ rmSync5(join24(dir, ".git"), { recursive: true, force: true });
11337
11515
  return [];
11338
11516
  } catch {
11339
11517
  return entries;
@@ -11342,10 +11520,10 @@ function cleanFailedCloneDir(dir) {
11342
11520
  return entries;
11343
11521
  }
11344
11522
  function validateCloneTarget(dir) {
11345
- if (!existsSync23(dir)) return false;
11523
+ if (!existsSync24(dir)) return false;
11346
11524
  let entries;
11347
11525
  try {
11348
- entries = readdirSync6(dir);
11526
+ entries = readdirSync8(dir);
11349
11527
  } catch (error) {
11350
11528
  if (error.code === "ENOTDIR") {
11351
11529
  throw new Refusal(`${dir} already exists and is not a directory.`, "dir_exists");
@@ -11377,7 +11555,7 @@ var clone_default = defineDeepspaceCommand({
11377
11555
  async run({ args }) {
11378
11556
  assertAppTargetResolvable(String(args.app));
11379
11557
  const selector = String(args.app).trim();
11380
- const dir = resolve17(String(args.dir ?? selector));
11558
+ const dir = resolve18(String(args.dir ?? selector));
11381
11559
  const dirPreexisted = validateCloneTarget(dir);
11382
11560
  const spinner2 = args.json ? null : createSpinner();
11383
11561
  spinner2?.start(`Resolving ${selector}\u2026`);
@@ -11550,7 +11728,7 @@ function pickPreviousRelease(releases) {
11550
11728
  var rollback_default = defineDeepspaceCommand({
11551
11729
  meta: {
11552
11730
  name: "rollback",
11553
- description: "Re-deploy a prior release's exact bundle (no rebuild)"
11731
+ description: "Re-deploy a prior release's exact bundle (no rebuild); the live secrets are kept, not the release's"
11554
11732
  },
11555
11733
  args: {
11556
11734
  release: {
@@ -11685,7 +11863,7 @@ import { defineCommand as defineCommand12 } from "citty";
11685
11863
 
11686
11864
  // src/cli/commands/workspace/new.ts
11687
11865
  import * as p25 from "@clack/prompts";
11688
- import { isAbsolute as isAbsolute6, resolve as resolve18 } from "path";
11866
+ import { isAbsolute as isAbsolute6, resolve as resolve19 } from "path";
11689
11867
 
11690
11868
  // src/cli/commands/workspace/analysis.ts
11691
11869
  import * as p23 from "@clack/prompts";
@@ -11819,25 +11997,25 @@ function printOverlaps(overlaps, json) {
11819
11997
  // src/cli/commands/workspace/local.ts
11820
11998
  import * as p24 from "@clack/prompts";
11821
11999
  import {
11822
- existsSync as existsSync24,
12000
+ existsSync as existsSync25,
11823
12001
  mkdirSync as mkdirSync7,
11824
- readFileSync as readFileSync25,
12002
+ readFileSync as readFileSync24,
11825
12003
  realpathSync as realpathSync6,
11826
12004
  renameSync as renameSync2,
11827
12005
  statSync as statSync3,
11828
12006
  writeFileSync as writeFileSync6
11829
12007
  } from "fs";
11830
- import { dirname as dirname11, isAbsolute as isAbsolute5, join as join22, relative as relative2, sep as sep2 } from "path";
12008
+ import { dirname as dirname11, isAbsolute as isAbsolute5, join as join25, relative as relative3, sep as sep3 } from "path";
11831
12009
  var WORKSPACE_MARKER = "deepspace-workspace.json";
11832
12010
  function excludeWorktreeDir(appDir, dir) {
11833
12011
  try {
11834
12012
  ensureLocalExclude(appDir, ".deepspace/");
11835
12013
  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("/");
12014
+ const rel = relative3(realpathSync6(repoToplevel(appDir)), realDir);
12015
+ if (!rel || rel === ".." || rel.startsWith(`..${sep3}`) || isAbsolute5(rel)) return;
12016
+ const pattern = rel.split(sep3).join("/");
11839
12017
  if (pattern === ".deepspace" || pattern.startsWith(".deepspace/")) return;
11840
- const relApp = relative2(realpathSync6(appDir), realDir).split(sep2).join("/");
12018
+ const relApp = relative3(realpathSync6(appDir), realDir).split(sep3).join("/");
11841
12019
  if (relApp === ".deepspace" || relApp.startsWith(".deepspace/")) return;
11842
12020
  ensureLocalExclude(appDir, `/${pattern}/`);
11843
12021
  } catch {
@@ -11848,7 +12026,7 @@ function worktreeMarkerPath(worktreeRoot) {
11848
12026
  const result = runGit(worktreeRoot, ["rev-parse", "--absolute-git-dir"], { allowFail: true });
11849
12027
  if (result.status !== 0) return null;
11850
12028
  const gitDir = result.stdout.toString("utf-8").trim();
11851
- return isAbsolute5(gitDir) ? join22(gitDir, WORKSPACE_MARKER) : null;
12029
+ return isAbsolute5(gitDir) ? join25(gitDir, WORKSPACE_MARKER) : null;
11852
12030
  } catch {
11853
12031
  return null;
11854
12032
  }
@@ -11884,7 +12062,7 @@ function isManagedWorkspaceWorktree(worktreeRoot, workspaceId) {
11884
12062
  const markerPath = worktreeMarkerPath(worktreeRoot);
11885
12063
  if (!markerPath) return false;
11886
12064
  try {
11887
- const marker = JSON.parse(readFileSync25(markerPath, "utf-8"));
12065
+ const marker = JSON.parse(readFileSync24(markerPath, "utf-8"));
11888
12066
  return marker.version === 1 && marker.workspaceId === workspaceId && marker.mode === "managed-linked";
11889
12067
  } catch {
11890
12068
  return false;
@@ -11893,11 +12071,11 @@ function isManagedWorkspaceWorktree(worktreeRoot, workspaceId) {
11893
12071
  function defaultWorkspaceRoot(appDir, workspaceId) {
11894
12072
  const primary = listWorktrees(appDir)[0];
11895
12073
  if (!primary) throw new Error("Git did not report a primary worktree");
11896
- return join22(primary.path, ".deepspace", "ws", workspaceId.slice(3).toLowerCase());
12074
+ return join25(primary.path, ".deepspace", "ws", workspaceId.slice(3).toLowerCase());
11897
12075
  }
11898
12076
  function appDirInWorktree(appDir, worktreeRoot) {
11899
- const appRelative = relative2(realpathSync6(repoToplevel(appDir)), realpathSync6(appDir));
11900
- return appRelative ? join22(worktreeRoot, appRelative) : worktreeRoot;
12077
+ const appRelative = relative3(realpathSync6(repoToplevel(appDir)), realpathSync6(appDir));
12078
+ return appRelative ? join25(worktreeRoot, appRelative) : worktreeRoot;
11901
12079
  }
11902
12080
  function primaryAppDir(appDir) {
11903
12081
  const primary = listWorktrees(appDir)[0];
@@ -11932,7 +12110,7 @@ function rematerializeWorkspaceWorktree(appDir, worktreeRoot, branch, workspaceI
11932
12110
  function inOwnLinkedWorktree(appDir, id) {
11933
12111
  if (workspaceIdFromBranch(currentBranch(appDir)) !== id) return false;
11934
12112
  try {
11935
- return statSync3(join22(repoToplevel(appDir), ".git")).isFile();
12113
+ return statSync3(join25(repoToplevel(appDir), ".git")).isFile();
11936
12114
  } catch {
11937
12115
  return false;
11938
12116
  }
@@ -12057,7 +12235,7 @@ function cleanupWorkspaceLocal(appDir, id, trunkBranch, options = {}) {
12057
12235
  }
12058
12236
  runGit(mainDir, ["worktree", "prune"], { allowFail: true });
12059
12237
  out.worktreeDir = null;
12060
- const leftover = existsSync24(linked.path);
12238
+ const leftover = existsSync25(linked.path);
12061
12239
  if (leftover) out.leftoverDir = linked.path;
12062
12240
  else out.worktreeRemoved = linked.path;
12063
12241
  deleteWsBranch(mainDir, branch, expectedBranchOid, out);
@@ -12137,7 +12315,7 @@ async function resolveTarget2(appArg) {
12137
12315
  const appDir = findAppDir();
12138
12316
  if (!appDir) {
12139
12317
  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).",
12318
+ `${noWranglerConfigMessage(process.cwd())} (Or use -a <app> for list, and for drop with an explicit ws_ id.)`,
12141
12319
  "not_in_app_repo"
12142
12320
  );
12143
12321
  }
@@ -12317,7 +12495,7 @@ var newWorkspaceCommand = defineDeepspaceCommand({
12317
12495
  idempotencyKey: mintIdempotencyKey()
12318
12496
  });
12319
12497
  const dirInput = typeof args.dir === "string" ? args.dir.trim() : "";
12320
- const worktreeRoot = dirInput ? isAbsolute6(dirInput) ? dirInput : resolve18(appDir, dirInput) : defaultWorkspaceRoot(appDir, id);
12498
+ const worktreeRoot = dirInput ? isAbsolute6(dirInput) ? dirInput : resolve19(appDir, dirInput) : defaultWorkspaceRoot(appDir, id);
12321
12499
  const dir = appDirInWorktree(appDir, worktreeRoot);
12322
12500
  try {
12323
12501
  materializeWorkspaceWorktree(appDir, worktreeRoot, branch, baseOid, id);
@@ -12355,14 +12533,14 @@ var newWorkspaceCommand = defineDeepspaceCommand({
12355
12533
 
12356
12534
  // src/cli/commands/workspace/attach.ts
12357
12535
  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";
12536
+ import { existsSync as existsSync26, readdirSync as readdirSync9, realpathSync as realpathSync7, rmSync as rmSync6 } from "fs";
12537
+ import { isAbsolute as isAbsolute7, resolve as resolve20 } from "path";
12360
12538
  function samePath(a, b) {
12361
12539
  const real = (p35) => {
12362
12540
  try {
12363
12541
  return realpathSync7(p35);
12364
12542
  } catch {
12365
- return resolve19(p35);
12543
+ return resolve20(p35);
12366
12544
  }
12367
12545
  };
12368
12546
  return real(a) === real(b);
@@ -12375,10 +12553,10 @@ function finishedWorkspaceMessage(id, view) {
12375
12553
  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
12554
  }
12377
12555
  function cleanFailedFreshAttachDir(dir) {
12378
- if (!existsSync25(dir)) return [];
12556
+ if (!existsSync26(dir)) return [];
12379
12557
  let entries;
12380
12558
  try {
12381
- entries = readdirSync7(dir);
12559
+ entries = readdirSync9(dir);
12382
12560
  } catch {
12383
12561
  return ["<unreadable>"];
12384
12562
  }
@@ -12452,12 +12630,12 @@ var attachWorkspaceCommand = defineDeepspaceCommand({
12452
12630
  );
12453
12631
  }
12454
12632
  const requestedDir = typeof args.dir === "string" ? args.dir.trim() : "";
12455
- const worktreeRoot = requestedDir ? isAbsolute7(requestedDir) ? requestedDir : resolve19(appDir, requestedDir) : defaultWorkspaceRoot(appDir, id);
12633
+ const worktreeRoot = requestedDir ? isAbsolute7(requestedDir) ? requestedDir : resolve20(appDir, requestedDir) : defaultWorkspaceRoot(appDir, id);
12456
12634
  const dir2 = appDirInWorktree(appDir, worktreeRoot);
12457
12635
  const localTip = resolveCommit(appDir, `refs/heads/${branch}`);
12458
12636
  if (localTip) {
12459
12637
  let existing = resolveWorkspaceWorktree(listWorktrees(appDir), id);
12460
- if (existing && !existsSync25(existing)) {
12638
+ if (existing && !existsSync26(existing)) {
12461
12639
  runGit(appDir, ["worktree", "prune"], { allowFail: true });
12462
12640
  existing = null;
12463
12641
  }
@@ -12571,11 +12749,11 @@ var attachWorkspaceCommand = defineDeepspaceCommand({
12571
12749
  extra: { status: view.workspace.status }
12572
12750
  });
12573
12751
  }
12574
- const dir = resolve19(
12752
+ const dir = resolve20(
12575
12753
  process.cwd(),
12576
12754
  (typeof args.dir === "string" ? args.dir.trim() : "") || `ws-${id.slice(3).toLowerCase()}`
12577
12755
  );
12578
- if (existsSync25(dir)) {
12756
+ if (existsSync26(dir)) {
12579
12757
  spinner2?.stop("Target exists.");
12580
12758
  throw new Refusal(`${dir} already exists \u2014 pass a different directory.`, "dir_exists");
12581
12759
  }
@@ -12912,13 +13090,13 @@ import * as p30 from "@clack/prompts";
12912
13090
 
12913
13091
  // src/cli/lib/validation.ts
12914
13092
  import { sync as spawnSync7 } from "cross-spawn";
12915
- import { readFileSync as readFileSync26 } from "fs";
12916
- import { join as join23 } from "path";
13093
+ import { readFileSync as readFileSync25 } from "fs";
13094
+ import { join as join26 } from "path";
12917
13095
  var MAX_SUMMARY_CHARS = 4e3;
12918
13096
  var MAX_OUTPUT_BUFFER = 64 * 1024 * 1024;
12919
13097
  function configuredCommand(appDir) {
12920
13098
  try {
12921
- const pkg2 = JSON.parse(readFileSync26(join23(appDir, "package.json"), "utf-8"));
13099
+ const pkg2 = JSON.parse(readFileSync25(join26(appDir, "package.json"), "utf-8"));
12922
13100
  if (pkg2.scripts?.validate) return `${detectPackageManager(appDir)} run --silent validate`;
12923
13101
  } catch {
12924
13102
  }
@@ -13994,13 +14172,13 @@ function sameRefs(left, right) {
13994
14172
 
13995
14173
  // src/cli/commands/update.ts
13996
14174
  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";
14175
+ import { existsSync as existsSync27, readFileSync as readFileSync26, writeFileSync as writeFileSync7 } from "fs";
14176
+ import { join as join27, resolve as resolve21 } from "path";
13999
14177
 
14000
14178
  // package.json
14001
14179
  var package_default = {
14002
14180
  name: "deepspace",
14003
- version: "0.23.0",
14181
+ version: "0.23.1",
14004
14182
  engines: {
14005
14183
  node: ">=22.15.0 <23 || >=24 <25 || >=26 <27"
14006
14184
  },
@@ -14175,24 +14353,25 @@ function gapIsTooWide(from, to) {
14175
14353
  return from.major === 0 && from.minor < OLDEST_SUPPORTED_MINOR;
14176
14354
  }
14177
14355
  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"));
14356
+ const installed = join27(appDir, "node_modules", "deepspace", "package.json");
14357
+ if (existsSync27(installed)) {
14358
+ const parsed2 = JSON.parse(readFileSync26(installed, "utf8"));
14181
14359
  if (typeof parsed2.version === "string") return parsed2.version;
14182
14360
  }
14183
- const manifest = join24(appDir, "package.json");
14184
- if (!existsSync26(manifest)) return null;
14185
- const parsed = JSON.parse(readFileSync27(manifest, "utf8"));
14361
+ const manifest = join27(appDir, "package.json");
14362
+ if (!existsSync27(manifest)) return null;
14363
+ const parsed = JSON.parse(readFileSync26(manifest, "utf8"));
14186
14364
  return parsed.dependencies?.deepspace ?? null;
14187
14365
  }
14188
14366
  function pinSdkVersion(appDir, version) {
14189
- const path = join24(appDir, "package.json");
14190
- const source = readFileSync27(path, "utf8");
14367
+ const path = join27(appDir, "package.json");
14368
+ const source = readFileSync26(path, "utf8");
14191
14369
  const manifest = JSON.parse(source);
14192
14370
  const dependencies = manifest.dependencies;
14193
14371
  if (!dependencies || typeof dependencies !== "object" || Array.isArray(dependencies)) return false;
14194
14372
  let changed = false;
14195
- if (Object.prototype.hasOwnProperty.call(dependencies, "deepspace") && typeof dependencies.deepspace === "string" && dependencies.deepspace !== `^${version}`) {
14373
+ const registrySpec = (spec) => !/^(file|link|workspace|git\+|github|https?):/.test(spec) && !spec.includes("/");
14374
+ if (Object.prototype.hasOwnProperty.call(dependencies, "deepspace") && typeof dependencies.deepspace === "string" && registrySpec(dependencies.deepspace) && dependencies.deepspace !== `^${version}`) {
14196
14375
  dependencies.deepspace = `^${version}`;
14197
14376
  changed = true;
14198
14377
  }
@@ -14213,9 +14392,9 @@ function buildPreviewSecretsUpgradeInstruction(appDir) {
14213
14392
  "vite.config.mts",
14214
14393
  "vite.config.js",
14215
14394
  "vite.config.mjs"
14216
- ].find((candidate) => existsSync26(join24(appDir, candidate)));
14395
+ ].find((candidate) => existsSync27(join27(appDir, candidate)));
14217
14396
  if (!configName) return null;
14218
- const source = readFileSync27(join24(appDir, configName), "utf8");
14397
+ const source = readFileSync26(join27(appDir, configName), "utf8");
14219
14398
  if (source.includes("deepspace-remove-build-preview-secrets") || source.includes("deepspaceBuild(") || source.includes("'deepspace/build'") || source.includes('"deepspace/build"')) {
14220
14399
  return null;
14221
14400
  }
@@ -14223,9 +14402,9 @@ function buildPreviewSecretsUpgradeInstruction(appDir) {
14223
14402
  }
14224
14403
  function usersSchemaVisibilityUpgradeInstruction(appDir) {
14225
14404
  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");
14405
+ const path = join27(appDir, schemaName);
14406
+ if (!existsSync27(path)) return null;
14407
+ const source = readFileSync26(path, "utf8");
14229
14408
  if (!/\bmember\s*:\s*\{[^}]*\bread\s*:\s*true\b/s.test(source)) return null;
14230
14409
  return `${schemaName}: review the member user-row visibility change at ${USERS_SCHEMA_VISIBILITY_GUIDE}; app update left this app-owned schema unchanged.`;
14231
14410
  }
@@ -14285,12 +14464,9 @@ var update_default = defineDeepspaceCommand({
14285
14464
  }
14286
14465
  },
14287
14466
  async run({ args }) {
14288
- const appDir = resolve20(".");
14467
+ const appDir = resolve21(".");
14289
14468
  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
- );
14469
+ throw new Refusal(noWranglerConfigMessage(join27(appDir, "wrangler.toml")), "not_in_app_repo");
14294
14470
  }
14295
14471
  assertSyncableRepo(appDir);
14296
14472
  const currentRaw = readAppSdkVersion(appDir);
@@ -14376,8 +14552,8 @@ var update_default = defineDeepspaceCommand({
14376
14552
 
14377
14553
  // src/cli/cli.ts
14378
14554
  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")) {
14555
+ var pkg = JSON.parse(readFileSync27(join28(__dirname, "..", "package.json"), "utf-8"));
14556
+ if (process.platform === "linux" && !process.env.SSL_CERT_FILE && existsSync28("/etc/ssl/certs/ca-certificates.crt")) {
14381
14557
  process.env.SSL_CERT_FILE = "/etc/ssl/certs/ca-certificates.crt";
14382
14558
  }
14383
14559
  var create2 = defineCommand14({