deepspace 0.22.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -993,18 +993,6 @@ ${line}
993
993
  }
994
994
  writeFileSync2(wranglerPath, src);
995
995
  }
996
- var APP_ID_PLACEHOLDER = "__APP_ID__";
997
- function stampAppIdInSources(cwd, appId, previousAppId) {
998
- const constantsPath = join2(resolve2(cwd), "src", "constants.ts");
999
- if (!existsSync3(constantsPath)) return false;
1000
- const needles = [APP_ID_PLACEHOLDER, ...previousAppId ? [previousAppId] : []];
1001
- const content = readFileSync5(constantsPath, "utf-8");
1002
- let next = content;
1003
- for (const needle of needles) next = next.replaceAll(needle, appId);
1004
- if (next === content) return false;
1005
- writeFileSync2(constantsPath, next);
1006
- return true;
1007
- }
1008
996
 
1009
997
  // src/cli/commands/dev.ts
1010
998
  import { resolve as resolve8, basename as basename2, join as join11 } from "path";
@@ -1589,6 +1577,7 @@ function resolveAppNameForEnv(config, envName) {
1589
1577
  if (!envBlock) {
1590
1578
  return {
1591
1579
  ok: false,
1580
+ code: "missing_env_block",
1592
1581
  reason: `wrangler.toml: no [env.${envName}] block found. Add one with a distinct \`name\` to deploy this environment.`
1593
1582
  };
1594
1583
  }
@@ -2193,6 +2182,7 @@ async function secretsApi(deployUrl, token, path, init = {}) {
2193
2182
  if (!res.ok) {
2194
2183
  const err = new Error(body.error ?? `Secrets request failed (${res.status})`);
2195
2184
  err.status = res.status;
2185
+ err.code = body.code;
2196
2186
  err.apiPath = path;
2197
2187
  throw err;
2198
2188
  }
@@ -2399,6 +2389,12 @@ import { join as join10 } from "path";
2399
2389
  import { pathToFileURL } from "url";
2400
2390
 
2401
2391
  // src/server/schemas/registry.ts
2392
+ var SYSTEM_ASSIGNED_COLUMNS = /* @__PURE__ */ new Set(["email", "name", "imageUrl", "role"]);
2393
+ var SYSTEM_MAINTAINED_COLUMNS = /* @__PURE__ */ new Set(["createdAt", "lastSeenAt"]);
2394
+ var SYSTEM_MANAGED_COLUMNS = /* @__PURE__ */ new Set([
2395
+ ...SYSTEM_ASSIGNED_COLUMNS,
2396
+ ...SYSTEM_MAINTAINED_COLUMNS
2397
+ ]);
2402
2398
  var USERS_COLUMNS = [
2403
2399
  { name: "email", storage: "text", interpretation: "plain" },
2404
2400
  { name: "name", storage: "text", interpretation: "plain" },
@@ -3063,7 +3059,7 @@ function ensurePlaywright(appDir) {
3063
3059
  import { readFileSync as readFileSync14 } from "fs";
3064
3060
 
3065
3061
  // src/testing/accounts.ts
3066
- import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync13, writeFileSync as writeFileSync5 } from "fs";
3062
+ import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync13 } from "fs";
3067
3063
  import { homedir as homedir2 } from "os";
3068
3064
  import { join as join12 } from "path";
3069
3065
  var TEST_ACCOUNTS_PATH = join12(homedir2(), ".deepspace", "test-accounts.json");
@@ -3102,7 +3098,9 @@ function reconcileTestAccountScopes(store, remote, scope) {
3102
3098
  };
3103
3099
  }
3104
3100
  function reconcileTestAccounts(local, remote) {
3105
- const localById = new Map(local.filter((account) => account.id).map((account) => [account.id, account]));
3101
+ const localById = new Map(
3102
+ local.filter((account) => account.id).map((account) => [account.id, account])
3103
+ );
3106
3104
  return remote.flatMap((account) => {
3107
3105
  const stored = localById.get(account.id);
3108
3106
  return stored ? [{ ...stored, ...account }] : [];
@@ -3137,7 +3135,9 @@ function readTestAccountStore() {
3137
3135
  return { version: 2, scopes: {} };
3138
3136
  }
3139
3137
  const candidate = raw;
3140
- const scopes = candidate.scopes && typeof candidate.scopes === "object" && !Array.isArray(candidate.scopes) ? Object.fromEntries(Object.entries(candidate.scopes).map(([key, value]) => [key, validTestAccounts(value)])) : {};
3138
+ const scopes = candidate.scopes && typeof candidate.scopes === "object" && !Array.isArray(candidate.scopes) ? Object.fromEntries(
3139
+ Object.entries(candidate.scopes).map(([key, value]) => [key, validTestAccounts(value)])
3140
+ ) : {};
3141
3141
  return { version: 2, scopes };
3142
3142
  } catch {
3143
3143
  return { version: 2, scopes: {} };
@@ -3153,7 +3153,7 @@ function validTestAccounts(value) {
3153
3153
  }
3154
3154
  function saveTestAccountStore(store) {
3155
3155
  mkdirSync5(join12(homedir2(), ".deepspace"), { recursive: true });
3156
- writeFileSync5(TEST_ACCOUNTS_PATH, JSON.stringify(store, null, 2), { mode: 384 });
3156
+ writeSecretFileSync(TEST_ACCOUNTS_PATH, JSON.stringify(store, null, 2));
3157
3157
  }
3158
3158
 
3159
3159
  // src/cli/lib/test-account-service.ts
@@ -3189,6 +3189,29 @@ async function createRemoteTestAccount(input) {
3189
3189
  }
3190
3190
  return { ...data, label: data.label ?? null };
3191
3191
  }
3192
+ async function recoverRemoteTestAccount(id) {
3193
+ const response = await fetch(`${AUTH_URL4}/api/auth/test-accounts/${id}/credential`, {
3194
+ method: "POST",
3195
+ headers: { Cookie: sessionCookie(), Origin: AUTH_URL4 }
3196
+ });
3197
+ const data = await response.json().catch(() => ({}));
3198
+ if (response.status === 404) {
3199
+ throw new Error(
3200
+ `No test account ${id} on this account. Run \`deepspace test accounts list\` for the current pool.`
3201
+ );
3202
+ }
3203
+ if (!response.ok || typeof data.password !== "string" || typeof data.email !== "string" || typeof data.userId !== "string") {
3204
+ throw new Error(data.error ?? "Failed to re-issue the test account credential");
3205
+ }
3206
+ return {
3207
+ id,
3208
+ email: data.email,
3209
+ userId: data.userId,
3210
+ label: data.label ?? null,
3211
+ createdAt: data.createdAt ?? Date.now(),
3212
+ password: data.password
3213
+ };
3214
+ }
3192
3215
  async function deleteRemoteTestAccount(id) {
3193
3216
  const response = await fetch(`${AUTH_URL4}/api/auth/test-accounts/${id}`, {
3194
3217
  method: "DELETE",
@@ -3341,10 +3364,10 @@ var test_default = defineDeepspaceCommand({
3341
3364
  if (forwarded.grep || forwarded.project || forwarded.headed) {
3342
3365
  console.error("note: --grep/--project/--headed apply to Playwright suites and are ignored by `unit`");
3343
3366
  }
3344
- exitCode = runVitest(appDir);
3367
+ exitCode = runVitest(appDir, wranglerEnv);
3345
3368
  break;
3346
3369
  case "all":
3347
- exitCode = runVitest(appDir);
3370
+ exitCode = runVitest(appDir, wranglerEnv);
3348
3371
  if (exitCode === 0) exitCode = runPlaywright(appDir, [], port, wranglerEnv, forwarded);
3349
3372
  break;
3350
3373
  case "default":
@@ -3375,7 +3398,7 @@ Available: smoke, api, e2e, unit, all`,
3375
3398
  return { data: { suite, port, appDir, wranglerEnv: wranglerEnv ?? null } };
3376
3399
  }
3377
3400
  });
3378
- function runPlaywright(appDir, testFiles, port, wranglerEnv, flags) {
3401
+ function runSuite(appDir, argv, wranglerEnv, extraEnv = {}) {
3379
3402
  let wranglerConfig;
3380
3403
  try {
3381
3404
  wranglerConfig = prepareWranglerEnvConfig(appDir, wranglerEnv);
@@ -3384,25 +3407,24 @@ function runPlaywright(appDir, testFiles, port, wranglerEnv, flags) {
3384
3407
  return 1;
3385
3408
  }
3386
3409
  try {
3387
- const result = spawnSync4("npx", playwrightTestArgs(testFiles, flags), {
3410
+ const result = spawnSync4("npx", argv, {
3388
3411
  cwd: appDir,
3389
3412
  stdio: "inherit",
3390
- env: wranglerViteEnv(process.env, wranglerConfig, {
3391
- DEEPSPACE_PORT: String(port),
3392
- PLAYWRIGHT_HTML_OUTPUT_DIR: ".deepspace/playwright-report"
3393
- })
3413
+ env: wranglerViteEnv(process.env, wranglerConfig, extraEnv)
3394
3414
  });
3395
3415
  return result.status ?? 1;
3396
3416
  } finally {
3397
3417
  wranglerConfig.cleanup();
3398
3418
  }
3399
3419
  }
3400
- function runVitest(appDir) {
3401
- const result = spawnSync4("npx", ["vitest", "run", "--passWithNoTests"], {
3402
- cwd: appDir,
3403
- stdio: "inherit"
3420
+ function runPlaywright(appDir, testFiles, port, wranglerEnv, flags = {}) {
3421
+ return runSuite(appDir, playwrightTestArgs(testFiles, flags), wranglerEnv, {
3422
+ DEEPSPACE_PORT: String(port),
3423
+ PLAYWRIGHT_HTML_OUTPUT_DIR: ".deepspace/playwright-report"
3404
3424
  });
3405
- return result.status ?? 1;
3425
+ }
3426
+ function runVitest(appDir, wranglerEnv) {
3427
+ return runSuite(appDir, ["vitest", "run", "--passWithNoTests"], wranglerEnv);
3406
3428
  }
3407
3429
 
3408
3430
  // src/cli/commands/screenshot.ts
@@ -3740,6 +3762,107 @@ Failed:
3740
3762
  return { data: { deleted: ok2, requested: targets.length, failures: [] } };
3741
3763
  }
3742
3764
  });
3765
+ var recover = defineDeepspaceCommand({
3766
+ meta: {
3767
+ name: "recover",
3768
+ description: "Re-issue the local credential for a test account you own"
3769
+ },
3770
+ args: {
3771
+ email: {
3772
+ type: "string",
3773
+ description: "Email of the test account to recover",
3774
+ required: false
3775
+ },
3776
+ id: {
3777
+ type: "string",
3778
+ description: "ID of the test account to recover",
3779
+ required: false
3780
+ },
3781
+ all: {
3782
+ type: "boolean",
3783
+ description: "Recover every pool account missing a local credential",
3784
+ required: false
3785
+ }
3786
+ },
3787
+ async run({ args }) {
3788
+ const selectors = [args.email, args.id, args.all].filter(Boolean).length;
3789
+ if (selectors === 0) {
3790
+ throw new Refusal("Provide --email <email>, --id <id>, or --all.", "missing_argument");
3791
+ }
3792
+ if (selectors > 1) {
3793
+ throw new Refusal("Provide exactly one of --email, --id, or --all.", "conflicting_arguments");
3794
+ }
3795
+ await ensureToken();
3796
+ let remote;
3797
+ try {
3798
+ remote = await fetchRemoteTestAccounts();
3799
+ } catch (err) {
3800
+ throw new Refusal(`Failed: ${err.message}`, "test_accounts_list_failed");
3801
+ }
3802
+ const localAccounts = loadAllTestAccounts();
3803
+ const known = new Set(localAccounts.map((account) => account.id));
3804
+ const localNames = new Map(
3805
+ localAccounts.flatMap(
3806
+ (account) => account.name ? [[account.email, account.name]] : []
3807
+ )
3808
+ );
3809
+ let targets;
3810
+ if (args.all) {
3811
+ targets = remote.filter((account) => !known.has(account.id));
3812
+ if (targets.length === 0) {
3813
+ if (!args.json) console.log("Every pool account already has a local credential.");
3814
+ return { data: { recovered: [], alreadyLocal: remote.length } };
3815
+ }
3816
+ } else {
3817
+ const match = args.id ? remote.find((account) => account.id === args.id) : remote.find((account) => account.email === args.email);
3818
+ if (!match) {
3819
+ throw new Refusal(
3820
+ `No test account ${args.id ? `with id ${args.id}` : `with email ${args.email}`} on this account.`,
3821
+ "test_account_not_found",
3822
+ { action: cliAction("deepspace", "test", "accounts", "list") }
3823
+ );
3824
+ }
3825
+ targets = [match];
3826
+ }
3827
+ const recovered = [];
3828
+ const failures = [];
3829
+ for (const target2 of targets) {
3830
+ try {
3831
+ const account = await recoverRemoteTestAccount(target2.id);
3832
+ const name = localNames.get(account.email);
3833
+ upsertTestAccount({
3834
+ id: account.id,
3835
+ email: account.email,
3836
+ password: account.password,
3837
+ userId: account.userId,
3838
+ label: account.label,
3839
+ ...name ? { name } : {},
3840
+ createdAt: account.createdAt
3841
+ });
3842
+ recovered.push({ id: account.id, email: account.email });
3843
+ } catch (err) {
3844
+ failures.push({ email: target2.email, error: err.message });
3845
+ }
3846
+ }
3847
+ if (failures.length > 0) {
3848
+ throw new Refusal(
3849
+ `Recovered ${recovered.length}/${targets.length} test account(s).
3850
+ Failed:
3851
+ ` + failures.map((failure) => ` ${failure.email}: ${failure.error}`).join("\n"),
3852
+ "test_account_recover_failed",
3853
+ { extra: { recovered, requested: targets.length, failures } }
3854
+ );
3855
+ }
3856
+ if (!args.json) {
3857
+ for (const account of recovered) console.log(`Recovered ${account.email}`);
3858
+ console.log(
3859
+ `
3860
+ Stored in ${TEST_ACCOUNTS_PATH}. The previous password (if any machine still holds one) no longer works \u2014 recover there too.`
3861
+ );
3862
+ }
3863
+ return { data: { recovered, requested: targets.length, failures: [] } };
3864
+ }
3865
+ });
3743
3866
  var test_accounts_default = defineCommand2({
3744
3867
  meta: {
3745
3868
  name: "test-accounts",
@@ -3749,7 +3872,8 @@ var test_accounts_default = defineCommand2({
3749
3872
  create,
3750
3873
  list,
3751
3874
  delete: del,
3752
- clear
3875
+ clear,
3876
+ recover
3753
3877
  }
3754
3878
  });
3755
3879
 
@@ -4469,7 +4593,7 @@ function declaredCompatibilityDate(config) {
4469
4593
  return typeof declared === "string" && /^\d{4}-\d{2}-\d{2}$/.test(declared) ? declared : null;
4470
4594
  }
4471
4595
  async function buildDeployBundle(options) {
4472
- const { appDir, appName, envName, output, spinner: spinner2 } = options;
4596
+ const { appDir, appName, appId, envName, output, spinner: spinner2 } = options;
4473
4597
  const junk = removeMacosJunk(appDir);
4474
4598
  if (junk > 0) p6.log.info(`Removed ${junk} macOS metadata file(s) (._*, .DS_Store)`);
4475
4599
  const lintFindings = await lintProjectSchemas(appDir);
@@ -4544,6 +4668,8 @@ async function buildDeployBundle(options) {
4544
4668
  const assets = collectAssets(clientDir);
4545
4669
  const assetConfig = readDeployAssetConfig(clientDir);
4546
4670
  spinner2.stop(`Collected ${assets.length} assets`);
4671
+ const appIdRefusal = clientAppIdRefusal(assets, appId, envName);
4672
+ if (appIdRefusal) output.die(appIdRefusal.message, appIdRefusal.code);
4547
4673
  const workerJs = readFileSync17(workerBundlePath, "utf-8");
4548
4674
  const doBindings = outputConfig.durable_objects?.bindings;
4549
4675
  const sqliteClasses = new Set(
@@ -4612,6 +4738,44 @@ function oversizedAssetRefusal(assets) {
4612
4738
  if (!tooBig) return null;
4613
4739
  return `${tooBig.path} is ${formatBytes(tooBig.size)}; the per-file limit for deploy assets is ${formatBytes(MAX_DEPLOY_ASSET_FILE_BYTES)}. ${LARGE_MEDIA_HOME}`;
4614
4740
  }
4741
+ var CLIENT_CODE_ASSET = /\.(?:js|mjs|html)$/i;
4742
+ var APP_ID_LITERAL = /app_[0-9A-HJKMNP-TV-Z]{26}/g;
4743
+ var APP_ID_DEFINE = "__DEEPSPACE_APP_ID__";
4744
+ var APP_ID_ADOPTION_STEPS = `Point the client at the build-injected id \u2014 the SDK's \`deepspace/build\` supplies it:
4745
+ \u2022 vite.config.ts: import { deepspaceBuild } from 'deepspace/build' and add
4746
+ deepspaceBuild({ appDir: fileURLToPath(new URL('.', import.meta.url)) }) to plugins
4747
+ \u2022 vitest.config.ts: import { appIdDefine } and set define: appIdDefine({ appDir })
4748
+ \u2022 src/constants.ts: declare const __DEEPSPACE_APP_ID__: string; export it as APP_ID
4749
+ (drop the frozen literal)
4750
+ 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.`;
4751
+ function clientAppIdRefusal(assets, appId, envName) {
4752
+ const foreign = /* @__PURE__ */ new Map();
4753
+ let unsubstituted;
4754
+ for (const asset of assets) {
4755
+ if (!CLIENT_CODE_ASSET.test(asset.path)) continue;
4756
+ const source = readFileSync17(asset.sourcePath, "utf-8");
4757
+ if (unsubstituted === void 0 && source.includes(APP_ID_DEFINE)) unsubstituted = asset.path;
4758
+ for (const found of source.matchAll(APP_ID_LITERAL)) {
4759
+ if (found[0] !== appId && !foreign.has(found[0])) foreign.set(found[0], asset.path);
4760
+ }
4761
+ }
4762
+ if (unsubstituted !== void 0) {
4763
+ return {
4764
+ code: "app_id_define_unsubstituted",
4765
+ 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.
4766
+ ${APP_ID_ADOPTION_STEPS}`
4767
+ };
4768
+ }
4769
+ if (foreign.size === 0) return null;
4770
+ const target2 = envName ? `--env ${envName} (${appId})` : appId;
4771
+ return {
4772
+ code: "app_id_env_mismatch",
4773
+ message: `Built client assets carry a different app id than the one being deployed. Deploying ${target2}, but the bundle contains:
4774
+ ` + [...foreign].map(([id, path]) => ` \u2022 ${id} in ${path}`).join("\n") + `
4775
+ 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.
4776
+ ${APP_ID_ADOPTION_STEPS}`
4777
+ };
4778
+ }
4615
4779
  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.";
4616
4780
  function collectAssets(dir) {
4617
4781
  const assets = [];
@@ -6251,7 +6415,7 @@ var deploy_default = defineCommand3({
6251
6415
  output.showIntro();
6252
6416
  const wranglerConfig = readWranglerConfig(appDir);
6253
6417
  const nameResult = resolveAppNameForEnv(wranglerConfig, envName);
6254
- if (!nameResult.ok) return output.die(nameResult.reason, "invalid_app_name");
6418
+ if (!nameResult.ok) return output.die(nameResult.reason, nameResult.code ?? "invalid_app_name");
6255
6419
  const declaredName = envName ? wranglerConfig.env?.[envName]?.name : wranglerConfig.name;
6256
6420
  if (declaredName !== nameResult.name) {
6257
6421
  const location = envName ? `[env.${envName}].name` : "`name`";
@@ -6267,7 +6431,14 @@ var deploy_default = defineCommand3({
6267
6431
  if (!appId) {
6268
6432
  return output.die(
6269
6433
  `No app id in wrangler.toml${envName ? ` for [env.${envName}]` : ""}. Run \`deepspace app init${envName ? ` --env ${envName}` : ""}\` to register this app, then re-deploy.`,
6270
- "app_not_initialized"
6434
+ "app_not_initialized",
6435
+ {
6436
+ action: {
6437
+ cwd: appDir,
6438
+ argv: ["deepspace", "app", "init", ...envName ? ["--env", envName] : []]
6439
+ },
6440
+ actionRequired: true
6441
+ }
6271
6442
  );
6272
6443
  }
6273
6444
  p12.log.info(`Id: ${appId}`);
@@ -6281,6 +6452,12 @@ var deploy_default = defineCommand3({
6281
6452
  output
6282
6453
  });
6283
6454
  const sourceState = await getAppSource(DEPLOY_URL3, token, appId);
6455
+ if (!sourceState.registered) {
6456
+ output.die(
6457
+ `${appId} is not registered. If this repo's id came from an older SDK's scaffold, run \`deepspace app init --new-id\` to register it as a fresh app; a brand-new app dir registers with \`deepspace app init\`.`,
6458
+ "app_not_registered"
6459
+ );
6460
+ }
6284
6461
  const repositoryPreflight = preflightDeployRepository({
6285
6462
  appDir,
6286
6463
  push: args.push !== false,
@@ -6293,6 +6470,7 @@ var deploy_default = defineCommand3({
6293
6470
  const bundle = await buildDeployBundle({
6294
6471
  appDir,
6295
6472
  appName,
6473
+ appId,
6296
6474
  envName,
6297
6475
  output,
6298
6476
  spinner: spinner2
@@ -6556,7 +6734,7 @@ async function warnIfPhantomApp(deployUrl, token, appId, explicit) {
6556
6734
  const apps = await listApps(deployUrl, token);
6557
6735
  if (!apps.some((app2) => app2.appId === appId)) {
6558
6736
  process.stderr.write(
6559
- `warning: ${appId} is not among your apps. If it exists and you have access (collaborator/admin), the server will use it; if it doesn't exist, this will register it as a NEW app under your account \u2014 check the id if that's not what you meant (a subdomain name also works with -a).
6737
+ `warning: ${appId} is not among your apps. If it exists and you have access (collaborator/admin), the server will use it; if it doesn't exist, the command will be refused (app_not_registered \u2014 ids are registered at \`deepspace app init\`). Check the id (a subdomain name also works with -a).
6560
6738
  `
6561
6739
  );
6562
6740
  }
@@ -7128,7 +7306,7 @@ import {
7128
7306
  readSync,
7129
7307
  readdirSync as readdirSync5,
7130
7308
  rmSync as rmSync4,
7131
- writeFileSync as writeFileSync6
7309
+ writeFileSync as writeFileSync5
7132
7310
  } from "fs";
7133
7311
  import { spawnSync as spawnSync6 } from "child_process";
7134
7312
  import { tmpdir as tmpdir2 } from "os";
@@ -7227,7 +7405,7 @@ function copyFileWithImportShift(src, dest, levels) {
7227
7405
  /(from\s+|import\s+|import\()(['"])(\.\.\/)/g,
7228
7406
  `$1$2${prefix}$3`
7229
7407
  );
7230
- writeFileSync6(dest, shifted);
7408
+ writeFileSync5(dest, shifted);
7231
7409
  }
7232
7410
  function installFiles(packageRoot, config, targetDir, emit) {
7233
7411
  let copied = 0;
@@ -7313,7 +7491,7 @@ export const schemas`
7313
7491
  return [unresolvedSchema(config.schema, reason, emit)];
7314
7492
  }
7315
7493
  content = withEntry;
7316
- writeFileSync6(schemasPath, content);
7494
+ writeFileSync5(schemasPath, content);
7317
7495
  emit(` Schema integrated: ${exportName} -> schemas.ts`);
7318
7496
  return [];
7319
7497
  }
@@ -7379,7 +7557,7 @@ ${registryEntry}`);
7379
7557
  integrated++;
7380
7558
  emit(` Action integrated: ${action.registerAs} (${action.exportName})`);
7381
7559
  }
7382
- if (integrated > 0) writeFileSync6(actionsPath, content);
7560
+ if (integrated > 0) writeFileSync5(actionsPath, content);
7383
7561
  return unresolved;
7384
7562
  }
7385
7563
  function integrateCode(config, targetDir, emit) {
@@ -7424,7 +7602,7 @@ ${content.slice(
7424
7602
  const prefix = insertion.placement === "before" ? "" : "\n";
7425
7603
  const suffix = insertion.placement === "before" ? "\n" : "";
7426
7604
  content = `${content.slice(0, insertAt)}${prefix}${insertion.insert}${suffix}${content.slice(insertAt)}`;
7427
- writeFileSync6(targetPath, content);
7605
+ writeFileSync5(targetPath, content);
7428
7606
  emit(` Integrated: ${insertion.file}`);
7429
7607
  }
7430
7608
  return unresolved;
@@ -7437,7 +7615,7 @@ function integrateIgnore(config, targetDir) {
7437
7615
  const missing = config.ignore.filter((pattern) => !present.has(pattern));
7438
7616
  if (missing.length === 0) return;
7439
7617
  const separator = current === "" || current.endsWith("\n") ? "" : "\n";
7440
- writeFileSync6(targetPath, `${current}${separator}${missing.join("\n")}
7618
+ writeFileSync5(targetPath, `${current}${separator}${missing.join("\n")}
7441
7619
  `);
7442
7620
  }
7443
7621
  function integrateWrangler(config, targetDir, emit) {
@@ -7474,7 +7652,7 @@ ${rateLimitBindingBlock(scope, binding)}
7474
7652
  }
7475
7653
  }
7476
7654
  if (changed) {
7477
- writeFileSync6(targetPath, source);
7655
+ writeFileSync5(targetPath, source);
7478
7656
  emit(" Integrated: wrangler.toml");
7479
7657
  } else emit(" Integration already present: wrangler.toml");
7480
7658
  return [];
@@ -7567,7 +7745,7 @@ ${css}`;
7567
7745
  integrated++;
7568
7746
  emit(` CSS integrated: ${cssFile} -> styles.css`);
7569
7747
  }
7570
- if (integrated > 0) writeFileSync6(stylesPath, styles);
7748
+ if (integrated > 0) writeFileSync5(stylesPath, styles);
7571
7749
  return unresolved;
7572
7750
  }
7573
7751
  function unresolvedRoute(route, config, reason, emit) {
@@ -7614,7 +7792,7 @@ function integrateRoute(config, targetDir, emit) {
7614
7792
  const entry = ` { path: '${routePath}', label: '${config.name}'${roles} },`;
7615
7793
  content = content.replace(NAV_MARKER, `${NAV_MARKER}
7616
7794
  ${entry}`);
7617
- writeFileSync6(navPath, content);
7795
+ writeFileSync5(navPath, content);
7618
7796
  emit(` Nav wired: ${routePath} (${config.name})`);
7619
7797
  emit(" Route: automatic (file-based routing via src/pages/)");
7620
7798
  return [];
@@ -7698,7 +7876,7 @@ function integrateDependencies(config, targetDir, runInstall, outputVisible, emi
7698
7876
  if (added.length === 0) {
7699
7877
  emit(" Dependencies already present");
7700
7878
  } else {
7701
- writeFileSync6(packagePath, `${JSON.stringify(pkg2, null, 2)}
7879
+ writeFileSync5(packagePath, `${JSON.stringify(pkg2, null, 2)}
7702
7880
  `, "utf-8");
7703
7881
  emit(` Added to package.json: ${added.map(({ name }) => name).join(", ")}`);
7704
7882
  if (skipped.length) emit(` Already present (kept your version): ${skipped.join(", ")}`);
@@ -10291,6 +10469,25 @@ var init_default = defineDeepspaceCommand({
10291
10469
  const envName = args.env || void 0;
10292
10470
  const existing = readAppId(appDir, envName);
10293
10471
  if (existing && !args["new-id"]) {
10472
+ const token2 = await ensureToken();
10473
+ let state;
10474
+ try {
10475
+ state = await getAppSource(DEPLOY_URL12, token2, existing);
10476
+ } catch (error) {
10477
+ if (error instanceof ApiError && error.code === "forbidden") {
10478
+ throw new Refusal(
10479
+ `wrangler.toml carries ${existing}, which is registered to another user. Run \`deepspace app init --new-id\` to fork this repo as your own app (new data and secrets; the original is untouched).`,
10480
+ "not_app_owner"
10481
+ );
10482
+ }
10483
+ throw error;
10484
+ }
10485
+ if (!state.registered) {
10486
+ throw new Refusal(
10487
+ `wrangler.toml carries ${existing}, but that id was never registered \u2014 it was minted locally by an older SDK. Ids are server-minted at registration now, and an existing unregistered id cannot be claimed. Run \`deepspace app init --new-id\` to register this repo as a fresh app (new data and secrets; nothing to migrate \u2014 the old id never had server-side state).`,
10488
+ "app_not_registered"
10489
+ );
10490
+ }
10294
10491
  if (!args.json) {
10295
10492
  console.log(`Already initialized: ${existing}${envName ? ` (env: ${envName})` : ""}`);
10296
10493
  console.log(`App dir: ${appDir}`);
@@ -10304,7 +10501,6 @@ var init_default = defineDeepspaceCommand({
10304
10501
  method: "POST"
10305
10502
  });
10306
10503
  writeAppId(appDir, appId, { wranglerEnv: envName, force: Boolean(args["new-id"]) });
10307
- if (!envName) stampAppIdInSources(appDir, appId, existing);
10308
10504
  const committedScaffold = commitScaffoldIfUnborn(appDir, token);
10309
10505
  if (!args.json) {
10310
10506
  if (existing) {
@@ -10747,6 +10943,24 @@ var push_default = defineDeepspaceCommand({
10747
10943
  );
10748
10944
  }
10749
10945
  const { token, appId } = await resolveTarget3();
10946
+ let sourceState;
10947
+ try {
10948
+ sourceState = await getAppSource(deployBaseUrl(), token, appId);
10949
+ } catch (error) {
10950
+ if (error instanceof ApiError && error.code === "forbidden") {
10951
+ throw new Refusal(
10952
+ `${appId} is registered to another user. Run \`deepspace app init --new-id\` to fork this repo as your own app (new data and secrets; the original is untouched).`,
10953
+ "not_app_owner"
10954
+ );
10955
+ }
10956
+ throw error;
10957
+ }
10958
+ if (!sourceState.registered) {
10959
+ throw new Refusal(
10960
+ `${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\`.`,
10961
+ "app_not_registered"
10962
+ );
10963
+ }
10750
10964
  const pullRecoveryCwd = selectedBranchCheckout(appDir, branch) ?? appDir;
10751
10965
  ensureSpaceRemote(appDir, appId);
10752
10966
  if (force) {
@@ -11611,7 +11825,7 @@ import {
11611
11825
  realpathSync as realpathSync6,
11612
11826
  renameSync as renameSync2,
11613
11827
  statSync as statSync3,
11614
- writeFileSync as writeFileSync7
11828
+ writeFileSync as writeFileSync6
11615
11829
  } from "fs";
11616
11830
  import { dirname as dirname11, isAbsolute as isAbsolute5, join as join22, relative as relative2, sep as sep2 } from "path";
11617
11831
  var WORKSPACE_MARKER = "deepspace-workspace.json";
@@ -11646,7 +11860,7 @@ function markManagedWorkspace(worktreeRoot, workspaceId) {
11646
11860
  const tempPath = `${markerPath}.${process.pid}.tmp`;
11647
11861
  try {
11648
11862
  mkdirSync7(dirname11(markerPath), { recursive: true });
11649
- writeFileSync7(tempPath, JSON.stringify(marker) + "\n");
11863
+ writeFileSync6(tempPath, JSON.stringify(marker) + "\n");
11650
11864
  renameSync2(tempPath, markerPath);
11651
11865
  return true;
11652
11866
  } catch {
@@ -13780,13 +13994,13 @@ function sameRefs(left, right) {
13780
13994
 
13781
13995
  // src/cli/commands/update.ts
13782
13996
  import * as p34 from "@clack/prompts";
13783
- import { existsSync as existsSync26, readFileSync as readFileSync27, writeFileSync as writeFileSync8 } from "fs";
13997
+ import { existsSync as existsSync26, readFileSync as readFileSync27, writeFileSync as writeFileSync7 } from "fs";
13784
13998
  import { join as join24, resolve as resolve20 } from "path";
13785
13999
 
13786
14000
  // package.json
13787
14001
  var package_default = {
13788
14002
  name: "deepspace",
13789
- version: "0.22.0",
14003
+ version: "0.23.0",
13790
14004
  engines: {
13791
14005
  node: ">=22.15.0 <23 || >=24 <25 || >=26 <27"
13792
14006
  },
@@ -13823,6 +14037,10 @@ var package_default = {
13823
14037
  types: "./dist/testing-mcp.d.ts",
13824
14038
  import: "./dist/testing-mcp.js"
13825
14039
  },
14040
+ "./build": {
14041
+ types: "./dist/build.d.ts",
14042
+ import: "./dist/build.js"
14043
+ },
13826
14044
  "./documentation": {
13827
14045
  types: "./dist/documentation.d.ts",
13828
14046
  import: "./dist/documentation.js"
@@ -13986,7 +14204,7 @@ function pinSdkVersion(appDir, version) {
13986
14204
  const eol = source.includes("\r\n") ? "\r\n" : "\n";
13987
14205
  const indent = source.match(/\r?\n([\t ]+)"/)?.[1] ?? " ";
13988
14206
  const finalEol = source.endsWith("\n") ? eol : "";
13989
- writeFileSync8(path, JSON.stringify(manifest, null, indent).replaceAll("\n", eol) + finalEol);
14207
+ writeFileSync7(path, JSON.stringify(manifest, null, indent).replaceAll("\n", eol) + finalEol);
13990
14208
  return true;
13991
14209
  }
13992
14210
  function buildPreviewSecretsUpgradeInstruction(appDir) {
@@ -13998,8 +14216,10 @@ function buildPreviewSecretsUpgradeInstruction(appDir) {
13998
14216
  ].find((candidate) => existsSync26(join24(appDir, candidate)));
13999
14217
  if (!configName) return null;
14000
14218
  const source = readFileSync27(join24(appDir, configName), "utf8");
14001
- if (source.includes("deepspace-remove-build-preview-secrets")) return null;
14002
- return `${configName}: add the build-preview secret cleanup from ${BUILD_PREVIEW_SECRETS_GUIDE}; app update left this app-owned file unchanged.`;
14219
+ if (source.includes("deepspace-remove-build-preview-secrets") || source.includes("deepspaceBuild(") || source.includes("'deepspace/build'") || source.includes('"deepspace/build"')) {
14220
+ return null;
14221
+ }
14222
+ 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.`;
14003
14223
  }
14004
14224
  function usersSchemaVisibilityUpgradeInstruction(appDir) {
14005
14225
  const schemaName = "src/schemas/users-schema.ts";