deepspace 0.22.1 → 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
1145
+ );
1146
+ }
1147
+ assertOneSectionPerAppId(config, path);
1148
+ return config;
1149
+ }
1150
+ function readWranglerConfig(appDir) {
1151
+ return readWranglerConfigFile(join2(appDir, "wrangler.toml"));
1152
+ }
1153
+ function readAppIdVar(config, envName) {
1154
+ return (envName ? config.env?.[envName]?.vars : config.vars)?.DEEPSPACE_APP_ID;
1155
+ }
1156
+ function assertOneSectionPerAppId(config, path) {
1157
+ const sections = /* @__PURE__ */ new Map();
1158
+ const record = (section, value) => {
1159
+ if (typeof value !== "string" || !APP_ID_RE.test(value)) return;
1160
+ sections.set(value, [...sections.get(value) ?? [], section]);
1161
+ };
1162
+ record("[vars]", config.vars?.DEEPSPACE_APP_ID);
1163
+ for (const envName of Object.keys(config.env ?? {})) {
1164
+ record(`[env.${envName}.vars]`, config.env?.[envName]?.vars?.DEEPSPACE_APP_ID);
1165
+ }
1166
+ for (const [appId, where] of sections) {
1167
+ if (where.length < 2) continue;
1168
+ throw new WranglerConfigError(
1169
+ path,
1170
+ `wrangler.toml: ${where.join(" and ")} ${where.length === 2 ? "both" : "all"} set DEEPSPACE_APP_ID = "${appId}" in ${path}. One id under several sections points every environment's data, secrets, and billing at the same app while the deploys go to different places \u2014 each environment is its own app \u2014 run \`deepspace app init --env <name>\` to mint one for it.`,
1171
+ "duplicate_app_id"
950
1172
  );
951
1173
  }
952
1174
  }
1175
+ function noWranglerConfigMessage(where) {
1176
+ return `No wrangler.toml at ${where}. Are you in a DeepSpace app directory? (Scaffold one with \`npm create deepspace\`, or cd into an existing app.)`;
1177
+ }
1178
+ function hasWranglerConfig(appDir) {
1179
+ return existsSync3(join2(appDir, "wrangler.toml"));
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,19 +1220,7 @@ ${header}
991
1220
  ${line}
992
1221
  `;
993
1222
  }
994
- writeFileSync2(wranglerPath, src);
995
- }
996
- var APP_ID_PLACEHOLDER = "__APP_ID__";
997
- function stampAppIdInSources(cwd, appId, previousAppId) {
998
- const constantsPath = join2(resolve2(cwd), "src", "constants.ts");
999
- if (!existsSync3(constantsPath)) return false;
1000
- const needles = [APP_ID_PLACEHOLDER, ...previousAppId ? [previousAppId] : []];
1001
- const content = readFileSync5(constantsPath, "utf-8");
1002
- let next = content;
1003
- for (const needle of needles) next = next.replaceAll(needle, appId);
1004
- if (next === content) return false;
1005
- writeFileSync2(constantsPath, next);
1006
- return true;
1223
+ writeFileSync3(wranglerPath, src);
1007
1224
  }
1008
1225
 
1009
1226
  // src/cli/commands/dev.ts
@@ -1011,26 +1228,18 @@ import { resolve as resolve8, basename as basename2, join as join11 } from "path
1011
1228
  import spawn2 from "cross-spawn";
1012
1229
 
1013
1230
  // src/cli/lib/app-context.ts
1014
- import { existsSync as existsSync4, readFileSync as readFileSync6, readdirSync } from "fs";
1015
- import { dirname as dirname3, join as join3, resolve as resolve3 } from "path";
1016
- 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";
1017
1233
  function detectAppName(cwd = process.cwd()) {
1018
- const wranglerPath = join3(resolve3(cwd), "wrangler.toml");
1019
- if (!existsSync4(wranglerPath)) return null;
1020
- try {
1021
- const config = parseToml2(readFileSync6(wranglerPath, "utf-8"));
1022
- return typeof config.name === "string" && config.name.length > 0 ? config.name : null;
1023
- } catch (error) {
1024
- throw new InputError(
1025
- `Could not parse ${wranglerPath}: ${error instanceof Error ? error.message : String(error)}`,
1026
- "invalid_config"
1027
- );
1028
- }
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;
1029
1238
  }
1030
1239
  function findAppDir(start = process.cwd()) {
1031
1240
  let dir = resolve3(start);
1032
1241
  for (; ; ) {
1033
- if (existsSync4(join3(dir, "wrangler.toml"))) return dir;
1242
+ if (existsSync5(join4(dir, "wrangler.toml"))) return dir;
1034
1243
  const parent = dirname3(dir);
1035
1244
  if (parent === dir) return null;
1036
1245
  dir = parent;
@@ -1038,7 +1247,7 @@ function findAppDir(start = process.cwd()) {
1038
1247
  }
1039
1248
  function findChildApps(dir) {
1040
1249
  try {
1041
- 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();
1042
1251
  } catch {
1043
1252
  return [];
1044
1253
  }
@@ -1046,19 +1255,19 @@ function findChildApps(dir) {
1046
1255
 
1047
1256
  // src/cli/lib/launch-config.ts
1048
1257
  import {
1049
- existsSync as existsSync5,
1258
+ existsSync as existsSync6,
1050
1259
  readFileSync as readFileSync8,
1051
1260
  realpathSync as realpathSync2,
1052
- writeFileSync as writeFileSync3,
1261
+ writeFileSync as writeFileSync4,
1053
1262
  mkdirSync as mkdirSync4,
1054
1263
  renameSync,
1055
1264
  unlinkSync
1056
1265
  } from "fs";
1057
- 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";
1058
1267
 
1059
1268
  // src/cli/lib/git/repository.ts
1060
1269
  import { appendFileSync, mkdirSync as mkdirSync3, readFileSync as readFileSync7 } from "fs";
1061
- 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";
1062
1271
 
1063
1272
  // src/cli/lib/git/process.ts
1064
1273
  import { sync as spawnSync } from "cross-spawn";
@@ -1262,7 +1471,7 @@ function listWorktrees(cwd) {
1262
1471
  }
1263
1472
  function ensureLocalExclude(cwd, pattern) {
1264
1473
  const commonDir = resolve4(cwd, gitLine(cwd, ["rev-parse", "--git-common-dir"]));
1265
- const excludePath = join4(commonDir, "info", "exclude");
1474
+ const excludePath = join5(commonDir, "info", "exclude");
1266
1475
  let current = "";
1267
1476
  try {
1268
1477
  current = readFileSync7(excludePath, "utf-8");
@@ -1284,7 +1493,7 @@ function ensureLocalExclude(cwd, pattern) {
1284
1493
  function writeLaunchFile(launchPath, config) {
1285
1494
  mkdirSync4(dirname5(launchPath), { recursive: true });
1286
1495
  const tmpPath = `${launchPath}.${process.pid}.tmp`;
1287
- writeFileSync3(tmpPath, JSON.stringify(config, null, 2) + "\n");
1496
+ writeFileSync4(tmpPath, JSON.stringify(config, null, 2) + "\n");
1288
1497
  try {
1289
1498
  renameSync(tmpPath, launchPath);
1290
1499
  } catch (err) {
@@ -1297,7 +1506,7 @@ function writeLaunchFile(launchPath, config) {
1297
1506
  }
1298
1507
  function readLaunchFile(launchPath) {
1299
1508
  const empty = { version: "0.0.1", configurations: [] };
1300
- if (!existsSync5(launchPath)) return empty;
1509
+ if (!existsSync6(launchPath)) return empty;
1301
1510
  const raw = readFileSync8(launchPath, "utf-8").trim();
1302
1511
  if (!raw) return empty;
1303
1512
  const parsed = JSON.parse(raw);
@@ -1305,7 +1514,7 @@ function readLaunchFile(launchPath) {
1305
1514
  return parsed;
1306
1515
  }
1307
1516
  function writeLaunchConfigIfMissing(appDir, appName, port, opts = {}) {
1308
- const launchPath = join5(appDir, ".claude", "launch.json");
1517
+ const launchPath = join6(appDir, ".claude", "launch.json");
1309
1518
  try {
1310
1519
  const config = readLaunchFile(launchPath);
1311
1520
  if (!config) return;
@@ -1346,7 +1555,7 @@ function writeLaunchConfigIfMissing(appDir, appName, port, opts = {}) {
1346
1555
  }
1347
1556
  function resolveAppLaunchPort(appDir) {
1348
1557
  try {
1349
- const config = readLaunchFile(join5(appDir, ".claude", "launch.json"));
1558
+ const config = readLaunchFile(join6(appDir, ".claude", "launch.json"));
1350
1559
  if (!config) return null;
1351
1560
  const appName = detectAppName(appDir) ?? basename(appDir);
1352
1561
  const entry = config.configurations.find((c) => c?.name === appName) ?? (config.configurations.length === 1 ? config.configurations[0] : void 0);
@@ -1400,15 +1609,15 @@ function deriveWorktreePort(worktreeKey) {
1400
1609
  return WORKTREE_PORT_MIN + hash % WORKTREE_PORT_RANGE;
1401
1610
  }
1402
1611
  function upsertWorktreeLaunchConfig(mainRepoRoot, worktreeName, appDir, opts) {
1403
- const launchPath = join5(mainRepoRoot, ".claude", "launch.json");
1612
+ const launchPath = join6(mainRepoRoot, ".claude", "launch.json");
1404
1613
  const entryName = WORKTREE_ENTRY_PREFIX + worktreeName;
1405
1614
  try {
1406
1615
  const config = readLaunchFile(launchPath);
1407
1616
  if (!config) return null;
1408
- const worktreesRoot = join5(mainRepoRoot, ".claude", "worktrees") + sep;
1617
+ const worktreesRoot = join6(mainRepoRoot, ".claude", "worktrees") + sep;
1409
1618
  config.configurations = config.configurations.filter((c) => {
1410
1619
  if (c?.name === entryName) return false;
1411
- 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);
1412
1621
  return !isStaleWorktreeEntry;
1413
1622
  });
1414
1623
  let port = opts.port;
@@ -1443,7 +1652,7 @@ function resolveWorktreePort(appDir) {
1443
1652
  const claude = detectClaudeWorktree(appDir);
1444
1653
  if (claude) {
1445
1654
  try {
1446
- const config = readLaunchFile(join5(claude.mainRepoRoot, ".claude", "launch.json"));
1655
+ const config = readLaunchFile(join6(claude.mainRepoRoot, ".claude", "launch.json"));
1447
1656
  const entry = config?.configurations.find(
1448
1657
  (c) => c?.name === WORKTREE_ENTRY_PREFIX + claude.worktreeName
1449
1658
  );
@@ -1467,231 +1676,41 @@ async function fetchPublicKey(authUrl) {
1467
1676
  }
1468
1677
  async function mintAppOwnerJwt(authUrl, callerJwt, appId) {
1469
1678
  const response = await fetch(`${authUrl}/api/auth/mint-app-token`, {
1470
- method: "POST",
1471
- headers: {
1472
- "Content-Type": "application/json",
1473
- Authorization: `Bearer ${callerJwt}`
1474
- },
1475
- body: JSON.stringify({ appId })
1476
- });
1477
- if (!response.ok) {
1478
- const detail = await response.text().catch(() => "");
1479
- throw new Error(`Failed to mint APP_OWNER_JWT (${response.status}): ${detail}`);
1480
- }
1481
- const body = await response.json();
1482
- if (!body.token) {
1483
- throw new Error(`Auth worker returned no token: ${body.error ?? "unknown error"}`);
1484
- }
1485
- return body.token;
1486
- }
1487
- async function fetchAppIdentityToken(deployUrl, callerJwt, appId) {
1488
- const response = await fetch(
1489
- `${deployUrl}/api/apps/${encodeURIComponent(appId)}/identity-token`,
1490
- {
1491
- method: "POST",
1492
- headers: { Authorization: `Bearer ${callerJwt}` }
1493
- }
1494
- );
1495
- if (response.status === 404) return null;
1496
- if (!response.ok) {
1497
- const detail = await response.text().catch(() => "");
1498
- throw new Error(`Failed to fetch APP_IDENTITY_TOKEN (${response.status}): ${detail}`);
1499
- }
1500
- const body = await response.json();
1501
- if (!body.token) {
1502
- throw new Error(`Deploy worker returned no token: ${body.error ?? "unknown error"}`);
1503
- }
1504
- return body.token;
1505
- }
1506
-
1507
- // src/cli/lib/wrangler-env.ts
1508
- import { existsSync as existsSync6, readFileSync as readFileSync9, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
1509
- import { join as join6 } from "path";
1510
- import { parse as parseToml3, stringify as stringifyToml } from "smol-toml";
1511
-
1512
- // src/server/rooms/app-name.ts
1513
- var APP_NAME_RULES = {
1514
- /** ^[a-z0-9](-?[a-z0-9])+$ — RFC 1035 host label, no consecutive dashes. */
1515
- pattern: /^[a-z0-9](?:-?[a-z0-9])+$/,
1516
- minLength: 2,
1517
- maxLength: 63
1518
- };
1519
- function validateAppName(raw) {
1520
- if (typeof raw !== "string") {
1521
- return { valid: false, reason: "App name must be a string" };
1522
- }
1523
- const name = raw.trim();
1524
- if (!name) {
1525
- return { valid: false, reason: "App name is required" };
1526
- }
1527
- if (name.length < APP_NAME_RULES.minLength) {
1528
- return { valid: false, reason: `App name "${name}" is too short (min ${APP_NAME_RULES.minLength} chars)` };
1529
- }
1530
- if (name.length > APP_NAME_RULES.maxLength) {
1531
- return { valid: false, reason: `App name "${name}" is too long (max ${APP_NAME_RULES.maxLength} chars)` };
1532
- }
1533
- if (!APP_NAME_RULES.pattern.test(name)) {
1534
- return {
1535
- valid: false,
1536
- 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.`
1537
- };
1538
- }
1539
- return { valid: true, name };
1540
- }
1541
- function resolveAppName(raw) {
1542
- const strict = validateAppName(raw);
1543
- if (strict.valid) return { ok: true, name: strict.name };
1544
- if (typeof raw !== "string") return { ok: false, reason: strict.reason };
1545
- const trimmed = raw.trim();
1546
- if (!trimmed) return { ok: false, reason: "App name is required" };
1547
- const sanitized = sanitizeAppName(trimmed);
1548
- const sanitizedStrict = validateAppName(sanitized);
1549
- if (!sanitizedStrict.valid) {
1550
- return {
1551
- ok: false,
1552
- 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.`
1553
- };
1554
- }
1555
- return {
1556
- ok: true,
1557
- name: sanitized,
1558
- warning: `App name "${trimmed}" doesn't conform to deploy rules \u2014 using "${sanitized}" instead. Update wrangler.toml's \`name\` to silence this warning.`
1559
- };
1560
- }
1561
- function sanitizeAppName(raw) {
1562
- return raw.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "");
1563
- }
1564
-
1565
- // src/cli/lib/wrangler-env.ts
1566
- var NON_INHERITABLE_ENV_KEYS = /* @__PURE__ */ new Set([
1567
- "define",
1568
- "vars",
1569
- "durable_objects",
1570
- "kv_namespaces",
1571
- "r2_buckets",
1572
- "ai_search_namespaces",
1573
- "ai_search",
1574
- "ratelimits",
1575
- "vectorize",
1576
- "services",
1577
- "queues",
1578
- "workflows",
1579
- "tail_consumers",
1580
- "secrets",
1581
- "secrets_store_secrets"
1582
- ]);
1583
- var generatedConfigSeq = 0;
1584
- function resolveAppNameForEnv(config, envName) {
1585
- if (!envName) {
1586
- return resolveAppName(config.name);
1587
- }
1588
- const envBlock = config.env?.[envName];
1589
- if (!envBlock) {
1590
- return {
1591
- ok: false,
1592
- reason: `wrangler.toml: no [env.${envName}] block found. Add one with a distinct \`name\` to deploy this environment.`
1593
- };
1594
- }
1595
- if (typeof envBlock.name !== "string" || envBlock.name.trim() === "") {
1596
- return {
1597
- ok: false,
1598
- 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.`
1599
- };
1600
- }
1601
- if (envBlock.name === config.name) {
1602
- return {
1603
- ok: false,
1604
- 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.`
1605
- };
1606
- }
1607
- return resolveAppName(envBlock.name);
1608
- }
1609
- function devVarsPathFor(appDir) {
1610
- return join6(appDir, ".dev.vars");
1611
- }
1612
- function prepareWranglerEnvConfig(appDir, envName) {
1613
- if (!envName) return { cleanup() {
1614
- } };
1615
- const config = readWranglerConfig(appDir);
1616
- const resolved = resolveAppNameForEnv(config, envName);
1617
- if (!resolved.ok) throw new Error(resolved.reason);
1618
- const flattened = flattenWranglerEnvConfig(config, envName);
1619
- const configPath = join6(
1620
- appDir,
1621
- `.wrangler.deepspace.${safeGeneratedConfigSegment(envName)}.${process.pid}.${++generatedConfigSeq}.toml`
1622
- );
1623
- writeFileSync4(
1624
- configPath,
1625
- [
1626
- "# Generated by DeepSpace for this CLI run.",
1627
- "# Do not edit; wrangler.toml remains the source of truth.",
1628
- stringifyToml(flattened)
1629
- ].join("\n").replace(/\s*$/u, "\n")
1630
- );
1631
- return {
1632
- configPath,
1633
- cleanup() {
1634
- rmSync2(configPath, { force: true });
1635
- }
1636
- };
1637
- }
1638
- function wranglerViteEnv(baseEnv, prepared, extraEnv = {}) {
1639
- const env = { ...baseEnv, ...extraEnv };
1640
- if (prepared.configPath) {
1641
- delete env.CLOUDFLARE_ENV;
1642
- env.CLOUDFLARE_VITE_WRANGLER_CONFIG_PATH = prepared.configPath;
1643
- }
1644
- return env;
1645
- }
1646
- function flattenWranglerEnvConfig(config, envName) {
1647
- const envRoot = asTomlRecord(config.env);
1648
- const envBlock = asTomlRecord(envRoot?.[envName]);
1649
- if (!envBlock) {
1650
- throw new Error(`wrangler.toml: no [env.${envName}] block found.`);
1651
- }
1652
- const flattened = {};
1653
- for (const [key, value] of Object.entries(config)) {
1654
- if (key === "env" || NON_INHERITABLE_ENV_KEYS.has(key)) continue;
1655
- flattened[key] = value;
1656
- }
1657
- for (const [key, value] of Object.entries(envBlock)) {
1658
- flattened[key] = value;
1659
- }
1660
- if (!("durable_objects" in envBlock) && config.durable_objects) {
1661
- flattened.durable_objects = config.durable_objects;
1679
+ method: "POST",
1680
+ headers: {
1681
+ "Content-Type": "application/json",
1682
+ Authorization: `Bearer ${callerJwt}`
1683
+ },
1684
+ body: JSON.stringify({ appId })
1685
+ });
1686
+ if (!response.ok) {
1687
+ const detail = await response.text().catch(() => "");
1688
+ throw new Error(`Failed to mint APP_OWNER_JWT (${response.status}): ${detail}`);
1662
1689
  }
