deepspace 0.13.0 → 0.15.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
@@ -3,8 +3,8 @@
3
3
  // src/cli/cli.ts
4
4
  import { defineCommand as defineCommand14, runMain, runCommand, renderUsage } from "citty";
5
5
  import { sync as spawnSync8 } from "cross-spawn";
6
- import { readFileSync as readFileSync28, existsSync as existsSync26 } from "fs";
7
- import { dirname as dirname10, join as join24 } from "path";
6
+ import { readFileSync as readFileSync29, existsSync as existsSync27 } from "fs";
7
+ import { dirname as dirname10, join as join25 } from "path";
8
8
  import { fileURLToPath as fileURLToPath3 } from "url";
9
9
 
10
10
  // src/cli/lib/spinner.ts
@@ -89,11 +89,12 @@ function printAction(action) {
89
89
 
90
90
  // src/cli/lib/api.ts
91
91
  var ApiError = class extends Error {
92
- constructor(message, status3, code, apiPath) {
92
+ constructor(message, status3, code, apiPath, details) {
93
93
  super(message);
94
94
  this.status = status3;
95
95
  this.code = code;
96
96
  this.apiPath = apiPath;
97
+ this.details = details;
97
98
  }
98
99
  };
99
100
  var refreshAuthToken = null;
@@ -387,6 +388,9 @@ var Refusal = class extends Error {
387
388
  this.extra = opts.extra ?? {};
388
389
  }
389
390
  };
391
+ function apiErrorDetails(err) {
392
+ return err instanceof ApiError ? err.details : void 0;
393
+ }
390
394
  function withoutReservedKeys(payload) {
391
395
  return Object.fromEntries(
392
396
  Object.entries(payload).filter(
@@ -450,7 +454,7 @@ function defineDeepspaceCommand(def) {
450
454
  ...refusal?.actionRequired ? { actionRequired: true } : {},
451
455
  error: message,
452
456
  ...refusal?.action ? { action: refusal.action } : {},
453
- ...withoutReservedKeys(refusal?.extra ?? {})
457
+ ...withoutReservedKeys(refusal?.extra ?? apiErrorDetails(err) ?? {})
454
458
  })
455
459
  );
456
460
  } else {
@@ -734,7 +738,7 @@ async function provisionProfile(jwt) {
734
738
  }
735
739
  }
736
740
  function sleep(ms) {
737
- return new Promise((resolve19) => setTimeout(resolve19, ms));
741
+ return new Promise((resolve20) => setTimeout(resolve20, ms));
738
742
  }
739
743
  function base64url(bytes) {
740
744
  return bytes.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
@@ -1305,7 +1309,7 @@ function upsertWorktreeLaunchConfig(mainRepoRoot, worktreeName, appDir, opts) {
1305
1309
  let port = opts.port;
1306
1310
  if (opts.probePort) {
1307
1311
  const taken = new Set(
1308
- config.configurations.map((c) => Number(c?.port)).filter((p34) => Number.isInteger(p34) && p34 > 0)
1312
+ config.configurations.map((c) => Number(c?.port)).filter((p35) => Number.isInteger(p35) && p35 > 0)
1309
1313
  );
1310
1314
  for (let i = 0; i < WORKTREE_PORT_RANGE && taken.has(port); i++) {
1311
1315
  port = WORKTREE_PORT_MIN + (port - WORKTREE_PORT_MIN + 1) % WORKTREE_PORT_RANGE;
@@ -2328,18 +2332,18 @@ function ensureInstallReady(appDir) {
2328
2332
  const logPath = join9(sentinelDir, "install.log");
2329
2333
  if (existsSync9(errPath)) {
2330
2334
  const detail = readFileSync11(errPath, "utf-8").trimEnd();
2331
- const log28 = existsSync9(logPath) ? ` Full log: ${logPath}.` : "";
2335
+ const log29 = existsSync9(logPath) ? ` Full log: ${logPath}.` : "";
2332
2336
  throw new Refusal(
2333
- `Dependency install failed: ${detail}.${log28} Run \`npm install\` (or \`bun install\`) manually, then retry.`,
2337
+ `Dependency install failed: ${detail}.${log29} Run \`npm install\` (or \`bun install\`) manually, then retry.`,
2334
2338
  "install_failed",
2335
2339
  { action: { cwd: appDir, argv: [detectPackageManager(appDir), "install"] } }
2336
2340
  );
2337
2341
  }
2338
2342
  if (existsSync9(startedPath) && !existsSync9(donePath)) {
2339
2343
  if (!installWorkerAlive(sentinelDir)) {
2340
- const log28 = existsSync9(logPath) ? ` See what happened: ${logPath}.` : "";
2344
+ const log29 = existsSync9(logPath) ? ` See what happened: ${logPath}.` : "";
2341
2345
  throw new Refusal(
2342
- `The dependency install is no longer running and never finished.${log28} Run \`npm install\` (or \`bun install\`) manually, then retry.`,
2346
+ `The dependency install is no longer running and never finished.${log29} Run \`npm install\` (or \`bun install\`) manually, then retry.`,
2343
2347
  "install_failed",
2344
2348
  { action: { cwd: appDir, argv: [detectPackageManager(appDir), "install"] } }
2345
2349
  );
@@ -2488,7 +2492,7 @@ function lintSchema(schema) {
2488
2492
  const offendingRoles = Object.entries(schema.permissions).filter(([, perms]) => perms.read === true).map(([role]) => role);
2489
2493
  if (offendingRoles.length > 0) {
2490
2494
  const usesAware = Object.values(schema.permissions).some(
2491
- (p34) => typeof p34.read === "string" && VISIBILITY_AWARE.includes(p34.read)
2495
+ (p35) => typeof p35.read === "string" && VISIBILITY_AWARE.includes(p35.read)
2492
2496
  );
2493
2497
  if (!usesAware) {
2494
2498
  warnings.push(
@@ -2573,11 +2577,11 @@ import { createServer, Socket } from "net";
2573
2577
  var DEFAULT_PORT = 5173;
2574
2578
  function isPortListening(port, host, timeoutMs = 500) {
2575
2579
  const target2 = host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host;
2576
- return new Promise((resolve19) => {
2580
+ return new Promise((resolve20) => {
2577
2581
  const socket = new Socket();
2578
2582
  const done = (listening) => {
2579
2583
  socket.destroy();
2580
- resolve19(listening);
2584
+ resolve20(listening);
2581
2585
  };
2582
2586
  socket.setTimeout(timeoutMs);
2583
2587
  socket.once("connect", () => done(true));
@@ -2590,7 +2594,7 @@ async function waitForPortListening(port, host, timeoutMs) {
2590
2594
  const deadline = Date.now() + timeoutMs;
2591
2595
  while (Date.now() < deadline) {
2592
2596
  if (await isPortListening(port, host)) return true;
2593
- await new Promise((resolve19) => setTimeout(resolve19, 250));
2597
+ await new Promise((resolve20) => setTimeout(resolve20, 250));
2594
2598
  }
2595
2599
  return false;
2596
2600
  }
@@ -3797,30 +3801,97 @@ import { defineCommand as defineCommand3 } from "citty";
3797
3801
  import { resolve as resolve12 } from "path";
3798
3802
 
3799
3803
  // src/cli/lib/edge-propagation.ts
3800
- var NOT_SERVING = ["Assets have not yet deployed", "No app configured"];
3801
- async function waitForEdgePropagation(url, timeoutMs) {
3804
+ import { request as httpRequest } from "http";
3805
+ import { request as httpsRequest } from "https";
3806
+
3807
+ // src/shared/app-routing.ts
3808
+ var RELEASE_STAMP_PATH = "/.well-known/deepspace/release.json";
3809
+ var SDK_RUN_WORKER_FIRST = [
3810
+ "/api/*",
3811
+ "/ws/*",
3812
+ "/internal/*",
3813
+ "/v1/*",
3814
+ "/_deepspace/*",
3815
+ "/llms.txt",
3816
+ "/llms-full.txt",
3817
+ "/.well-known/mcp",
3818
+ "/.well-known/mcp.json",
3819
+ "/.well-known/mcp/*"
3820
+ ];
3821
+ var DOCUMENTATION_PATHS = [
3822
+ "/_documentation",
3823
+ "/_documentation/*",
3824
+ "/_documentation-root",
3825
+ "/_documentation-root/*"
3826
+ ];
3827
+ var RESERVED_RUN_WORKER_FIRST = [
3828
+ ...SDK_RUN_WORKER_FIRST,
3829
+ ...DOCUMENTATION_PATHS
3830
+ ];
3831
+
3832
+ // src/cli/lib/edge-propagation.ts
3833
+ var CONSECUTIVE_MATCHES = 10;
3834
+ var MATCH_INTERVAL_MS = 250;
3835
+ async function waitForLiveRelease(url, releaseStamp, timeoutMs) {
3836
+ if (!releaseStamp) return "unverifiable";
3802
3837
  const deadline = Date.now() + timeoutMs;
3803
3838
  let attempt = 0;
3804
3839
  let streak = 0;
3805
3840
  while (Date.now() < deadline) {
3806
3841
  attempt++;
3807
- try {
3808
- const response = await fetch(url, { redirect: "manual" });
3809
- const body = await response.text();
3810
- if (!NOT_SERVING.some((marker) => body.includes(marker))) {
3811
- streak++;
3812
- if (streak >= 3) return true;
3813
- await new Promise((resolve19) => setTimeout(resolve19, 1500));
3814
- continue;
3815
- }
3816
- streak = 0;
3817
- } catch {
3818
- streak = 0;
3842
+ const probe = new URL(RELEASE_STAMP_PATH, url);
3843
+ probe.searchParams.set("probe", `${Date.now()}-${attempt}`);
3844
+ if (await servesRelease(probe, releaseStamp)) {
3845
+ streak++;
3846
+ if (streak >= CONSECUTIVE_MATCHES) return "confirmed";
3847
+ await sleep2(MATCH_INTERVAL_MS);
3848
+ continue;
3819
3849
  }
3820
- const waitMs = Math.min(8e3, 1e3 * 2 ** (attempt - 1));
3821
- await new Promise((resolve19) => setTimeout(resolve19, waitMs));
3850
+ streak = 0;
3851
+ await sleep2(Math.min(4e3, 500 * 2 ** Math.min(attempt - 1, 3)));
3852
+ }
3853
+ return "unconfirmed";
3854
+ }
3855
+ async function servesRelease(probe, releaseStamp) {
3856
+ const body = await getOnFreshConnection(probe);
3857
+ if (body === null) return false;
3858
+ try {
3859
+ return JSON.parse(body).release === releaseStamp;
3860
+ } catch {
3861
+ return false;
3822
3862
  }
3823
- return false;
3863
+ }
3864
+ function getOnFreshConnection(url, timeoutMs = 5e3) {
3865
+ const send = url.protocol === "http:" ? httpRequest : httpsRequest;
3866
+ return new Promise((resolve20) => {
3867
+ const request2 = send(
3868
+ url,
3869
+ { method: "GET", agent: false, timeout: timeoutMs, headers: { "cache-control": "no-cache" } },
3870
+ (response) => {
3871
+ if (response.statusCode !== 200) {
3872
+ response.resume();
3873
+ resolve20(null);
3874
+ return;
3875
+ }
3876
+ let body = "";
3877
+ response.setEncoding("utf8");
3878
+ response.on("data", (chunk) => {
3879
+ if (body.length < 4096) body += chunk;
3880
+ });
3881
+ response.on("end", () => resolve20(body));
3882
+ response.on("error", () => resolve20(null));
3883
+ }
3884
+ );
3885
+ request2.on("timeout", () => {
3886
+ request2.destroy();
3887
+ resolve20(null);
3888
+ });
3889
+ request2.on("error", () => resolve20(null));
3890
+ request2.end();
3891
+ });
3892
+ }
3893
+ function sleep2(ms) {
3894
+ return new Promise((resolve20) => setTimeout(resolve20, ms));
3824
3895
  }
3825
3896
 
3826
3897
  // src/shared/app-files.ts
@@ -3840,6 +3911,13 @@ function planUploadParts(totalBytes, partSize) {
3840
3911
  }
3841
3912
  return parts;
3842
3913
  }
3914
+ var APP_STORAGE_LIMIT_BYTES = {
3915
+ test: 0,
3916
+ free: 128 * 1024 * 1024,
3917
+ starter: 512 * 1024 * 1024,
3918
+ premium: 2 * 1024 * 1024 * 1024,
3919
+ admin: 10 * 1024 * 1024 * 1024
3920
+ };
3843
3921
  var MAX_DEPLOY_ASSET_FILE_BYTES = 25 * 1024 * 1024;
3844
3922
  var DANGEROUS_MIME_TYPES = /* @__PURE__ */ new Set([
3845
3923
  "text/html",
@@ -3907,7 +3985,7 @@ function readDocumentationDeployManifest(appDir, clientDir) {
3907
3985
  return JSON.parse(readFileSync15(manifestPath, "utf8"));
3908
3986
  }
3909
3987
 
3910
- // src/cli/commands/migrate/app-migrations.ts
3988
+ // src/cli/commands/update/app-migrations.ts
3911
3989
  import {
3912
3990
  closeSync,
3913
3991
  constants,
@@ -3924,7 +4002,7 @@ import { join as join15 } from "path";
3924
4002
  // src/shared/protocol/app-migrations.ts
3925
4003
  var APP_MIGRATION_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/;
3926
4004
  var MAX_APP_MIGRATION_IDS = 256;
3927
- var CANONICAL_APP_IDENTITY_MIGRATION_ID = "2026-08-canonical-app-identity-wire";
4005
+ var WORKER_OWNED_NOT_FOUND_MIGRATION_ID = "2026-08-worker-owned-not-found";
3928
4006
  function validateAppMigrationIds(value) {
3929
4007
  if (!Array.isArray(value)) {
3930
4008
  return { valid: false, reason: "app migrations must be a JSON string array" };
@@ -3950,14 +4028,14 @@ function validateAppMigrationIds(value) {
3950
4028
  return { valid: true, ids };
3951
4029
  }
3952
4030
 
3953
- // src/cli/commands/migrate/app-migrations.ts
4031
+ // src/cli/commands/update/app-migrations.ts
3954
4032
  var APP_MIGRATIONS_MANIFEST = "deepspace.migrations.json";
3955
4033
  var SOURCE_MIGRATIONS = [
3956
4034
  {
3957
- id: CANONICAL_APP_IDENTITY_MIGRATION_ID,
3958
- description: "Use the immutable app id for authenticated platform requests",
3959
- transform: migrateCanonicalIdentityWire,
3960
- findBlockers: findLegacyIdentityWireBlockers
4035
+ id: WORKER_OWNED_NOT_FOUND_MIGRATION_ID,
4036
+ description: "Let the worker answer misses, so deleted files 404 instead of serving HTML",
4037
+ transform: migrateWorkerOwnedNotFound,
4038
+ findBlockers: findWorkerOwnedNotFoundBlockers
3961
4039
  }
3962
4040
  ];
3963
4041
  var APP_MIGRATION_DEFINITIONS = SOURCE_MIGRATIONS.map(
@@ -4002,7 +4080,7 @@ function planAppMigrations(appDir) {
4002
4080
  const before = readRegularFile(absolutePath);
4003
4081
  let after = before;
4004
4082
  for (const migration of pendingMigrations) {
4005
- const result = migration.transform(after);
4083
+ const result = migration.transform(after, repoPath);
4006
4084
  after = result.source;
4007
4085
  if (result.replacements > 0) {
4008
4086
  const planned = perMigration.get(migration.id) ?? {
@@ -4039,9 +4117,6 @@ function planAppMigrations(appDir) {
4039
4117
  };
4040
4118
  }
4041
4119
  function applyAppMigrationPlan(appDir, plan) {
4042
- if (plan.blockers.length > 0) {
4043
- throw new Error("Cannot apply an app migration plan with unresolved blockers.");
4044
- }
4045
4120
  const repo = repoToplevel(appDir);
4046
4121
  for (const edit of plan.edits) {
4047
4122
  const absolutePath = join15(repo, edit.path);
@@ -4101,119 +4176,78 @@ function writeRegularFile(path, before, after) {
4101
4176
  }
4102
4177
  function isSourcePath(repoPath, appPrefix) {
4103
4178
  if (appPrefix && !repoPath.startsWith(appPrefix)) return false;
4179
+ if (repoPath.endsWith("wrangler.toml")) return true;
4104
4180
  return /\.(?:[cm]?[jt]sx?)$/.test(repoPath);
4105
4181
  }
4106
- function migrateCanonicalIdentityWire(source) {
4182
+ function migrateWorkerOwnedNotFound(source, file) {
4107
4183
  let replacements = 0;
4108
- const lines = source.split("\n");
4109
- for (let index = 0; index < lines.length; index++) {
4110
- let line = lines[index];
4111
- line = line.replace(
4112
- /(\.set\(\s*)(['"])x-app-name\2(\s*,\s*)([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\.APP_NAME(\s*\))/g,
4113
- (_match, open, quote, separator, receiver, close) => {
4114
- replacements++;
4115
- return `${open}${quote}x-app-id${quote}${separator}${receiver}.DEEPSPACE_APP_ID${close}`;
4116
- }
4117
- );
4118
- line = line.replace(
4119
- /(['"])x-app-name\1(\s*:\s*)([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\.APP_NAME/g,
4120
- (_match, quote, separator, receiver) => {
4121
- replacements++;
4122
- return `${quote}x-app-id${quote}${separator}${receiver}.DEEPSPACE_APP_ID`;
4123
- }
4124
- );
4125
- line = line.replace(
4126
- /(\[\s*)(['"])x-app-name\2(\s*\]\s*=\s*)([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\.APP_NAME/g,
4127
- (_match, open, quote, separator, receiver) => {
4128
- replacements++;
4129
- return `${open}${quote}x-app-id${quote}${separator}${receiver}.DEEPSPACE_APP_ID`;
4130
- }
4131
- );
4132
- line = line.replace(
4133
- /(forwardedParams\.set\(\s*)(['"])appName\2(\s*,\s*)([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\.APP_NAME(\s*\))/g,
4134
- (_match, open, quote, separator, receiver, close) => {
4135
- replacements++;
4136
- return `${open}${quote}appId${quote}${separator}${receiver}.DEEPSPACE_APP_ID${close}`;
4137
- }
4138
- );
4139
- lines[index] = line;
4140
- }
4141
- if (replacements === 0) return { source, replacements };
4142
- let migrated = lines.join("\n");
4143
- if (/forwardedParams\.set\(\s*['"]appId['"]/.test(migrated)) {
4144
- migrated = migrated.replaceAll("injectAppName", "injectAppId").replaceAll("`?appName=...`", "`?appId=...`").replaceAll("Inject appName into the query string", "Inject appId into the query string").replaceAll("caller-supplied appName", "caller-supplied appId").replaceAll(
4145
- "/_deepspace/subscriptions/plans?appName=",
4146
- "/_deepspace/subscriptions/plans?appId="
4147
- );
4184
+ if (file.endsWith("wrangler.toml")) {
4185
+ const next = source.replace(/not_found_handling\s*=\s*"single-page-application"/g, () => {
4186
+ replacements++;
4187
+ return 'not_found_handling = "none"';
4188
+ });
4189
+ return { source: next, replacements };
4148
4190
  }
4149
- migrated = migrated.replaceAll(
4150
- "APP_IDENTITY_TOKEN + APP_NAME",
4151
- "APP_IDENTITY_TOKEN + DEEPSPACE_APP_ID"
4191
+ const retargeted = source.replace(
4192
+ /(url\.pathname\s*=\s*)(['"])\/index\.html\2/g,
4193
+ (_match, assignment, quote) => {
4194
+ replacements++;
4195
+ return `${assignment}${quote}/${quote}`;
4196
+ }
4152
4197
  );
4153
- return {
4154
- source: ensureCanonicalIdentityProperties(migrated),
4155
- replacements
4156
- };
4198
+ const guarded = insertFileMissGuard(retargeted);
4199
+ return { source: guarded.source, replacements: replacements + guarded.replacements };
4157
4200
  }
4158
- function ensureCanonicalIdentityProperties(source) {
4201
+ var SHELL_FALLBACK_LINE = /^(\s*)url\.pathname\s*=\s*(['"])\/\2\s*$/;
4202
+ var GUARD_LOOKBACK_LINES = 8;
4203
+ function insertFileMissGuard(source) {
4159
4204
  const lines = source.split("\n");
4205
+ let replacements = 0;
4160
4206
  for (let index = lines.length - 1; index >= 0; index--) {
4161
- const match = /^(\s*)(?:readonly\s+)?APP_NAME\??\s*:\s*string\s*([;,]?)\s*$/.exec(lines[index]);
4162
- if (!match || blockAlreadyDeclaresCanonicalId(lines, index, match[1].length)) continue;
4163
- const suffix = match[2] ?? "";
4164
- lines.splice(index + 1, 0, `${match[1]}DEEPSPACE_APP_ID: string${suffix}`);
4207
+ const match = SHELL_FALLBACK_LINE.exec(lines[index]);
4208
+ if (!match || hasFileMissGuard(lines, index)) continue;
4209
+ const indent = match[1];
4210
+ lines.splice(
4211
+ index,
4212
+ 0,
4213
+ `${indent}// A FILE, not a client route: a miss must 404. Returning the shell here`,
4214
+ `${indent}// is HTML parsed as JavaScript, which is a blank page.`,
4215
+ `${indent}if (${FILE_MISS_TEST}) {`,
4216
+ `${indent} return c.json({ error: 'not_found' }, 404)`,
4217
+ `${indent}}`
4218
+ );
4219
+ replacements++;
4165
4220
  }
4166
- return lines.join("\n");
4221
+ return { source: lines.join("\n"), replacements };
4167
4222
  }
4168
- function blockAlreadyDeclaresCanonicalId(lines, propertyIndex, propertyIndent) {
4169
- for (const direction of [-1, 1]) {
4170
- for (let index = propertyIndex + direction; index >= 0 && index < lines.length; index += direction) {
4171
- const line = lines[index];
4172
- if (/^\s*(?:readonly\s+)?DEEPSPACE_APP_ID\??\s*:/.test(line)) return true;
4173
- const trimmed = line.trimStart();
4174
- if (!trimmed) continue;
4175
- const indent = line.length - trimmed.length;
4176
- if (indent < propertyIndent && (trimmed.includes("{") || trimmed.startsWith("}"))) break;
4177
- }
4178
- }
4179
- return false;
4223
+ var FILE_MISS_TEST = "url.pathname.slice(url.pathname.lastIndexOf('/') + 1).includes('.')";
4224
+ function hasFileMissGuard(lines, fallbackIndex) {
4225
+ const start = Math.max(0, fallbackIndex - GUARD_LOOKBACK_LINES);
4226
+ return lines.slice(start, fallbackIndex).some((line) => line.includes(FILE_MISS_TEST) || /\bnamesAFile\s*\(/.test(line));
4180
4227
  }
4181
- function findLegacyIdentityWireBlockers(source, file) {
4228
+ function findWorkerOwnedNotFoundBlockers(source, file) {
4229
+ if (file.endsWith("wrangler.toml")) return [];
4230
+ if (!source.includes("ASSETS")) return [];
4182
4231
  const blockers = [];
4183
- for (const [index, line] of source.split("\n").entries()) {
4184
- const trimmed = line.trimStart();
4185
- if (!/['"]x-app-name['"]/.test(line) || /\.delete\(\s*['"]x-app-name['"]/.test(line) || trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*")) {
4186
- continue;
4187
- }
4232
+ const lines = source.split("\n");
4233
+ for (const [index, line] of lines.entries()) {
4234
+ if (!/['"`]\/index\.html/.test(line)) continue;
4188
4235
  blockers.push({
4189
- migrationId: CANONICAL_APP_IDENTITY_MIGRATION_ID,
4236
+ migrationId: WORKER_OWNED_NOT_FOUND_MIGRATION_ID,
4190
4237
  file,
4191
4238
  line: index + 1,
4192
- message: "This app still sends the removed x-app-name identity header in a form the safe migration cannot rewrite."
4239
+ message: `This worker still asks the asset layer for "/index.html". Deploys now set not_found_handling = "none", and "/index.html" redirects to "/" \u2014 the browser follows it and loses the path it asked for. Fetch "/" instead, and 404 first when the last path segment contains a "." (a file, not a client route).`
4193
4240
  });
4194
4241
  }
4195
4242
  return blockers;
4196
4243
  }
4197
4244
 
4198
4245
  // src/cli/commands/deploy/build.ts
4199
- var RESERVED_RUN_WORKER_FIRST = /* @__PURE__ */ new Set([
4200
- "/api/*",
4201
- "/ws/*",
4202
- "/internal/*",
4203
- "/v1/*",
4204
- "/_deepspace/*",
4205
- "/_documentation",
4206
- "/_documentation/*",
4207
- "/_documentation-root",
4208
- "/_documentation-root/*",
4209
- // Agent-protocol paths: the worker must see them so they cannot fall through
4210
- // to the SPA shell (templates/base/src/server/http-routes.ts reserves them).
4211
- "/llms.txt",
4212
- "/llms-full.txt",
4213
- "/.well-known/mcp",
4214
- "/.well-known/mcp.json",
4215
- "/.well-known/mcp/*"
4216
- ]);
4246
+ var RESERVED_RUN_WORKER_FIRST2 = new Set(RESERVED_RUN_WORKER_FIRST);
4247
+ function declaredCompatibilityDate(config) {
4248
+ const declared = config.compatibility_date;
4249
+ return typeof declared === "string" && /^\d{4}-\d{2}-\d{2}$/.test(declared) ? declared : null;
4250
+ }
4217
4251
  async function buildDeployBundle(options) {
4218
4252
  const { appDir, appName, envName, sharedDevVarsCache, output, spinner: spinner2 } = options;
4219
4253
  const junk = removeMacosJunk(appDir);
@@ -4325,7 +4359,10 @@ async function buildDeployBundle(options) {
4325
4359
  appMigrations,
4326
4360
  doManifest,
4327
4361
  customBindings,
4328
- extraRoutes
4362
+ extraRoutes,
4363
+ compatibilityDate: declaredCompatibilityDate(outputConfig),
4364
+ compatibilityFlags: Array.isArray(outputConfig.compatibility_flags) ? outputConfig.compatibility_flags.filter((flag) => typeof flag === "string") : [],
4365
+ notFoundHandling: typeof outputConfig.assets?.not_found_handling === "string" ? outputConfig.assets.not_found_handling : null
4329
4366
  };
4330
4367
  }
4331
4368
  function extractRunWorkerFirst(config) {
@@ -4335,7 +4372,7 @@ function extractRunWorkerFirst(config) {
4335
4372
  for (const entry of raw) {
4336
4373
  if (typeof entry !== "string") continue;
4337
4374
  const route = entry.trim();
4338
- if (!route.startsWith("/") || RESERVED_RUN_WORKER_FIRST.has(route) || seen.has(route)) continue;
4375
+ if (!route.startsWith("/") || RESERVED_RUN_WORKER_FIRST2.has(route) || seen.has(route)) continue;
4339
4376
  seen.add(route);
4340
4377
  routes.push(route);
4341
4378
  }
@@ -5381,7 +5418,7 @@ async function pushWithTransientRetry(push) {
5381
5418
  const message = errorMessage3(error);
5382
5419
  const transient = /(?:HTTP |error: )(?:429|503)\b/i.test(message);
5383
5420
  if (transient && attempt < backoffMs.length && Date.now() + backoffMs[attempt] < deadline) {
5384
- await new Promise((resolve19) => setTimeout(resolve19, backoffMs[attempt]));
5421
+ await new Promise((resolve20) => setTimeout(resolve20, backoffMs[attempt]));
5385
5422
  continue;
5386
5423
  }
5387
5424
  throw error;
@@ -5473,6 +5510,15 @@ async function deployBuiltBundle(options) {
5473
5510
  if (Object.keys(bundle.assetConfig).length) {
5474
5511
  form.append("assetConfig", JSON.stringify(bundle.assetConfig));
5475
5512
  }
5513
+ if (bundle.compatibilityDate) {
5514
+ form.append("compatibilityDate", bundle.compatibilityDate);
5515
+ }
5516
+ if (bundle.compatibilityFlags.length) {
5517
+ form.append("compatibilityFlags", JSON.stringify(bundle.compatibilityFlags));
5518
+ }
5519
+ if (bundle.notFoundHandling) {
5520
+ form.append("notFoundHandling", bundle.notFoundHandling);
5521
+ }
5476
5522
  form.append("name", appName);
5477
5523
  if (confirmRename) form.append("confirmRename", "true");
5478
5524
  if (shouldSendLineage(repository.commitOid, repository.recoverable)) {
@@ -5726,7 +5772,7 @@ async function postWithRetry(url, makeInit, {
5726
5772
  lastError = error;
5727
5773
  }
5728
5774
  if (attempt < attempts) {
5729
- await new Promise((resolve19) => setTimeout(resolve19, Math.min(8e3, 500 * 2 ** (attempt - 1))));
5775
+ await new Promise((resolve20) => setTimeout(resolve20, Math.min(8e3, 500 * 2 ** (attempt - 1))));
5730
5776
  }
5731
5777
  }
5732
5778
  if (lastResponse) return lastResponse;
@@ -5846,46 +5892,6 @@ function errorMessage5(error) {
5846
5892
  return error instanceof Error ? error.message : String(error);
5847
5893
  }
5848
5894
 
5849
- // src/cli/commands/migrate/legacy-app-id.ts
5850
- var LEGACY_APP_ID_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/;
5851
- function readLegacyAppId(cwd = process.cwd(), wranglerEnv) {
5852
- const cfg = readWranglerIdentityConfig(cwd);
5853
- if (!cfg) return null;
5854
- const slot = wranglerEnv ? cfg.env?.[wranglerEnv] : cfg;
5855
- if (!slot) return null;
5856
- const declaredId = slot.vars?.DEEPSPACE_APP_ID;
5857
- if (declaredId !== void 0) {
5858
- if (typeof declaredId === "string" && LEGACY_APP_ID_RE.test(declaredId)) return declaredId;
5859
- throw new InputError(
5860
- "DEEPSPACE_APP_ID is present but invalid; correct it before migrating.",
5861
- "invalid_app_id"
5862
- );
5863
- }
5864
- const appName = typeof slot.vars?.APP_NAME === "string" ? slot.vars.APP_NAME : null;
5865
- const workerName = typeof slot.name === "string" ? slot.name : null;
5866
- if (!appName) return null;
5867
- if (!workerName) {
5868
- throw new InputError(
5869
- "Legacy migration requires both Worker name and APP_NAME to be present and identical.",
5870
- "ambiguous_legacy_app_id"
5871
- );
5872
- }
5873
- if (appName !== workerName) {
5874
- throw new InputError(
5875
- `Legacy APP_NAME "${appName}" does not match Worker name "${workerName}"; migration cannot choose safely.`,
5876
- "ambiguous_legacy_app_id"
5877
- );
5878
- }
5879
- return !APP_ID_RE.test(appName) && LEGACY_APP_ID_RE.test(appName) ? appName : null;
5880
- }
5881
- function legacyDeployRefusal(appDir, wranglerEnv) {
5882
- const legacyAppId = readLegacyAppId(appDir, wranglerEnv);
5883
- return legacyAppId ? {
5884
- error: `This checkout still uses legacy app identity ${legacyAppId}. Run \`deepspace app migrate --dry-run\`, then complete \`deepspace app migrate\` before deploying.`,
5885
- code: "legacy_app_migration_required"
5886
- } : null;
5887
- }
5888
-
5889
5895
  // src/cli/commands/deploy.ts
5890
5896
  var DEPLOY_URL3 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
5891
5897
  var deploy_default = defineCommand3({
@@ -5969,10 +5975,6 @@ var deploy_default = defineCommand3({
5969
5975
  }
5970
5976
  const appName = nameResult.name;
5971
5977
  let appId = readAppId(appDir, envName);
5972
- if (!appId) {
5973
- const refusal = legacyDeployRefusal(appDir, envName);
5974
- if (refusal) output.die(refusal.error, refusal.code);
5975
- }
5976
5978
  p12.log.info(envName ? `App: ${appName} (env: ${envName})` : `App: ${appName}`);
5977
5979
  const { token, ownerId } = await authenticate(appDir, output);
5978
5980
  if (!appId) {
@@ -6038,15 +6040,18 @@ var deploy_default = defineCommand3({
6038
6040
  output,
6039
6041
  spinner: spinner2
6040
6042
  });
6043
+ let serving = "unverifiable";
6041
6044
  if (body.url) {
6042
- spinner2.message("Waiting for edge propagation...");
6043
- const ready = await waitForEdgePropagation(body.url, 9e4);
6044
- if (!ready) {
6045
- spinner2.stop("Deployed (edge propagation still in progress after 90s)");
6045
+ spinner2.message("Waiting for the edge to serve this release...");
6046
+ serving = await waitForLiveRelease(body.url, body.releaseStamp, 9e4);
6047
+ if (serving !== "confirmed") {
6048
+ spinner2.stop(
6049
+ serving === "unconfirmed" ? "Deployed \u2014 the edge is still rolling this release out" : "Deployed \u2014 serving could not be verified from here"
6050
+ );
6046
6051
  p12.log.warn(
6047
- "First requests may briefly hit the assets transitional page; retry in a minute."
6052
+ serving === "unconfirmed" ? "Some requests still get the previous release. Cloudflare rolls a version out per edge machine; it usually settles within a couple of minutes. Wait before asserting against it." : "This release carries no serving stamp (older platform, or a resumed deploy), so the CLI cannot tell which release the edge answers with."
6048
6053
  );
6049
- p12.log.info(`URL: ${body.url} (deploy accepted; serving not yet verified from here)`);
6054
+ p12.log.info(`URL: ${body.url}`);
6050
6055
  await syncCommerce(appDir, appId, token, output.nonInteractive);
6051
6056
  if (output.json) {
6052
6057
  return output.emitJson({
@@ -6056,7 +6061,10 @@ var deploy_default = defineCommand3({
6056
6061
  url: body.url,
6057
6062
  releaseId: body.releaseId ?? null,
6058
6063
  bundleRetained: body.bundleRetained ?? null,
6059
- edgePropagating: true,
6064
+ // `serving` says what was actually established; `edgePropagating`
6065
+ // stays for callers written against the old shape.
6066
+ serving,
6067
+ edgePropagating: serving === "unconfirmed",
6060
6068
  recoverable: repository.recoverable,
6061
6069
  ...staleBaseGuardFields(body)
6062
6070
  });
@@ -6074,6 +6082,9 @@ var deploy_default = defineCommand3({
6074
6082
  appId,
6075
6083
  appName,
6076
6084
  url: body.url ?? null,
6085
+ // Always present, so a caller branches on one field instead of
6086
+ // inferring success from the absence of `edgePropagating`.
6087
+ serving,
6077
6088
  releaseId: body.releaseId ?? null,
6078
6089
  bundleRetained: body.bundleRetained ?? null,
6079
6090
  recoverable: repository.recoverable,
@@ -6528,7 +6539,7 @@ function formatEvent(e, color, now = Date.now()) {
6528
6539
  const client = e.source === "client" ? paint("35", "CLIENT", color) + " " : "";
6529
6540
  return `${time} ${client}${tag} ${body}`;
6530
6541
  }
6531
- var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
6542
+ var sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
6532
6543
  var logs_default = defineCommand4({
6533
6544
  meta: {
6534
6545
  name: "logs",
@@ -6648,7 +6659,7 @@ var logs_default = defineCommand4({
6648
6659
  }
6649
6660
  }
6650
6661
  for (; ; ) {
6651
- await sleep2(backoff || interval);
6662
+ await sleep3(backoff || interval);
6652
6663
  const freshToken = await ensureToken();
6653
6664
  let page;
6654
6665
  try {
@@ -7818,11 +7829,11 @@ function fmtDate(iso) {
7818
7829
  }
7819
7830
  async function confirm4(message) {
7820
7831
  process.stdout.write(`${message} [y/N] `);
7821
- return new Promise((resolve19) => {
7832
+ return new Promise((resolve20) => {
7822
7833
  process.stdin.setEncoding("utf-8");
7823
7834
  process.stdin.once("data", (data) => {
7824
7835
  const answer = data.toString().trim().toLowerCase();
7825
- resolve19(answer === "y" || answer === "yes");
7836
+ resolve20(answer === "y" || answer === "yes");
7826
7837
  });
7827
7838
  });
7828
7839
  }
@@ -8174,8 +8185,14 @@ async function request(baseUrl, token, path, init = {}) {
8174
8185
  if (res.ok) return res;
8175
8186
  const text2 = await res.text();
8176
8187
  let code;
8188
+ let details;
8177
8189
  try {
8178
- code = JSON.parse(text2.trim()).code;
8190
+ const body = JSON.parse(text2.trim());
8191
+ code = typeof body.code === "string" ? body.code : void 0;
8192
+ const rest = Object.fromEntries(
8193
+ Object.entries(body).filter(([key]) => key !== "error" && key !== "code")
8194
+ );
8195
+ if (Object.keys(rest).length > 0) details = rest;
8179
8196
  } catch {
8180
8197
  }
8181
8198
  throw new ApiError(
@@ -8185,7 +8202,8 @@ async function request(baseUrl, token, path, init = {}) {
8185
8202
  // oversized request short is not a different failure from the worker
8186
8203
  // refusing it, and two codes made it look like one.
8187
8204
  code ?? (res.status === 413 ? "too_large" : res.status >= 500 ? "server_error" : "http_error"),
8188
- path
8205
+ path,
8206
+ details
8189
8207
  );
8190
8208
  }
8191
8209
  async function readJson(res, path) {
@@ -8458,8 +8476,14 @@ var list3 = defineDeepspaceCommand({
8458
8476
  const more = limit < MAX_LIST_LIMIT ? "raise --limit or narrow with --prefix" : "narrow with --prefix";
8459
8477
  if (result.truncated) console.log(`(truncated at ${limit} \u2014 ${more})`);
8460
8478
  }
8479
+ if (result.storage) {
8480
+ const { usedBytes, limitBytes } = result.storage;
8481
+ console.log(
8482
+ limitBytes === void 0 ? `Storage: ${formatBytes(usedBytes)} used (limit unavailable right now)` : `Storage: ${formatBytes(usedBytes)} of ${formatBytes(limitBytes)} used`
8483
+ );
8484
+ }
8461
8485
  }
8462
- return { data: { appId, files, truncated: result.truncated } };
8486
+ return { data: { appId, files, truncated: result.truncated, storage: result.storage ?? null } };
8463
8487
  }
8464
8488
  });
8465
8489
  var get = defineDeepspaceCommand({
@@ -8542,8 +8566,8 @@ var list4 = defineDeepspaceCommand({
8542
8566
  }
8543
8567
  if (pending.length) {
8544
8568
  console.log(`PENDING INVITES ON ${app2}`);
8545
- for (const p34 of pending) {
8546
- console.log(` ${p34.email} (expires ${new Date(p34.expiresAt).toLocaleDateString()})`);
8569
+ for (const p35 of pending) {
8570
+ console.log(` ${p35.email} (expires ${new Date(p35.expiresAt).toLocaleDateString()})`);
8547
8571
  }
8548
8572
  }
8549
8573
  }
@@ -8587,7 +8611,7 @@ var add = defineDeepspaceCommand({
8587
8611
  }
8588
8612
  if (!args.json) {
8589
8613
  console.log(
8590
- `\u2713 Invite sent to ${res.email} (expires ${expires}). They become a collaborator when they sign in.`
8614
+ `\u2713 Invite sent to ${res.email} (expires ${expires}). They accept it from the emailed link, or by signing in and running \`deepspace app collaborators accept ${app2}\`.`
8591
8615
  );
8592
8616
  }
8593
8617
  return { data: { ...res } };
@@ -8641,7 +8665,7 @@ var cancel3 = defineDeepspaceCommand({
8641
8665
  `/api/app-collaborators/${encodeURIComponent(app2)}`
8642
8666
  );
8643
8667
  const target2 = email.trim().toLowerCase();
8644
- const found = pending.find((p34) => p34.email.toLowerCase() === target2);
8668
+ const found = pending.find((p35) => p35.email.toLowerCase() === target2);
8645
8669
  if (!found) {
8646
8670
  throw new Refusal(`${email} has no pending invite on ${app2}`, "no_pending_invite", {
8647
8671
  action: cliAction("deepspace", "app", "collaborators", "list", "--app", app2)
@@ -8656,9 +8680,59 @@ var cancel3 = defineDeepspaceCommand({
8656
8680
  return { data: { cancelled: found } };
8657
8681
  }
8658
8682
  });
8683
+ var invites = defineDeepspaceCommand({
8684
+ meta: { name: "invites", description: "List invites waiting for you" },
8685
+ async run({ args }) {
8686
+ const token = await ensureToken();
8687
+ const { invites: invites2 } = await api2(
8688
+ token,
8689
+ "/api/app-collaborators/invites/mine"
8690
+ );
8691
+ if (!args.json) {
8692
+ if (!invites2.length) {
8693
+ console.log("No pending invites for your email.");
8694
+ } else {
8695
+ for (const invite of invites2) {
8696
+ console.log(
8697
+ `${invite.appId} invited by ${invite.invitedByName} (expires ${new Date(invite.expiresAt).toLocaleDateString()})`
8698
+ );
8699
+ }
8700
+ console.log("Accept one with `deepspace app collaborators accept <app-id>`.");
8701
+ }
8702
+ }
8703
+ return { data: { invites: invites2 } };
8704
+ }
8705
+ });
8706
+ var accept = defineDeepspaceCommand({
8707
+ meta: { name: "accept", description: "Accept an invite to collaborate on an app" },
8708
+ args: {
8709
+ app: { type: "positional", description: "App id (see `collaborators invites`)", required: true }
8710
+ },
8711
+ async run({ args }) {
8712
+ const appId = String(args.app);
8713
+ const token = await ensureToken();
8714
+ const res = await api2(
8715
+ token,
8716
+ `/api/app-collaborators/invites/${encodeURIComponent(appId)}/accept`,
8717
+ { method: "POST" }
8718
+ );
8719
+ if (!args.json) {
8720
+ console.log(
8721
+ res.alreadyMember ? `\u2022 You already have access to ${res.appId}.` : `\u2713 You can now deploy ${res.appId}.`
8722
+ );
8723
+ }
8724
+ return {
8725
+ data: { ...res },
8726
+ // `pull` fast-forwards a checkout this caller does not have yet — the
8727
+ // invite just granted access, so there is nothing local to update.
8728
+ // `clone` is the step that actually works from here.
8729
+ action: cliAction("deepspace", "clone", res.appId)
8730
+ };
8731
+ }
8732
+ });
8659
8733
  var collaborators_default = defineCommand7({
8660
8734
  meta: { name: "collaborators", description: "Manage who can deploy your app" },
8661
- subCommands: { list: list4, add, remove, cancel: cancel3 }
8735
+ subCommands: { list: list4, add, remove, cancel: cancel3, invites, accept }
8662
8736
  });
8663
8737
 
8664
8738
  // src/cli/commands/transfer.ts
@@ -8743,7 +8817,7 @@ var status2 = defineDeepspaceCommand({
8743
8817
  return { data: { app: app2, transfer } };
8744
8818
  }
8745
8819
  });
8746
- var accept = defineDeepspaceCommand({
8820
+ var accept2 = defineDeepspaceCommand({
8747
8821
  meta: { name: "accept", description: "Accept a transfer offered to you" },
8748
8822
  args: {
8749
8823
  app: { type: "string", alias: "a", description: "App id of the offered app", required: true }
@@ -8784,7 +8858,7 @@ var cancel4 = defineDeepspaceCommand({
8784
8858
  });
8785
8859
  var transfer_default = defineCommand8({
8786
8860
  meta: { name: "transfer", description: "Transfer app ownership to another user" },
8787
- subCommands: { offer, status: status2, accept, cancel: cancel4 }
8861
+ subCommands: { offer, status: status2, accept: accept2, cancel: cancel4 }
8788
8862
  });
8789
8863
 
8790
8864
  // src/cli/commands/integrations.ts
@@ -10942,13 +11016,14 @@ Re-run with --allow-do-deletion to accept the data loss.`,
10942
11016
  "invalid_response"
10943
11017
  );
10944
11018
  }
10945
- let edgePropagating = false;
11019
+ let serving = "unverifiable";
10946
11020
  if (result.url) {
10947
- spinner2?.message("Waiting for edge propagation...");
10948
- edgePropagating = !await waitForEdgePropagation(result.url, 9e4);
11021
+ spinner2?.message("Waiting for the edge to serve this release...");
11022
+ serving = await waitForLiveRelease(result.url, result.releaseStamp, 9e4);
10949
11023
  }
11024
+ const edgePropagating = serving === "unconfirmed";
10950
11025
  spinner2?.stop(
10951
- edgePropagating ? `Rolled back to ${result.rolledBackTo ?? releaseId} (edge propagation still in progress after 90s).` : `Rolled back to ${result.rolledBackTo ?? releaseId}.`
11026
+ serving === "confirmed" ? `Rolled back to ${result.rolledBackTo ?? releaseId}.` : serving === "unconfirmed" ? `Rolled back to ${result.rolledBackTo ?? releaseId} \u2014 the edge is still rolling it out.` : `Rolled back to ${result.rolledBackTo ?? releaseId} \u2014 serving could not be verified from here.`
10952
11027
  );
10953
11028
  const doClassGuard = result.doClassGuard === "unverified" ? "unverified" : void 0;
10954
11029
  if (!args.json) {
@@ -10979,6 +11054,8 @@ Re-run with --allow-do-deletion to accept the data loss.`,
10979
11054
  url: result.url ?? null,
10980
11055
  versionId: result.versionId ?? null,
10981
11056
  wranglerEnv: wranglerEnv ?? null,
11057
+ // Always present, matching `deploy`: one field to branch on.
11058
+ serving,
10982
11059
  ...edgePropagating ? { edgePropagating: true } : {},
10983
11060
  ...doClassGuard ? { doClassGuard } : {}
10984
11061
  }
@@ -12982,541 +13059,183 @@ function sameRefs(left, right) {
12982
13059
  return left.every((ref) => expected.get(ref.name) === ref.oid);
12983
13060
  }
12984
13061
 
12985
- // src/cli/commands/migrate/identity-api.ts
12986
- async function getIdentityMigration(deployUrl, token, appId) {
12987
- try {
12988
- const result = await apiFetch(
12989
- deployUrl,
12990
- token,
12991
- `/api/apps/${encodeURIComponent(appId)}/identity-migration`
12992
- );
12993
- return result.migration;
12994
- } catch (error) {
12995
- if (error instanceof ApiError && error.code === "migration_not_found") return null;
12996
- throw error;
12997
- }
12998
- }
12999
- async function prepareIdentityMigration(deployUrl, token, legacyAppId, input) {
13000
- const result = await apiFetch(
13001
- deployUrl,
13002
- token,
13003
- `/api/apps/${encodeURIComponent(legacyAppId)}/identity-migration/prepare`,
13004
- { method: "POST", body: JSON.stringify(input) }
13062
+ // src/cli/commands/update.ts
13063
+ import * as p34 from "@clack/prompts";
13064
+ import { existsSync as existsSync26, readFileSync as readFileSync28, writeFileSync as writeFileSync10 } from "fs";
13065
+ import { join as join24, resolve as resolve19 } from "path";
13066
+ var REGISTRY = "https://registry.npmjs.org/deepspace/latest";
13067
+ var DOCS_URL = "https://documentation.deep.space/cli-reference/commands";
13068
+ var OLDEST_SUPPORTED_MINOR = 12;
13069
+ function parseVersion(raw) {
13070
+ const match = /^(\d+)\.(\d+)\.(\d+)/.exec(raw.trim().replace(/^[\^~]/, ""));
13071
+ if (!match) return null;
13072
+ return { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]) };
13073
+ }
13074
+ function gapIsTooWide(from, to) {
13075
+ if (from.major !== to.major) return true;
13076
+ return from.major === 0 && from.minor < OLDEST_SUPPORTED_MINOR;
13077
+ }
13078
+ function readAppSdkVersion(appDir) {
13079
+ const installed = join24(appDir, "node_modules", "deepspace", "package.json");
13080
+ if (existsSync26(installed)) {
13081
+ const parsed2 = JSON.parse(readFileSync28(installed, "utf8"));
13082
+ if (typeof parsed2.version === "string") return parsed2.version;
13083
+ }
13084
+ const manifest = join24(appDir, "package.json");
13085
+ if (!existsSync26(manifest)) return null;
13086
+ const parsed = JSON.parse(readFileSync28(manifest, "utf8"));
13087
+ return parsed.dependencies?.deepspace ?? null;
13088
+ }
13089
+ function pinSdkVersion(appDir, version) {
13090
+ const path = join24(appDir, "package.json");
13091
+ const source = readFileSync28(path, "utf8");
13092
+ const updated = source.replace(
13093
+ /("deepspace"\s*:\s*")[^"]+(")/,
13094
+ (_match, head, tail) => `${head}^${version}${tail}`
13005
13095
  );
13006
- return result.migration;
13007
- }
13008
- async function inspectIdentityMigration(deployUrl, token, legacyAppId, input) {
13009
- const result = await apiFetch(
13010
- deployUrl,
13011
- token,
13012
- `/api/apps/${encodeURIComponent(legacyAppId)}/identity-migration/inspect`,
13013
- { method: "POST", body: JSON.stringify(input) }
13014
- );
13015
- return result.inventory;
13016
- }
13017
- async function commitIdentityMigration(deployUrl, token, appId, input) {
13018
- const result = await apiFetch(
13019
- deployUrl,
13020
- token,
13021
- `/api/apps/${encodeURIComponent(appId)}/identity-migration/commit`,
13022
- { method: "POST", body: JSON.stringify(input) }
13023
- );
13024
- return result.migration;
13096
+ if (updated === source) return false;
13097
+ writeFileSync10(path, updated);
13098
+ return true;
13025
13099
  }
13026
- async function cancelIdentityMigration(deployUrl, token, appId) {
13027
- await apiFetch(
13028
- deployUrl,
13029
- token,
13030
- `/api/apps/${encodeURIComponent(appId)}/identity-migration`,
13031
- { method: "DELETE" }
13100
+ async function latestPublishedVersion() {
13101
+ const response = await fetch(REGISTRY, { signal: AbortSignal.timeout(15e3) }).catch(
13102
+ (error) => {
13103
+ throw new Refusal(
13104
+ `Could not reach the npm registry to find the latest version: ${error instanceof Error ? error.message : String(error)}. Pass --to <version> to update without a lookup.`,
13105
+ "registry_unreachable"
13106
+ );
13107
+ }
13032
13108
  );
13109
+ if (!response.ok) {
13110
+ throw new Refusal(
13111
+ `The npm registry answered ${response.status} for the latest deepspace version. Pass --to <version> to update without a lookup.`,
13112
+ "registry_unreachable"
13113
+ );
13114
+ }
13115
+ const body = await response.json();
13116
+ if (typeof body.version !== "string") {
13117
+ throw new Refusal("The npm registry returned no version for deepspace.", "registry_unreachable");
13118
+ }
13119
+ return body.version;
13033
13120
  }
13034
- async function verifyIdentityMigration(deployUrl, token, appId) {
13035
- const result = await apiFetch(
13036
- deployUrl,
13037
- token,
13038
- `/api/apps/${encodeURIComponent(appId)}/identity-migration/verify`,
13039
- { method: "POST" }
13040
- );
13041
- return result.migration;
13121
+ function reportPlan(plan, applied) {
13122
+ for (const migration of plan.pending) {
13123
+ const verb = applied ? "Applied" : "Would apply";
13124
+ p34.log.success(
13125
+ `${verb}: ${migration.description} \u2014 ${migration.replacements} change(s) in ${migration.files.join(", ")}`
13126
+ );
13127
+ }
13128
+ if (plan.blockers.length) {
13129
+ p34.log.warn(
13130
+ `${plan.blockers.length} change(s) could not be made for you \u2014 this file differs from the version the change was written against:`
13131
+ );
13132
+ for (const blocker of plan.blockers) {
13133
+ p34.log.message(` ${blocker.file}:${blocker.line}
13134
+ ${blocker.message}`);
13135
+ }
13136
+ }
13042
13137
  }
13043
-
13044
- // src/cli/commands/migrate.ts
13045
- var STRICT_APP_ID_RE2 = /^app_[0-9A-HJKMNP-TV-Z]{26}$/;
13046
- var migrate_default = defineDeepspaceCommand({
13138
+ var update_default = defineDeepspaceCommand({
13047
13139
  meta: {
13048
- name: "migrate",
13049
- description: "Apply pending DeepSpace app migrations without moving retained data"
13140
+ name: "update",
13141
+ description: "Move this app onto the current SDK and apply the source changes it requires"
13050
13142
  },
13051
13143
  args: {
13052
- remote: {
13144
+ to: {
13053
13145
  type: "string",
13054
- description: "GitHub remote name when it cannot be inferred (default: origin)",
13055
- required: false
13056
- },
13057
- env: {
13058
- type: "string",
13059
- alias: "e",
13060
- description: "wrangler.toml environment whose app id should be migrated",
13146
+ description: "Target version (default: the latest published deepspace)",
13061
13147
  required: false
13062
13148
  },
13063
13149
  "dry-run": {
13064
13150
  type: "boolean",
13065
- description: "Inspect prerequisites and show the migration without changing anything",
13066
- default: false
13067
- },
13068
- cancel: {
13069
- type: "boolean",
13070
- description: "Cancel a prepared migration after restoring and publishing the legacy id",
13151
+ description: "Report what would change without touching the checkout",
13071
13152
  default: false
13072
13153
  }
13073
13154
  },
13074
13155
  async run({ args }) {
13075
- const recoveryMode = args.cancel === true ? "cancel" : null;
13076
- const exclusiveModes = [args["dry-run"], args.cancel].filter(Boolean).length;
13077
- if (exclusiveModes > 1) {
13078
- throw new Refusal("Use only one of --dry-run or --cancel.", "conflicting_migration_mode");
13079
- }
13080
- const appDir = findAppDir();
13081
- if (!appDir) {
13082
- throw new Refusal("Run this command from a DeepSpace app Git checkout.", "not_in_app_repo");
13083
- }
13084
- assertSyncableRepo(appDir);
13085
- const envName = typeof args.env === "string" && args.env ? args.env : void 0;
13086
- const currentAppId = readAppId(appDir, envName) ?? readLegacyAppId(appDir, envName);
13087
- if (!currentAppId) {
13156
+ const appDir = resolve19(".");
13157
+ if (!hasWranglerConfig(appDir)) {
13088
13158
  throw new Refusal(
13089
- "wrangler.toml has no unambiguous DeepSpace app identity for this environment.",
13090
- "missing_app_id"
13159
+ "No wrangler.toml found \u2014 run this from inside a DeepSpace app directory.",
13160
+ "not_in_app_repo"
13091
13161
  );
13092
13162
  }
13093
- if (!STRICT_APP_ID_RE2.test(currentAppId)) requireTrackedWrangler(appDir);
13094
- const token = await ensureToken();
13095
- const deployUrl = deployBaseUrl();
13096
- const existingMigration = await getIdentityMigration(deployUrl, token, currentAppId);
13097
- if (recoveryMode) {
13098
- if (!existingMigration) {
13099
- if (!args.json) console.log("No reversible identity migration is recorded.");
13100
- return { data: { status: "no_migration", appId: currentAppId } };
13101
- }
13102
- return recoverMigration({
13103
- mode: recoveryMode,
13104
- appDir,
13105
- envName,
13106
- remoteName: typeof args.remote === "string" ? args.remote : void 0,
13107
- currentAppId,
13108
- token,
13109
- deployUrl,
13110
- migration: existingMigration,
13111
- json: args.json
13112
- });
13113
- }
13114
- const sourcePlan = planAppMigrations(appDir);
13115
- if (sourcePlan.blockers.length > 0) {
13116
- const first = sourcePlan.blockers[0];
13163
+ assertSyncableRepo(appDir);
13164
+ const currentRaw = readAppSdkVersion(appDir);
13165
+ if (!currentRaw) {
13117
13166
  throw new Refusal(
13118
- `Migration ${first.migrationId} needs a manual source update at ${first.file}:${first.line}. ${first.message}`,
13119
- "source_migration_blocked",
13120
- { extra: { blockers: sourcePlan.blockers } }
13167
+ "This app does not depend on `deepspace` \u2014 nothing to update.",
13168
+ "not_in_app_repo"
13121
13169
  );
13122
13170
  }
13123
- if (STRICT_APP_ID_RE2.test(currentAppId)) {
13124
- if (!existingMigration) {
13125
- if (args["dry-run"] && sourcePlan.pending.length > 0) {
13126
- reportSourcePlan(sourcePlan, args.json);
13127
- return {
13128
- data: {
13129
- status: "ready",
13130
- appId: currentAppId,
13131
- sourceMigrations: sourcePlan.pending
13132
- }
13133
- };
13134
- }
13135
- if (sourcePlan.pending.length > 0) {
13136
- requireSourceMigrationWorktree(appDir);
13137
- applyAppMigrationPlan(appDir, sourcePlan);
13138
- throw sourceMigrationCommitRequired(appDir, sourcePlan);
13139
- }
13140
- await requireSourceMigrationDeploy(appDir, currentAppId, deployUrl, token, {
13141
- envName,
13142
- remoteName: typeof args.remote === "string" ? args.remote : void 0
13143
- });
13144
- reportUpToDate(currentAppId, args.json);
13145
- return { data: { status: "up_to_date", appId: currentAppId } };
13146
- }
13147
- if (sourcePlan.pending.length === 0 && existingMigration.status === "verified") {
13148
- await requireSourceMigrationDeploy(appDir, currentAppId, deployUrl, token, {
13149
- envName,
13150
- remoteName: typeof args.remote === "string" ? args.remote : void 0
13151
- });
13152
- }
13153
- if (args["dry-run"]) {
13154
- reportMigration(existingMigration, args.json);
13155
- reportSourcePlan(sourcePlan, args.json);
13156
- return {
13157
- data: {
13158
- status: existingMigration.status,
13159
- migration: existingMigration,
13160
- sourceMigrations: sourcePlan.pending
13161
- }
13162
- };
13163
- }
13164
- if (sourcePlan.pending.length > 0) {
13165
- requireSourceMigrationWorktree(appDir, existingMigration.status === "prepared");
13166
- applyAppMigrationPlan(appDir, sourcePlan);
13167
- throw existingMigration.status === "prepared" ? commitRequired(appDir, existingMigration, envName, sourcePlan.changedFiles) : sourceMigrationCommitRequired(appDir, sourcePlan);
13168
- }
13169
- return continuePreparedMigration({
13170
- appDir,
13171
- envName,
13172
- remoteName: typeof args.remote === "string" ? args.remote : void 0,
13173
- token,
13174
- deployUrl,
13175
- migration: existingMigration,
13176
- json: args.json
13177
- });
13178
- }
13179
- if (existingMigration && existingMigration.status !== "prepared" && existingMigration.status !== "rolled_back") {
13171
+ const targetRaw = typeof args.to === "string" && args.to.trim() ? args.to.trim() : await latestPublishedVersion();
13172
+ const current = parseVersion(currentRaw);
13173
+ const target2 = parseVersion(targetRaw);
13174
+ if (!current || !target2) {
13180
13175
  throw new Refusal(
13181
- `The registry already uses ${existingMigration.appId}, but wrangler.toml still uses ${currentAppId}. Restore the migration commit before deploying.`,
13182
- "migration_config_stale",
13183
- { extra: { migration: existingMigration } }
13176
+ `Could not read a version to update from ("${currentRaw}") or to ("${targetRaw}").`,
13177
+ "invalid_version"
13184
13178
  );
13185
13179
  }
13186
- const remote = requireGitHubRemote2(
13187
- appDir,
13188
- typeof args.remote === "string" ? args.remote : void 0
13189
- );
13190
- const head = requirePublishedHead2(appDir, remote);
13191
- const proposedAppId = existingMigration?.appId ?? mintAppId();
13192
- const inventory = await inspectIdentityMigration(deployUrl, token, currentAppId, {
13193
- destinationAppId: proposedAppId,
13194
- repository: remote.repository
13195
- });
13196
- if (!inventory.ready) {
13180
+ if (gapIsTooWide(current, target2)) {
13197
13181
  throw new Refusal(
13198
- `Migration preflight is blocked: ${inventory.blockers.map((blocker) => blocker.message).join("; ")}`,
13199
- "migration_preflight_blocked",
13200
- { extra: { inventory } }
13182
+ `This app is on deepspace ${currentRaw} and the current release is ${targetRaw}. That gap is too wide to migrate automatically \u2014 the changes between them are structural, not mechanical. Follow the upgrade guide instead: ${DOCS_URL}`,
13183
+ "version_gap_too_wide",
13184
+ { actionRequired: true, extra: { currentVersion: currentRaw, targetVersion: targetRaw } }
13201
13185
  );
13202
13186
  }
13203
- if (args["dry-run"]) {
13204
- if (!args.json) {
13205
- console.log(`Legacy app: ${currentAppId}`);
13206
- console.log(`GitHub: ${remote.repository} (${head.branch} @ ${head.oid.slice(0, 10)})`);
13207
- console.log(
13208
- `Immutable app id: ${proposedAppId}${existingMigration ? "" : " (not reserved)"}`
13209
- );
13210
- console.log(
13211
- `Registry rows to re-key: app=1 routes=${inventory.rekey.routes.length} collaborators=${inventory.rekey.collaborators.length} pendingInvites=${inventory.rekey.pendingCollaborators.length} transfers=${inventory.rekey.transfer ? 1 : 0}`
13212
- );
13213
- console.log(
13214
- `Physical stores retained at ${inventory.app.resourceId}: ${inventory.retainedPhysicalStores.map((store) => store.kind).join(", ")}`
13215
- );
13216
- console.log("No changes made.");
13217
- }
13218
- reportSourcePlan(sourcePlan, args.json);
13219
- return {
13220
- data: {
13221
- status: "ready",
13222
- legacyAppId: currentAppId,
13223
- proposedAppId,
13224
- resourceId: inventory.app.resourceId,
13225
- repository: remote.repository,
13226
- branch: head.branch,
13227
- commitOid: head.oid,
13228
- sourceClaimRequired: inventory.app.sourceClaimRequired,
13229
- sourceMigrations: sourcePlan.pending,
13230
- inventory
13231
- }
13232
- };
13233
- }
13234
- const migration = existingMigration?.status === "prepared" ? existingMigration : await prepareIdentityMigration(deployUrl, token, currentAppId, {
13235
- destinationAppId: proposedAppId,
13236
- repository: remote.repository,
13237
- expectedSourceRevision: inventory.app.sourceRevision
13238
- });
13239
- applyAppMigrationPlan(appDir, sourcePlan);
13240
- writeAppId(appDir, migration.appId, { wranglerEnv: envName, force: true });
13241
- throw commitRequired(appDir, migration, envName, sourcePlan.changedFiles);
13242
- }
13243
- });
13244
- async function recoverMigration(input) {
13245
- const { appDir, envName, remoteName, currentAppId, token, deployUrl, migration, json } = input;
13246
- if (migration.status === "rolled_back") {
13247
- if (currentAppId !== migration.legacyAppId) {
13248
- writeAppId(appDir, migration.legacyAppId, { wranglerEnv: envName, force: true });
13249
- throw restoreLegacyCommitRequired(appDir, migration, envName);
13250
- }
13251
- reportMigration(migration, json);
13252
- return { data: { status: "rolled_back", migration } };
13253
- }
13254
- if (migration.status !== "prepared") {
13255
- throw new Refusal(
13256
- "This migration is already committed. Complete it by deploying the canonical app.",
13257
- "forward_recovery_required",
13258
- { extra: { migration } }
13259
- );
13260
- }
13261
- if (currentAppId !== migration.legacyAppId) {
13262
- writeAppId(appDir, migration.legacyAppId, { wranglerEnv: envName, force: true });
13263
- throw restoreLegacyCommitRequired(appDir, migration, envName);
13264
- }
13265
- const remote = requireGitHubRemote2(appDir, remoteName, migration.repository);
13266
- requirePublishedHead2(appDir, remote);
13267
- await cancelIdentityMigration(deployUrl, token, migration.legacyAppId);
13268
- if (!json) console.log(`Canceled ${migration.legacyAppId} \u2192 ${migration.appId}.`);
13269
- return { data: { status: "cancelled", legacyAppId: migration.legacyAppId } };
13270
- }
13271
- async function continuePreparedMigration(input) {
13272
- const { appDir, envName, remoteName, token, deployUrl, migration, json } = input;
13273
- if (migration.status === "rolled_back") {
13274
- throw new Refusal(
13275
- `This migration was rolled back. Restore DEEPSPACE_APP_ID to ${migration.legacyAppId} before continuing.`,
13276
- "migration_rolled_back"
13277
- );
13278
- }
13279
- if (migration.status === "verified") {
13280
- reportMigration(migration, json);
13281
- return { data: { status: "verified", migration } };
13282
- }
13283
- if (migration.status === "committed") {
13284
- await requireSourceMigrationDeploy(appDir, migration.appId, deployUrl, token, {
13285
- envName,
13286
- remoteName,
13287
- after: migration.committedAt ?? void 0
13288
- });
13289
- const verified = await verifyIdentityMigration(deployUrl, token, migration.appId);
13290
- reportMigration(verified, json);
13291
- return { data: { status: "verified", migration: verified } };
13292
- }
13293
- if (!isWorkTreeClean(appDir)) {
13294
- const wranglerPath = wranglerRepoPath(appDir);
13295
- if (statusFiles(repoToplevel(appDir)).includes(wranglerPath)) {
13296
- throw commitRequired(appDir, migration, envName);
13297
- }
13298
- throw new Refusal(
13299
- "Commit or discard unrelated working-tree changes before completing the migration.",
13300
- "dirty_worktree"
13301
- );
13302
- }
13303
- const remote = requireGitHubRemote2(appDir, remoteName, migration.repository);
13304
- const head = requireLocalHead2(appDir);
13305
- const remoteOid = remoteBranchOid(appDir, remote.name, head.branch);
13306
- if (remoteOid !== head.oid) {
13307
- throw new Refusal(
13308
- `GitHub ${remote.repository} does not have ${head.branch} at ${head.oid.slice(0, 10)}. Push the migration commit, then rerun.`,
13309
- "github_push_required",
13310
- {
13311
- actionRequired: true,
13312
- action: { cwd: appDir, argv: ["git", "push", remote.name, head.branch] },
13313
- extra: { appId: migration.appId, repository: remote.repository, ...head }
13314
- }
13315
- );
13316
- }
13317
- const committed = await commitIdentityMigration(deployUrl, token, migration.appId, {
13318
- commitOid: head.oid,
13319
- branch: head.branch
13320
- });
13321
- throw deployRequired(appDir, committed, envName);
13322
- }
13323
- function requireGitHubRemote2(appDir, remoteName, repository) {
13324
- const selected = selectGitHubRemote(appDir, { name: remoteName, repository });
13325
- if (selected) return selected;
13326
- throw new Refusal(
13327
- remoteName ? `Remote "${remoteName}" is not the expected GitHub repository.` : repository ? `No local GitHub remote points at ${repository}. Add one or pass --remote <name>.` : "No unambiguous GitHub remote was found. Add `origin` or pass --remote <name>.",
13328
- "github_remote_required"
13329
- );
13330
- }
13331
- function requireLocalHead2(appDir) {
13332
- const branch = currentBranch(appDir);
13333
- if (!branch) throw new Refusal("HEAD is detached; switch to a branch first.", "detached_head");
13334
- const oid = resolveCommit(appDir, "HEAD");
13335
- if (!oid) throw new Refusal("The repository has no commit to publish.", "no_commits");
13336
- return { branch, oid };
13337
- }
13338
- function requirePublishedHead2(appDir, remote) {
13339
- if (!isWorkTreeClean(appDir)) {
13340
- throw new Refusal(
13341
- "Commit or discard local changes before preparing migration.",
13342
- "dirty_worktree"
13343
- );
13344
- }
13345
- const head = requireLocalHead2(appDir);
13346
- if (remoteBranchOid(appDir, remote.name, head.branch) !== head.oid) {
13347
- throw new Refusal(
13348
- `GitHub ${remote.repository} does not have ${head.branch} at ${head.oid.slice(0, 10)}. Push it, then rerun.`,
13349
- "github_push_required",
13350
- {
13351
- actionRequired: true,
13352
- action: { cwd: appDir, argv: ["git", "push", remote.name, head.branch] },
13353
- extra: { repository: remote.repository, ...head }
13187
+ const dryRun = args["dry-run"] === true;
13188
+ const plan = planAppMigrations(appDir);
13189
+ const pinned = dryRun ? false : pinSdkVersion(appDir, targetRaw);
13190
+ if (!dryRun && plan.edits.length) applyAppMigrationPlan(appDir, plan);
13191
+ if (!args.json) {
13192
+ p34.log.info(`deepspace ${currentRaw} \u2192 ${targetRaw}`);
13193
+ if (pinned) p34.log.success("package.json now asks for the current SDK");
13194
+ if (!plan.pending.length && !plan.blockers.length) {
13195
+ p34.log.success("No source changes required.");
13354
13196
  }
13355
- );
13356
- }
13357
- return head;
13358
- }
13359
- function commitRequired(appDir, migration, envName, additionalPaths = []) {
13360
- const wranglerPath = wranglerRepoPath(appDir);
13361
- const migrationPaths = [.../* @__PURE__ */ new Set([wranglerPath, ...additionalPaths])].sort();
13362
- return new Refusal(
13363
- `Prepared ${migration.legacyAppId} \u2192 ${migration.appId}. Commit the deterministic app migration changes, then rerun.`,
13364
- "migration_commit_required",
13365
- {
13366
- actionRequired: true,
13367
- action: {
13368
- cwd: repoToplevel(appDir),
13369
- argv: [
13370
- "git",
13371
- "commit",
13372
- "--only",
13373
- "--message",
13374
- `Migrate DeepSpace app identity to ${migration.appId}`,
13375
- "--",
13376
- ...migrationPaths
13377
- ]
13378
- },
13379
- extra: { migration, env: envName ?? null }
13197
+ reportPlan(plan, !dryRun);
13380
13198
  }
13381
- );
13382
- }
13383
- function sourceMigrationCommitRequired(appDir, plan) {
13384
- const migrationIds = plan.pending.map((migration) => migration.id);
13385
- return new Refusal(
13386
- `Applied ${migrationIds.join(", ")}. Commit the deterministic app migration changes, then rerun.`,
13387
- "migration_commit_required",
13388
- {
13389
- actionRequired: true,
13390
- action: {
13391
- cwd: repoToplevel(appDir),
13392
- argv: [
13393
- "git",
13394
- "commit",
13395
- "--only",
13396
- "--message",
13397
- "Apply DeepSpace app migrations",
13398
- "--",
13399
- ...plan.changedFiles
13400
- ]
13401
- },
13402
- extra: { sourceMigrations: plan.pending }
13403
- }
13404
- );
13405
- }
13406
- async function requireSourceMigrationDeploy(appDir, appId, deployUrl, token, options = {}) {
13407
- const { release: release2 } = await repoApi(deployUrl, token, appId).latestRelease();
13408
- const deployedIds = releaseMigrationIds(release2?.config);
13409
- const pendingIds = APP_MIGRATION_DEFINITIONS.map(({ id }) => id).filter(
13410
- (id) => !deployedIds.has(id)
13411
- );
13412
- const releaseAfterCutover = options.after === void 0 || typeof release2?.createdAt === "string" && Number.isFinite(Date.parse(release2.createdAt)) && Date.parse(release2.createdAt) > Date.parse(options.after);
13413
- if (pendingIds.length === 0 && releaseAfterCutover) return;
13414
- const sourceState = await getAppSource(deployUrl, token, appId);
13415
- if (sourceState.source?.provider === "github") {
13416
- const remote = requireGitHubRemote2(appDir, options.remoteName, sourceState.source.repository);
13417
- requirePublishedHead2(appDir, remote);
13418
- }
13419
- throw new Refusal(
13420
- pendingIds.length > 0 ? `App migrations ${pendingIds.join(", ")} are not in the live release. Deploy them, then rerun.` : "The canonical app has not been deployed since its identity cutover. Deploy it, then rerun.",
13421
- "migration_deploy_required",
13422
- {
13423
- actionRequired: true,
13424
- action: {
13425
- cwd: appDir,
13426
- argv: ["deepspace", "deploy", ...options.envName ? ["--env", options.envName] : []]
13427
- },
13428
- extra: { appId, sourceMigrations: pendingIds, after: options.after ?? null }
13429
- }
13430
- );
13431
- }
13432
- function releaseMigrationIds(config) {
13433
- if (config === null || typeof config !== "object" || Array.isArray(config)) return /* @__PURE__ */ new Set();
13434
- const validation = validateAppMigrationIds(
13435
- config.appMigrations ?? []
13436
- );
13437
- return new Set(validation.valid ? validation.ids : []);
13438
- }
13439
- function requireSourceMigrationWorktree(appDir, allowDirtyWrangler = false) {
13440
- if (isWorkTreeClean(appDir)) return;
13441
- const changed = statusFiles(repoToplevel(appDir));
13442
- if (allowDirtyWrangler && changed.every((path) => path === wranglerRepoPath(appDir))) return;
13443
- throw new Refusal(
13444
- "Commit or discard unrelated working-tree changes before applying app migrations.",
13445
- "dirty_worktree"
13446
- );
13447
- }
13448
- function restoreLegacyCommitRequired(appDir, migration, envName) {
13449
- const wranglerPath = wranglerRepoPath(appDir);
13450
- return new Refusal(
13451
- `Restored ${migration.legacyAppId} in wrangler.toml. Commit and push this recovery change, then rerun.`,
13452
- "migration_restore_commit_required",
13453
- {
13454
- actionRequired: true,
13455
- action: {
13456
- cwd: repoToplevel(appDir),
13457
- argv: [
13458
- "git",
13459
- "commit",
13460
- "--only",
13461
- "--message",
13462
- `Restore DeepSpace app identity to ${migration.legacyAppId}`,
13463
- "--",
13464
- wranglerPath
13465
- ]
13466
- },
13467
- extra: { migration, env: envName ?? null }
13199
+ if (plan.blockers.length) {
13200
+ throw new Refusal(
13201
+ `${plan.blockers.length} change(s) need a human or agent edit; each is listed above with its file, line, and the change required.`,
13202
+ "manual_changes_required",
13203
+ {
13204
+ actionRequired: true,
13205
+ extra: {
13206
+ currentVersion: currentRaw,
13207
+ targetVersion: targetRaw,
13208
+ applied: plan.pending.map((migration) => migration.id),
13209
+ blockers: plan.blockers
13210
+ }
13211
+ }
13212
+ );
13468
13213
  }
13469
- );
13470
- }
13471
- function wranglerRepoPath(appDir) {
13472
- return `${gitLine(appDir, ["rev-parse", "--show-prefix"])}wrangler.toml`;
13473
- }
13474
- function requireTrackedWrangler(appDir) {
13475
- const path = wranglerRepoPath(appDir);
13476
- if (runGit(repoToplevel(appDir), ["ls-files", "--error-unmatch", "--", path], { allowFail: true }).status !== 0) {
13477
- throw new Refusal(
13478
- "Track, commit, and push wrangler.toml before migrating; GitHub must already contain the deployment configuration.",
13479
- "wrangler_not_tracked"
13480
- );
13481
- }
13482
- }
13483
- function deployRequired(appDir, migration, envName) {
13484
- return new Refusal(
13485
- `Identity cutover committed; deploy once to verify ${migration.appId} on the existing physical resources.`,
13486
- "migration_deploy_required",
13487
- {
13488
- actionRequired: true,
13489
- action: {
13490
- cwd: appDir,
13491
- argv: ["deepspace", "deploy", ...envName ? ["--env", envName] : []]
13214
+ return {
13215
+ data: {
13216
+ currentVersion: currentRaw,
13217
+ targetVersion: targetRaw,
13218
+ pinned,
13219
+ dryRun,
13220
+ applied: plan.pending.map((migration) => ({
13221
+ id: migration.id,
13222
+ description: migration.description,
13223
+ files: migration.files,
13224
+ replacements: migration.replacements
13225
+ }))
13492
13226
  },
13493
- extra: { migration, env: envName ?? null }
13494
- }
13495
- );
13496
- }
13497
- function reportUpToDate(appId, json) {
13498
- if (!json) console.log(`No pending DeepSpace app migrations for ${appId}.`);
13499
- }
13500
- function reportSourcePlan(plan, json) {
13501
- if (json || plan.pending.length === 0) return;
13502
- for (const migration of plan.pending) {
13503
- console.log(
13504
- `Pending source migration: ${migration.id} (${migration.files.length} file${migration.files.length === 1 ? "" : "s"})`
13505
- );
13227
+ // The dependency is pinned but not installed: installing is the package
13228
+ // manager's job, and guessing which one an app uses is how a CLI breaks
13229
+ // a pnpm or bun project.
13230
+ ...pinned ? { action: cliAction("npm", "install") } : {}
13231
+ };
13506
13232
  }
13507
- }
13508
- function reportMigration(migration, json) {
13509
- if (json) return;
13510
- console.log(`Migration: ${migration.legacyAppId} \u2192 ${migration.appId}`);
13511
- console.log(`Status: ${migration.status}`);
13512
- console.log(`Physical resources: ${migration.resourceId}`);
13513
- console.log(`GitHub: ${migration.repository}`);
13514
- }
13233
+ });
13515
13234
 
13516
13235
  // src/cli/cli.ts
13517
13236
  var __dirname = dirname10(fileURLToPath3(import.meta.url));
13518
- var pkg = JSON.parse(readFileSync28(join24(__dirname, "..", "package.json"), "utf-8"));
13519
- if (process.platform === "linux" && !process.env.SSL_CERT_FILE && existsSync26("/etc/ssl/certs/ca-certificates.crt")) {
13237
+ var pkg = JSON.parse(readFileSync29(join25(__dirname, "..", "package.json"), "utf-8"));
13238
+ if (process.platform === "linux" && !process.env.SSL_CERT_FILE && existsSync27("/etc/ssl/certs/ca-certificates.crt")) {
13520
13239
  process.env.SSL_CERT_FILE = "/etc/ssl/certs/ca-certificates.crt";
13521
13240
  }
13522
13241
  var create2 = defineCommand14({
@@ -13565,7 +13284,7 @@ var auth = defineCommand14({
13565
13284
  var app = defineCommand14({
13566
13285
  meta: {
13567
13286
  name: "app",
13568
- description: "The app itself: create, init, list, files, source, migrate, undeploy, transfer, collaborators, domain, usage"
13287
+ description: "The app itself: create, init, list, files, source, update, undeploy, transfer, collaborators, domain, usage"
13569
13288
  },
13570
13289
  subCommands: {
13571
13290
  create: create2,
@@ -13578,7 +13297,7 @@ var app = defineCommand14({
13578
13297
  domain: domain_default,
13579
13298
  usage: usage_default,
13580
13299
  source: source_default,
13581
- migrate: migrate_default
13300
+ update: update_default
13582
13301
  }
13583
13302
  });
13584
13303
  var devGroup = defineCommand14({