1663
- const envVars = asTomlRecord(envBlock.vars) ?? {};
1664
- if (!("APP_NAME" in envVars) && typeof envBlock.name === "string") {
1665
- flattened.vars = { ...envVars, APP_NAME: envBlock.name };
1690
+ const body = await response.json();
1691
+ if (!body.token) {
1692
+ throw new Error(`Auth worker returned no token: ${body.error ?? "unknown error"}`);
1666
1693
  }
1667
- return flattened;
1668
- }
1669
- function asTomlRecord(value) {
1670
- return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
1671
- }
1672
- function safeGeneratedConfigSegment(value) {
1673
- return value.replace(/[^A-Za-z0-9._-]/gu, "_");
1694
+ return body.token;
1674
1695
  }
1675
- var WranglerConfigError = class extends Error {
1676
- constructor(path, cause) {
1677
- const detail = cause instanceof Error ? cause.message : String(cause);
1678
- super(`wrangler.toml: malformed TOML at ${path}: ${detail}`);
1679
- this.path = path;
1680
- this.cause = cause;
1681
- this.name = "WranglerConfigError";
1696
+ async function fetchAppIdentityToken(deployUrl, callerJwt, appId) {
1697
+ const response = await fetch(
1698
+ `${deployUrl}/api/apps/${encodeURIComponent(appId)}/identity-token`,
1699
+ {
1700
+ method: "POST",
1701
+ headers: { Authorization: `Bearer ${callerJwt}` }
1702
+ }
1703
+ );
1704
+ if (response.status === 404) return null;
1705
+ if (!response.ok) {
1706
+ const detail = await response.text().catch(() => "");
1707
+ throw new Error(`Failed to fetch APP_IDENTITY_TOKEN (${response.status}): ${detail}`);
1682
1708
  }
1683
- };
1684
- function readWranglerConfig(appDir) {
1685
- const path = join6(appDir, "wrangler.toml");
1686
- const raw = readFileSync9(path, "utf-8");
1687
- try {
1688
- return parseToml3(raw);
1689
- } catch (err) {
1690
- throw new WranglerConfigError(path, err);
1709
+ const body = await response.json();
1710
+ if (!body.token) {
1711
+ throw new Error(`Deploy worker returned no token: ${body.error ?? "unknown error"}`);
1691
1712
  }
1692
- }
1693
- function hasWranglerConfig(appDir) {
1694
- return existsSync6(join6(appDir, "wrangler.toml"));
1713
+ return body.token;
1695
1714
  }
1696
1715
 
1697
1716
  // src/cli/lib/dev-vars.ts
@@ -1733,15 +1752,15 @@ async function writeDevVars(appDir, ownerId, callerJwt, wranglerEnv, opts = {})
1733
1752
  }
1734
1753
 
1735
1754
  // src/cli/lib/install-status.ts
1736
- 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";
1737
1756
  import { dirname as dirname6, join as join8, resolve as resolve6 } from "path";
1738
1757
 
1739
1758
  // src/cli/lib/package-manager.ts
1740
- import { existsSync as existsSync7, readFileSync as readFileSync10 } from "fs";
1759
+ import { existsSync as existsSync7, readFileSync as readFileSync9 } from "fs";
1741
1760
  import { join as join7 } from "path";
1742
1761
  function detectPackageManager(appDir) {
1743
1762
  try {
1744
- const pkg2 = JSON.parse(readFileSync10(join7(appDir, "package.json"), "utf-8"));
1763
+ const pkg2 = JSON.parse(readFileSync9(join7(appDir, "package.json"), "utf-8"));
1745
1764
  const name = typeof pkg2.packageManager === "string" ? pkg2.packageManager.split("@")[0] : "";
1746
1765
  if (name === "npm" || name === "pnpm" || name === "yarn" || name === "bun") return name;
1747
1766
  } catch {
@@ -1779,7 +1798,7 @@ function ensureInstallReady(appDir) {
1779
1798
  const donePath = join8(sentinelDir, "install.done");
1780
1799
  const logPath = join8(sentinelDir, "install.log");
1781
1800
  if (existsSync8(errPath)) {
1782
- const detail = readFileSync11(errPath, "utf-8").trimEnd();
1801
+ const detail = readFileSync10(errPath, "utf-8").trimEnd();
1783
1802
  const log29 = existsSync8(logPath) ? ` Full log: ${logPath}.` : "";
1784
1803
  throw new Refusal(
1785
1804
  `Dependency install failed: ${detail}.${log29} Run \`npm install\` (or \`bun install\`) manually, then retry.`,
@@ -1835,7 +1854,7 @@ function canonicalPath2(path) {
1835
1854
  function installWorkerAlive(sentinelDir) {
1836
1855
  const pidPath = join8(sentinelDir, "install.pid");
1837
1856
  if (!existsSync8(pidPath)) return false;
1838
- const pid = Number(readFileSync11(pidPath, "utf-8").trim());
1857
+ const pid = Number(readFileSync10(pidPath, "utf-8").trim());
1839
1858
  if (!Number.isSafeInteger(pid) || pid <= 0) return false;
1840
1859
  try {
1841
1860
  process.kill(pid, 0);
@@ -2191,11 +2210,7 @@ async function secretsApi(deployUrl, token, path, init = {}) {
2191
2210
  });
2192
2211
  const body = await res.json().catch(() => ({}));
2193
2212
  if (!res.ok) {
2194
- const err = new Error(body.error ?? `Secrets request failed (${res.status})`);
2195
- err.status = res.status;
2196
- err.code = body.code;
2197
- err.apiPath = path;
2198
- throw err;
2213
+ throw new ApiError(body.error ?? `Secrets request failed (${res.status})`, res.status, body.code, path);
2199
2214
  }
2200
2215
  return body;
2201
2216
  }
@@ -2400,6 +2415,12 @@ import { join as join10 } from "path";
2400
2415
  import { pathToFileURL } from "url";
2401
2416
 
2402
2417
  // src/server/schemas/registry.ts
2418
+ var SYSTEM_ASSIGNED_COLUMNS = /* @__PURE__ */ new Set(["email", "name", "imageUrl", "role"]);
2419
+ var SYSTEM_MAINTAINED_COLUMNS = /* @__PURE__ */ new Set(["createdAt", "lastSeenAt"]);
2420
+ var SYSTEM_MANAGED_COLUMNS = /* @__PURE__ */ new Set([
2421
+ ...SYSTEM_ASSIGNED_COLUMNS,
2422
+ ...SYSTEM_MAINTAINED_COLUMNS
2423
+ ]);
2403
2424
  var USERS_COLUMNS = [
2404
2425
  { name: "email", storage: "text", interpretation: "plain" },
2405
2426
  { name: "name", storage: "text", interpretation: "plain" },
@@ -2452,6 +2473,23 @@ function lintSchema(schema) {
2452
2473
  }
2453
2474
  return warnings;
2454
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
+ }
2455
2493
 
2456
2494
  // src/cli/lib/schema-lint.ts
2457
2495
  async function lintProjectSchemas(appDir) {
@@ -2480,15 +2518,20 @@ async function lintProjectSchemas(appDir) {
2480
2518
  return null;
2481
2519
  }
2482
2520
  if (!Array.isArray(schemas)) return null;
2483
- const findings = [];
2484
- for (const schema of schemas) {
2485
- if (!schema || typeof schema !== "object") continue;
2521
+ const wellFormed = schemas.filter((schema) => {
2522
+ if (!schema || typeof schema !== "object") return false;
2486
2523
  try {
2487
- findings.push(...lintSchema(schema));
2524
+ lintSchema(schema);
2525
+ return true;
2488
2526
  } catch {
2527
+ return false;
2489
2528
  }
2529
+ });
2530
+ try {
2531
+ return lintSchemas(wellFormed);
2532
+ } catch {
2533
+ return null;
2490
2534
  }
2491
- return findings;
2492
2535
  }
2493
2536
  var MAX_PRINTED_FINDINGS = 5;
2494
2537
  function formatSchemaLintFindings(findings) {
@@ -2508,11 +2551,11 @@ import { createServer, Socket } from "net";
2508
2551
  var DEFAULT_PORT = 5173;
2509
2552
  function isPortListening(port, host, timeoutMs = 500) {
2510
2553
  const target2 = host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host;
2511
- return new Promise((resolve21) => {
2554
+ return new Promise((resolve22) => {
2512
2555
  const socket = new Socket();
2513
2556
  const done = (listening) => {
2514
2557
  socket.destroy();
2515
- resolve21(listening);
2558
+ resolve22(listening);
2516
2559
  };
2517
2560
  socket.setTimeout(timeoutMs);
2518
2561
  socket.once("connect", () => done(true));
@@ -2525,7 +2568,7 @@ async function waitForPortListening(port, host, timeoutMs, signal) {
2525
2568
  const deadline = Date.now() + timeoutMs;
2526
2569
  while (Date.now() < deadline && !signal?.aborted) {
2527
2570
  if (await isPortListening(port, host)) return true;
2528
- await new Promise((resolve21) => setTimeout(resolve21, 250));
2571
+ await new Promise((resolve22) => setTimeout(resolve22, 250));
2529
2572
  }
2530
2573
  return false;
2531
2574
  }
@@ -2743,7 +2786,7 @@ Free it with \`${killCmd}\`, or start on another port: \`deepspace dev start --p
2743
2786
 
2744
2787
  // src/cli/commands/kill.ts
2745
2788
  import { spawnSync as spawnSync2 } from "child_process";
2746
- import { readFileSync as readFileSync12, readdirSync as readdirSync3, readlinkSync } from "fs";
2789
+ import { readFileSync as readFileSync11, readdirSync as readdirSync3, readlinkSync } from "fs";
2747
2790
  import { setTimeout as wait } from "timers/promises";
2748
2791
  var SIGTERM_GRACE_MS = 1500;
2749
2792
  var IS_WIN = process.platform === "win32";
@@ -2894,7 +2937,7 @@ function listenerPidsViaProc(port) {
2894
2937
  for (const f of ["/proc/net/tcp", "/proc/net/tcp6"]) {
2895
2938
  let data;
2896
2939
  try {
2897
- data = readFileSync12(f, "utf-8");
2940
+ data = readFileSync11(f, "utf-8");
2898
2941
  } catch {
2899
2942
  continue;
2900
2943
  }
@@ -3032,7 +3075,8 @@ function isAlive(pid) {
3032
3075
  }
3033
3076
 
3034
3077
  // src/cli/commands/test.ts
3035
- 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";
3036
3080
  import { sync as spawnSync4 } from "cross-spawn";
3037
3081
 
3038
3082
  // src/cli/lib/playwright.ts
@@ -3061,10 +3105,10 @@ function ensurePlaywright(appDir) {
3061
3105
  }
3062
3106
 
3063
3107
  // src/cli/lib/test-account-service.ts
3064
- import { readFileSync as readFileSync14 } from "fs";
3108
+ import { readFileSync as readFileSync13 } from "fs";
3065
3109
 
3066
3110
  // src/testing/accounts.ts
3067
- import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync13, writeFileSync as writeFileSync5 } from "fs";
3111
+ import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync12 } from "fs";
3068
3112
  import { homedir as homedir2 } from "os";
3069
3113
  import { join as join12 } from "path";
3070
3114
  var TEST_ACCOUNTS_PATH = join12(homedir2(), ".deepspace", "test-accounts.json");
@@ -3103,7 +3147,9 @@ function reconcileTestAccountScopes(store, remote, scope) {
3103
3147
  };
3104
3148
  }
3105
3149
  function reconcileTestAccounts(local, remote) {
3106
- const localById = new Map(local.filter((account) => account.id).map((account) => [account.id, account]));
3150
+ const localById = new Map(
3151
+ local.filter((account) => account.id).map((account) => [account.id, account])
3152
+ );
3107
3153
  return remote.flatMap((account) => {
3108
3154
  const stored = localById.get(account.id);
3109
3155
  return stored ? [{ ...stored, ...account }] : [];
@@ -3132,13 +3178,15 @@ function removeTestAccounts(ids, emails = [], scope = currentTestAccountScope())
3132
3178
  function readTestAccountStore() {
3133
3179
  if (!existsSync11(TEST_ACCOUNTS_PATH)) return { version: 2, scopes: {} };
3134
3180
  try {
3135
- const raw = JSON.parse(readFileSync13(TEST_ACCOUNTS_PATH, "utf-8"));
3181
+ const raw = JSON.parse(readFileSync12(TEST_ACCOUNTS_PATH, "utf-8"));
3136
3182
  if (Array.isArray(raw)) return { version: 2, scopes: {} };
3137
3183
  if (!raw || typeof raw !== "object") {
3138
3184
  return { version: 2, scopes: {} };
3139
3185
  }
3140
3186
  const candidate = raw;
3141
- const scopes = candidate.scopes && typeof candidate.scopes === "object" && !Array.isArray(candidate.scopes) ? Object.fromEntries(Object.entries(candidate.scopes).map(([key, value]) => [key, validTestAccounts(value)])) : {};
3187
+ const scopes = candidate.scopes && typeof candidate.scopes === "object" && !Array.isArray(candidate.scopes) ? Object.fromEntries(
3188
+ Object.entries(candidate.scopes).map(([key, value]) => [key, validTestAccounts(value)])
3189
+ ) : {};
3142
3190
  return { version: 2, scopes };
3143
3191
  } catch {
3144
3192
  return { version: 2, scopes: {} };
@@ -3154,14 +3202,14 @@ function validTestAccounts(value) {
3154
3202
  }
3155
3203
  function saveTestAccountStore(store) {
3156
3204
  mkdirSync5(join12(homedir2(), ".deepspace"), { recursive: true });
3157
- writeFileSync5(TEST_ACCOUNTS_PATH, JSON.stringify(store, null, 2), { mode: 384 });
3205
+ writeSecretFileSync(TEST_ACCOUNTS_PATH, JSON.stringify(store, null, 2));
3158
3206
  }
3159
3207
 
3160
3208
  // src/cli/lib/test-account-service.ts
3161
3209
  var SESSION_COOKIE2 = "__Secure-better-auth.session_token";
3162
3210
  var AUTH_URL4 = process.env.DEEPSPACE_AUTH_URL ?? PLATFORM_URLS.auth;
3163
3211
  function sessionCookie() {
3164
- const token = readFileSync14(SESSION_PATH, "utf-8").trim();
3212
+ const token = readFileSync13(SESSION_PATH, "utf-8").trim();
3165
3213
  return `${SESSION_COOKIE2}=${encodeURIComponent(token)}`;
3166
3214
  }
3167
3215
  async function fetchRemoteTestAccounts() {
@@ -3190,6 +3238,30 @@ async function createRemoteTestAccount(input) {
3190
3238
  }
3191
3239
  return { ...data, label: data.label ?? null };
3192
3240
  }
3241
+ async function recoverRemoteTestAccount(id) {
3242
+ const response = await fetch(`${AUTH_URL4}/api/auth/test-accounts/${id}/credential`, {
3243
+ method: "POST",
3244
+ headers: { Cookie: sessionCookie(), Origin: AUTH_URL4 }
3245
+ });
3246
+ const data = await response.json().catch(() => ({}));
3247
+ if (response.status === 404) {
3248
+ throw new Error(
3249
+ `No test account ${id} on this account. Run \`deepspace test accounts list\` for the current pool.`
3250
+ );
3251
+ }
3252
+ if (!response.ok || typeof data.password !== "string" || typeof data.email !== "string" || typeof data.userId !== "string") {
3253
+ throw new Error(data.error ?? "Failed to re-issue the test account credential");
3254
+ }
3255
+ return {
3256
+ id,
3257
+ email: data.email,
3258
+ userId: data.userId,
3259
+ label: data.label ?? null,
3260
+ name: typeof data.name === "string" ? data.name : null,
3261
+ createdAt: data.createdAt ?? Date.now(),
3262
+ password: data.password
3263
+ };
3264
+ }
3193
3265
  async function deleteRemoteTestAccount(id) {
3194
3266
  const response = await fetch(`${AUTH_URL4}/api/auth/test-accounts/${id}`, {
3195
3267
  method: "DELETE",
@@ -3209,6 +3281,28 @@ async function syncTestAccountStore() {
3209
3281
  // src/cli/commands/test.ts
3210
3282
  var DEPLOY_URL2 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
3211
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
+ }
3212
3306
  function playwrightTestArgs(testFiles, flags = {}) {
3213
3307
  return [
3214
3308
  "playwright",
@@ -3328,6 +3422,7 @@ var test_default = defineDeepspaceCommand({
3328
3422
  ensurePlaywright(appDir);
3329
3423
  }
3330
3424
  let exitCode = 0;
3425
+ let skippedSpecs = [];
3331
3426
  switch (suite) {
3332
3427
  case "smoke":
3333
3428
  exitCode = runPlaywright(appDir, ["tests/smoke.spec.ts"], port, wranglerEnv, forwarded);
@@ -3342,20 +3437,20 @@ var test_default = defineDeepspaceCommand({
3342
3437
  if (forwarded.grep || forwarded.project || forwarded.headed) {
3343
3438
  console.error("note: --grep/--project/--headed apply to Playwright suites and are ignored by `unit`");
3344
3439
  }
3345
- exitCode = runVitest(appDir);
3440
+ exitCode = runVitest(appDir, wranglerEnv);
3346
3441
  break;
3347
3442
  case "all":
3348
- exitCode = runVitest(appDir);
3443
+ exitCode = runVitest(appDir, wranglerEnv);
3349
3444
  if (exitCode === 0) exitCode = runPlaywright(appDir, [], port, wranglerEnv, forwarded);
3350
3445
  break;
3351
3446
  case "default":
3352
- exitCode = runPlaywright(
3353
- appDir,
3354
- ["tests/smoke.spec.ts", "tests/api.spec.ts"],
3355
- port,
3356
- wranglerEnv,
3357
- forwarded
3358
- );
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
+ }
3359
3454
  break;
3360
3455
  default:
3361
3456
  if (suite.endsWith(".spec.ts")) {
@@ -3370,13 +3465,13 @@ Available: smoke, api, e2e, unit, all`,
3370
3465
  }
3371
3466
  if (exitCode !== 0) {
3372
3467
  throw new Refusal(`Test suite '${suite}' failed (exit ${exitCode}).`, "tests_failed", {
3373
- extra: { suite, port, exitCode }
3468
+ extra: { suite, port, exitCode, skippedSpecs }
3374
3469
  });
3375
3470
  }
3376
- return { data: { suite, port, appDir, wranglerEnv: wranglerEnv ?? null } };
3471
+ return { data: { suite, port, appDir, wranglerEnv: wranglerEnv ?? null, skippedSpecs } };
3377
3472
  }
3378
3473
  });
3379
- function runPlaywright(appDir, testFiles, port, wranglerEnv, flags) {
3474
+ function runSuite(appDir, argv, wranglerEnv, extraEnv = {}) {
3380
3475
  let wranglerConfig;
3381
3476
  try {
3382
3477
  wranglerConfig = prepareWranglerEnvConfig(appDir, wranglerEnv);
@@ -3385,25 +3480,24 @@ function runPlaywright(appDir, testFiles, port, wranglerEnv, flags) {
3385
3480
  return 1;
3386
3481
  }
3387
3482
  try {
3388
- const result = spawnSync4("npx", playwrightTestArgs(testFiles, flags), {
3483
+ const result = spawnSync4("npx", argv, {
3389
3484
  cwd: appDir,
3390
3485
  stdio: "inherit",
3391
- env: wranglerViteEnv(process.env, wranglerConfig, {
3392
- DEEPSPACE_PORT: String(port),
3393
- PLAYWRIGHT_HTML_OUTPUT_DIR: ".deepspace/playwright-report"
3394
- })
3486
+ env: wranglerViteEnv(process.env, wranglerConfig, extraEnv)
3395
3487
  });
3396
3488
  return result.status ?? 1;
3397
3489
  } finally {
3398
3490
  wranglerConfig.cleanup();
3399
3491
  }
3400
3492
  }
3401
- function runVitest(appDir) {
3402
- const result = spawnSync4("npx", ["vitest", "run", "--passWithNoTests"], {
3403
- cwd: appDir,
3404
- stdio: "inherit"
3493
+ function runPlaywright(appDir, testFiles, port, wranglerEnv, flags = {}) {
3494
+ return runSuite(appDir, playwrightTestArgs(testFiles, flags), wranglerEnv, {
3495
+ DEEPSPACE_PORT: String(port),
3496
+ PLAYWRIGHT_HTML_OUTPUT_DIR: ".deepspace/playwright-report"
3405
3497
  });
3406
- return result.status ?? 1;
3498
+ }
3499
+ function runVitest(appDir, wranglerEnv) {
3500
+ return runSuite(appDir, ["vitest", "run", "--passWithNoTests"], wranglerEnv);
3407
3501
  }
3408
3502
 
3409
3503
  // src/cli/commands/screenshot.ts
@@ -3741,6 +3835,107 @@ Failed:
3741
3835
  return { data: { deleted: ok2, requested: targets.length, failures: [] } };
3742
3836
  }
3743
3837
  });
3838
+ var recover = defineDeepspaceCommand({
3839
+ meta: {
3840
+ name: "recover",
3841
+ description: "Re-issue the local credential for a test account you own"
3842
+ },
3843
+ args: {
3844
+ email: {
3845
+ type: "string",
3846
+ description: "Email of the test account to recover",
3847
+ required: false
3848
+ },
3849
+ id: {
3850
+ type: "string",
3851
+ description: "ID of the test account to recover",
3852
+ required: false
3853
+ },
3854
+ all: {
3855
+ type: "boolean",
3856
+ description: "Recover every pool account missing a local credential",
3857
+ required: false
3858
+ }
3859
+ },
3860
+ async run({ args }) {
3861
+ const selectors = [args.email, args.id, args.all].filter(Boolean).length;
3862
+ if (selectors === 0) {
3863
+ throw new Refusal("Provide --email <email>, --id <id>, or --all.", "missing_argument");
3864
+ }
3865
+ if (selectors > 1) {
3866
+ throw new Refusal("Provide exactly one of --email, --id, or --all.", "conflicting_arguments");
3867
+ }
3868
+ await ensureToken();
3869
+ let remote;
3870
+ try {
3871
+ remote = await fetchRemoteTestAccounts();
3872
+ } catch (err) {
3873
+ throw new Refusal(`Failed: ${err.message}`, "test_accounts_list_failed");
3874
+ }
3875
+ const localAccounts = loadAllTestAccounts();
3876
+ const known = new Set(localAccounts.map((account) => account.id));
3877
+ const localNames = new Map(
3878
+ localAccounts.flatMap(
3879
+ (account) => account.name ? [[account.email, account.name]] : []
3880
+ )
3881
+ );
3882
+ let targets;
3883
+ if (args.all) {
3884
+ targets = remote.filter((account) => !known.has(account.id));
3885
+ if (targets.length === 0) {
3886
+ if (!args.json) console.log("Every pool account already has a local credential.");
3887
+ return { data: { recovered: [], alreadyLocal: remote.length } };
3888
+ }
3889
+ } else {
3890
+ const match = args.id ? remote.find((account) => account.id === args.id) : remote.find((account) => account.email === args.email);
3891
+ if (!match) {
3892
+ throw new Refusal(
3893
+ `No test account ${args.id ? `with id ${args.id}` : `with email ${args.email}`} on this account.`,
3894
+ "test_account_not_found",
3895
+ { action: cliAction("deepspace", "test", "accounts", "list") }
3896
+ );
3897
+ }
3898
+ targets = [match];
3899
+ }
3900
+ const recovered = [];
3901
+ const failures = [];
3902
+ for (const target2 of targets) {
3903
+ try {
3904
+ const account = await recoverRemoteTestAccount(target2.id);
3905
+ const name = account.name ?? localNames.get(account.email);
3906
+ upsertTestAccount({
3907
+ id: account.id,
3908
+ email: account.email,
3909
+ password: account.password,
3910
+ userId: account.userId,
3911
+ label: account.label,
3912
+ ...name ? { name } : {},
3913
+ createdAt: account.createdAt
3914
+ });
3915
+ recovered.push({ id: account.id, email: account.email });
3916
+ } catch (err) {
3917
+ failures.push({ email: target2.email, error: err.message });
3918
+ }
3919
+ }
3920
+ if (failures.length > 0) {
3921
+ throw new Refusal(
3922
+ `Recovered ${recovered.length}/${targets.length} test account(s).
3923
+ Failed:
3924
+ ` + failures.map((failure) => ` ${failure.email}: ${failure.error}`).join("\n"),
3925
+ "test_account_recover_failed",
3926
+ { extra: { recovered, requested: targets.length, failures } }
3927
+ );
3928
+ }
3929
+ if (!args.json) {
3930
+ for (const account of recovered) console.log(`Recovered ${account.email}`);
3931
+ console.log(
3932
+ `
3933
+ Stored in ${TEST_ACCOUNTS_PATH}. The previous password (if any machine still holds one) no longer works \u2014 recover there too.`
3934
+ );
3935
+ }
3936
+ return { data: { recovered, requested: targets.length, failures: [] } };
3937
+ }
3938
+ });
3744
3939
  var test_accounts_default = defineCommand2({
3745
3940
  meta: {
3746
3941
  name: "test-accounts",
@@ -3750,14 +3945,15 @@ var test_accounts_default = defineCommand2({
3750
3945
  create,
3751
3946
  list,
3752
3947
  delete: del,
3753
- clear
3948
+ clear,
3949
+ recover
3754
3950
  }
3755
3951
  });
3756
3952
 
3757
3953
  // src/cli/commands/deploy.ts
3758
3954
  import * as p12 from "@clack/prompts";
3759
3955
  import { defineCommand as defineCommand3 } from "citty";
3760
- import { resolve as resolve13 } from "path";
3956
+ import { join as join19, resolve as resolve14 } from "path";
3761
3957
 
3762
3958
  // src/cli/lib/edge-propagation.ts
3763
3959
  import { request as httpRequest } from "http";
@@ -3822,14 +4018,14 @@ async function servesRelease(probe, releaseStamp) {
3822
4018
  }
3823
4019
  function getOnFreshConnection(url, timeoutMs = 5e3) {
3824
4020
  const send = url.protocol === "http:" ? httpRequest : httpsRequest;
3825
- return new Promise((resolve21) => {
4021
+ return new Promise((resolve22) => {
3826
4022
  const request2 = send(
3827
4023
  url,
3828
4024
  { method: "GET", agent: false, timeout: timeoutMs, headers: { "cache-control": "no-cache" } },
3829
4025
  (response) => {
3830
4026
  if (response.statusCode !== 200) {
3831
4027
  response.resume();
3832
- resolve21(null);
4028
+ resolve22(null);
3833
4029
  return;
3834
4030
  }
3835
4031
  let body = "";
@@ -3837,20 +4033,20 @@ function getOnFreshConnection(url, timeoutMs = 5e3) {
3837
4033
  response.on("data", (chunk) => {
3838
4034
  if (body.length < 4096) body += chunk;
3839
4035
  });
3840
- response.on("end", () => resolve21(body));
3841
- response.on("error", () => resolve21(null));
4036
+ response.on("end", () => resolve22(body));
4037
+ response.on("error", () => resolve22(null));
3842
4038
  }
3843
4039
  );
3844
4040
  request2.on("timeout", () => {
3845
4041
  request2.destroy();
3846
- resolve21(null);
4042
+ resolve22(null);
3847
4043
  });
3848
- request2.on("error", () => resolve21(null));
4044
+ request2.on("error", () => resolve22(null));
3849
4045
  request2.end();
3850
4046
  });
3851
4047
  }
3852
4048
  function sleep2(ms) {
3853
- return new Promise((resolve21) => setTimeout(resolve21, ms));
4049
+ return new Promise((resolve22) => setTimeout(resolve22, ms));
3854
4050
  }
3855
4051
 
3856
4052
  // src/shared/app-files.ts
@@ -3915,36 +4111,49 @@ function describeFilesFailure(status3, body) {
3915
4111
  import * as p6 from "@clack/prompts";
3916
4112
  import { execSync as execSync2 } from "child_process";
3917
4113
  import { createHash as createHash2 } from "crypto";
3918
- import { existsSync as existsSync14, lstatSync as lstatSync2, readFileSync as readFileSync17, readdirSync as readdirSync4, unlinkSync as unlinkSync2 } from "fs";
3919
- 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
+ }
3920
4129
 
3921
4130
  // src/documentation/deploy.ts
3922
- import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
3923
- import { join as join13 } from "path";
4131
+ import { existsSync as existsSync13, readFileSync as readFileSync14 } from "fs";
4132
+ import { join as join15 } from "path";
3924
4133
  function readDocumentationDeployManifest(appDir, clientDir) {
3925
- if (!existsSync12(join13(appDir, "documentation.json"))) return void 0;
3926
- const manifestPath = join13(clientDir, "_documentation", "manifest.json");
3927
- 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)) {
3928
4137
  throw new Error(
3929
4138
  "documentation.json exists but the build produced no documentation manifest. Ensure vite.config.ts includes deepSpaceDocumentation() from deepspace/documentation."
3930
4139
  );
3931
4140
  }
3932
- return JSON.parse(readFileSync15(manifestPath, "utf8"));
4141
+ return JSON.parse(readFileSync14(manifestPath, "utf8"));
3933
4142
  }
3934
4143
 
3935
4144
  // src/cli/commands/update/app-migrations.ts
3936
4145
  import {
3937
4146
  closeSync,
3938
4147
  constants,
3939
- existsSync as existsSync13,
4148
+ existsSync as existsSync14,
3940
4149
  fstatSync,
3941
4150
  ftruncateSync,
3942
- lstatSync,
4151
+ lstatSync as lstatSync2,
3943
4152
  openSync,
3944
- readFileSync as readFileSync16,
4153
+ readFileSync as readFileSync15,
3945
4154
  writeSync
3946
4155
  } from "fs";
3947
- import { join as join14 } from "path";
4156
+ import { join as join16 } from "path";
3948
4157
 
3949
4158
  // src/shared/protocol/app-migrations.ts
3950
4159
  var APP_MIGRATION_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/;
@@ -3996,8 +4205,8 @@ var APP_MIGRATION_DEFINITIONS = SOURCE_MIGRATIONS.map(
3996
4205
  ({ id, description }) => ({ id, description })
3997
4206
  );
3998
4207
  function readAppliedAppMigrations(appDir) {
3999
- const path = join14(appDir, APP_MIGRATIONS_MANIFEST);
4000
- if (!existsSync13(path)) return [];
4208
+ const path = join16(appDir, APP_MIGRATIONS_MANIFEST);
4209
+ if (!existsSync14(path)) return [];
4001
4210
  const source = readRegularFile(path);
4002
4211
  let parsed;
4003
4212
  try {
@@ -4023,10 +4232,10 @@ function planAppMigrations(appDir) {
4023
4232
  const edits = [];
4024
4233
  for (const repoPath of tracked) {
4025
4234
  if (!isSourcePath(repoPath, appPrefix)) continue;
4026
- const absolutePath = join14(repo, repoPath);
4235
+ const absolutePath = join16(repo, repoPath);
4027
4236
  let stat;
4028
4237
  try {
4029
- stat = lstatSync(absolutePath);
4238
+ stat = lstatSync2(absolutePath);
4030
4239
  } catch {
4031
4240
  continue;
4032
4241
  }
@@ -4060,8 +4269,8 @@ function planAppMigrations(appDir) {
4060
4269
  });
4061
4270
  if (pending.length > 0 && blockers.length === 0) {
4062
4271
  const manifestPath = `${appPrefix}${APP_MIGRATIONS_MANIFEST}`;
4063
- const absoluteManifestPath = join14(repo, manifestPath);
4064
- const before = existsSync13(absoluteManifestPath) ? readRegularFile(absoluteManifestPath) : null;
4272
+ const absoluteManifestPath = join16(repo, manifestPath);
4273
+ const before = existsSync14(absoluteManifestPath) ? readRegularFile(absoluteManifestPath) : null;
4065
4274
  const after = `${JSON.stringify([...appliedIds, ...pending.map(({ id }) => id)], null, 2)}
4066
4275
  `;
4067
4276
  if (before !== after) edits.push({ path: manifestPath, before, after });
@@ -4076,13 +4285,13 @@ function planAppMigrations(appDir) {
4076
4285
  function applyAppMigrationPlan(appDir, plan) {
4077
4286
  const repo = repoToplevel(appDir);
4078
4287
  for (const edit of plan.edits) {
4079
- const absolutePath = join14(repo, edit.path);
4080
- const current = existsSync13(absolutePath) ? readRegularFile(absolutePath) : null;
4288
+ const absolutePath = join16(repo, edit.path);
4289
+ const current = existsSync14(absolutePath) ? readRegularFile(absolutePath) : null;
4081
4290
  if (current !== edit.before) {
4082
4291
  throw new Error(`Migration input changed after planning: ${edit.path}`);
4083
4292
  }
4084
4293
  }
4085
- 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);
4086
4295
  }
4087
4296
  function normalizeEol(source) {
4088
4297
  return source.replaceAll("\r\n", "\n");
@@ -4091,18 +4300,18 @@ function restoreEol(source, checkout) {
4091
4300
  return checkout.includes("\r\n") ? source.replaceAll("\n", "\r\n") : source;
4092
4301
  }
4093
4302
  function readRegularFile(path) {
4094
- const before = lstatSync(path);
4303
+ const before = lstatSync2(path);
4095
4304
  if (!before.isFile() || before.isSymbolicLink()) {
4096
4305
  throw new Error(`Migration input must be a regular file, not a symlink: ${path}`);
4097
4306
  }
4098
4307
  const fd = openSync(path, constants.O_RDONLY);
4099
4308
  try {
4100
4309
  const opened = fstatSync(fd);
4101
- const after = lstatSync(path);
4310
+ const after = lstatSync2(path);
4102
4311
  if (!opened.isFile() || opened.dev !== after.dev || opened.ino !== after.ino) {
4103
4312
  throw new Error(`Migration input changed while opening: ${path}`);
4104
4313
  }
4105
- return readFileSync16(fd, "utf-8");
4314
+ return readFileSync15(fd, "utf-8");
4106
4315
  } finally {
4107
4316
  closeSync(fd);
4108
4317
  }
@@ -4117,18 +4326,18 @@ function writeRegularFile(path, before, after) {
4117
4326
  }
4118
4327
  return;
4119
4328
  }
4120
- const pathStat = lstatSync(path);
4329
+ const pathStat = lstatSync2(path);
4121
4330
  if (!pathStat.isFile() || pathStat.isSymbolicLink()) {
4122
4331
  throw new Error(`Migration input must be a regular file, not a symlink: ${path}`);
4123
4332
  }
4124
4333
  const fd = openSync(path, constants.O_RDWR);
4125
4334
  try {
4126
4335
  const opened = fstatSync(fd);
4127
- const currentPath = lstatSync(path);
4336
+ const currentPath = lstatSync2(path);
4128
4337
  if (!opened.isFile() || opened.dev !== currentPath.dev || opened.ino !== currentPath.ino) {
4129
4338
  throw new Error(`Migration input changed while opening: ${path}`);
4130
4339
  }
4131
- if (readFileSync16(fd, "utf-8") !== before) {
4340
+ if (readFileSync15(fd, "utf-8") !== before) {
4132
4341
  throw new Error(`Migration input changed after planning: ${path}`);
4133
4342
  }
4134
4343
  ftruncateSync(fd, 0);
@@ -4455,22 +4664,12 @@ function findWorkerOwnedNotFoundBlockers(source, file) {
4455
4664
 
4456
4665
  // src/cli/commands/deploy/build.ts
4457
4666
  var RESERVED_RUN_WORKER_FIRST2 = new Set(RESERVED_RUN_WORKER_FIRST);
4458
- function removeBuildDevVars(workerDir) {
4459
- const path = join15(workerDir, ".dev.vars");
4460
- if (!existsSync14(path)) return false;
4461
- const stat = lstatSync2(path);
4462
- if (!stat.isFile() || stat.isSymbolicLink()) {
4463
- throw new Error(`Refusing unsafe build secret path: ${path}`);
4464
- }
4465
- unlinkSync2(path);
4466
- return true;
4467
- }
4468
4667
  function declaredCompatibilityDate(config) {
4469
4668
  const declared = config.compatibility_date;
4470
4669
  return typeof declared === "string" && /^\d{4}-\d{2}-\d{2}$/.test(declared) ? declared : null;
4471
4670
  }
4472
4671
  async function buildDeployBundle(options) {
4473
- const { appDir, appName, envName, output, spinner: spinner2 } = options;
4672
+ const { appDir, appName, appId, envName, output, spinner: spinner2 } = options;
4474
4673
  const junk = removeMacosJunk(appDir);
4475
4674
  if (junk > 0) p6.log.info(`Removed ${junk} macOS metadata file(s) (._*, .DS_Store)`);
4476
4675
  const lintFindings = await lintProjectSchemas(appDir);
@@ -4501,19 +4700,19 @@ async function buildDeployBundle(options) {
4501
4700
  preparedWranglerConfig?.cleanup();
4502
4701
  }
4503
4702
  spinner2.stop(`Built (${Math.round((Date.now() - buildStarted) / 1e3)}s)`);
4504
- const deployConfigPath = join15(appDir, ".wrangler", "deploy", "config.json");
4505
- if (!existsSync14(deployConfigPath)) {
4703
+ const deployConfigPath = join17(appDir, ".wrangler", "deploy", "config.json");
4704
+ if (!existsSync15(deployConfigPath)) {
4506
4705
  output.die(
4507
4706
  "Build output config not found at .wrangler/deploy/config.json",
4508
4707
  "build_output_missing"
4509
4708
  );
4510
4709
  }
4511
- const deployConfig = JSON.parse(readFileSync17(deployConfigPath, "utf-8"));
4512
- const outputWranglerPath = resolve11(dirname7(deployConfigPath), deployConfig.configPath);
4513
- if (!existsSync14(outputWranglerPath)) {
4710
+ const deployConfig = JSON.parse(readFileSync16(deployConfigPath, "utf-8"));
4711
+ const outputWranglerPath = resolve12(dirname7(deployConfigPath), deployConfig.configPath);
4712
+ if (!existsSync15(outputWranglerPath)) {
4514
4713
  output.die(`Output wrangler.json not found at ${outputWranglerPath}`, "build_output_missing");
4515
4714
  }
4516
- const outputConfig = JSON.parse(readFileSync17(outputWranglerPath, "utf-8"));
4715
+ const outputConfig = JSON.parse(readFileSync16(outputWranglerPath, "utf-8"));
4517
4716
  if (outputConfig.name && outputConfig.name !== appName) {
4518
4717
  const envHint = envName ? ` (--env ${envName})` : "";
4519
4718
  output.die(
@@ -4527,12 +4726,12 @@ async function buildDeployBundle(options) {
4527
4726
  } catch (error) {
4528
4727
  output.die(errorMessage(error), "build_output_unsafe");
4529
4728
  }
4530
- const workerBundlePath = join15(workerDir, outputConfig.main);
4531
- const clientDir = outputConfig.assets?.directory ? resolve11(workerDir, outputConfig.assets.directory) : null;
4532
- 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)) {
4533
4732
  output.die(`Worker bundle not found at ${workerBundlePath}`, "build_output_missing");
4534
4733
  }
4535
- if (!clientDir || !existsSync14(clientDir)) {
4734
+ if (!clientDir || !existsSync15(clientDir)) {
4536
4735
  return output.die(`Client assets not found at ${clientDir}`, "build_output_missing");
4537
4736
  }
4538
4737
  try {
@@ -4545,7 +4744,9 @@ async function buildDeployBundle(options) {
4545
4744
  const assets = collectAssets(clientDir);
4546
4745
  const assetConfig = readDeployAssetConfig(clientDir);
4547
4746
  spinner2.stop(`Collected ${assets.length} assets`);
4548
- const workerJs = readFileSync17(workerBundlePath, "utf-8");
4747
+ const appIdRefusal = clientAppIdRefusal(assets, appId, envName);
4748
+ if (appIdRefusal) output.die(appIdRefusal.message, appIdRefusal.code);
4749
+ const workerJs = readFileSync16(workerBundlePath, "utf-8");
4549
4750
  const doBindings = outputConfig.durable_objects?.bindings;
4550
4751
  const sqliteClasses = new Set(
4551
4752
  outputConfig.migrations?.flatMap((migration) => migration.new_sqlite_classes ?? []) ?? []
@@ -4613,18 +4814,56 @@ function oversizedAssetRefusal(assets) {
4613
4814
  if (!tooBig) return null;
4614
4815
  return `${tooBig.path} is ${formatBytes(tooBig.size)}; the per-file limit for deploy assets is ${formatBytes(MAX_DEPLOY_ASSET_FILE_BYTES)}. ${LARGE_MEDIA_HOME}`;
4615
4816
  }
4817
+ var CLIENT_CODE_ASSET = /\.(?:js|mjs|html)$/i;
4818
+ var APP_ID_LITERAL = /app_[0-9A-HJKMNP-TV-Z]{26}/g;
4819
+ var APP_ID_DEFINE = "__DEEPSPACE_APP_ID__";
4820
+ var APP_ID_ADOPTION_STEPS = `Point the client at the build-injected id \u2014 the SDK's \`deepspace/build\` supplies it:
4821
+ \u2022 vite.config.ts: import { deepspaceBuild } from 'deepspace/build' and add
4822
+ deepspaceBuild({ appDir: fileURLToPath(new URL('.', import.meta.url)) }) to plugins
4823
+ \u2022 vitest.config.ts: import { appIdDefine } and set define: appIdDefine({ appDir })
4824
+ \u2022 src/constants.ts: declare const __DEEPSPACE_APP_ID__: string; export it as APP_ID
4825
+ (drop the frozen literal)
4826
+ Apply all three together: src/constants.ts on its own builds clean and then throws in the browser. A newly scaffolded app already has this.`;
4827
+ function clientAppIdRefusal(assets, appId, envName) {
4828
+ const foreign = /* @__PURE__ */ new Map();
4829
+ let unsubstituted;
4830
+ for (const asset of assets) {
4831
+ if (!CLIENT_CODE_ASSET.test(asset.path)) continue;
4832
+ const source = readFileSync16(asset.sourcePath, "utf-8");
4833
+ if (unsubstituted === void 0 && source.includes(APP_ID_DEFINE)) unsubstituted = asset.path;
4834
+ for (const found of source.matchAll(APP_ID_LITERAL)) {
4835
+ if (found[0] !== appId && !foreign.has(found[0])) foreign.set(found[0], asset.path);
4836
+ }
4837
+ }
4838
+ if (unsubstituted !== void 0) {
4839
+ return {
4840
+ code: "app_id_define_unsubstituted",
4841
+ message: `Built client assets still contain \`${APP_ID_DEFINE}\` (first in ${unsubstituted}) \u2014 nothing replaced it at build time. The browser evaluates that identifier and throws ReferenceError before the app renders, so this bundle is a blank page for every visitor. src/constants.ts was moved onto the build-time app id without the build config that supplies it.
4842
+ ${APP_ID_ADOPTION_STEPS}`
4843
+ };
4844
+ }
4845
+ if (foreign.size === 0) return null;
4846
+ const target2 = envName ? `--env ${envName} (${appId})` : appId;
4847
+ return {
4848
+ code: "app_id_env_mismatch",
4849
+ message: `Built client assets carry a different app id than the one being deployed. Deploying ${target2}, but the bundle contains:
4850
+ ` + [...foreign].map(([id, path]) => ` \u2022 ${id} in ${path}`).join("\n") + `
4851
+ The browser keys its RecordRooms to the id in the bundle, so this would serve ${envName ? `${envName}'s` : "this app's"} worker against another app's data. The usual cause is an app scaffolded before the id was build-injected: src/constants.ts still holds \`export const APP_ID = 'app_\u2026'\` frozen at scaffold time.
4852
+ ${APP_ID_ADOPTION_STEPS}`
4853
+ };
4854
+ }
4616
4855
  var LARGE_MEDIA_HOME = "Large media belongs in your app files (`deepspace app files put`), not in the deploy bundle \u2014 move it out of `public/` and reference it at runtime.";
4617
4856
  function collectAssets(dir) {
4618
4857
  const assets = [];
4619
4858
  const walk = (currentDir, prefix) => {
4620
- for (const entry of readdirSync4(currentDir, { withFileTypes: true })) {
4859
+ for (const entry of readdirSync6(currentDir, { withFileTypes: true })) {
4621
4860
  if (isDeployAssetControlFile(entry.name)) continue;
4622
- const fullPath = join15(currentDir, entry.name);
4861
+ const fullPath = join17(currentDir, entry.name);
4623
4862
  const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
4624
4863
  if (entry.isDirectory()) {
4625
4864
  walk(fullPath, relativePath);
4626
4865
  } else {
4627
- const bytes = readFileSync17(fullPath);
4866
+ const bytes = readFileSync16(fullPath);
4628
4867
  assets.push({
4629
4868
  path: `/${relativePath}`,
4630
4869
  hash: createHash2("sha256").update(bytes).digest("hex"),
@@ -4643,9 +4882,9 @@ function readDeployAssetConfig(clientDir) {
4643
4882
  ["_headers", "_headers"],
4644
4883
  ["_redirects", "_redirects"]
4645
4884
  ]) {
4646
- const filePath = join15(clientDir, file);
4647
- if (!existsSync14(filePath)) continue;
4648
- const content = readFileSync17(filePath, "utf8");
4885
+ const filePath = join17(clientDir, file);
4886
+ if (!existsSync15(filePath)) continue;
4887
+ const content = readFileSync16(filePath, "utf8");
4649
4888
  if (content.trim()) config[key] = content;
4650
4889
  }
4651
4890
  return config;
@@ -4659,8 +4898,8 @@ function errorMessage(error) {
4659
4898
 
4660
4899
  // src/cli/commands/deploy/commerce.ts
4661
4900
  import * as p7 from "@clack/prompts";
4662
- import { existsSync as existsSync15 } from "fs";
4663
- import { join as join16 } from "path";
4901
+ import { existsSync as existsSync16 } from "fs";
4902
+ import { join as join18 } from "path";
4664
4903
  import { pathToFileURL as pathToFileURL2 } from "url";
4665
4904
 
4666
4905
  // src/shared/api-error.ts
@@ -4712,10 +4951,10 @@ function errorMessage2(error) {
4712
4951
  return error instanceof Error ? error.message : String(error);
4713
4952
  }
4714
4953
  async function loadDeclaration(appDir, sourceFile, outputFile, exportName) {
4715
- const sourcePath = join16(appDir, "src", sourceFile);
4716
- if (!existsSync15(sourcePath)) return void 0;
4954
+ const sourcePath = join18(appDir, "src", sourceFile);
4955
+ if (!existsSync16(sourcePath)) return void 0;
4717
4956
  const esbuild = await import("esbuild");
4718
- const outputPath = join16(appDir, ".wrangler", "deploy", outputFile);
4957
+ const outputPath = join18(appDir, ".wrangler", "deploy", outputFile);
4719
4958
  await esbuild.build({
4720
4959
  entryPoints: [sourcePath],
4721
4960
  bundle: true,
@@ -4808,7 +5047,7 @@ Stripe will keep charging them until you cancel. Cancel at the end of their curr
4808
5047
  }
4809
5048
  }
4810
5049
  async function syncSubscriptionPlans(appDir, appName, token, nonInteractive) {
4811
- if (!existsSync15(join16(appDir, "src", "subscriptions.ts"))) return;
5050
+ if (!existsSync16(join18(appDir, "src", "subscriptions.ts"))) return;
4812
5051
  let plans;
4813
5052
  try {
4814
5053
  plans = await loadDeclaration(
@@ -4863,7 +5102,7 @@ async function syncSubscriptionPlans(appDir, appName, token, nonInteractive) {
4863
5102
  }
4864
5103
  }
4865
5104
  async function syncOneTimeProducts(appDir, appName, token) {
4866
- if (!existsSync15(join16(appDir, "src", "products.ts"))) return;
5105
+ if (!existsSync16(join18(appDir, "src", "products.ts"))) return;
4867
5106
  let products;
4868
5107
  try {
4869
5108
  products = await loadDeclaration(
@@ -4965,7 +5204,7 @@ function createDeployOutput(json) {
4965
5204
 
4966
5205
  // src/cli/commands/deploy/request.ts
4967
5206
  import * as p10 from "@clack/prompts";
4968
- import { readFileSync as readFileSync18 } from "fs";
5207
+ import { readFileSync as readFileSync17 } from "fs";
4969
5208
 
4970
5209
  // src/cli/commands/deploy/repository.ts
4971
5210
  import * as p9 from "@clack/prompts";
@@ -5156,9 +5395,9 @@ function repoApi(deployUrl, token, appId) {
5156
5395
  }
5157
5396
 
5158
5397
  // src/cli/lib/vc-remote.ts
5159
- import { existsSync as existsSync16 } from "fs";
5398
+ import { existsSync as existsSync17 } from "fs";
5160
5399
  import { tmpdir } from "os";
5161
- import { isAbsolute as isAbsolute3, relative, resolve as resolve12 } from "path";
5400
+ import { isAbsolute as isAbsolute3, relative as relative2, resolve as resolve13 } from "path";
5162
5401
  function spaceRemoteName(environment = DEEPSPACE_ENV) {
5163
5402
  if (environment === "staging") return "space-staging";
5164
5403
  if (environment === "production") return "space";
@@ -5204,14 +5443,14 @@ function entryIsTransient(cwd) {
5204
5443
  const entry = process.argv[1];
5205
5444
  if (!entry) return true;
5206
5445
  if (/[\\/]_npx[\\/]/.test(entry) || entry.startsWith(tmpdir())) return true;
5207
- const rel = relative(resolve12(cwd), resolve12(entry));
5446
+ const rel = relative2(resolve13(cwd), resolve13(entry));
5208
5447
  return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel);
5209
5448
  }
5210
5449
  function helperEntryUsable(command) {
5211
5450
  const match = /^!(?:DEEPSPACE_ENV=(production|staging) )?'([^']*)' '([^']*)' git-credential$/.exec(command);
5212
5451
  const expectedEnvironment = DEEPSPACE_ENV === "production" ? void 0 : DEEPSPACE_ENV;
5213
5452
  if (!match || match[1] !== expectedEnvironment) return false;
5214
- return existsSync16(match[2]) && existsSync16(match[3]);
5453
+ return existsSync17(match[2]) && existsSync17(match[3]);
5215
5454
  }
5216
5455
  function credentialHelperCommand(environment = DEEPSPACE_ENV) {
5217
5456
  const entry = process.argv[1];
@@ -5346,12 +5585,6 @@ function classifyPushTransportFailure(error, cwd) {
5346
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.`
5347
5586
  };
5348
5587
  }
5349
- if (/(?:HTTP |error: )422\b/i.test(message)) {
5350
- return {
5351
- code: "source_managed_by_github",
5352
- error: "This app uses GitHub source. Manage commits with normal Git/GitHub; `deepspace deploy` ships the local working tree without changing Git."
5353
- };
5354
- }
5355
5588
  if (/(?:HTTP |error: )429\b/i.test(message)) {
5356
5589
  return {
5357
5590
  code: "rate_limited",
@@ -5361,6 +5594,12 @@ function classifyPushTransportFailure(error, cwd) {
5361
5594
  if (/(?:HTTP |error: )413\b/i.test(message)) {
5362
5595
  return { code: "push_too_large", error: pushTooLargeMessage(cwd) };
5363
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
+ }
5364
5603
  return null;
5365
5604
  }
5366
5605
  function pushToSpace(cwd, token, refspec, opts = {}) {
@@ -5732,7 +5971,7 @@ async function pushWithTransientRetry(push) {
5732
5971
  const message = errorMessage3(error);
5733
5972
  const transient = /(?:HTTP |error: )(?:429|503)\b/i.test(message);
5734
5973
  if (transient && attempt < backoffMs.length && Date.now() + backoffMs[attempt] < deadline) {
5735
- await new Promise((resolve21) => setTimeout(resolve21, backoffMs[attempt]));
5974
+ await new Promise((resolve22) => setTimeout(resolve22, backoffMs[attempt]));
5736
5975
  continue;
5737
5976
  }
5738
5977
  throw error;
@@ -6016,7 +6255,7 @@ async function uploadDeployAssets(options) {
6016
6255
  // against the plan before it touches storage, and a stream body
6017
6256
  // would be sent chunked with no length at all; a retry also needs a
6018
6257
  // body it can send again.
6019
- body: readFileSync18(asset.sourcePath)
6258
+ body: readFileSync17(asset.sourcePath)
6020
6259
  })
6021
6260
  );
6022
6261
  if (!response.ok) {
@@ -6097,7 +6336,7 @@ async function postWithRetry(url, makeInit, {
6097
6336
  lastError = error;
6098
6337
  }
6099
6338
  if (attempt < attempts) {
6100
- 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))));
6101
6340
  }
6102
6341
  }
6103
6342
  if (lastResponse) return lastResponse;
@@ -6233,12 +6472,12 @@ var deploy_default = defineCommand3({
6233
6472
  async run({ args }) {
6234
6473
  const output = createDeployOutput(args.json === true);
6235
6474
  preflightNodeVersion("deploy");
6236
- const appDir = resolve13(args.dir ?? ".");
6475
+ const appDir = resolve14(args.dir ?? ".");
6237
6476
  const selectorRefusal = blankSelectorRefusal(args);
6238
6477
  if (selectorRefusal) output.die(selectorRefusal.error, selectorRefusal.code);
6239
6478
  const envName = typeof args.env === "string" && args.env.trim() ? args.env.trim() : void 0;
6240
6479
  if (!hasWranglerConfig(appDir)) {
6241
- 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");
6242
6481
  }
6243
6482
  try {
6244
6483
  ensureInstallReady(appDir);
@@ -6252,7 +6491,7 @@ var deploy_default = defineCommand3({
6252
6491
  output.showIntro();
6253
6492
  const wranglerConfig = readWranglerConfig(appDir);
6254
6493
  const nameResult = resolveAppNameForEnv(wranglerConfig, envName);
6255
- if (!nameResult.ok) return output.die(nameResult.reason, "invalid_app_name");
6494
+ if (!nameResult.ok) return output.die(nameResult.reason, nameResult.code ?? "invalid_app_name");
6256
6495
  const declaredName = envName ? wranglerConfig.env?.[envName]?.name : wranglerConfig.name;
6257
6496
  if (declaredName !== nameResult.name) {
6258
6497
  const location = envName ? `[env.${envName}].name` : "`name`";
@@ -6279,6 +6518,13 @@ var deploy_default = defineCommand3({
6279
6518
  );
6280
6519
  }
6281
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
+ }
6282
6528
  const secretsCache = await loadDeploySecrets({
6283
6529
  deployUrl: DEPLOY_URL3,
6284
6530
  appDir,
@@ -6288,12 +6534,17 @@ var deploy_default = defineCommand3({
6288
6534
  token,
6289
6535
  output
6290
6536
  });
6291
- const sourceState = await getAppSource(DEPLOY_URL3, token, appId);
6292
- if (!sourceState.registered) {
6293
- output.die(
6294
- `${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\`.`,
6295
- "app_not_registered"
6296
- );
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;
6297
6548
  }
6298
6549
  const repositoryPreflight = preflightDeployRepository({
6299
6550
  appDir,
@@ -6307,6 +6558,7 @@ var deploy_default = defineCommand3({
6307
6558
  const bundle = await buildDeployBundle({
6308
6559
  appDir,
6309
6560
  appName,
6561
+ appId,
6310
6562
  envName,
6311
6563
  output,
6312
6564
  spinner: spinner2
@@ -6338,7 +6590,7 @@ var deploy_default = defineCommand3({
6338
6590
  appId,
6339
6591
  appName,
6340
6592
  token,
6341
- rename: args.rename === true,
6593
+ rename: confirmRename,
6342
6594
  claimReleased: args["claim-released"] === true,
6343
6595
  ignoreStale: Boolean(args["ignore-stale"]),
6344
6596
  bundle,
@@ -6405,6 +6657,25 @@ async function syncCommerce(appDir, appId, token, nonInteractive) {
6405
6657
  await syncSubscriptionPlans(appDir, appId, token, nonInteractive);
6406
6658
  await syncOneTimeProducts(appDir, appId, token);
6407
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
+ }
6408
6679
  function blankSelectorRefusal(args) {
6409
6680
  if (args.env !== void 0 && !String(args.env).trim()) {
6410
6681
  return {
@@ -6465,15 +6736,18 @@ function requireAppIdArg(explicit) {
6465
6736
  function liveAppUrl(app2) {
6466
6737
  return app2.status === "active" && app2.deployedAt !== null ? app2.url : null;
6467
6738
  }
6468
- async function listApps(deployUrl, token) {
6469
- const { apps } = await apiFetch(deployUrl, token, "/api/apps");
6470
- 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)) {
6471
6742
  throw new InputError(
6472
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?).`,
6473
6744
  "invalid_response"
6474
6745
  );
6475
6746
  }
6476
- return apps;
6747
+ return { apps: body.apps, pendingTransfers: body.pendingTransfers ?? [] };
6748
+ }
6749
+ async function listApps(deployUrl, token) {
6750
+ return (await listAppsPage(deployUrl, token)).apps;
6477
6751
  }
6478
6752
  function matchAppSelector(apps, selector) {
6479
6753
  const byId = apps.find((app2) => app2.appId === selector);
@@ -6715,17 +6989,17 @@ var apps_default = defineDeepspaceCommand({
6715
6989
  },
6716
6990
  async run({ args }) {
6717
6991
  const token = await ensureToken();
6718
- const apps = await listApps(DEPLOY_URL5, token);
6992
+ const { apps, pendingTransfers } = await listAppsPage(DEPLOY_URL5, token);
6719
6993
  if (!args.json) {
6720
- if (!apps.length) {
6994
+ if (!apps.length && !pendingTransfers.length) {
6721
6995
  console.log("No apps yet. Create one with `npx create-deepspace <name>` and `deepspace deploy`.");
6722
- } else {
6996
+ } else if (apps.length) {
6723
6997
  const rows = [...apps].sort(
6724
6998
  (left, right) => Number(right.status !== "undeployed") - Number(left.status !== "undeployed")
6725
6999
  );
6726
7000
  const roleOf = (a) => a.role ?? "owner";
6727
7001
  const roleWidth = Math.max(4, ...rows.map((a) => roleOf(a).length));
6728
- const nameWidth = Math.max(4, ...rows.map((a) => (a.name ?? "\u2014").length));
7002
+ const nameWidth = Math.max(4, ...rows.map((a) => nameCell(a).length));
6729
7003
  const statusWidth = Math.max(6, ...rows.map((a) => a.status.length));
6730
7004
  const idWidth = Math.max(6, ...rows.map((a) => a.appId.length));
6731
7005
  console.log(
@@ -6734,14 +7008,36 @@ var apps_default = defineDeepspaceCommand({
6734
7008
  for (const a of rows) {
6735
7009
  const url = liveAppUrl(a) ?? "(not deployed)";
6736
7010
  console.log(
6737
- `${(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}`
6738
7012
  );
6739
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
+ );
6740
7032
  }
6741
7033
  }
6742
- return { data: { apps } };
7034
+ return { data: { apps, pendingTransfers } };
6743
7035
  }
6744
7036
  });
7037
+ function nameCell(app2) {
7038
+ if (app2.name) return app2.name;
7039
+ return app2.reservedName ? `${app2.reservedName} (reserved)` : "\u2014";
7040
+ }
6745
7041
 
6746
7042
  // src/cli/commands/logs.ts
6747
7043
  import { defineCommand as defineCommand4 } from "citty";
@@ -7125,37 +7421,37 @@ var usage_default = defineDeepspaceCommand({
7125
7421
  });
7126
7422
 
7127
7423
  // src/cli/commands/add.ts
7128
- import { existsSync as existsSync18, readFileSync as readFileSync20, realpathSync as realpathSync4 } from "fs";
7129
- 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";
7130
7426
  import { pathToFileURL as pathToFileURL3 } from "url";
7131
7427
 
7132
7428
  // src/cli/commands/feature-installer.ts
7133
7429
  import {
7134
7430
  closeSync as closeSync2,
7135
7431
  copyFileSync,
7136
- existsSync as existsSync17,
7432
+ existsSync as existsSync18,
7137
7433
  fstatSync as fstatSync2,
7138
7434
  mkdirSync as mkdirSync6,
7139
7435
  mkdtempSync,
7140
7436
  openSync as openSync2,
7141
- readFileSync as readFileSync19,
7437
+ readFileSync as readFileSync18,
7142
7438
  readSync,
7143
- readdirSync as readdirSync5,
7439
+ readdirSync as readdirSync7,
7144
7440
  rmSync as rmSync4,
7145
- writeFileSync as writeFileSync6
7441
+ writeFileSync as writeFileSync5
7146
7442
  } from "fs";
7147
7443
  import { spawnSync as spawnSync6 } from "child_process";
7148
7444
  import { tmpdir as tmpdir2 } from "os";
7149
- 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";
7150
7446
  import { fileURLToPath } from "url";
7151
7447
  var NAV_MARKER = "// \u2500\u2500 Features add nav items below this line \u2500\u2500";
7152
7448
  function featurePackageRoot(moduleUrl = import.meta.url) {
7153
7449
  let dir = dirname8(fileURLToPath(moduleUrl));
7154
7450
  for (; ; ) {
7155
- const packagePath = join17(dir, "package.json");
7156
- if (existsSync17(packagePath)) {
7451
+ const packagePath = join20(dir, "package.json");
7452
+ if (existsSync18(packagePath)) {
7157
7453
  try {
7158
- const pkg2 = JSON.parse(readFileSync19(packagePath, "utf-8"));
7454
+ const pkg2 = JSON.parse(readFileSync18(packagePath, "utf-8"));
7159
7455
  if (pkg2.name === "deepspace") return dir;
7160
7456
  } catch {
7161
7457
  }
@@ -7166,13 +7462,13 @@ function featurePackageRoot(moduleUrl = import.meta.url) {
7166
7462
  }
7167
7463
  }
7168
7464
  function hasFeatureCatalog(packageRoot = featurePackageRoot()) {
7169
- return existsSync17(join17(packageRoot, "features"));
7465
+ return existsSync18(join20(packageRoot, "features"));
7170
7466
  }
7171
7467
  function readFeatureConfig(id, packageRoot = featurePackageRoot()) {
7172
- const configPath = join17(packageRoot, "features", id, "feature.json");
7173
- if (!existsSync17(configPath)) return null;
7468
+ const configPath = join20(packageRoot, "features", id, "feature.json");
7469
+ if (!existsSync18(configPath)) return null;
7174
7470
  try {
7175
- const config = JSON.parse(readFileSync19(configPath, "utf-8"));
7471
+ const config = JSON.parse(readFileSync18(configPath, "utf-8"));
7176
7472
  if (typeof config.id !== "string" || typeof config.name !== "string" || !Array.isArray(config.files)) {
7177
7473
  return null;
7178
7474
  }
@@ -7182,9 +7478,9 @@ function readFeatureConfig(id, packageRoot = featurePackageRoot()) {
7182
7478
  }
7183
7479
  }
7184
7480
  function listFeatureConfigs(packageRoot = featurePackageRoot()) {
7185
- const featuresDir = join17(packageRoot, "features");
7186
- if (!existsSync17(featuresDir)) return [];
7187
- 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));
7188
7484
  }
7189
7485
  function featureCatalog(packageRoot = featurePackageRoot()) {
7190
7486
  return listFeatureConfigs(packageRoot).map((config) => ({
@@ -7221,7 +7517,7 @@ function suggestFeature(featureId, packageRoot = featurePackageRoot()) {
7221
7517
  return closest !== null && closestDistance <= threshold ? closest : null;
7222
7518
  }
7223
7519
  function resolveTargetDir(raw) {
7224
- return isAbsolute4(raw) ? raw : resolve14(raw);
7520
+ return isAbsolute4(raw) ? raw : resolve15(raw);
7225
7521
  }
7226
7522
  function resolveFeatureDestPath(fileDest, route) {
7227
7523
  const pages = "src/pages/";
@@ -7237,11 +7533,11 @@ function copyFile(src, dest) {
7237
7533
  function copyFileWithImportShift(src, dest, levels) {
7238
7534
  mkdirSync6(dirname8(dest), { recursive: true });
7239
7535
  const prefix = "../".repeat(levels);
7240
- const shifted = readFileSync19(src, "utf-8").replace(
7536
+ const shifted = readFileSync18(src, "utf-8").replace(
7241
7537
  /(from\s+|import\s+|import\()(['"])(\.\.\/)/g,
7242
7538
  `$1$2${prefix}$3`
7243
7539
  );
7244
- writeFileSync6(dest, shifted);
7540
+ writeFileSync5(dest, shifted);
7245
7541
  }
7246
7542
  function installFiles(packageRoot, config, targetDir, emit) {
7247
7543
  let copied = 0;
@@ -7250,12 +7546,12 @@ function installFiles(packageRoot, config, targetDir, emit) {
7250
7546
  const preexisting = [];
7251
7547
  const missing = [];
7252
7548
  for (const file of config.files) {
7253
- const srcPath = join17(packageRoot, "features", config.id, file.src);
7549
+ const srcPath = join20(packageRoot, "features", config.id, file.src);
7254
7550
  const resolvedDest = resolveFeatureDestPath(file.dest, config.route);
7255
- const destPath = join17(targetDir, resolvedDest);
7256
- const existed = existsSync17(destPath);
7551
+ const destPath = join20(targetDir, resolvedDest);
7552
+ const existed = existsSync18(destPath);
7257
7553
  if (existed) preexisting.push(resolvedDest);
7258
- if (!existsSync17(srcPath)) {
7554
+ if (!existsSync18(srcPath)) {
7259
7555
  emit(` Warning: source not found: ${file.src}`, "stderr");
7260
7556
  if (!existed) missing.push(resolvedDest);
7261
7557
  continue;
@@ -7292,11 +7588,11 @@ function unresolvedSchema(schema, reason, emit) {
7292
7588
  }
7293
7589
  function integrateSchema(config, targetDir, emit) {
7294
7590
  if (!config.schema) return [];
7295
- const schemasPath = join17(targetDir, "src", "schemas.ts");
7296
- if (!existsSync17(schemasPath)) {
7591
+ const schemasPath = join20(targetDir, "src", "schemas.ts");
7592
+ if (!existsSync18(schemasPath)) {
7297
7593
  return [unresolvedSchema(config.schema, "src/schemas.ts was not found.", emit)];
7298
7594
  }
7299
- let content = readFileSync19(schemasPath, "utf-8");
7595
+ let content = readFileSync18(schemasPath, "utf-8");
7300
7596
  const { exportName, importPath, spreadOperator } = config.schema;
7301
7597
  const importPattern = new RegExp(`^import\\s+\\{[^}]*\\b${exportName}\\b`, "m");
7302
7598
  if (importPattern.test(content)) {
@@ -7327,7 +7623,7 @@ export const schemas`
7327
7623
  return [unresolvedSchema(config.schema, reason, emit)];
7328
7624
  }
7329
7625
  content = withEntry;
7330
- writeFileSync6(schemasPath, content);
7626
+ writeFileSync5(schemasPath, content);
7331
7627
  emit(` Schema integrated: ${exportName} -> schemas.ts`);
7332
7628
  return [];
7333
7629
  }
@@ -7349,11 +7645,11 @@ function unresolvedActions(actions, reason, emit) {
7349
7645
  }
7350
7646
  function integrateActions(config, targetDir, emit) {
7351
7647
  if (!config.actions?.length) return [];
7352
- const actionsPath = join17(targetDir, "src", "actions", "index.ts");
7353
- if (!existsSync17(actionsPath)) {
7648
+ const actionsPath = join20(targetDir, "src", "actions", "index.ts");
7649
+ if (!existsSync18(actionsPath)) {
7354
7650
  return [unresolvedActions(config.actions, "src/actions/index.ts was not found.", emit)];
7355
7651
  }
7356
- let content = readFileSync19(actionsPath, "utf-8");
7652
+ let content = readFileSync18(actionsPath, "utf-8");
7357
7653
  const registryPattern = /export const actions:\s*Record<string,\s*ActionHandler(?:<[^>]*>)?>\s*=\s*\{/;
7358
7654
  if (!registryPattern.test(content)) {
7359
7655
  const reason = "src/actions/index.ts has an unexpected structure.";
@@ -7393,14 +7689,14 @@ ${registryEntry}`);
7393
7689
  integrated++;
7394
7690
  emit(` Action integrated: ${action.registerAs} (${action.exportName})`);
7395
7691
  }
7396
- if (integrated > 0) writeFileSync6(actionsPath, content);
7692
+ if (integrated > 0) writeFileSync5(actionsPath, content);
7397
7693
  return unresolved;
7398
7694
  }
7399
7695
  function integrateCode(config, targetDir, emit) {
7400
7696
  const unresolved = [];
7401
7697
  for (const insertion of config.code ?? []) {
7402
- const targetPath = join17(targetDir, insertion.file);
7403
- if (!existsSync17(targetPath)) {
7698
+ const targetPath = join20(targetDir, insertion.file);
7699
+ if (!existsSync18(targetPath)) {
7404
7700
  unresolved.push({
7405
7701
  area: "code",
7406
7702
  reason: `${insertion.file} was not found.`,
@@ -7408,7 +7704,7 @@ function integrateCode(config, targetDir, emit) {
7408
7704
  });
7409
7705
  continue;
7410
7706
  }
7411
- const original = readFileSync19(targetPath, "utf-8");
7707
+ const original = readFileSync18(targetPath, "utf-8");
7412
7708
  if (original.includes(insertion.insert.trim())) {
7413
7709
  emit(` Integration already present: ${insertion.file}`);
7414
7710
  continue;
@@ -7438,35 +7734,35 @@ ${content.slice(
7438
7734
  const prefix = insertion.placement === "before" ? "" : "\n";
7439
7735
  const suffix = insertion.placement === "before" ? "\n" : "";
7440
7736
  content = `${content.slice(0, insertAt)}${prefix}${insertion.insert}${suffix}${content.slice(insertAt)}`;
7441
- writeFileSync6(targetPath, content);
7737
+ writeFileSync5(targetPath, content);
7442
7738
  emit(` Integrated: ${insertion.file}`);
7443
7739
  }
7444
7740
  return unresolved;
7445
7741
  }
7446
7742
  function integrateIgnore(config, targetDir) {
7447
7743
  if (!config.ignore?.length) return;
7448
- const targetPath = join17(targetDir, ".gitignore");
7449
- const current = existsSync17(targetPath) ? readFileSync19(targetPath, "utf-8") : "";
7744
+ const targetPath = join20(targetDir, ".gitignore");
7745
+ const current = existsSync18(targetPath) ? readFileSync18(targetPath, "utf-8") : "";
7450
7746
  const present = new Set(current.split(/\r?\n/).map((line) => line.trim()));
7451
7747
  const missing = config.ignore.filter((pattern) => !present.has(pattern));
7452
7748
  if (missing.length === 0) return;
7453
7749
  const separator = current === "" || current.endsWith("\n") ? "" : "\n";
7454
- writeFileSync6(targetPath, `${current}${separator}${missing.join("\n")}
7750
+ writeFileSync5(targetPath, `${current}${separator}${missing.join("\n")}
7455
7751
  `);
7456
7752
  }
7457
7753
  function integrateWrangler(config, targetDir, emit) {
7458
7754
  const runWorkerFirst = config.wrangler?.runWorkerFirst === true;
7459
7755
  const rateLimits = config.wrangler?.rateLimits ?? [];
7460
7756
  if (!runWorkerFirst && rateLimits.length === 0) return [];
7461
- const targetPath = join17(targetDir, "wrangler.toml");
7757
+ const targetPath = join20(targetDir, "wrangler.toml");
7462
7758
  const steps = [
7463
7759
  ...runWorkerFirst ? ["Set assets.run_worker_first = true in wrangler.toml."] : [],
7464
7760
  ...rateLimits.map((binding) => `Add the ${binding.name} rate-limit binding to wrangler.toml.`)
7465
7761
  ];
7466
- if (!existsSync17(targetPath)) {
7762
+ if (!existsSync18(targetPath)) {
7467
7763
  return [{ area: "wrangler", reason: "wrangler.toml was not found.", steps }];
7468
7764
  }
7469
- let source = readFileSync19(targetPath, "utf-8");
7765
+ let source = readFileSync18(targetPath, "utf-8");
7470
7766
  if (runWorkerFirst && !/^\[assets\]\s*$/m.test(source)) {
7471
7767
  return [{ area: "wrangler", reason: "wrangler.toml has no [assets] section.", steps }];
7472
7768
  }
@@ -7488,7 +7784,7 @@ ${rateLimitBindingBlock(scope, binding)}
7488
7784
  }
7489
7785
  }
7490
7786
  if (changed) {
7491
- writeFileSync6(targetPath, source);
7787
+ writeFileSync5(targetPath, source);
7492
7788
  emit(" Integrated: wrangler.toml");
7493
7789
  } else emit(" Integration already present: wrangler.toml");
7494
7790
  return [];
@@ -7542,8 +7838,8 @@ function rateLimitBindingBlock(scope, binding) {
7542
7838
  }
7543
7839
  function integrateCss(packageRoot, config, targetDir, emit) {
7544
7840
  if (!config.css?.length) return [];
7545
- const stylesPath = join17(targetDir, "src", "styles.css");
7546
- if (!existsSync17(stylesPath)) {
7841
+ const stylesPath = join20(targetDir, "src", "styles.css");
7842
+ if (!existsSync18(stylesPath)) {
7547
7843
  emit(" Warning: Cannot integrate CSS \u2014 src/styles.css not found");
7548
7844
  return [
7549
7845
  {
@@ -7555,12 +7851,12 @@ function integrateCss(packageRoot, config, targetDir, emit) {
7555
7851
  }
7556
7852
  ];
7557
7853
  }
7558
- let styles = readFileSync19(stylesPath, "utf-8");
7854
+ let styles = readFileSync18(stylesPath, "utf-8");
7559
7855
  let integrated = 0;
7560
7856
  const unresolved = [];
7561
7857
  for (const cssFile of config.css) {
7562
- const srcPath = join17(packageRoot, "features", config.id, cssFile);
7563
- if (!existsSync17(srcPath)) {
7858
+ const srcPath = join20(packageRoot, "features", config.id, cssFile);
7859
+ if (!existsSync18(srcPath)) {
7564
7860
  emit(` Warning: CSS source not found: ${cssFile}`, "stderr");
7565
7861
  unresolved.push({
7566
7862
  area: "css",
@@ -7569,7 +7865,7 @@ function integrateCss(packageRoot, config, targetDir, emit) {
7569
7865
  });
7570
7866
  continue;
7571
7867
  }
7572
- const css = readFileSync19(srcPath, "utf-8");
7868
+ const css = readFileSync18(srcPath, "utf-8");
7573
7869
  const fingerprint = css.match(/\.([\w-]+)\s*\{|@keyframes\s+([\w-]+)/);
7574
7870
  const marker = fingerprint?.[1] ?? fingerprint?.[2];
7575
7871
  if (marker && styles.includes(marker)) {
@@ -7581,7 +7877,7 @@ ${css}`;
7581
7877
  integrated++;
7582
7878
  emit(` CSS integrated: ${cssFile} -> styles.css`);
7583
7879
  }
7584
- if (integrated > 0) writeFileSync6(stylesPath, styles);
7880
+ if (integrated > 0) writeFileSync5(stylesPath, styles);
7585
7881
  return unresolved;
7586
7882
  }
7587
7883
  function unresolvedRoute(route, config, reason, emit) {
@@ -7603,11 +7899,11 @@ function routeRolesSuffix(route) {
7603
7899
  }
7604
7900
  function integrateRoute(config, targetDir, emit) {
7605
7901
  if (!config.route) return [];
7606
- const navPath = join17(targetDir, "src", "nav.ts");
7607
- if (!existsSync17(navPath)) {
7902
+ const navPath = join20(targetDir, "src", "nav.ts");
7903
+ if (!existsSync18(navPath)) {
7608
7904
  return [unresolvedRoute(config.route, config, "src/nav.ts was not found.", emit)];
7609
7905
  }
7610
- let content = readFileSync19(navPath, "utf-8");
7906
+ let content = readFileSync18(navPath, "utf-8");
7611
7907
  const routePath = config.route.path;
7612
7908
  if (content.includes(`'${routePath}'`)) {
7613
7909
  emit(` Nav already present: ${routePath}`);
@@ -7628,7 +7924,7 @@ function integrateRoute(config, targetDir, emit) {
7628
7924
  const entry = ` { path: '${routePath}', label: '${config.name}'${roles} },`;
7629
7925
  content = content.replace(NAV_MARKER, `${NAV_MARKER}
7630
7926
  ${entry}`);
7631
- writeFileSync6(navPath, content);
7927
+ writeFileSync5(navPath, content);
7632
7928
  emit(` Nav wired: ${routePath} (${config.name})`);
7633
7929
  emit(" Route: automatic (file-based routing via src/pages/)");
7634
7930
  return [];
@@ -7654,10 +7950,10 @@ function runPackageInstall(packageManager, targetDir, outputVisible) {
7654
7950
  const child = spawnSync6(packageManager, ["install"], { ...options, stdio: "inherit" });
7655
7951
  return { error: child.error, status: child.status };
7656
7952
  }
7657
- const captureDir = mkdtempSync(join17(tmpdir2(), "deepspace-add-install-"));
7953
+ const captureDir = mkdtempSync(join20(tmpdir2(), "deepspace-add-install-"));
7658
7954
  let outputFd;
7659
7955
  try {
7660
- outputFd = openSync2(join17(captureDir, "output.log"), "w+");
7956
+ outputFd = openSync2(join20(captureDir, "output.log"), "w+");
7661
7957
  const child = spawnSync6(packageManager, ["install"], {
7662
7958
  ...options,
7663
7959
  stdio: ["ignore", outputFd, outputFd]
@@ -7681,14 +7977,14 @@ function integrateDependencies(config, targetDir, runInstall, outputVisible, emi
7681
7977
  if (declarations.every(({ values }) => !values || Object.keys(values).length === 0)) {
7682
7978
  return { added: [], installRan: false, packageManager };
7683
7979
  }
7684
- const packagePath = join17(targetDir, "package.json");
7685
- if (!existsSync17(packagePath)) {
7980
+ const packagePath = join20(targetDir, "package.json");
7981
+ if (!existsSync18(packagePath)) {
7686
7982
  emit(" Could not patch package.json (file not found)");
7687
7983
  return { added: [], installRan: false, packageManager };
7688
7984
  }
7689
7985
  let pkg2;
7690
7986
  try {
7691
- pkg2 = JSON.parse(readFileSync19(packagePath, "utf-8"));
7987
+ pkg2 = JSON.parse(readFileSync18(packagePath, "utf-8"));
7692
7988
  } catch (error) {
7693
7989
  emit(
7694
7990
  ` Could not patch package.json (parse error: ${error instanceof Error ? error.message : String(error)})`
@@ -7712,7 +8008,7 @@ function integrateDependencies(config, targetDir, runInstall, outputVisible, emi
7712
8008
  if (added.length === 0) {
7713
8009
  emit(" Dependencies already present");
7714
8010
  } else {
7715
- writeFileSync6(packagePath, `${JSON.stringify(pkg2, null, 2)}
8011
+ writeFileSync5(packagePath, `${JSON.stringify(pkg2, null, 2)}
7716
8012
  `, "utf-8");
7717
8013
  emit(` Added to package.json: ${added.map(({ name }) => name).join(", ")}`);
7718
8014
  if (skipped.length) emit(` Already present (kept your version): ${skipped.join(", ")}`);
@@ -7748,7 +8044,7 @@ ${child.diagnosticTail}` : "";
7748
8044
  }
7749
8045
  function dependencySnapshot(targetDir) {
7750
8046
  try {
7751
- const pkg2 = JSON.parse(readFileSync19(join17(targetDir, "package.json"), "utf-8"));
8047
+ const pkg2 = JSON.parse(readFileSync18(join20(targetDir, "package.json"), "utf-8"));
7752
8048
  return {
7753
8049
  dependencies: pkg2.dependencies ?? {},
7754
8050
  devDependencies: pkg2.devDependencies ?? {}
@@ -7760,7 +8056,7 @@ function dependencySnapshot(targetDir) {
7760
8056
  function installedNavFeatures(packageRoot, targetDir) {
7761
8057
  return listFeatureConfigs(packageRoot).filter((feature) => feature.category === "nav" && feature.files.length > 0).filter((feature) => {
7762
8058
  const signature = resolveFeatureDestPath(feature.files[0].dest, feature.route);
7763
- return existsSync17(join17(targetDir, signature));
8059
+ return existsSync18(join20(targetDir, signature));
7764
8060
  }).map((feature) => feature.id);
7765
8061
  }
7766
8062
  function warnNavConflicts(packageRoot, config, targetDir, emit) {
@@ -7993,11 +8289,11 @@ function renderFeatureInfo(config) {
7993
8289
  return lines.join("\n");
7994
8290
  }
7995
8291
  function isDeepSpaceApp(dir) {
7996
- if (existsSync18(join18(dir, "wrangler.toml"))) return true;
7997
- const packagePath = join18(dir, "package.json");
7998
- 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;
7999
8295
  try {
8000
- const pkg2 = JSON.parse(readFileSync20(packagePath, "utf-8"));
8296
+ const pkg2 = JSON.parse(readFileSync19(packagePath, "utf-8"));
8001
8297
  return Boolean(pkg2.dependencies?.deepspace || pkg2.devDependencies?.deepspace);
8002
8298
  } catch {
8003
8299
  return false;
@@ -8006,8 +8302,8 @@ function isDeepSpaceApp(dir) {
8006
8302
  function installedPackageRoot(fromDir) {
8007
8303
  let dir = fromDir;
8008
8304
  for (; ; ) {
8009
- const candidate = join18(dir, "node_modules", "deepspace");
8010
- 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;
8011
8307
  const parent = dirname9(dir);
8012
8308
  if (parent === dir) return null;
8013
8309
  dir = parent;
@@ -8024,9 +8320,9 @@ async function targetInstaller(appDir) {
8024
8320
  if (realpathSync4(installedRoot) === realpathSync4(featurePackageRoot())) {
8025
8321
  return { installFeature };
8026
8322
  }
8027
- const entry = join18(installedRoot, "dist", "feature-installer.js");
8028
- const catalog = join18(installedRoot, "features");
8029
- 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)) {
8030
8326
  throw new Refusal(
8031
8327
  `The app's installed deepspace package does not contain the typed feature installer.
8032
8328
  Expected: ${entry}
@@ -8118,8 +8414,8 @@ var add_default = defineDeepspaceCommand({
8118
8414
  }
8119
8415
  return { data: { features: featureCatalog(root) } };
8120
8416
  }
8121
- const appDir = resolve15(args.dir ?? ".");
8122
- if (!existsSync18(appDir)) {
8417
+ const appDir = resolve16(args.dir ?? ".");
8418
+ if (!existsSync19(appDir)) {
8123
8419
  throw new Refusal(
8124
8420
  `Target directory not found: ${appDir}
8125
8421
  Create a new app first: deepspace app create <name>`,
@@ -8199,11 +8495,11 @@ function fmtDate(iso) {
8199
8495
  if (!iso) return "\u2014";
8200
8496
  return iso.slice(0, 10);
8201
8497
  }
8202
- async function confirm4(message) {
8498
+ async function confirm5(message) {
8203
8499
  process.stdout.write(`${message} [y/N] `);
8204
- const answer = await new Promise((resolve21) => {
8500
+ const answer = await new Promise((resolve22) => {
8205
8501
  process.stdin.setEncoding("utf-8");
8206
- process.stdin.once("data", (data) => resolve21(data.toString().trim().toLowerCase()));
8502
+ process.stdin.once("data", (data) => resolve22(data.toString().trim().toLowerCase()));
8207
8503
  });
8208
8504
  process.stdin.pause();
8209
8505
  return answer === "y" || answer === "yes";
@@ -8278,7 +8574,7 @@ var buy = defineDeepspaceCommand({
8278
8574
  "confirmation_required"
8279
8575
  );
8280
8576
  }
8281
- const ok2 = await confirm4(
8577
+ const ok2 = await confirm5(
8282
8578
  `Buy ${domain} for ${price}/yr (auto-renews; non-refundable), attach to ${appId}? Provisioning takes ${expectedTime}.`
8283
8579
  );
8284
8580
  if (!ok2) {
@@ -8448,7 +8744,7 @@ var detach = defineDeepspaceCommand({
8448
8744
  "confirmation_required"
8449
8745
  );
8450
8746
  }
8451
- const ok2 = await confirm4(
8747
+ const ok2 = await confirm5(
8452
8748
  `Detach ${domain}? Routing will stop; the domain stays registered (auto-renew is unchanged).`
8453
8749
  );
8454
8750
  if (!ok2) {
@@ -8494,8 +8790,8 @@ var domain_default = defineCommand5({
8494
8790
 
8495
8791
  // src/cli/commands/app-files.ts
8496
8792
  import { defineCommand as defineCommand6 } from "citty";
8497
- import { basename as basename4, resolve as resolve16 } from "path";
8498
- 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";
8499
8795
 
8500
8796
  // src/cli/lib/app-files-api.ts
8501
8797
  import { createWriteStream, openAsBlob, statSync } from "fs";
@@ -8773,8 +9069,8 @@ var put = defineDeepspaceCommand({
8773
9069
  app: { type: "string", description: "App id or live name (defaults to ./wrangler.toml)" }
8774
9070
  },
8775
9071
  async run({ args }) {
8776
- const localPath = resolve16(String(args.file));
8777
- if (!existsSync19(localPath) || !statSync2(localPath).isFile()) {
9072
+ const localPath = resolve17(String(args.file));
9073
+ if (!existsSync20(localPath) || !statSync2(localPath).isFile()) {
8778
9074
  throw new Refusal(`No such file: ${args.file}`, "file_not_found");
8779
9075
  }
8780
9076
  const key = requireKey(args.key ?? basename4(localPath));
@@ -8874,7 +9170,7 @@ var get = defineDeepspaceCommand({
8874
9170
  },
8875
9171
  async run({ args }) {
8876
9172
  const key = requireKey(String(args.key));
8877
- const destination = resolve16(String(args.out ?? basename4(key)));
9173
+ const destination = resolve17(String(args.out ?? basename4(key)));
8878
9174
  const { token, appId } = await target(args.app);
8879
9175
  const result = await downloadAppFile(PLATFORM_URL, token, appId, key, destination);
8880
9176
  if (!args.json) console.log(`\u2713 ${key} \u2192 ${destination} (${formatBytes(result.bytes)})`);
@@ -8954,7 +9250,10 @@ var list4 = defineDeepspaceCommand({
8954
9250
  }
8955
9251
  });
8956
9252
  var add = defineDeepspaceCommand({
8957
- 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
+ },
8958
9257
  args: {
8959
9258
  email: {
8960
9259
  type: "positional",
@@ -9164,7 +9463,11 @@ var offer = defineDeepspaceCommand({
9164
9463
  if (replaced) console.log(`\u25B2 Replaced the pending offer to ${pendingTo}.`);
9165
9464
  console.log(
9166
9465
  `\u2713 Offered ${app2} to ${email} (expires ${expiresAt}).
9167
- 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.`
9168
9471
  );
9169
9472
  }
9170
9473
  return {
@@ -9191,7 +9494,9 @@ var status2 = defineDeepspaceCommand({
9191
9494
  return { data: { app: app2, transfer: null } };
9192
9495
  }
9193
9496
  if (!args.json) {
9194
- 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
+ );
9195
9500
  }
9196
9501
  return { data: { app: app2, transfer } };
9197
9502
  }
@@ -9211,15 +9516,32 @@ var accept2 = defineDeepspaceCommand({
9211
9516
  const msg = err instanceof Error ? err.message : String(err);
9212
9517
  throw new Refusal(msg, msg.startsWith("No app id") ? "not_in_app_repo" : "invalid_app");
9213
9518
  }
9214
- 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`, {
9215
9520
  method: "POST"
9216
9521
  });
9217
9522
  if (!args.json) {
9218
9523
  console.log(
9219
- `\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.`
9220
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
+ }
9221
9535
  }
9222
- 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
+ };
9223
9545
  }
9224
9546
  });
9225
9547
  var cancel4 = defineDeepspaceCommand({
@@ -9244,7 +9566,7 @@ var transfer_default = defineCommand8({
9244
9566
  import { defineCommand as defineCommand9 } from "citty";
9245
9567
 
9246
9568
  // src/cli/commands/_invoke-impl.ts
9247
- import { readFileSync as readFileSync21 } from "fs";
9569
+ import { readFileSync as readFileSync20 } from "fs";
9248
9570
  import * as p15 from "@clack/prompts";
9249
9571
  var API_URL7 = process.env.DEEPSPACE_API_URL ?? PLATFORM_URLS.api;
9250
9572
  var DEFAULT_TIMEOUT_MS = 12e4;
@@ -9285,7 +9607,7 @@ function resolveBody(opts) {
9285
9607
  if (opts.body != null) {
9286
9608
  raw = opts.body;
9287
9609
  } else if (opts.bodyFile != null) {
9288
- 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");
9289
9611
  } else {
9290
9612
  return "{}";
9291
9613
  }
@@ -9579,8 +9901,8 @@ var integrations_default = defineCommand9({
9579
9901
 
9580
9902
  // src/cli/commands/feedback.ts
9581
9903
  import * as p16 from "@clack/prompts";
9582
- import { existsSync as existsSync20, readFileSync as readFileSync22 } from "fs";
9583
- 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";
9584
9906
  import { fileURLToPath as fileURLToPath2 } from "url";
9585
9907
  import { release } from "os";
9586
9908
  var API_URL8 = process.env.DEEPSPACE_API_URL ?? PLATFORM_URLS.api;
@@ -9603,12 +9925,12 @@ function readCliVersion() {
9603
9925
  try {
9604
9926
  const here = dirname10(fileURLToPath2(import.meta.url));
9605
9927
  const candidates = [
9606
- join19(here, "..", "package.json"),
9607
- join19(here, "..", "..", "..", "package.json")
9928
+ join22(here, "..", "package.json"),
9929
+ join22(here, "..", "..", "..", "package.json")
9608
9930
  ];
9609
9931
  for (const path of candidates) {
9610
- if (!existsSync20(path)) continue;
9611
- const pkg2 = JSON.parse(readFileSync22(path, "utf-8"));
9932
+ if (!existsSync21(path)) continue;
9933
+ const pkg2 = JSON.parse(readFileSync21(path, "utf-8"));
9612
9934
  if (pkg2.name === "deepspace" && pkg2.version) return pkg2.version;
9613
9935
  }
9614
9936
  } catch {
@@ -9804,7 +10126,7 @@ Context: deepspace ${payload.cliVersion} \xB7 ${payload.nodeVersion} \xB7 ${payl
9804
10126
 
9805
10127
  // src/cli/commands/secrets.ts
9806
10128
  import { defineCommand as defineCommand10 } from "citty";
9807
- import { readFileSync as readFileSync23 } from "fs";
10129
+ import { readFileSync as readFileSync22 } from "fs";
9808
10130
 
9809
10131
  // src/cli/lib/citty-args.ts
9810
10132
  function dedupePositionals(bound, rest) {
@@ -9906,6 +10228,7 @@ function warnIfDebugRoutesEnabled(secrets) {
9906
10228
  return true;
9907
10229
  }
9908
10230
  var APPLY_HINT = "Run `deepspace deploy` to apply \u2014 secrets take effect at deploy time.";
10231
+ var APPLIES_AT_DEPLOY = { appliesAtDeploy: true };
9909
10232
  var LOCAL_DEV_HINT = "Running `deepspace dev start`? Restart it to load the change locally.";
9910
10233
  var list6 = defineCommand10({
9911
10234
  meta: { name: "list", description: "List masked secrets in a config" },
@@ -9972,7 +10295,7 @@ var set = defineCommand10({
9972
10295
  const n = Object.keys(secrets).length;
9973
10296
  ok(
9974
10297
  args.json === true,
9975
- { appId: t.appId, config: t.configName, set: Object.keys(secrets) },
10298
+ { appId: t.appId, config: t.configName, set: Object.keys(secrets), ...APPLIES_AT_DEPLOY },
9976
10299
  () => {
9977
10300
  console.log(`Set ${n} secret${n === 1 ? "" : "s"} in ${t.configName}.`);
9978
10301
  if (!warnIfDebugRoutesEnabled(secrets)) {
@@ -10045,7 +10368,7 @@ var del2 = defineCommand10({
10045
10368
  }
10046
10369
  }
10047
10370
  const note3 = absent > 0 ? ` (${absent} already absent)` : "";
10048
- 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 }, () => {
10049
10372
  console.log(
10050
10373
  `Deleted ${deleted} secret${deleted === 1 ? "" : "s"} from ${t.configName}.${note3}`
10051
10374
  );
@@ -10074,7 +10397,7 @@ var upload = defineCommand10({
10074
10397
  async run({ args }) {
10075
10398
  try {
10076
10399
  const t = await resolveTarget(args);
10077
- 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");
10078
10401
  const secrets = parseSecretsUpload(content);
10079
10402
  if (Object.keys(secrets).length === 0)
10080
10403
  throw new InputError("No secrets found in the input.", "empty_input");
@@ -10082,7 +10405,7 @@ var upload = defineCommand10({
10082
10405
  const uploaded = Object.keys(secrets);
10083
10406
  ok(
10084
10407
  args.json === true,
10085
- { 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 },
10086
10409
  () => {
10087
10410
  console.log(`Uploaded ${uploaded.length} secrets to ${t.configName}.`);
10088
10411
  if (!warnIfDebugRoutesEnabled(secrets)) console.log(APPLY_HINT);
@@ -10257,12 +10580,19 @@ var secrets_default = defineCommand10({
10257
10580
  });
10258
10581
 
10259
10582
  // src/cli/commands/init.ts
10260
- import { existsSync as existsSync21 } from "fs";
10261
- import { join as join20 } from "path";
10583
+ import { existsSync as existsSync22 } from "fs";
10584
+ import { join as join23 } from "path";
10262
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
+ }
10263
10593
  function commitScaffoldIfUnborn(appDir, token) {
10264
10594
  try {
10265
- if (!existsSync21(join20(appDir, ".git"))) return false;
10595
+ if (!existsSync22(join23(appDir, ".git"))) return false;
10266
10596
  if (runGit(appDir, ["rev-parse", "--verify", "HEAD"], { allowFail: true }).status === 0) {
10267
10597
  return false;
10268
10598
  }
@@ -10337,16 +10667,15 @@ var init_default = defineDeepspaceCommand({
10337
10667
  method: "POST"
10338
10668
  });
10339
10669
  writeAppId(appDir, appId, { wranglerEnv: envName, force: Boolean(args["new-id"]) });
10340
- if (!envName) stampAppIdInSources(appDir, appId, existing);
10341
10670
  const committedScaffold = commitScaffoldIfUnborn(appDir, token);
10342
10671
  if (!args.json) {
10672
+ const envSuffix = envName ? ` (env: ${envName})` : "";
10673
+ const commitNote = committedScaffold ? " \u2014 initial scaffold commit created." : " \u2014 commit wrangler.toml.";
10343
10674
  if (existing) {
10344
- console.log(`Forked: ${existing} \u2192 ${appId}${envName ? ` (env: ${envName})` : ""}`);
10675
+ console.log(`Forked: ${existing} \u2192 ${appId}${envSuffix}${commitNote}`);
10345
10676
  console.log("Registered as a NEW app under your account; the original is untouched.");
10346
10677
  } else {
10347
- console.log(
10348
- `Registered ${appId}${envName ? ` (env: ${envName})` : ""} to your account` + (committedScaffold ? " \u2014 initial scaffold commit created." : " \u2014 commit wrangler.toml.")
10349
- );
10678
+ console.log(`Registered ${appId}${envSuffix} to your account${commitNote}`);
10350
10679
  console.log("The first deploy claims the `name` subdomain.");
10351
10680
  }
10352
10681
  console.log(`App dir: ${appDir}`);
@@ -10359,7 +10688,18 @@ var init_default = defineDeepspaceCommand({
10359
10688
  committedScaffold,
10360
10689
  appDir,
10361
10690
  env: envName ?? null
10362
- }
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
+ } : {}
10363
10703
  };
10364
10704
  }
10365
10705
  });
@@ -10367,7 +10707,7 @@ var init_default = defineDeepspaceCommand({
10367
10707
  // src/cli/commands/status.ts
10368
10708
  import { defineCommand as defineCommand11 } from "citty";
10369
10709
  import * as p17 from "@clack/prompts";
10370
- import { existsSync as existsSync22, readFileSync as readFileSync24 } from "fs";
10710
+ import { existsSync as existsSync23, readFileSync as readFileSync23 } from "fs";
10371
10711
  function timeAgo(iso) {
10372
10712
  const ms = Date.now() - Date.parse(iso);
10373
10713
  if (!Number.isFinite(ms) || ms < 0) return iso;
@@ -10452,7 +10792,7 @@ var status_default = defineCommand11({
10452
10792
  json.services = urls;
10453
10793
  if (urlOverrides.length > 0) json.urlOverrides = urlOverrides;
10454
10794
  }
10455
- facts.loggedIn = existsSync22(SESSION_PATH);
10795
+ facts.loggedIn = existsSync23(SESSION_PATH);
10456
10796
  let userId = null;
10457
10797
  if (!facts.loggedIn) {
10458
10798
  lines.push(["Session", "not logged in"]);
@@ -10461,7 +10801,7 @@ var status_default = defineCommand11({
10461
10801
  let who = "logged in";
10462
10802
  try {
10463
10803
  const payload = decodeJwtPayload(
10464
- readFileSync24(TOKEN_PATH, "utf-8").trim()
10804
+ readFileSync23(TOKEN_PATH, "utf-8").trim()
10465
10805
  );
10466
10806
  if (payload.email) who = payload.email;
10467
10807
  userId = payload.sub ?? null;
@@ -10705,11 +11045,7 @@ var push_default = defineDeepspaceCommand({
10705
11045
  const force = args.force === true;
10706
11046
  const appDir = findAppDir();
10707
11047
  try {
10708
- if (!appDir)
10709
- throw new Refusal(
10710
- "No wrangler.toml found \u2014 run from inside an app directory.",
10711
- "not_in_app_repo"
10712
- );
11048
+ if (!appDir) throw new Refusal(noWranglerConfigMessage(process.cwd()), "not_in_app_repo");
10713
11049
  assertSyncableRepo(appDir);
10714
11050
  if (branchArg !== void 0) {
10715
11051
  const b = branchArg.trim();
@@ -10732,9 +11068,11 @@ var push_default = defineDeepspaceCommand({
10732
11068
  "unknown_branch"
10733
11069
  );
10734
11070
  }
11071
+ const uninitialized = hasWranglerConfig(appDir) && readAppId(appDir) === null;
10735
11072
  throw new Refusal(
10736
- `Branch "${branch}" has no commits yet \u2014 commit first, then push.`,
10737
- "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"] } } : {}
10738
11076
  );
10739
11077
  }
10740
11078
  const spinner2 = args.json ? null : createSpinner();
@@ -10798,6 +11136,13 @@ var push_default = defineDeepspaceCommand({
10798
11136
  "app_not_registered"
10799
11137
  );
10800
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
+ }
10801
11146
  const pullRecoveryCwd = selectedBranchCheckout(appDir, branch) ?? appDir;
10802
11147
  ensureSpaceRemote(appDir, appId);
10803
11148
  if (force) {
@@ -10937,11 +11282,7 @@ var pull_default = defineDeepspaceCommand({
10937
11282
  const branchArg = args.branch === void 0 ? void 0 : String(args.branch);
10938
11283
  const appArg = typeof args.app === "string" ? args.app : void 0;
10939
11284
  const appDir = findAppDir();
10940
- if (!appDir)
10941
- throw new Refusal(
10942
- "No wrangler.toml found \u2014 run from inside an app directory.",
10943
- "not_in_app_repo"
10944
- );
11285
+ if (!appDir) throw new Refusal(noWranglerConfigMessage(process.cwd()), "not_in_app_repo");
10945
11286
  if (branchArg !== void 0) {
10946
11287
  const b = branchArg.trim();
10947
11288
  if (!b) throw new Refusal("--branch was given an empty branch name.", "invalid_branch");
@@ -11159,18 +11500,18 @@ var pull_default = defineDeepspaceCommand({
11159
11500
 
11160
11501
  // src/cli/commands/clone.ts
11161
11502
  import * as p20 from "@clack/prompts";
11162
- import { existsSync as existsSync23, readdirSync as readdirSync6, rmSync as rmSync5 } from "fs";
11163
- 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";
11164
11505
  function cleanFailedCloneDir(dir) {
11165
11506
  let entries;
11166
11507
  try {
11167
- entries = readdirSync6(dir);
11508
+ entries = readdirSync8(dir);
11168
11509
  } catch {
11169
- return existsSync23(dir) ? ["<unreadable>"] : [];
11510
+ return existsSync24(dir) ? ["<unreadable>"] : [];
11170
11511
  }
11171
11512
  if (entries.length === 1 && entries[0] === ".git") {
11172
11513
  try {
11173
- rmSync5(join21(dir, ".git"), { recursive: true, force: true });
11514
+ rmSync5(join24(dir, ".git"), { recursive: true, force: true });
11174
11515
  return [];
11175
11516
  } catch {
11176
11517
  return entries;
@@ -11179,10 +11520,10 @@ function cleanFailedCloneDir(dir) {
11179
11520
  return entries;
11180
11521
  }
11181
11522
  function validateCloneTarget(dir) {
11182
- if (!existsSync23(dir)) return false;
11523
+ if (!existsSync24(dir)) return false;
11183
11524
  let entries;
11184
11525
  try {
11185
- entries = readdirSync6(dir);
11526
+ entries = readdirSync8(dir);
11186
11527
  } catch (error) {
11187
11528
  if (error.code === "ENOTDIR") {
11188
11529
  throw new Refusal(`${dir} already exists and is not a directory.`, "dir_exists");
@@ -11214,7 +11555,7 @@ var clone_default = defineDeepspaceCommand({
11214
11555
  async run({ args }) {
11215
11556
  assertAppTargetResolvable(String(args.app));
11216
11557
  const selector = String(args.app).trim();
11217
- const dir = resolve17(String(args.dir ?? selector));
11558
+ const dir = resolve18(String(args.dir ?? selector));
11218
11559
  const dirPreexisted = validateCloneTarget(dir);
11219
11560
  const spinner2 = args.json ? null : createSpinner();
11220
11561
  spinner2?.start(`Resolving ${selector}\u2026`);
@@ -11387,7 +11728,7 @@ function pickPreviousRelease(releases) {
11387
11728
  var rollback_default = defineDeepspaceCommand({
11388
11729
  meta: {
11389
11730
  name: "rollback",
11390
- 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"
11391
11732
  },
11392
11733
  args: {
11393
11734
  release: {
@@ -11522,7 +11863,7 @@ import { defineCommand as defineCommand12 } from "citty";
11522
11863
 
11523
11864
  // src/cli/commands/workspace/new.ts
11524
11865
  import * as p25 from "@clack/prompts";
11525
- import { isAbsolute as isAbsolute6, resolve as resolve18 } from "path";
11866
+ import { isAbsolute as isAbsolute6, resolve as resolve19 } from "path";
11526
11867
 
11527
11868
  // src/cli/commands/workspace/analysis.ts
11528
11869
  import * as p23 from "@clack/prompts";
@@ -11656,25 +11997,25 @@ function printOverlaps(overlaps, json) {
11656
11997
  // src/cli/commands/workspace/local.ts
11657
11998
  import * as p24 from "@clack/prompts";
11658
11999
  import {
11659
- existsSync as existsSync24,
12000
+ existsSync as existsSync25,
11660
12001
  mkdirSync as mkdirSync7,
11661
- readFileSync as readFileSync25,
12002
+ readFileSync as readFileSync24,
11662
12003
  realpathSync as realpathSync6,
11663
12004
  renameSync as renameSync2,
11664
12005
  statSync as statSync3,
11665
- writeFileSync as writeFileSync7
12006
+ writeFileSync as writeFileSync6
11666
12007
  } from "fs";
11667
- 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";
11668
12009
  var WORKSPACE_MARKER = "deepspace-workspace.json";
11669
12010
  function excludeWorktreeDir(appDir, dir) {
11670
12011
  try {
11671
12012
  ensureLocalExclude(appDir, ".deepspace/");
11672
12013
  const realDir = realpathSync6(dir);
11673
- const rel = relative2(realpathSync6(repoToplevel(appDir)), realDir);
11674
- if (!rel || rel === ".." || rel.startsWith(`..${sep2}`) || isAbsolute5(rel)) return;
11675
- 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("/");
11676
12017
  if (pattern === ".deepspace" || pattern.startsWith(".deepspace/")) return;
11677
- const relApp = relative2(realpathSync6(appDir), realDir).split(sep2).join("/");
12018
+ const relApp = relative3(realpathSync6(appDir), realDir).split(sep3).join("/");
11678
12019
  if (relApp === ".deepspace" || relApp.startsWith(".deepspace/")) return;
11679
12020
  ensureLocalExclude(appDir, `/${pattern}/`);
11680
12021
  } catch {
@@ -11685,7 +12026,7 @@ function worktreeMarkerPath(worktreeRoot) {
11685
12026
  const result = runGit(worktreeRoot, ["rev-parse", "--absolute-git-dir"], { allowFail: true });
11686
12027
  if (result.status !== 0) return null;
11687
12028
  const gitDir = result.stdout.toString("utf-8").trim();
11688
- return isAbsolute5(gitDir) ? join22(gitDir, WORKSPACE_MARKER) : null;
12029
+ return isAbsolute5(gitDir) ? join25(gitDir, WORKSPACE_MARKER) : null;
11689
12030
  } catch {
11690
12031
  return null;
11691
12032
  }
@@ -11697,7 +12038,7 @@ function markManagedWorkspace(worktreeRoot, workspaceId) {
11697
12038
  const tempPath = `${markerPath}.${process.pid}.tmp`;
11698
12039
  try {
11699
12040
  mkdirSync7(dirname11(markerPath), { recursive: true });
11700
- writeFileSync7(tempPath, JSON.stringify(marker) + "\n");
12041
+ writeFileSync6(tempPath, JSON.stringify(marker) + "\n");
11701
12042
  renameSync2(tempPath, markerPath);
11702
12043
  return true;
11703
12044
  } catch {
@@ -11721,7 +12062,7 @@ function isManagedWorkspaceWorktree(worktreeRoot, workspaceId) {
11721
12062
  const markerPath = worktreeMarkerPath(worktreeRoot);
11722
12063
  if (!markerPath) return false;
11723
12064
  try {
11724
- const marker = JSON.parse(readFileSync25(markerPath, "utf-8"));
12065
+ const marker = JSON.parse(readFileSync24(markerPath, "utf-8"));
11725
12066
  return marker.version === 1 && marker.workspaceId === workspaceId && marker.mode === "managed-linked";
11726
12067
  } catch {
11727
12068
  return false;
@@ -11730,11 +12071,11 @@ function isManagedWorkspaceWorktree(worktreeRoot, workspaceId) {
11730
12071
  function defaultWorkspaceRoot(appDir, workspaceId) {
11731
12072
  const primary = listWorktrees(appDir)[0];
11732
12073
  if (!primary) throw new Error("Git did not report a primary worktree");
11733
- return join22(primary.path, ".deepspace", "ws", workspaceId.slice(3).toLowerCase());
12074
+ return join25(primary.path, ".deepspace", "ws", workspaceId.slice(3).toLowerCase());
11734
12075
  }
11735
12076
  function appDirInWorktree(appDir, worktreeRoot) {
11736
- const appRelative = relative2(realpathSync6(repoToplevel(appDir)), realpathSync6(appDir));
11737
- return appRelative ? join22(worktreeRoot, appRelative) : worktreeRoot;
12077
+ const appRelative = relative3(realpathSync6(repoToplevel(appDir)), realpathSync6(appDir));
12078
+ return appRelative ? join25(worktreeRoot, appRelative) : worktreeRoot;
11738
12079
  }
11739
12080
  function primaryAppDir(appDir) {
11740
12081
  const primary = listWorktrees(appDir)[0];
@@ -11769,7 +12110,7 @@ function rematerializeWorkspaceWorktree(appDir, worktreeRoot, branch, workspaceI
11769
12110
  function inOwnLinkedWorktree(appDir, id) {
11770
12111
  if (workspaceIdFromBranch(currentBranch(appDir)) !== id) return false;
11771
12112
  try {
11772
- return statSync3(join22(repoToplevel(appDir), ".git")).isFile();
12113
+ return statSync3(join25(repoToplevel(appDir), ".git")).isFile();
11773
12114
  } catch {
11774
12115
  return false;
11775
12116
  }
@@ -11894,7 +12235,7 @@ function cleanupWorkspaceLocal(appDir, id, trunkBranch, options = {}) {
11894
12235
  }
11895
12236
  runGit(mainDir, ["worktree", "prune"], { allowFail: true });
11896
12237
  out.worktreeDir = null;
11897
- const leftover = existsSync24(linked.path);
12238
+ const leftover = existsSync25(linked.path);
11898
12239
  if (leftover) out.leftoverDir = linked.path;
11899
12240
  else out.worktreeRemoved = linked.path;
11900
12241
  deleteWsBranch(mainDir, branch, expectedBranchOid, out);
@@ -11974,7 +12315,7 @@ async function resolveTarget2(appArg) {
11974
12315
  const appDir = findAppDir();
11975
12316
  if (!appDir) {
11976
12317
  throw new Refusal(
11977
- "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.)`,
11978
12319
  "not_in_app_repo"
11979
12320
  );
11980
12321
  }
@@ -12154,7 +12495,7 @@ var newWorkspaceCommand = defineDeepspaceCommand({
12154
12495
  idempotencyKey: mintIdempotencyKey()
12155
12496
  });
12156
12497
  const dirInput = typeof args.dir === "string" ? args.dir.trim() : "";
12157
- 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);
12158
12499
  const dir = appDirInWorktree(appDir, worktreeRoot);
12159
12500
  try {
12160
12501
  materializeWorkspaceWorktree(appDir, worktreeRoot, branch, baseOid, id);
@@ -12192,14 +12533,14 @@ var newWorkspaceCommand = defineDeepspaceCommand({
12192
12533
 
12193
12534
  // src/cli/commands/workspace/attach.ts
12194
12535
  import * as p26 from "@clack/prompts";
12195
- import { existsSync as existsSync25, readdirSync as readdirSync7, realpathSync as realpathSync7, rmSync as rmSync6 } from "fs";
12196
- 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";
12197
12538
  function samePath(a, b) {
12198
12539
  const real = (p35) => {
12199
12540
  try {
12200
12541
  return realpathSync7(p35);
12201
12542
  } catch {
12202
- return resolve19(p35);
12543
+ return resolve20(p35);
12203
12544
  }
12204
12545
  };
12205
12546
  return real(a) === real(b);
@@ -12212,10 +12553,10 @@ function finishedWorkspaceMessage(id, view) {
12212
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.`;
12213
12554
  }
12214
12555
  function cleanFailedFreshAttachDir(dir) {
12215
- if (!existsSync25(dir)) return [];
12556
+ if (!existsSync26(dir)) return [];
12216
12557
  let entries;
12217
12558
  try {
12218
- entries = readdirSync7(dir);
12559
+ entries = readdirSync9(dir);
12219
12560
  } catch {
12220
12561
  return ["<unreadable>"];
12221
12562
  }
@@ -12289,12 +12630,12 @@ var attachWorkspaceCommand = defineDeepspaceCommand({
12289
12630
  );
12290
12631
  }
12291
12632
  const requestedDir = typeof args.dir === "string" ? args.dir.trim() : "";
12292
- 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);
12293
12634
  const dir2 = appDirInWorktree(appDir, worktreeRoot);
12294
12635
  const localTip = resolveCommit(appDir, `refs/heads/${branch}`);
12295
12636
  if (localTip) {
12296
12637
  let existing = resolveWorkspaceWorktree(listWorktrees(appDir), id);
12297
- if (existing && !existsSync25(existing)) {
12638
+ if (existing && !existsSync26(existing)) {
12298
12639
  runGit(appDir, ["worktree", "prune"], { allowFail: true });
12299
12640
  existing = null;
12300
12641
  }
@@ -12408,11 +12749,11 @@ var attachWorkspaceCommand = defineDeepspaceCommand({
12408
12749
  extra: { status: view.workspace.status }
12409
12750
  });
12410
12751
  }
12411
- const dir = resolve19(
12752
+ const dir = resolve20(
12412
12753
  process.cwd(),
12413
12754
  (typeof args.dir === "string" ? args.dir.trim() : "") || `ws-${id.slice(3).toLowerCase()}`
12414
12755
  );
12415
- if (existsSync25(dir)) {
12756
+ if (existsSync26(dir)) {
12416
12757
  spinner2?.stop("Target exists.");
12417
12758
  throw new Refusal(`${dir} already exists \u2014 pass a different directory.`, "dir_exists");
12418
12759
  }
@@ -12749,13 +13090,13 @@ import * as p30 from "@clack/prompts";
12749
13090
 
12750
13091
  // src/cli/lib/validation.ts
12751
13092
  import { sync as spawnSync7 } from "cross-spawn";
12752
- import { readFileSync as readFileSync26 } from "fs";
12753
- import { join as join23 } from "path";
13093
+ import { readFileSync as readFileSync25 } from "fs";
13094
+ import { join as join26 } from "path";
12754
13095
  var MAX_SUMMARY_CHARS = 4e3;
12755
13096
  var MAX_OUTPUT_BUFFER = 64 * 1024 * 1024;
12756
13097
  function configuredCommand(appDir) {
12757
13098
  try {
12758
- const pkg2 = JSON.parse(readFileSync26(join23(appDir, "package.json"), "utf-8"));
13099
+ const pkg2 = JSON.parse(readFileSync25(join26(appDir, "package.json"), "utf-8"));
12759
13100
  if (pkg2.scripts?.validate) return `${detectPackageManager(appDir)} run --silent validate`;
12760
13101
  } catch {
12761
13102
  }
@@ -13831,13 +14172,13 @@ function sameRefs(left, right) {
13831
14172
 
13832
14173
  // src/cli/commands/update.ts
13833
14174
  import * as p34 from "@clack/prompts";
13834
- import { existsSync as existsSync26, readFileSync as readFileSync27, writeFileSync as writeFileSync8 } from "fs";
13835
- 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";
13836
14177
 
13837
14178
  // package.json
13838
14179
  var package_default = {
13839
14180
  name: "deepspace",
13840
- version: "0.22.1",
14181
+ version: "0.23.1",
13841
14182
  engines: {
13842
14183
  node: ">=22.15.0 <23 || >=24 <25 || >=26 <27"
13843
14184
  },
@@ -13874,6 +14215,10 @@ var package_default = {
13874
14215
  types: "./dist/testing-mcp.d.ts",
13875
14216
  import: "./dist/testing-mcp.js"
13876
14217
  },
14218
+ "./build": {
14219
+ types: "./dist/build.d.ts",
14220
+ import: "./dist/build.js"
14221
+ },
13877
14222
  "./documentation": {
13878
14223
  types: "./dist/documentation.d.ts",
13879
14224
  import: "./dist/documentation.js"
@@ -14008,24 +14353,25 @@ function gapIsTooWide(from, to) {
14008
14353
  return from.major === 0 && from.minor < OLDEST_SUPPORTED_MINOR;
14009
14354
  }
14010
14355
  function readAppSdkVersion(appDir) {
14011
- const installed = join24(appDir, "node_modules", "deepspace", "package.json");
14012
- if (existsSync26(installed)) {
14013
- 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"));
14014
14359
  if (typeof parsed2.version === "string") return parsed2.version;
14015
14360
  }
14016
- const manifest = join24(appDir, "package.json");
14017
- if (!existsSync26(manifest)) return null;
14018
- 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"));
14019
14364
  return parsed.dependencies?.deepspace ?? null;
14020
14365
  }
14021
14366
  function pinSdkVersion(appDir, version) {
14022
- const path = join24(appDir, "package.json");
14023
- const source = readFileSync27(path, "utf8");
14367
+ const path = join27(appDir, "package.json");
14368
+ const source = readFileSync26(path, "utf8");
14024
14369
  const manifest = JSON.parse(source);
14025
14370
  const dependencies = manifest.dependencies;
14026
14371
  if (!dependencies || typeof dependencies !== "object" || Array.isArray(dependencies)) return false;
14027
14372
  let changed = false;
14028
- 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}`) {
14029
14375
  dependencies.deepspace = `^${version}`;
14030
14376
  changed = true;
14031
14377
  }
@@ -14037,7 +14383,7 @@ function pinSdkVersion(appDir, version) {
14037
14383
  const eol = source.includes("\r\n") ? "\r\n" : "\n";
14038
14384
  const indent = source.match(/\r?\n([\t ]+)"/)?.[1] ?? " ";
14039
14385
  const finalEol = source.endsWith("\n") ? eol : "";
14040
- writeFileSync8(path, JSON.stringify(manifest, null, indent).replaceAll("\n", eol) + finalEol);
14386
+ writeFileSync7(path, JSON.stringify(manifest, null, indent).replaceAll("\n", eol) + finalEol);
14041
14387
  return true;
14042
14388
  }
14043
14389
  function buildPreviewSecretsUpgradeInstruction(appDir) {
@@ -14046,17 +14392,19 @@ function buildPreviewSecretsUpgradeInstruction(appDir) {
14046
14392
  "vite.config.mts",
14047
14393
  "vite.config.js",
14048
14394
  "vite.config.mjs"
14049
- ].find((candidate) => existsSync26(join24(appDir, candidate)));
14395
+ ].find((candidate) => existsSync27(join27(appDir, candidate)));
14050
14396
  if (!configName) return null;
14051
- const source = readFileSync27(join24(appDir, configName), "utf8");
14052
- if (source.includes("deepspace-remove-build-preview-secrets")) return null;
14053
- return `${configName}: add the build-preview secret cleanup from ${BUILD_PREVIEW_SECRETS_GUIDE}; app update left this app-owned file unchanged.`;
14397
+ const source = readFileSync26(join27(appDir, configName), "utf8");
14398
+ if (source.includes("deepspace-remove-build-preview-secrets") || source.includes("deepspaceBuild(") || source.includes("'deepspace/build'") || source.includes('"deepspace/build"')) {
14399
+ return null;
14400
+ }
14401
+ return `${configName}: adopt the deepspaceBuild() plugin from deepspace/build (which owns this cleanup), or add the inline cleanup from ${BUILD_PREVIEW_SECRETS_GUIDE}; app update left this app-owned file unchanged.`;
14054
14402
  }
14055
14403
  function usersSchemaVisibilityUpgradeInstruction(appDir) {
14056
14404
  const schemaName = "src/schemas/users-schema.ts";
14057
- const path = join24(appDir, schemaName);
14058
- if (!existsSync26(path)) return null;
14059
- const source = readFileSync27(path, "utf8");
14405
+ const path = join27(appDir, schemaName);
14406
+ if (!existsSync27(path)) return null;
14407
+ const source = readFileSync26(path, "utf8");
14060
14408
  if (!/\bmember\s*:\s*\{[^}]*\bread\s*:\s*true\b/s.test(source)) return null;
14061
14409
  return `${schemaName}: review the member user-row visibility change at ${USERS_SCHEMA_VISIBILITY_GUIDE}; app update left this app-owned schema unchanged.`;
14062
14410
  }
@@ -14116,12 +14464,9 @@ var update_default = defineDeepspaceCommand({
14116
14464
  }
14117
14465
  },
14118
14466
  async run({ args }) {
14119
- const appDir = resolve20(".");
14467
+ const appDir = resolve21(".");
14120
14468
  if (!hasWranglerConfig(appDir)) {
14121
- throw new Refusal(
14122
- "No wrangler.toml found \u2014 run this from inside a DeepSpace app directory.",
14123
- "not_in_app_repo"
14124
- );
14469
+ throw new Refusal(noWranglerConfigMessage(join27(appDir, "wrangler.toml")), "not_in_app_repo");
14125
14470
  }
14126
14471
  assertSyncableRepo(appDir);
14127
14472
  const currentRaw = readAppSdkVersion(appDir);
@@ -14207,8 +14552,8 @@ var update_default = defineDeepspaceCommand({
14207
14552
 
14208
14553
  // src/cli/cli.ts
14209
14554
  var __dirname = dirname12(fileURLToPath3(import.meta.url));
14210
- var pkg = JSON.parse(readFileSync28(join25(__dirname, "..", "package.json"), "utf-8"));
14211
- 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")) {
14212
14557
  process.env.SSL_CERT_FILE = "/etc/ssl/certs/ca-certificates.crt";
14213
14558
  }
14214
14559
  var create2 = defineCommand14({