deepspace 0.8.2 → 0.9.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
@@ -221,6 +221,62 @@ function wrapCommandErrors(cmd) {
221
221
  return cmd;
222
222
  }
223
223
 
224
+ // src/cli/lib/command-suggestions.ts
225
+ function editDistance(a, b) {
226
+ const left = a.toLowerCase();
227
+ const right = b.toLowerCase();
228
+ const dp = Array.from({ length: left.length + 1 }, (_, i) => [
229
+ i,
230
+ ...new Array(right.length).fill(0)
231
+ ]);
232
+ for (let j = 1; j <= right.length; j++) dp[0][j] = j;
233
+ for (let i = 1; i <= left.length; i++) {
234
+ for (let j = 1; j <= right.length; j++) {
235
+ dp[i][j] = Math.min(
236
+ dp[i - 1][j] + 1,
237
+ dp[i][j - 1] + 1,
238
+ dp[i - 1][j - 1] + (left[i - 1] === right[j - 1] ? 0 : 1)
239
+ );
240
+ }
241
+ }
242
+ return dp[left.length][right.length];
243
+ }
244
+ function commandPaths(table, prefix = []) {
245
+ const paths = [];
246
+ for (const [name, command] of Object.entries(table)) {
247
+ const path = [...prefix, name];
248
+ paths.push(path);
249
+ if (command.subCommands) paths.push(...commandPaths(command.subCommands, path));
250
+ }
251
+ return paths;
252
+ }
253
+ function closestCommandPath(input, table) {
254
+ const ranked = commandPaths(table).map((path) => ({ path, distance: editDistance(input, path.at(-1) ?? "") })).sort(
255
+ (a, b) => a.distance - b.distance || a.path.length - b.path.length || a.path.join(" ").localeCompare(b.path.join(" "))
256
+ );
257
+ return ranked[0]?.path ?? null;
258
+ }
259
+ function findUnknownCommand(argv, rootCommands) {
260
+ let table = rootCommands;
261
+ const accepted = [];
262
+ for (const token of argv) {
263
+ if (!table || token.startsWith("-")) return null;
264
+ const command = table[token];
265
+ if (!command) {
266
+ const relativeSuggestion = closestCommandPath(token, table);
267
+ if (!relativeSuggestion) return null;
268
+ return {
269
+ attemptedPath: ["deepspace", ...accepted, token],
270
+ helpPath: ["deepspace", ...accepted],
271
+ suggestion: ["deepspace", ...accepted, ...relativeSuggestion]
272
+ };
273
+ }
274
+ accepted.push(token);
275
+ table = command.subCommands;
276
+ }
277
+ return null;
278
+ }
279
+
224
280
  // src/cli/env.ts
225
281
  function resolveDeepSpaceEnvironment(value) {
226
282
  if (value === void 0 || value === "production") return "production";
@@ -653,7 +709,7 @@ async function provisionProfile(jwt) {
653
709
  }
654
710
  }
655
711
  function sleep(ms) {
656
- return new Promise((resolve18) => setTimeout(resolve18, ms));
712
+ return new Promise((resolve19) => setTimeout(resolve19, ms));
657
713
  }
658
714
  function base64url(bytes) {
659
715
  return bytes.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
@@ -730,8 +786,9 @@ import { join as join3, resolve } from "path";
730
786
  import { parse as parseToml } from "smol-toml";
731
787
 
732
788
  // src/server/utils/registry-client.ts
733
- var APP_ID_RE = /^(app_[0-9A-HJKMNP-TV-Z]{26}|[a-z0-9][a-z0-9_-]{0,63})$/;
734
- var STRICT_APP_ID_RE = /^app_[0-9A-HJKMNP-TV-Z]{26}$/;
789
+ var APP_ID_RE = /^app_[0-9A-HJKMNP-TV-Z]{26}$/;
790
+ var STRICT_APP_ID_RE = APP_ID_RE;
791
+ var LEGACY_APP_ID_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/;
735
792
 
736
793
  // src/cli/lib/app-identity.ts
737
794
  var CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
@@ -760,44 +817,59 @@ function mintUlid(now = Date.now()) {
760
817
  function mintAppId(now = Date.now()) {
761
818
  return `app_${mintUlid(now)}`;
762
819
  }
763
- async function resolveExistingAppId(deployUrl, token, appName) {
764
- const headers = { Authorization: `Bearer ${token}` };
765
- try {
766
- const res = await fetch(`${deployUrl}/api/apps`, { headers });
767
- if (res.ok) {
768
- const body = await res.json();
769
- const match = body.apps?.find((a) => a.name === appName);
770
- if (match?.appId) return { kind: "adopted", appId: match.appId, owned: true };
771
- }
772
- } catch {
773
- }
774
- try {
775
- const res = await fetch(
776
- `${deployUrl}/api/apps/${encodeURIComponent(appName)}/analytics?period=1h`,
777
- { headers }
778
- );
779
- if (res.ok) return { kind: "adopted", appId: appName, owned: false };
780
- if (res.status === 403) return { kind: "taken" };
781
- } catch {
782
- }
783
- return { kind: "none" };
784
- }
785
- function readAppId(cwd = process.cwd(), wranglerEnv) {
820
+ function readWranglerConfig(cwd) {
786
821
  const wranglerPath = join3(resolve(cwd), "wrangler.toml");
787
822
  if (!existsSync3(wranglerPath)) return null;
788
- let cfg;
789
823
  try {
790
- cfg = parseToml(readFileSync4(wranglerPath, "utf-8"));
824
+ return parseToml(readFileSync4(wranglerPath, "utf-8"));
791
825
  } catch (err) {
792
826
  throw new InputError(
793
827
  `Could not parse ${wranglerPath}: ${err instanceof Error ? err.message : String(err)}`,
794
828
  "invalid_config"
795
829
  );
796
830
  }
831
+ }
832
+ function readAppId(cwd = process.cwd(), wranglerEnv) {
833
+ const cfg = readWranglerConfig(cwd);
834
+ if (!cfg) return null;
797
835
  const vars = wranglerEnv ? cfg.env?.[wranglerEnv]?.vars : cfg.vars;
798
836
  const id = vars?.DEEPSPACE_APP_ID;
799
837
  return typeof id === "string" && APP_ID_RE.test(id) ? id : null;
800
838
  }
839
+ function readLegacyAppId(cwd = process.cwd(), wranglerEnv) {
840
+ const cfg = readWranglerConfig(cwd);
841
+ if (!cfg) return null;
842
+ const slot = wranglerEnv ? cfg.env?.[wranglerEnv] : cfg;
843
+ if (!slot) return null;
844
+ const declaredId = slot.vars?.DEEPSPACE_APP_ID;
845
+ if (declaredId !== void 0) {
846
+ if (typeof declaredId === "string" && LEGACY_APP_ID_RE.test(declaredId)) return declaredId;
847
+ throw new InputError(
848
+ "DEEPSPACE_APP_ID is present but invalid; correct it before migrating.",
849
+ "invalid_app_id"
850
+ );
851
+ }
852
+ const appName = typeof slot.vars?.APP_NAME === "string" ? slot.vars.APP_NAME : null;
853
+ const workerName = typeof slot.name === "string" ? slot.name : null;
854
+ if (!appName) return null;
855
+ if (!workerName) {
856
+ throw new InputError(
857
+ "Legacy migration requires both Worker name and APP_NAME to be present and identical.",
858
+ "ambiguous_legacy_app_id"
859
+ );
860
+ }
861
+ if (appName !== workerName) {
862
+ throw new InputError(
863
+ `Legacy APP_NAME "${appName}" does not match Worker name "${workerName}"; migration cannot choose safely.`,
864
+ "ambiguous_legacy_app_id"
865
+ );
866
+ }
867
+ const legacyAppId = appName;
868
+ if (!legacyAppId || !LEGACY_APP_ID_RE.test(legacyAppId) || APP_ID_RE.test(legacyAppId)) {
869
+ return null;
870
+ }
871
+ return legacyAppId;
872
+ }
801
873
  function writeAppId(cwd, appId, opts = {}) {
802
874
  const wranglerPath = join3(resolve(cwd), "wrangler.toml");
803
875
  if (!existsSync3(wranglerPath)) {
@@ -836,7 +908,7 @@ ${line}
836
908
  }
837
909
 
838
910
  // src/cli/commands/dev.ts
839
- import { resolve as resolve6, basename as basename2, join as join12 } from "path";
911
+ import { resolve as resolve7, basename as basename2, join as join12 } from "path";
840
912
  import spawn from "cross-spawn";
841
913
 
842
914
  // src/cli/lib/app-context.ts
@@ -1230,7 +1302,7 @@ function upsertWorktreeLaunchConfig(mainRepoRoot, worktreeName, appDir, opts) {
1230
1302
  let port = opts.port;
1231
1303
  if (opts.probePort) {
1232
1304
  const taken = new Set(
1233
- config.configurations.map((c) => Number(c?.port)).filter((p34) => Number.isInteger(p34) && p34 > 0)
1305
+ config.configurations.map((c) => Number(c?.port)).filter((p35) => Number.isInteger(p35) && p35 > 0)
1234
1306
  );
1235
1307
  for (let i = 0; i < WORKTREE_PORT_RANGE && taken.has(port); i++) {
1236
1308
  port = WORKTREE_PORT_MIN + (port - WORKTREE_PORT_MIN + 1) % WORKTREE_PORT_RANGE;
@@ -1341,6 +1413,8 @@ var RESERVED_BINDING_NAMES = /* @__PURE__ */ new Set([
1341
1413
  "ASSETS",
1342
1414
  "PLATFORM_WORKER",
1343
1415
  "API_WORKER",
1416
+ "DEEPSPACE_APP_ID",
1417
+ "DEEPSPACE_RESOURCE_ID",
1344
1418
  "APP_NAME",
1345
1419
  "OWNER_USER_ID",
1346
1420
  "AUTH_JWT_PUBLIC_KEY",
@@ -1358,7 +1432,9 @@ function validateBindingManifest(manifest) {
1358
1432
  const seenNames = /* @__PURE__ */ new Set();
1359
1433
  for (const entry of manifest) {
1360
1434
  if (!entry || typeof entry !== "object") {
1361
- errors.push({ reason: `Entry must be an object (got ${entry === null ? "null" : typeof entry})` });
1435
+ errors.push({
1436
+ reason: `Entry must be an object (got ${entry === null ? "null" : typeof entry})`
1437
+ });
1362
1438
  continue;
1363
1439
  }
1364
1440
  const e = entry;
@@ -1407,7 +1483,8 @@ function requiredFieldError(b) {
1407
1483
  case "r2_bucket":
1408
1484
  return b.bucket_name ? null : `r2_bucket binding "${b.name}" missing bucket_name (or "auto")`;
1409
1485
  case "kv_namespace": {
1410
- if (!b.namespace_id) return `kv_namespace binding "${b.name}" missing namespace_id (or "auto")`;
1486
+ if (!b.namespace_id)
1487
+ return `kv_namespace binding "${b.name}" missing namespace_id (or "auto")`;
1411
1488
  if (b.namespace_id === AUTO_PROVISION_SENTINEL && !b.title) {
1412
1489
  return `kv_namespace binding "${b.name}" with namespace_id="auto" requires "title"`;
1413
1490
  }
@@ -1909,7 +1986,7 @@ function prepareWranglerEnvConfig(appDir, envName, opts = {}) {
1909
1986
  if (!opts.sharedDevVarsCache) return { wranglerEnv: envName, cleanup() {
1910
1987
  } };
1911
1988
  warnIfLegacyEnvDevVarsExists(appDir, envName, opts.warn);
1912
- const config = readWranglerConfig(appDir);
1989
+ const config = readWranglerConfig2(appDir);
1913
1990
  const resolved = resolveAppNameForEnv(config, envName);
1914
1991
  if (!resolved.ok) throw new Error(resolved.reason);
1915
1992
  const flattened = flattenWranglerEnvConfig(config, envName);
@@ -1973,7 +2050,7 @@ var WranglerConfigError = class extends Error {
1973
2050
  this.name = "WranglerConfigError";
1974
2051
  }
1975
2052
  };
1976
- function readWranglerConfig(appDir) {
2053
+ function readWranglerConfig2(appDir) {
1977
2054
  const path = join7(appDir, "wrangler.toml");
1978
2055
  const raw = readFileSync8(path, "utf-8");
1979
2056
  try {
@@ -2170,8 +2247,8 @@ ${custom}
2170
2247
  }
2171
2248
 
2172
2249
  // src/cli/lib/install-status.ts
2173
- import { existsSync as existsSync9, readFileSync as readFileSync11 } from "fs";
2174
- import { dirname as dirname4, join as join9 } from "path";
2250
+ import { existsSync as existsSync9, readFileSync as readFileSync11, realpathSync as realpathSync2 } from "fs";
2251
+ import { dirname as dirname4, join as join9, resolve as resolve5 } from "path";
2175
2252
 
2176
2253
  // src/cli/lib/package-manager.ts
2177
2254
  import { existsSync as existsSync8, readFileSync as readFileSync10 } from "fs";
@@ -2217,18 +2294,18 @@ function ensureInstallReady(appDir) {
2217
2294
  const logPath = join9(sentinelDir, "install.log");
2218
2295
  if (existsSync9(errPath)) {
2219
2296
  const detail = readFileSync11(errPath, "utf-8").trimEnd();
2220
- const log28 = existsSync9(logPath) ? ` Full log: ${logPath}.` : "";
2297
+ const log29 = existsSync9(logPath) ? ` Full log: ${logPath}.` : "";
2221
2298
  throw new Refusal(
2222
- `Dependency install failed: ${detail}.${log28} Run \`npm install\` (or \`bun install\`) manually, then retry.`,
2299
+ `Dependency install failed: ${detail}.${log29} Run \`npm install\` (or \`bun install\`) manually, then retry.`,
2223
2300
  "install_failed",
2224
2301
  { action: { cwd: appDir, argv: [detectPackageManager(appDir), "install"] } }
2225
2302
  );
2226
2303
  }
2227
2304
  if (existsSync9(startedPath) && !existsSync9(donePath)) {
2228
2305
  if (!installWorkerAlive(sentinelDir)) {
2229
- const log28 = existsSync9(logPath) ? ` See what happened: ${logPath}.` : "";
2306
+ const log29 = existsSync9(logPath) ? ` See what happened: ${logPath}.` : "";
2230
2307
  throw new Refusal(
2231
- `The dependency install is no longer running and never finished.${log28} Run \`npm install\` (or \`bun install\`) manually, then retry.`,
2308
+ `The dependency install is no longer running and never finished.${log29} Run \`npm install\` (or \`bun install\`) manually, then retry.`,
2232
2309
  "install_failed",
2233
2310
  { action: { cwd: appDir, argv: [detectPackageManager(appDir), "install"] } }
2234
2311
  );
@@ -2248,14 +2325,27 @@ function ensureInstallReady(appDir) {
2248
2325
  );
2249
2326
  }
2250
2327
  function resolvesDeepspace(fromDir) {
2251
- let dir = fromDir;
2328
+ let dir = canonicalPath2(fromDir);
2329
+ let checkoutRoot = null;
2330
+ try {
2331
+ checkoutRoot = canonicalPath2(repoToplevel(dir));
2332
+ } catch {
2333
+ }
2252
2334
  for (; ; ) {
2253
2335
  if (existsSync9(join9(dir, "node_modules", "deepspace", "package.json"))) return true;
2336
+ if (dir === checkoutRoot) return false;
2254
2337
  const parent = dirname4(dir);
2255
2338
  if (parent === dir) return false;
2256
2339
  dir = parent;
2257
2340
  }
2258
2341
  }
2342
+ function canonicalPath2(path) {
2343
+ try {
2344
+ return realpathSync2(path);
2345
+ } catch {
2346
+ return resolve5(path);
2347
+ }
2348
+ }
2259
2349
  function installWorkerAlive(sentinelDir) {
2260
2350
  const pidPath = join9(sentinelDir, "install.pid");
2261
2351
  if (!existsSync9(pidPath)) return false;
@@ -2273,7 +2363,7 @@ function installWorkerAlive(sentinelDir) {
2273
2363
  import { execFileSync } from "child_process";
2274
2364
  import { existsSync as existsSync10 } from "fs";
2275
2365
  import * as nodeModule from "module";
2276
- import { resolve as resolve5 } from "path";
2366
+ import { resolve as resolve6 } from "path";
2277
2367
  function preflightNodeVersion(command, hasRegisterHooks = typeof nodeModule.registerHooks === "function") {
2278
2368
  if (hasRegisterHooks) return;
2279
2369
  throw new Refusal(
@@ -2284,7 +2374,7 @@ function preflightNodeVersion(command, hasRegisterHooks = typeof nodeModule.regi
2284
2374
  var DLL_NOT_FOUND = -1073741515;
2285
2375
  function preflightWindowsWorkerd(appDir) {
2286
2376
  if (process.platform !== "win32") return;
2287
- const workerdExe = resolve5(
2377
+ const workerdExe = resolve6(
2288
2378
  appDir,
2289
2379
  "node_modules",
2290
2380
  "@cloudflare",
@@ -2364,7 +2454,7 @@ function lintSchema(schema) {
2364
2454
  const offendingRoles = Object.entries(schema.permissions).filter(([, perms]) => perms.read === true).map(([role]) => role);
2365
2455
  if (offendingRoles.length > 0) {
2366
2456
  const usesAware = Object.values(schema.permissions).some(
2367
- (p34) => typeof p34.read === "string" && VISIBILITY_AWARE.includes(p34.read)
2457
+ (p35) => typeof p35.read === "string" && VISIBILITY_AWARE.includes(p35.read)
2368
2458
  );
2369
2459
  if (!usesAware) {
2370
2460
  warnings.push(
@@ -2445,10 +2535,10 @@ function formatSchemaLintFindings(findings) {
2445
2535
  import { createServer } from "net";
2446
2536
  var DEFAULT_PORT = 5173;
2447
2537
  function checkPortAvailable(port, host = "0.0.0.0") {
2448
- return new Promise((resolve18) => {
2538
+ return new Promise((resolve19) => {
2449
2539
  const srv = createServer();
2450
- srv.once("error", () => resolve18(false));
2451
- srv.once("listening", () => srv.close(() => resolve18(true)));
2540
+ srv.once("error", () => resolve19(false));
2541
+ srv.once("listening", () => srv.close(() => resolve19(true)));
2452
2542
  srv.listen(port, host);
2453
2543
  });
2454
2544
  }
@@ -2503,7 +2593,7 @@ var dev_default = defineDeepspaceCommand({
2503
2593
  };
2504
2594
  preflightNodeVersion("dev start");
2505
2595
  const wranglerEnv = typeof args.env === "string" && args.env.trim() ? args.env.trim() : void 0;
2506
- const start = resolve6(args.dir ?? ".");
2596
+ const start = resolve7(args.dir ?? ".");
2507
2597
  const appDir = findAppDir(start);
2508
2598
  if (!appDir) {
2509
2599
  throw new Refusal(noAppDirMessage(start, findChildApps(start)), "no_app_dir");
@@ -2914,7 +3004,7 @@ function isAlive(pid) {
2914
3004
  }
2915
3005
 
2916
3006
  // src/cli/commands/test.ts
2917
- import { resolve as resolve7 } from "path";
3007
+ import { resolve as resolve8 } from "path";
2918
3008
  import { sync as spawnSync4 } from "cross-spawn";
2919
3009
 
2920
3010
  // src/cli/lib/playwright.ts
@@ -2974,7 +3064,7 @@ var test_default = defineDeepspaceCommand({
2974
3064
  preflightNodeVersion("test run");
2975
3065
  const suite = args.suite ?? "default";
2976
3066
  const wranglerEnv = typeof args.env === "string" && args.env.trim() ? args.env.trim() : void 0;
2977
- const start = resolve7(".");
3067
+ const start = resolve8(".");
2978
3068
  const appDir = findAppDir(start);
2979
3069
  if (!appDir) {
2980
3070
  throw new Refusal(noAppDirMessage(start, findChildApps(start)), "no_app_dir");
@@ -3113,7 +3203,7 @@ function runVitest(appDir) {
3113
3203
 
3114
3204
  // src/cli/commands/screenshot.ts
3115
3205
  import { sync as spawnSync5 } from "cross-spawn";
3116
- import { resolve as resolve8 } from "path";
3206
+ import { resolve as resolve9 } from "path";
3117
3207
  var screenshot_default = defineDeepspaceCommand({
3118
3208
  meta: {
3119
3209
  name: "screenshot",
@@ -3142,7 +3232,7 @@ var screenshot_default = defineDeepspaceCommand({
3142
3232
  }
3143
3233
  },
3144
3234
  async run({ args }) {
3145
- const appDir = resolve8(".");
3235
+ const appDir = resolve9(".");
3146
3236
  ensurePlaywright(appDir);
3147
3237
  const url = String(args.url);
3148
3238
  const output = String(args.output);
@@ -3499,13 +3589,13 @@ var test_accounts_default = defineCommand2({
3499
3589
  // src/cli/commands/deploy.ts
3500
3590
  import * as p12 from "@clack/prompts";
3501
3591
  import { defineCommand as defineCommand3 } from "citty";
3502
- import { resolve as resolve11 } from "path";
3592
+ import { resolve as resolve12 } from "path";
3503
3593
 
3504
3594
  // src/cli/commands/deploy/build.ts
3505
3595
  import * as p6 from "@clack/prompts";
3506
3596
  import { execSync as execSync2 } from "child_process";
3507
3597
  import { existsSync as existsSync13, readFileSync as readFileSync14, readdirSync as readdirSync4 } from "fs";
3508
- import { dirname as dirname5, join as join14, resolve as resolve9 } from "path";
3598
+ import { dirname as dirname5, join as join14, resolve as resolve10 } from "path";
3509
3599
  var RESERVED_RUN_WORKER_FIRST = /* @__PURE__ */ new Set([
3510
3600
  "/api/*",
3511
3601
  "/ws/*",
@@ -3549,7 +3639,7 @@ async function buildDeployBundle(options) {
3549
3639
  );
3550
3640
  }
3551
3641
  const deployConfig = JSON.parse(readFileSync14(deployConfigPath, "utf-8"));
3552
- const outputWranglerPath = resolve9(dirname5(deployConfigPath), deployConfig.configPath);
3642
+ const outputWranglerPath = resolve10(dirname5(deployConfigPath), deployConfig.configPath);
3553
3643
  if (!existsSync13(outputWranglerPath)) {
3554
3644
  output.die(`Output wrangler.json not found at ${outputWranglerPath}`, "build_output_missing");
3555
3645
  }
@@ -3563,7 +3653,7 @@ async function buildDeployBundle(options) {
3563
3653
  }
3564
3654
  const workerDir = dirname5(outputWranglerPath);
3565
3655
  const workerBundlePath = join14(workerDir, outputConfig.main);
3566
- const clientDir = outputConfig.assets?.directory ? resolve9(workerDir, outputConfig.assets.directory) : null;
3656
+ const clientDir = outputConfig.assets?.directory ? resolve10(workerDir, outputConfig.assets.directory) : null;
3567
3657
  if (!existsSync13(workerBundlePath)) {
3568
3658
  output.die(`Worker bundle not found at ${workerBundlePath}`, "build_output_missing");
3569
3659
  }
@@ -4100,7 +4190,7 @@ function repoApi(deployUrl, token, appId) {
4100
4190
  // src/cli/lib/vc-remote.ts
4101
4191
  import { existsSync as existsSync15 } from "fs";
4102
4192
  import { tmpdir } from "os";
4103
- import { isAbsolute as isAbsolute3, relative, resolve as resolve10 } from "path";
4193
+ import { isAbsolute as isAbsolute3, relative, resolve as resolve11 } from "path";
4104
4194
  var SPACE_REMOTE = "space";
4105
4195
  function deployBaseUrl() {
4106
4196
  return process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
@@ -4117,6 +4207,16 @@ function gitAuthEnv(token, base = deployBaseUrl()) {
4117
4207
  [`GIT_CONFIG_VALUE_${index}`]: `Authorization: Bearer ${token}`
4118
4208
  };
4119
4209
  }
4210
+ function gitSourceImportEnv(token, revision, base = deployBaseUrl()) {
4211
+ const env = gitAuthEnv(token, base);
4212
+ const index = Number(env.GIT_CONFIG_COUNT);
4213
+ return {
4214
+ ...env,
4215
+ GIT_CONFIG_COUNT: String(index + 1),
4216
+ [`GIT_CONFIG_KEY_${index}`]: `http.${base.replace(/\/+$/, "")}.extraHeader`,
4217
+ [`GIT_CONFIG_VALUE_${index}`]: `X-DeepSpace-Source-Revision: ${revision}`
4218
+ };
4219
+ }
4120
4220
  function runGitRemote(cwd, token, args, opts = {}) {
4121
4221
  return runGit(cwd, args, { ...opts, env: { ...gitAuthEnv(token), ...opts.env ?? {} } });
4122
4222
  }
@@ -4124,7 +4224,7 @@ function entryIsTransient(cwd) {
4124
4224
  const entry = process.argv[1];
4125
4225
  if (!entry) return true;
4126
4226
  if (/[\\/]_npx[\\/]/.test(entry) || entry.startsWith(tmpdir())) return true;
4127
- const rel = relative(resolve10(cwd), resolve10(entry));
4227
+ const rel = relative(resolve11(cwd), resolve11(entry));
4128
4228
  return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel);
4129
4229
  }
4130
4230
  function helperEntryUsable(command) {
@@ -4244,6 +4344,12 @@ function classifyPushTransportFailure(error) {
4244
4344
  error: `A first push to this new app hit your plan's active-app quota. Use \`deepspace app list\` to choose an app to undeploy, or upgrade your plan, then retry.`
4245
4345
  };
4246
4346
  }
4347
+ if (/(?:HTTP |error: )422\b/i.test(message)) {
4348
+ return {
4349
+ code: "source_managed_by_github",
4350
+ error: "This app uses GitHub source. Push with normal Git/GitHub, then run `deepspace deploy`."
4351
+ };
4352
+ }
4247
4353
  if (/(?:HTTP |error: )429\b/i.test(message)) {
4248
4354
  return {
4249
4355
  code: "rate_limited",
@@ -4322,22 +4428,138 @@ function resolveWorkspaceWorktree(worktrees, workspaceId) {
4322
4428
  return worktrees.find((worktree) => worktree.branch === branch)?.path ?? null;
4323
4429
  }
4324
4430
 
4431
+ // src/cli/lib/source-control.ts
4432
+ function githubRepositoryFromUrl(url) {
4433
+ const trimmed = url.trim();
4434
+ let path = null;
4435
+ const scp = /^(?:[^@\s]+@)?github\.com:([^\s]+)$/i.exec(trimmed);
4436
+ if (scp) {
4437
+ path = scp[1];
4438
+ } else {
4439
+ try {
4440
+ const parsed = new URL(trimmed);
4441
+ if (parsed.hostname.toLowerCase() !== "github.com") return null;
4442
+ path = parsed.pathname.replace(/^\/+/, "");
4443
+ } catch {
4444
+ return null;
4445
+ }
4446
+ }
4447
+ const repository = path.replace(/\/+$/, "").replace(/\.git$/i, "");
4448
+ if (!/^[A-Za-z0-9][A-Za-z0-9-]{0,38}\/[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/.test(repository)) {
4449
+ return null;
4450
+ }
4451
+ return repository.toLowerCase();
4452
+ }
4453
+ function listGitHubRemotes(cwd) {
4454
+ const names = runGit(cwd, ["remote"], { allowFail: true }).stdout.toString("utf-8").split("\n").map((name) => name.trim()).filter(Boolean);
4455
+ const remotes = [];
4456
+ for (const name of names) {
4457
+ const result = runGit(cwd, ["remote", "get-url", name], { allowFail: true });
4458
+ if (result.status !== 0) continue;
4459
+ const url = result.stdout.toString("utf-8").trim();
4460
+ const repository = githubRepositoryFromUrl(url);
4461
+ if (repository) remotes.push({ name, repository, url });
4462
+ }
4463
+ return remotes;
4464
+ }
4465
+ function selectGitHubRemote(cwd, options = {}) {
4466
+ const remotes = listGitHubRemotes(cwd);
4467
+ if (options.name) return remotes.find((remote) => remote.name === options.name) ?? null;
4468
+ const repository = options.repository?.toLowerCase();
4469
+ const matching = repository ? remotes.filter((remote) => remote.repository === repository) : remotes;
4470
+ return matching.find((remote) => remote.name === "origin") ?? (matching.length === 1 ? matching[0] : null);
4471
+ }
4472
+ function remotePublicRefs(cwd, remote) {
4473
+ const output = runGit(cwd, ["ls-remote", "--refs", remote]).stdout.toString("utf-8");
4474
+ return output.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => {
4475
+ const [oid, name] = line.split(/\s+/, 2);
4476
+ return { oid, name };
4477
+ }).filter(
4478
+ (ref) => /^[0-9a-f]{40}$/.test(ref.oid) && (ref.name.startsWith("refs/heads/") || ref.name.startsWith("refs/tags/"))
4479
+ );
4480
+ }
4481
+ function remoteBranchOid(cwd, remote, branch) {
4482
+ return remotePublicRefs(cwd, remote).find((ref) => ref.name === `refs/heads/${branch}`)?.oid ?? null;
4483
+ }
4484
+ function localTransferRefs(cwd, prefix, target) {
4485
+ const output = runGit(cwd, [
4486
+ "for-each-ref",
4487
+ "--format=%(refname) %(objectname)",
4488
+ prefix
4489
+ ]).stdout.toString("utf-8");
4490
+ return output.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => {
4491
+ const [name, oid] = line.split(/\s+/, 2);
4492
+ return { name: `${target}/${name.slice(`${prefix}/`.length)}`, oid };
4493
+ }).filter((ref) => /^[0-9a-f]{40}$/.test(ref.oid));
4494
+ }
4495
+
4496
+ // src/cli/lib/source-api.ts
4497
+ async function getAppSource(deployUrl, token, appId) {
4498
+ try {
4499
+ const state = await apiFetch(
4500
+ deployUrl,
4501
+ token,
4502
+ `/api/apps/${encodeURIComponent(appId)}/source`
4503
+ );
4504
+ return { ...state, registered: true };
4505
+ } catch (error) {
4506
+ if (error instanceof ApiError && error.code === "app_not_found") {
4507
+ return { appId, source: null, revision: 0, registered: false };
4508
+ }
4509
+ throw error;
4510
+ }
4511
+ }
4512
+ function setAppSource(deployUrl, token, appId, input) {
4513
+ return apiFetch(
4514
+ deployUrl,
4515
+ token,
4516
+ `/api/apps/${encodeURIComponent(appId)}/source`,
4517
+ { method: "POST", body: JSON.stringify(input) }
4518
+ );
4519
+ }
4520
+
4325
4521
  // src/cli/commands/deploy/repository.ts
4326
4522
  async function syncDeployRepository(options) {
4327
- const { deployUrl, appDir, appId, token, push, ignoreStale, output } = options;
4523
+ const { deployUrl, appDir, appId, token, push, ignoreStale, output, sourceState } = options;
4328
4524
  let commitOid = null;
4329
4525
  let recoverable = false;
4330
4526
  const deployKey = mintIdempotencyKey();
4331
- if (!push) {
4527
+ let source = sourceState.source;
4528
+ let sourceRevision = sourceState.revision;
4529
+ const baseReleaseId = null;
4530
+ if (!push && source?.provider !== "github") {
4332
4531
  try {
4333
4532
  assertSyncableRepo(appDir);
4334
4533
  commitOid = resolveCommit(appDir, "HEAD");
4335
4534
  } catch {
4336
4535
  }
4337
- return { commitOid, recoverable, deployKey };
4536
+ return { commitOid, recoverable, deployKey, source, sourceRevision, baseReleaseId };
4338
4537
  }
4339
4538
  try {
4340
4539
  assertSyncableRepo(appDir);
4540
+ if (source?.provider === "github") {
4541
+ const github = syncGitHubDeploy({
4542
+ deployUrl,
4543
+ appDir,
4544
+ appId,
4545
+ token,
4546
+ source,
4547
+ ignoreStale,
4548
+ output
4549
+ });
4550
+ return await github.then((result) => ({
4551
+ ...result,
4552
+ deployKey,
4553
+ source,
4554
+ sourceRevision
4555
+ }));
4556
+ }
4557
+ if (!source && listGitHubRemotes(appDir).length > 0) {
4558
+ output.die(
4559
+ "This app has a GitHub remote but no claimed source. Choose once with `deepspace app source github` (manual GitHub ownership) or `deepspace app source deepspace` (packaged DeepSpace source), then deploy again.",
4560
+ "source_unclaimed"
4561
+ );
4562
+ }
4341
4563
  ensureSpaceRemote(appDir, appId);
4342
4564
  const branch = currentBranch(appDir);
4343
4565
  const workspaceBranchId = workspaceIdFromBranch(branch);
@@ -4351,7 +4573,9 @@ async function syncDeployRepository(options) {
4351
4573
  throw new Error(refusal.error);
4352
4574
  }
4353
4575
  const tip = resolveCommit(appDir, `refs/heads/${branch}`);
4354
- if (!tip) return { commitOid, recoverable, deployKey };
4576
+ if (!tip) {
4577
+ return { commitOid, recoverable, deployKey, source, sourceRevision, baseReleaseId };
4578
+ }
4355
4579
  const secretFiles = workspaceBranchId ? [] : trackedSecretFiles(appDir, tip);
4356
4580
  if (workspaceBranchId) {
4357
4581
  const { view } = await repoApi(deployUrl, token, appId).getWorkspace(workspaceBranchId);
@@ -4427,11 +4651,81 @@ async function syncDeployRepository(options) {
4427
4651
  }
4428
4652
  }
4429
4653
  commitOid = tip;
4654
+ if (!source) {
4655
+ const claimed = await getAppSource(deployUrl, token, appId);
4656
+ source = claimed.source;
4657
+ sourceRevision = claimed.revision;
4658
+ }
4430
4659
  } catch (error) {
4431
4660
  const failure = deployRepositoryFailure(error);
4432
4661
  output.die(failure.error, failure.code);
4433
4662
  }
4434
- return { commitOid, recoverable, deployKey };
4663
+ return { commitOid, recoverable, deployKey, source, sourceRevision, baseReleaseId };
4664
+ }
4665
+ async function syncGitHubDeploy(options) {
4666
+ const { deployUrl, appDir, appId, token, source, ignoreStale, output } = options;
4667
+ if (!isWorkTreeClean(appDir)) {
4668
+ const refusal = dirtyWorktreeRefusal(currentBranch(appDir));
4669
+ output.die(refusal.error, refusal.code);
4670
+ }
4671
+ const branch = currentBranch(appDir);
4672
+ if (!branch) {
4673
+ const refusal = detachedHeadRefusal();
4674
+ output.die(refusal.error, refusal.code);
4675
+ throw new Error(refusal.error);
4676
+ }
4677
+ const tip = resolveCommit(appDir, "HEAD");
4678
+ if (!tip) {
4679
+ output.die("The repository has no commit to deploy.", "no_commits");
4680
+ throw new Error("The repository has no commit to deploy.");
4681
+ }
4682
+ const remote = selectGitHubRemote(appDir, { repository: source.repository });
4683
+ if (!remote) {
4684
+ output.die(
4685
+ `No local GitHub remote points at ${source.repository}. Add one, then deploy again.`,
4686
+ "github_remote_required"
4687
+ );
4688
+ throw new Error(`No local GitHub remote points at ${source.repository}.`);
4689
+ }
4690
+ const secretFiles = trackedSecretFiles(appDir, tip);
4691
+ if (secretFiles.length > 0) {
4692
+ output.die(
4693
+ `Refusing to deploy a GitHub commit that tracks secret file(s): ${secretFiles.join(", ")}. Remove them from Git history, push the repaired branch, then deploy.`,
4694
+ "secret_in_history"
4695
+ );
4696
+ }
4697
+ if (remoteBranchOid(appDir, remote.name, branch) !== tip) {
4698
+ output.die(
4699
+ `GitHub ${source.repository} does not have ${branch} at ${tip.slice(0, 10)}. DeepSpace never pushes GitHub source; publish it manually, then deploy again.`,
4700
+ "github_push_required",
4701
+ {
4702
+ actionRequired: true,
4703
+ action: { cwd: appDir, argv: ["git", "push", remote.name, branch] }
4704
+ }
4705
+ );
4706
+ }
4707
+ const latest = await repoApi(deployUrl, token, appId).latestRelease().then((result) => result.release);
4708
+ if (latest?.commitOid && latest.commitOid !== tip) {
4709
+ const known = resolveCommit(appDir, latest.commitOid) !== null;
4710
+ const containsLive = known && isAncestor(appDir, latest.commitOid, tip);
4711
+ if ((!known || !containsLive) && !ignoreStale) {
4712
+ output.die(
4713
+ known ? `This GitHub branch does not contain live release #${latest.seq} (${latest.commitOid.slice(0, 10)}). Integrate the published GitHub history, then redeploy.` : `The live release commit ${latest.commitOid.slice(0, 10)} is not in this checkout. Fetch ${source.repository}, integrate it, then redeploy.`,
4714
+ "stale_base",
4715
+ {
4716
+ actionRequired: true,
4717
+ action: { cwd: appDir, argv: ["git", "fetch", remote.name] }
4718
+ }
4719
+ );
4720
+ }
4721
+ if (!containsLive) {
4722
+ p9.log.warn(
4723
+ "Deploying GitHub history that does not contain the live release (--ignore-stale)."
4724
+ );
4725
+ }
4726
+ }
4727
+ p9.log.info(`Verified GitHub ${source.repository}:${branch} at ${tip.slice(0, 10)} (no push).`);
4728
+ return { commitOid: tip, recoverable: true, baseReleaseId: latest?.id ?? null };
4435
4729
  }
4436
4730
  function classifyRemoteState(appDir, token, branch, localTip) {
4437
4731
  let remoteTip = null;
@@ -4473,7 +4767,7 @@ async function pushWithTransientRetry(push) {
4473
4767
  } catch (error) {
4474
4768
  const message = errorMessage3(error);
4475
4769
  if (/(?:HTTP |error: )(?:429|503)\b/i.test(message) && attempt < backoffMs.length) {
4476
- await new Promise((resolve18) => setTimeout(resolve18, backoffMs[attempt]));
4770
+ await new Promise((resolve19) => setTimeout(resolve19, backoffMs[attempt]));
4477
4771
  continue;
4478
4772
  }
4479
4773
  throw error;
@@ -4601,6 +4895,14 @@ async function deployBuiltBundle(options) {
4601
4895
  if (shouldSendLineage(repository.commitOid, repository.recoverable)) {
4602
4896
  form.append("commitOid", repository.commitOid);
4603
4897
  }
4898
+ if (repository.source) {
4899
+ form.append("sourceProvider", repository.source.provider);
4900
+ form.append("sourceRevision", String(repository.sourceRevision));
4901
+ if (repository.source.provider === "github") {
4902
+ form.append("sourceRepository", repository.source.repository);
4903
+ }
4904
+ }
4905
+ if (repository.baseReleaseId) form.append("baseReleaseId", repository.baseReleaseId);
4604
4906
  if (ignoreStale) form.append("ignoreStale", "true");
4605
4907
  form.append("deployKey", repository.deployKey);
4606
4908
  return form;
@@ -4627,7 +4929,7 @@ async function deployBuiltBundle(options) {
4627
4929
  }
4628
4930
  let body = await response.json().catch(() => ({}));
4629
4931
  for (let attempt = 0; response.status === 503 && body.code === "release_finalization_pending" && attempt < 3; attempt++) {
4630
- await new Promise((resolve18) => setTimeout(resolve18, 250 * 2 ** attempt));
4932
+ await new Promise((resolve19) => setTimeout(resolve19, 250 * 2 ** attempt));
4631
4933
  response = await postCommit();
4632
4934
  body = await response.json().catch(() => ({}));
4633
4935
  }
@@ -4736,7 +5038,7 @@ async function postWithRetry(url, makeInit, {
4736
5038
  lastError = error;
4737
5039
  }
4738
5040
  if (attempt < attempts) {
4739
- await new Promise((resolve18) => setTimeout(resolve18, Math.min(8e3, 500 * 2 ** (attempt - 1))));
5041
+ await new Promise((resolve19) => setTimeout(resolve19, Math.min(8e3, 500 * 2 ** (attempt - 1))));
4740
5042
  }
4741
5043
  }
4742
5044
  if (lastResponse) return lastResponse;
@@ -4885,15 +5187,10 @@ var deploy_default = defineCommand3({
4885
5187
  description: "Deploy even though hand-edited .dev.vars secrets are absent from the store (they will NOT be deployed, and any a previous deploy set are dropped).",
4886
5188
  default: false
4887
5189
  },
4888
- adopt: {
4889
- type: "boolean",
4890
- description: "Confirm adopting an existing app you can deploy but do not own (collaborator/admin on-behalf) when wrangler.toml has no DEEPSPACE_APP_ID. Without this flag an interactive prompt asks.",
4891
- default: false
4892
- },
4893
5190
  // Citty maps --no-push to the negation of an argument named `push`.
4894
5191
  push: {
4895
5192
  type: "boolean",
4896
- description: "Sync the branch to the app's cloud repo before deploying (--no-push to skip; the release then may point at source the platform cannot recover).",
5193
+ description: "Sync DeepSpace source before deploying. The legacy --no-push escape hatch skips recoverable source lineage; GitHub apps are verified and never pushed automatically.",
4897
5194
  default: true
4898
5195
  },
4899
5196
  "ignore-stale": {
@@ -4910,7 +5207,7 @@ var deploy_default = defineCommand3({
4910
5207
  async run({ args }) {
4911
5208
  const output = createDeployOutput(args.json === true);
4912
5209
  preflightNodeVersion("deploy");
4913
- const appDir = resolve11(args.dir ?? ".");
5210
+ const appDir = resolve12(args.dir ?? ".");
4914
5211
  const selectorRefusal = blankSelectorRefusal(args);
4915
5212
  if (selectorRefusal) output.die(selectorRefusal.error, selectorRefusal.code);
4916
5213
  const envName = typeof args.env === "string" && args.env.trim() ? args.env.trim() : void 0;
@@ -4927,7 +5224,7 @@ var deploy_default = defineCommand3({
4927
5224
  }
4928
5225
  p12.intro("Deploying DeepSpace app");
4929
5226
  output.showIntro();
4930
- const wranglerConfig = readWranglerConfig(appDir);
5227
+ const wranglerConfig = readWranglerConfig2(appDir);
4931
5228
  const nameResult = resolveAppNameForEnv(wranglerConfig, envName);
4932
5229
  if (!nameResult.ok) return output.die(nameResult.reason, "invalid_app_name");
4933
5230
  const declaredName = envName ? wranglerConfig.env?.[envName]?.name : wranglerConfig.name;
@@ -4940,35 +5237,21 @@ var deploy_default = defineCommand3({
4940
5237
  }
4941
5238
  const appName = nameResult.name;
4942
5239
  let appId = readAppId(appDir, envName);
4943
- p12.log.info(envName ? `App: ${appName} (env: ${envName})` : `App: ${appName}`);
4944
- const { token, ownerId } = await authenticate(appDir, output);
4945
5240
  if (!appId) {
4946
- const existing = await resolveExistingAppId(DEPLOY_URL3, token, appName);
4947
- if (existing.kind === "taken") {
5241
+ const legacyAppId = readLegacyAppId(appDir, envName);
5242
+ if (legacyAppId) {
4948
5243
  output.die(
4949
- `"${appName}" is taken by an app you don't have deploy access to. Ask the owner to add you as a collaborator (\`deepspace app collaborators add\`), or pick a different \`name\` in wrangler.toml.`,
4950
- "name_taken"
5244
+ `This checkout still uses legacy app identity ${legacyAppId}. Run \`deepspace app migrate --dry-run\`, then complete \`deepspace app migrate\` before deploying.`,
5245
+ "legacy_app_migration_required"
4951
5246
  );
4952
5247
  }
4953
- if (existing.kind === "adopted" && !existing.owned && !args.adopt) {
4954
- if (output.nonInteractive) {
4955
- output.die(
4956
- `"${appName}" is an existing app owned by another account that you can deploy on-behalf (collaborator/admin). Confirmation needs --adopt here \u2014 re-run with --adopt to deploy it, or pick a different \`name\` in wrangler.toml if you meant a new app.`,
4957
- "confirmation_required"
4958
- );
4959
- }
4960
- const confirmed = await p12.confirm({
4961
- message: `"${appName}" is an existing app owned by another account that you can deploy on-behalf (you are a collaborator or admin). Deploy it? (Meant a new app? Pick a different \`name\` in wrangler.toml.)`
4962
- });
4963
- if (p12.isCancel(confirmed) || !confirmed) {
4964
- output.die("Deploy cancelled.", "cancelled");
4965
- }
4966
- }
4967
- appId = existing.kind === "adopted" ? existing.appId : mintAppId();
5248
+ }
5249
+ p12.log.info(envName ? `App: ${appName} (env: ${envName})` : `App: ${appName}`);
5250
+ const { token, ownerId } = await authenticate(appDir, output);
5251
+ if (!appId) {
5252
+ appId = mintAppId();
4968
5253
  writeAppId(appDir, appId, { wranglerEnv: envName });
4969
- p12.log.info(
4970
- existing.kind === "adopted" ? `Adopted existing app id ${appId} for ${appName} \u2014 commit wrangler.toml.` : `Minted app id ${appId} \u2014 commit wrangler.toml.`
4971
- );
5254
+ p12.log.info(`Minted app id ${appId} \u2014 commit wrangler.toml.`);
4972
5255
  }
4973
5256
  p12.log.info(`Id: ${appId}`);
4974
5257
  const secretsCache = await loadDeploySecrets({
@@ -4980,6 +5263,7 @@ var deploy_default = defineCommand3({
4980
5263
  token,
4981
5264
  output
4982
5265
  });
5266
+ const sourceState = await getAppSource(DEPLOY_URL3, token, appId);
4983
5267
  const repository = await syncDeployRepository({
4984
5268
  deployUrl: DEPLOY_URL3,
4985
5269
  appDir,
@@ -4987,7 +5271,8 @@ var deploy_default = defineCommand3({
4987
5271
  token,
4988
5272
  push: args.push !== false,
4989
5273
  ignoreStale: Boolean(args["ignore-stale"]),
4990
- output
5274
+ output,
5275
+ sourceState
4991
5276
  });
4992
5277
  const spinner2 = createSpinner();
4993
5278
  const bundle = await buildDeployBundle({
@@ -5101,7 +5386,7 @@ async function waitForAssetsReady(url, timeoutMs) {
5101
5386
  if (!body.includes("Assets have not yet deployed") && !body.includes("No app configured")) {
5102
5387
  streak++;
5103
5388
  if (streak >= 3) return true;
5104
- await new Promise((resolve18) => setTimeout(resolve18, 1500));
5389
+ await new Promise((resolve19) => setTimeout(resolve19, 1500));
5105
5390
  continue;
5106
5391
  }
5107
5392
  streak = 0;
@@ -5109,7 +5394,7 @@ async function waitForAssetsReady(url, timeoutMs) {
5109
5394
  streak = 0;
5110
5395
  }
5111
5396
  const waitMs = Math.min(8e3, 1e3 * 2 ** (attempt - 1));
5112
- await new Promise((resolve18) => setTimeout(resolve18, waitMs));
5397
+ await new Promise((resolve19) => setTimeout(resolve19, waitMs));
5113
5398
  }
5114
5399
  return false;
5115
5400
  }
@@ -5741,8 +6026,8 @@ var usage_default = defineDeepspaceCommand({
5741
6026
  });
5742
6027
 
5743
6028
  // src/cli/commands/add.ts
5744
- import { existsSync as existsSync18, readFileSync as readFileSync17, realpathSync as realpathSync2 } from "fs";
5745
- import { dirname as dirname7, join as join17, resolve as resolve13 } from "path";
6029
+ import { existsSync as existsSync18, readFileSync as readFileSync17, realpathSync as realpathSync3 } from "fs";
6030
+ import { dirname as dirname7, join as join17, resolve as resolve14 } from "path";
5746
6031
  import { pathToFileURL as pathToFileURL3 } from "url";
5747
6032
 
5748
6033
  // src/cli/commands/feature-installer.ts
@@ -5762,7 +6047,7 @@ import {
5762
6047
  } from "fs";
5763
6048
  import { spawnSync as spawnSync6 } from "child_process";
5764
6049
  import { tmpdir as tmpdir2 } from "os";
5765
- import { dirname as dirname6, isAbsolute as isAbsolute4, join as join16, resolve as resolve12 } from "path";
6050
+ import { dirname as dirname6, isAbsolute as isAbsolute4, join as join16, resolve as resolve13 } from "path";
5766
6051
  import { fileURLToPath } from "url";
5767
6052
  var NAV_MARKER = "// \u2500\u2500 Features add nav items below this line \u2500\u2500";
5768
6053
  function featurePackageRoot(moduleUrl = import.meta.url) {
@@ -5810,7 +6095,7 @@ function featureCatalog(packageRoot = featurePackageRoot()) {
5810
6095
  description: config.description ?? null
5811
6096
  }));
5812
6097
  }
5813
- function editDistance(a, b) {
6098
+ function editDistance2(a, b) {
5814
6099
  const row = Array.from({ length: b.length + 1 }, (_, index) => index);
5815
6100
  for (let i = 1; i <= a.length; i++) {
5816
6101
  let previous = row[0];
@@ -5827,7 +6112,7 @@ function suggestFeature(featureId, packageRoot = featurePackageRoot()) {
5827
6112
  let closest = null;
5828
6113
  let closestDistance = Number.POSITIVE_INFINITY;
5829
6114
  for (const feature of listFeatureConfigs(packageRoot)) {
5830
- const distance = editDistance(featureId, feature.id);
6115
+ const distance = editDistance2(featureId, feature.id);
5831
6116
  if (distance < closestDistance) {
5832
6117
  closest = feature.id;
5833
6118
  closestDistance = distance;
@@ -5837,7 +6122,7 @@ function suggestFeature(featureId, packageRoot = featurePackageRoot()) {
5837
6122
  return closest !== null && closestDistance <= threshold ? closest : null;
5838
6123
  }
5839
6124
  function resolveTargetDir(raw) {
5840
- return isAbsolute4(raw) ? raw : resolve12(raw);
6125
+ return isAbsolute4(raw) ? raw : resolve13(raw);
5841
6126
  }
5842
6127
  function resolveFeatureDestPath(fileDest, route) {
5843
6128
  const pages = "src/pages/";
@@ -6482,7 +6767,7 @@ async function targetInstaller(appDir) {
6482
6767
  "feature_installer_missing"
6483
6768
  );
6484
6769
  }
6485
- if (realpathSync2(installedRoot) === realpathSync2(featurePackageRoot())) {
6770
+ if (realpathSync3(installedRoot) === realpathSync3(featurePackageRoot())) {
6486
6771
  return { installFeature };
6487
6772
  }
6488
6773
  const entry = join17(installedRoot, "dist", "feature-installer.js");
@@ -6579,7 +6864,7 @@ var add_default = defineDeepspaceCommand({
6579
6864
  }
6580
6865
  return { data: { features: featureCatalog(root) } };
6581
6866
  }
6582
- const appDir = resolve13(args.dir ?? ".");
6867
+ const appDir = resolve14(args.dir ?? ".");
6583
6868
  if (!existsSync18(appDir)) {
6584
6869
  throw new Refusal(
6585
6870
  `Target directory not found: ${appDir}
@@ -6659,13 +6944,13 @@ function fmtDate(iso) {
6659
6944
  if (!iso) return "\u2014";
6660
6945
  return iso.slice(0, 10);
6661
6946
  }
6662
- async function confirm5(message) {
6947
+ async function confirm4(message) {
6663
6948
  process.stdout.write(`${message} [y/N] `);
6664
- return new Promise((resolve18) => {
6949
+ return new Promise((resolve19) => {
6665
6950
  process.stdin.setEncoding("utf-8");
6666
6951
  process.stdin.once("data", (data) => {
6667
6952
  const answer = data.toString().trim().toLowerCase();
6668
- resolve18(answer === "y" || answer === "yes");
6953
+ resolve19(answer === "y" || answer === "yes");
6669
6954
  });
6670
6955
  });
6671
6956
  }
@@ -6739,7 +7024,7 @@ var buy = defineDeepspaceCommand({
6739
7024
  "confirmation_required"
6740
7025
  );
6741
7026
  }
6742
- const ok2 = await confirm5(
7027
+ const ok2 = await confirm4(
6743
7028
  `Buy ${domain} for ${price}/yr (auto-renews; non-refundable), attach to ${appId}? Provisioning takes ${expectedTime}.`
6744
7029
  );
6745
7030
  if (!ok2) {
@@ -6909,7 +7194,7 @@ var detach = defineDeepspaceCommand({
6909
7194
  "confirmation_required"
6910
7195
  );
6911
7196
  }
6912
- const ok2 = await confirm5(
7197
+ const ok2 = await confirm4(
6913
7198
  `Detach ${domain}? Routing will stop; the domain stays registered (auto-renew is unchanged).`
6914
7199
  );
6915
7200
  if (!ok2) {
@@ -6992,8 +7277,8 @@ var list3 = defineDeepspaceCommand({
6992
7277
  }
6993
7278
  if (pending.length) {
6994
7279
  console.log(`PENDING INVITES ON ${app2}`);
6995
- for (const p34 of pending) {
6996
- console.log(` ${p34.email} (expires ${new Date(p34.expiresAt).toLocaleDateString()})`);
7280
+ for (const p35 of pending) {
7281
+ console.log(` ${p35.email} (expires ${new Date(p35.expiresAt).toLocaleDateString()})`);
6997
7282
  }
6998
7283
  }
6999
7284
  }
@@ -7091,7 +7376,7 @@ var cancel3 = defineDeepspaceCommand({
7091
7376
  `/api/app-collaborators/${encodeURIComponent(app2)}`
7092
7377
  );
7093
7378
  const target = email.trim().toLowerCase();
7094
- const found = pending.find((p34) => p34.email.toLowerCase() === target);
7379
+ const found = pending.find((p35) => p35.email.toLowerCase() === target);
7095
7380
  if (!found) {
7096
7381
  throw new Refusal(`${email} has no pending invite on ${app2}`, "no_pending_invite", {
7097
7382
  action: cliAction("deepspace", "app", "collaborators", "list", "--app", app2)
@@ -7562,7 +7847,7 @@ var integrations_default = defineCommand8({
7562
7847
  // src/cli/commands/library.ts
7563
7848
  import { defineCommand as defineCommand9 } from "citty";
7564
7849
  import { existsSync as existsSync19, readFileSync as readFileSync19 } from "fs";
7565
- import { join as join18, resolve as resolve14 } from "path";
7850
+ import { join as join18, resolve as resolve15 } from "path";
7566
7851
  import { parse as parseToml4 } from "smol-toml";
7567
7852
  import * as p16 from "@clack/prompts";
7568
7853
  var DEFAULT_LIBRARY_APP = "deepdotspace-site";
@@ -7632,7 +7917,7 @@ var publish = defineDeepspaceCommand({
7632
7917
  if (appErr) throw new Refusal(appErr, "invalid_app");
7633
7918
  let appName = str(args.app)?.trim();
7634
7919
  if (!appName) {
7635
- const appDir = resolve14(str(args.dir) ?? ".");
7920
+ const appDir = resolve15(str(args.dir) ?? ".");
7636
7921
  const wranglerPath = join18(appDir, "wrangler.toml");
7637
7922
  if (!existsSync19(wranglerPath)) {
7638
7923
  throw new Refusal(
@@ -7818,7 +8103,7 @@ function detectAppName2() {
7818
8103
  try {
7819
8104
  const cwd = process.cwd();
7820
8105
  if (!hasWranglerConfig(cwd)) return void 0;
7821
- const config = readWranglerConfig(cwd);
8106
+ const config = readWranglerConfig2(cwd);
7822
8107
  return typeof config.name === "string" ? config.name : void 0;
7823
8108
  } catch {
7824
8109
  return void 0;
@@ -8707,24 +8992,50 @@ var status_default = defineCommand11({
8707
8992
  commitOid: rel.commitOid,
8708
8993
  createdAt: rel.createdAt,
8709
8994
  url: rel.url,
8710
- byYou
8995
+ byYou,
8996
+ source: rel.source,
8997
+ sourceRevision: rel.sourceRevision
8711
8998
  };
8712
8999
  };
8713
9000
  var reportWorkspace = reportWorkspace2, reportRemoteFailure = reportRemoteFailure2, reportRelease = reportRelease2;
8714
9001
  const api4 = repoApi(deployBaseUrl(), token, appId);
9002
+ const sourceResult = await observeRemote(getAppSource(deployBaseUrl(), token, appId));
9003
+ const source = sourceResult.ok ? sourceResult.value.source : null;
9004
+ const githubSource = source?.provider === "github";
9005
+ if (!sourceResult.ok) {
9006
+ reportRemoteFailure2("Source", "source", sourceResult.error);
9007
+ } else if (githubSource) {
9008
+ lines.push(["Source", `GitHub \xB7 ${source.repository} (manual Git ownership)`]);
9009
+ json.source = { ...source, revision: sourceResult.value.revision };
9010
+ } else if (source?.provider === "deepspace") {
9011
+ lines.push([
9012
+ "Source",
9013
+ `DeepSpace \xB7 packaged Git (revision ${sourceResult.value.revision})`
9014
+ ]);
9015
+ json.source = { ...source, revision: sourceResult.value.revision };
9016
+ } else {
9017
+ lines.push(["Source", "unclaimed \xB7 next normal deploy defaults to DeepSpace"]);
9018
+ json.source = null;
9019
+ }
8715
9020
  const [refsResult, wsResult, releaseResult] = await Promise.all([
8716
- observeRemote(api4.getRefs()),
8717
- facts.workspaceId ? observeRemote(api4.getWorkspace(facts.workspaceId).then((r) => r.view)) : Promise.resolve({ ok: true, value: null }),
9021
+ githubSource ? Promise.resolve({ ok: true, value: null }) : observeRemote(api4.getRefs()),
9022
+ facts.workspaceId && !githubSource ? observeRemote(api4.getWorkspace(facts.workspaceId).then((r) => r.view)) : Promise.resolve({ ok: true, value: null }),
8718
9023
  observeRemote(api4.latestRelease().then((r) => r.release))
8719
9024
  ]);
8720
9025
  if (facts.workspaceId) {
8721
- if (!wsResult.ok) reportRemoteFailure2("Workspace", "workspace", wsResult.error);
9026
+ if (githubSource) {
9027
+ lines.push(["Workspace", "DeepSpace workspaces are disabled for GitHub source"]);
9028
+ json.workspace = { state: "disabled", provider: "github" };
9029
+ } else if (!wsResult.ok) reportRemoteFailure2("Workspace", "workspace", wsResult.error);
8722
9030
  else if (wsResult.value) reportWorkspace2(wsResult.value);
8723
9031
  }
8724
9032
  const refs = refsResult.ok ? refsResult.value : void 0;
8725
9033
  const trunkRef = refs?.refs.find((r) => `refs/heads/${branch}` === r.name);
8726
9034
  if (!facts.workspaceId && branch) {
8727
- if (!refsResult.ok) reportRemoteFailure2("Trunk", "trunk", refsResult.error);
9035
+ if (githubSource) {
9036
+ lines.push(["Trunk", `${branch} is owned through normal Git/GitHub`]);
9037
+ json.trunk = { state: "external", provider: "github", branch };
9038
+ } else if (!refsResult.ok) reportRemoteFailure2("Trunk", "trunk", refsResult.error);
8728
9039
  else {
8729
9040
  const remoteOid = trunkRef?.oid ?? null;
8730
9041
  const localOid = resolveCommit(appDir, `refs/heads/${branch}`);
@@ -8965,7 +9276,7 @@ var push_default = defineDeepspaceCommand({
8965
9276
 
8966
9277
  // src/cli/commands/pull.ts
8967
9278
  import * as p20 from "@clack/prompts";
8968
- import { realpathSync as realpathSync3 } from "fs";
9279
+ import { realpathSync as realpathSync4 } from "fs";
8969
9280
  function targetedVcAction(command, cwd, appId, branch) {
8970
9281
  return {
8971
9282
  cwd,
@@ -8989,7 +9300,7 @@ function workspaceBranchPullRefusal(branch, trunkBranch) {
8989
9300
  function worktreeHoldingBranch(worktrees, branch, selfPath) {
8990
9301
  const real = (pth) => {
8991
9302
  try {
8992
- return realpathSync3(pth);
9303
+ return realpathSync4(pth);
8993
9304
  } catch {
8994
9305
  return pth;
8995
9306
  }
@@ -9245,7 +9556,7 @@ var pull_default = defineDeepspaceCommand({
9245
9556
  // src/cli/commands/clone.ts
9246
9557
  import * as p21 from "@clack/prompts";
9247
9558
  import { existsSync as existsSync22, readdirSync as readdirSync6, rmSync as rmSync5 } from "fs";
9248
- import { resolve as resolve15, basename as basename3, join as join20 } from "path";
9559
+ import { resolve as resolve16, basename as basename3, join as join20 } from "path";
9249
9560
  function cleanFailedCloneDir(dir) {
9250
9561
  let entries;
9251
9562
  try {
@@ -9299,7 +9610,7 @@ var clone_default = defineDeepspaceCommand({
9299
9610
  async run({ args }) {
9300
9611
  assertAppTargetResolvable(String(args.app));
9301
9612
  const selector = String(args.app).trim();
9302
- const dir = resolve15(String(args.dir ?? selector));
9613
+ const dir = resolve16(String(args.dir ?? selector));
9303
9614
  const dirPreexisted = validateCloneTarget(dir);
9304
9615
  const spinner2 = args.json ? null : createSpinner();
9305
9616
  spinner2?.start(`Resolving ${selector}\u2026`);
@@ -9553,7 +9864,7 @@ import { defineCommand as defineCommand12 } from "citty";
9553
9864
 
9554
9865
  // src/cli/commands/workspace/new.ts
9555
9866
  import * as p26 from "@clack/prompts";
9556
- import { isAbsolute as isAbsolute6, resolve as resolve16 } from "path";
9867
+ import { isAbsolute as isAbsolute6, resolve as resolve17 } from "path";
9557
9868
 
9558
9869
  // src/cli/commands/workspace/analysis.ts
9559
9870
  import * as p24 from "@clack/prompts";
@@ -9678,18 +9989,18 @@ function printOverlaps(overlaps, json) {
9678
9989
 
9679
9990
  // src/cli/commands/workspace/local.ts
9680
9991
  import * as p25 from "@clack/prompts";
9681
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync23, realpathSync as realpathSync4, renameSync as renameSync2, statSync, writeFileSync as writeFileSync9 } from "fs";
9992
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync23, realpathSync as realpathSync5, renameSync as renameSync2, statSync, writeFileSync as writeFileSync9 } from "fs";
9682
9993
  import { dirname as dirname9, isAbsolute as isAbsolute5, join as join21, relative as relative2, sep as sep2 } from "path";
9683
9994
  var WORKSPACE_MARKER = "deepspace-workspace.json";
9684
9995
  function excludeWorktreeDir(appDir, dir) {
9685
9996
  try {
9686
9997
  ensureLocalExclude(appDir, ".deepspace/");
9687
- const realDir = realpathSync4(dir);
9688
- const rel = relative2(realpathSync4(repoToplevel(appDir)), realDir);
9998
+ const realDir = realpathSync5(dir);
9999
+ const rel = relative2(realpathSync5(repoToplevel(appDir)), realDir);
9689
10000
  if (!rel || rel === ".." || rel.startsWith(`..${sep2}`) || isAbsolute5(rel)) return;
9690
10001
  const pattern = rel.split(sep2).join("/");
9691
10002
  if (pattern === ".deepspace" || pattern.startsWith(".deepspace/")) return;
9692
- const relApp = relative2(realpathSync4(appDir), realDir).split(sep2).join("/");
10003
+ const relApp = relative2(realpathSync5(appDir), realDir).split(sep2).join("/");
9693
10004
  if (relApp === ".deepspace" || relApp.startsWith(".deepspace/")) return;
9694
10005
  ensureLocalExclude(appDir, `/${pattern}/`);
9695
10006
  } catch {
@@ -9719,6 +10030,19 @@ function markManagedWorkspace(worktreeRoot, workspaceId) {
9719
10030
  return false;
9720
10031
  }
9721
10032
  }
10033
+ function rollbackUnmarkedWorkspace(appDir, worktreeRoot, branch, createdOid) {
10034
+ const removed = runGit(appDir, ["worktree", "remove", worktreeRoot], { allowFail: true });
10035
+ if (removed.status !== 0) {
10036
+ return `git worktree remove failed: ${removed.stderr.toString("utf-8").trim() || "unknown git error"}`;
10037
+ }
10038
+ const deleted = runGit(appDir, ["update-ref", "-d", `refs/heads/${branch}`, createdOid], {
10039
+ allowFail: true
10040
+ });
10041
+ if (deleted.status !== 0) {
10042
+ return `the checkout was removed but branch ${branch} remains: ${deleted.stderr.toString("utf-8").trim() || "unknown git error"}`;
10043
+ }
10044
+ return null;
10045
+ }
9722
10046
  function isManagedWorkspaceWorktree(worktreeRoot, workspaceId) {
9723
10047
  const markerPath = worktreeMarkerPath(worktreeRoot);
9724
10048
  if (!markerPath) return false;
@@ -9735,13 +10059,21 @@ function defaultWorkspaceRoot(appDir, workspaceId) {
9735
10059
  return join21(primary.path, ".deepspace", "ws", workspaceId.slice(3).toLowerCase());
9736
10060
  }
9737
10061
  function appDirInWorktree(appDir, worktreeRoot) {
9738
- const appRelative = relative2(realpathSync4(repoToplevel(appDir)), realpathSync4(appDir));
10062
+ const appRelative = relative2(realpathSync5(repoToplevel(appDir)), realpathSync5(appDir));
9739
10063
  return appRelative ? join21(worktreeRoot, appRelative) : worktreeRoot;
9740
10064
  }
9741
- function materializeWorkspaceWorktree(appDir, worktreeRoot, branch, startPoint, workspaceId) {
10065
+ function materializeWorkspaceWorktree(appDir, worktreeRoot, branch, startPoint, workspaceId, options = {}) {
10066
+ const createdOid = resolveCommit(appDir, startPoint);
10067
+ if (!createdOid) throw new Error(`Could not resolve workspace start point ${startPoint}`);
9742
10068
  addWorktreeNewBranch(appDir, worktreeRoot, branch, startPoint);
10069
+ const marked = (options.markManaged ?? markManagedWorkspace)(worktreeRoot, workspaceId);
10070
+ if (!marked) {
10071
+ const rollbackError = rollbackUnmarkedWorkspace(appDir, worktreeRoot, branch, createdOid);
10072
+ throw new Error(
10073
+ `Could not record DeepSpace ownership for ${worktreeRoot}.` + (rollbackError ? ` Automatic rollback was incomplete: ${rollbackError}` : " The just-created checkout and branch were rolled back.")
10074
+ );
10075
+ }
9743
10076
  excludeWorktreeDir(appDir, worktreeRoot);
9744
- markManagedWorkspace(worktreeRoot, workspaceId);
9745
10077
  }
9746
10078
  function inOwnLinkedWorktree(appDir, id) {
9747
10079
  if (workspaceIdFromBranch(currentBranch(appDir)) !== id) return false;
@@ -9799,7 +10131,7 @@ function cleanupWorkspaceLocal(appDir, id, trunkBranch) {
9799
10131
  }
9800
10132
  let inside = false;
9801
10133
  try {
9802
- inside = realpathSync4(repoToplevel(appDir)) === realpathSync4(linked.path);
10134
+ inside = realpathSync5(repoToplevel(appDir)) === realpathSync5(linked.path);
9803
10135
  } catch {
9804
10136
  inside = false;
9805
10137
  }
@@ -10039,7 +10371,7 @@ var newWorkspaceCommand = defineDeepspaceCommand({
10039
10371
  idempotencyKey: mintIdempotencyKey()
10040
10372
  });
10041
10373
  const dirInput = typeof args.dir === "string" ? args.dir.trim() : "";
10042
- const worktreeRoot = dirInput ? isAbsolute6(dirInput) ? dirInput : resolve16(appDir, dirInput) : defaultWorkspaceRoot(appDir, id);
10374
+ const worktreeRoot = dirInput ? isAbsolute6(dirInput) ? dirInput : resolve17(appDir, dirInput) : defaultWorkspaceRoot(appDir, id);
10043
10375
  const dir = appDirInWorktree(appDir, worktreeRoot);
10044
10376
  try {
10045
10377
  materializeWorkspaceWorktree(appDir, worktreeRoot, branch, baseOid, id);
@@ -10078,7 +10410,7 @@ var newWorkspaceCommand = defineDeepspaceCommand({
10078
10410
  // src/cli/commands/workspace/attach.ts
10079
10411
  import * as p27 from "@clack/prompts";
10080
10412
  import { existsSync as existsSync23, readdirSync as readdirSync7, rmSync as rmSync6 } from "fs";
10081
- import { isAbsolute as isAbsolute7, resolve as resolve17 } from "path";
10413
+ import { isAbsolute as isAbsolute7, resolve as resolve18 } from "path";
10082
10414
  function finishedWorkspaceMessage(id, view) {
10083
10415
  const workspace = view.workspace;
10084
10416
  if (workspace.status === "landed") {
@@ -10162,7 +10494,7 @@ var attachWorkspaceCommand = defineDeepspaceCommand({
10162
10494
  );
10163
10495
  }
10164
10496
  const requestedDir = typeof args.dir === "string" ? args.dir.trim() : "";
10165
- const worktreeRoot = requestedDir ? isAbsolute7(requestedDir) ? requestedDir : resolve17(appDir, requestedDir) : defaultWorkspaceRoot(appDir, id);
10497
+ const worktreeRoot = requestedDir ? isAbsolute7(requestedDir) ? requestedDir : resolve18(appDir, requestedDir) : defaultWorkspaceRoot(appDir, id);
10166
10498
  const dir2 = appDirInWorktree(appDir, worktreeRoot);
10167
10499
  if (resolveCommit(appDir, `refs/heads/${branch}`)) {
10168
10500
  throw new Refusal(
@@ -10201,7 +10533,7 @@ var attachWorkspaceCommand = defineDeepspaceCommand({
10201
10533
  extra: { status: view.workspace.status }
10202
10534
  });
10203
10535
  }
10204
- const dir = resolve17(
10536
+ const dir = resolve18(
10205
10537
  process.cwd(),
10206
10538
  (typeof args.dir === "string" ? args.dir.trim() : "") || `ws-${id.slice(3).toLowerCase()}`
10207
10539
  );
@@ -11164,6 +11496,721 @@ var git_credential_default = defineCommand13({
11164
11496
  }
11165
11497
  });
11166
11498
 
11499
+ // src/cli/commands/source.ts
11500
+ import * as p34 from "@clack/prompts";
11501
+ var TRANSFER_PREFIX = "refs/deepspace/source-transfer";
11502
+ var source_default = defineDeepspaceCommand({
11503
+ meta: {
11504
+ name: "source",
11505
+ description: "Show or change the app\u2019s one authoritative Git repository"
11506
+ },
11507
+ args: {
11508
+ provider: {
11509
+ type: "positional",
11510
+ description: "Source provider: github or deepspace (omit to show)",
11511
+ required: false
11512
+ },
11513
+ app: {
11514
+ type: "string",
11515
+ alias: "a",
11516
+ description: "App id or subdomain name (default: current app)",
11517
+ required: false
11518
+ },
11519
+ remote: {
11520
+ type: "string",
11521
+ description: "GitHub remote name when it cannot be inferred (default: origin)",
11522
+ required: false
11523
+ }
11524
+ },
11525
+ async run({ args }) {
11526
+ const requested = typeof args.provider === "string" ? args.provider.trim().toLowerCase() : "";
11527
+ if (requested && requested !== "github" && requested !== "deepspace") {
11528
+ throw new Refusal("Source must be `github` or `deepspace`.", "invalid_source");
11529
+ }
11530
+ const token = await ensureToken();
11531
+ const deployUrl = deployBaseUrl();
11532
+ const appId = await resolveAppTarget(
11533
+ deployUrl,
11534
+ token,
11535
+ typeof args.app === "string" ? args.app : void 0
11536
+ );
11537
+ const state = await getAppSource(deployUrl, token, appId);
11538
+ if (!requested) {
11539
+ reportSource(appId, state, args.json);
11540
+ return { data: sourceData(state) };
11541
+ }
11542
+ const appDir = findAppDir();
11543
+ if (!appDir) {
11544
+ throw new Refusal(
11545
+ "Changing source requires a local app checkout with wrangler.toml and Git history.",
11546
+ "not_in_app_repo"
11547
+ );
11548
+ }
11549
+ assertSyncableRepo(appDir);
11550
+ const remoteArg = typeof args.remote === "string" ? args.remote.trim() : void 0;
11551
+ const result = requested === "github" ? await moveToGitHub({ appDir, appId, token, deployUrl, state, remoteArg }) : await moveToDeepSpace({ appDir, appId, token, deployUrl, state, remoteArg });
11552
+ if (!args.json) {
11553
+ p34.log.success(
11554
+ result.source.provider === "github" ? `Source: GitHub \xB7 ${result.source.repository} (revision ${result.revision})` : `Source: DeepSpace (revision ${result.revision})`
11555
+ );
11556
+ }
11557
+ return { data: { appId, source: result.source, revision: result.revision } };
11558
+ }
11559
+ });
11560
+ function sourceData(state) {
11561
+ return {
11562
+ appId: state.appId,
11563
+ source: state.source,
11564
+ revision: state.revision,
11565
+ registered: state.registered
11566
+ };
11567
+ }
11568
+ function reportSource(appId, state, json) {
11569
+ if (json) return;
11570
+ console.log(`App: ${appId}`);
11571
+ if (state.source?.provider === "github") {
11572
+ console.log(`Source: GitHub \xB7 ${state.source.repository}`);
11573
+ } else if (state.source?.provider === "deepspace") {
11574
+ console.log("Source: DeepSpace");
11575
+ } else {
11576
+ console.log("Source: unclaimed (the first normal deploy defaults to DeepSpace)");
11577
+ }
11578
+ console.log(`Revision: ${state.revision}`);
11579
+ }
11580
+ async function moveToGitHub(input) {
11581
+ const { appDir, appId, token, deployUrl, state, remoteArg } = input;
11582
+ const selected = requireGitHubRemote(appDir, remoteArg);
11583
+ const target = { provider: "github", repository: selected.repository };
11584
+ if (sameSource(state.source, target)) return { source: target, revision: state.revision };
11585
+ if (state.source?.provider === "deepspace") {
11586
+ requireLocalHead(appDir);
11587
+ const api4 = repoApi(deployUrl, token, appId);
11588
+ const { views } = await api4.listWorkspaces();
11589
+ if (views.length > 0) {
11590
+ throw new Refusal(
11591
+ "Land or drop every active DeepSpace workspace before moving source to GitHub.",
11592
+ "active_workspaces"
11593
+ );
11594
+ }
11595
+ ensureSpaceRemote(appDir, appId);
11596
+ runGitRemote(appDir, token, [
11597
+ "fetch",
11598
+ "--quiet",
11599
+ "--prune",
11600
+ SPACE_REMOTE,
11601
+ `+refs/heads/*:${TRANSFER_PREFIX}/heads/*`,
11602
+ `+refs/tags/*:${TRANSFER_PREFIX}/tags/*`
11603
+ ]);
11604
+ const { refs } = await api4.getRefs().then((value) => {
11605
+ if (!value)
11606
+ throw new Refusal("The DeepSpace source repository is unavailable.", "source_unavailable");
11607
+ return value;
11608
+ });
11609
+ const wanted = refs.filter(isPublicRef).map(({ name, oid }) => ({ name, oid }));
11610
+ const fetched = transferRefs(appDir);
11611
+ if (!sameRefs(fetched, wanted)) {
11612
+ throw new Refusal(
11613
+ "DeepSpace branches or tags changed while they were being fetched. Retry the transfer from fresh refs.",
11614
+ "source_changed"
11615
+ );
11616
+ }
11617
+ const latest = await api4.latestRelease().then((value) => value.release);
11618
+ requireReleaseReachable(appDir, latest, wanted);
11619
+ const remote = new Map(
11620
+ remotePublicRefs(appDir, selected.name).map((ref) => [ref.name, ref.oid])
11621
+ );
11622
+ const missing = wanted.filter((ref) => remote.get(ref.name) !== ref.oid);
11623
+ if (missing.length > 0) {
11624
+ const refspecs = missing.map((ref) => transferRefspec(ref.name));
11625
+ throw new Refusal(
11626
+ `GitHub ${selected.repository} is missing ${missing.length} authoritative branch/tag ref(s). Push them manually, then rerun this command.`,
11627
+ "github_push_required",
11628
+ {
11629
+ actionRequired: true,
11630
+ action: { cwd: appDir, argv: ["git", "push", "--atomic", selected.name, ...refspecs] },
11631
+ extra: {
11632
+ appId,
11633
+ repository: selected.repository,
11634
+ missing: missing.map((ref) => ref.name)
11635
+ }
11636
+ }
11637
+ );
11638
+ }
11639
+ return setAppSource(deployUrl, token, appId, {
11640
+ source: target,
11641
+ expectedRevision: state.revision,
11642
+ refs: wanted,
11643
+ ...releaseExpectation(latest)
11644
+ });
11645
+ }
11646
+ requirePublishedHead(appDir, selected, appId);
11647
+ return setAppSource(deployUrl, token, appId, {
11648
+ source: target,
11649
+ expectedRevision: state.revision
11650
+ });
11651
+ }
11652
+ async function moveToDeepSpace(input) {
11653
+ const { appDir, appId, token, deployUrl, state, remoteArg } = input;
11654
+ const target = { provider: "deepspace" };
11655
+ if (sameSource(state.source, target)) return { source: target, revision: state.revision };
11656
+ if (state.source?.provider !== "github") {
11657
+ return setAppSource(deployUrl, token, appId, {
11658
+ source: target,
11659
+ expectedRevision: state.revision
11660
+ });
11661
+ }
11662
+ const selected = requireGitHubRemote(appDir, remoteArg, state.source.repository);
11663
+ const { branch, oid } = requirePublishedHead(appDir, selected);
11664
+ fetchGitHubTransferRefs(appDir, selected.name);
11665
+ const refs = transferRefs(appDir);
11666
+ const latest = await repoApi(deployUrl, token, appId).latestRelease().then((value) => value.release);
11667
+ requireReleaseReachable(appDir, latest, refs);
11668
+ ensureSpaceRemote(appDir, appId);
11669
+ if (refs.length === 0) {
11670
+ throw new Refusal("GitHub has no branches or tags to import.", "source_empty");
11671
+ }
11672
+ const pushed = runGit(
11673
+ appDir,
11674
+ [
11675
+ "push",
11676
+ "--force",
11677
+ "--atomic",
11678
+ "--prune",
11679
+ SPACE_REMOTE,
11680
+ `${TRANSFER_PREFIX}/heads/*:refs/heads/*`,
11681
+ `${TRANSFER_PREFIX}/tags/*:refs/tags/*`
11682
+ ],
11683
+ { allowFail: true, env: gitSourceImportEnv(token, state.revision, deployUrl) }
11684
+ );
11685
+ if (pushed.status !== 0) {
11686
+ throw new Refusal(
11687
+ `DeepSpace source import failed: ${pushed.stderr.toString("utf-8").trim() || "git push failed"}`,
11688
+ "source_import_failed"
11689
+ );
11690
+ }
11691
+ return setAppSource(deployUrl, token, appId, {
11692
+ source: target,
11693
+ expectedRevision: state.revision,
11694
+ branch,
11695
+ commitOid: oid,
11696
+ refs,
11697
+ ...releaseExpectation(latest)
11698
+ });
11699
+ }
11700
+ function requireGitHubRemote(appDir, remoteArg, repository) {
11701
+ const selected = selectGitHubRemote(appDir, { name: remoteArg, repository });
11702
+ if (selected) return selected;
11703
+ throw new Refusal(
11704
+ remoteArg ? `Remote "${remoteArg}" is not a GitHub repository${repository ? ` for ${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>.",
11705
+ "github_remote_required"
11706
+ );
11707
+ }
11708
+ function requirePublishedHead(appDir, remote, appId = "") {
11709
+ const head = requireLocalHead(appDir);
11710
+ if (remoteBranchOid(appDir, remote.name, head.branch) !== head.oid) {
11711
+ throw githubPushRequired(appDir, appId, remote, head.branch, head.oid);
11712
+ }
11713
+ return head;
11714
+ }
11715
+ function requireLocalHead(appDir) {
11716
+ if (!isWorkTreeClean(appDir)) {
11717
+ throw new Refusal("Commit or discard local changes before changing source.", "dirty_worktree");
11718
+ }
11719
+ const branch = currentBranch(appDir);
11720
+ if (!branch) throw new Refusal("HEAD is detached; switch to a branch first.", "detached_head");
11721
+ const oid = resolveCommit(appDir, "HEAD");
11722
+ if (!oid) throw new Refusal("The repository has no commit to publish.", "no_commits");
11723
+ return { branch, oid };
11724
+ }
11725
+ function githubPushRequired(appDir, appId, remote, branch, oid) {
11726
+ return new Refusal(
11727
+ `GitHub ${remote.repository} does not have ${branch} at ${oid.slice(0, 10)}. Push it manually, then rerun.`,
11728
+ "github_push_required",
11729
+ {
11730
+ actionRequired: true,
11731
+ action: { cwd: appDir, argv: ["git", "push", remote.name, branch] },
11732
+ extra: { ...appId ? { appId } : {}, repository: remote.repository, branch, oid }
11733
+ }
11734
+ );
11735
+ }
11736
+ function fetchGitHubTransferRefs(appDir, remote) {
11737
+ runGit(appDir, [
11738
+ "fetch",
11739
+ "--quiet",
11740
+ "--prune",
11741
+ remote,
11742
+ `+refs/heads/*:${TRANSFER_PREFIX}/heads/*`,
11743
+ `+refs/tags/*:${TRANSFER_PREFIX}/tags/*`
11744
+ ]);
11745
+ }
11746
+ function transferRefs(appDir) {
11747
+ return localTransferRefs(appDir, `${TRANSFER_PREFIX}/heads`, "refs/heads").concat(
11748
+ localTransferRefs(appDir, `${TRANSFER_PREFIX}/tags`, "refs/tags")
11749
+ );
11750
+ }
11751
+ function transferRefspec(ref) {
11752
+ if (ref.startsWith("refs/heads/")) {
11753
+ return `${TRANSFER_PREFIX}/heads/${ref.slice("refs/heads/".length)}:${ref}`;
11754
+ }
11755
+ return `${TRANSFER_PREFIX}/tags/${ref.slice("refs/tags/".length)}:${ref}`;
11756
+ }
11757
+ function isPublicRef(ref) {
11758
+ return ref.name.startsWith("refs/heads/") || ref.name.startsWith("refs/tags/");
11759
+ }
11760
+ function requireReleaseReachable(appDir, release2, refs) {
11761
+ if (!release2?.commitOid) return;
11762
+ const known = resolveCommit(appDir, release2.commitOid) !== null;
11763
+ const reachable = known && refs.some((ref) => isAncestor(appDir, release2.commitOid, ref.oid));
11764
+ if (!reachable) {
11765
+ throw new Refusal(
11766
+ `Live release #${release2.seq} (${release2.commitOid.slice(0, 10)}) is not reachable from a public branch or tag. Redeploy a public branch before changing source.`,
11767
+ "release_not_public"
11768
+ );
11769
+ }
11770
+ }
11771
+ function releaseExpectation(release2) {
11772
+ return {
11773
+ expectedReleaseId: release2?.id ?? null,
11774
+ expectedReleaseCommitOid: release2?.commitOid ?? null
11775
+ };
11776
+ }
11777
+ function sameSource(left, right) {
11778
+ return left?.provider === right.provider && (left.provider !== "github" || right.provider === "github" && left.repository === right.repository);
11779
+ }
11780
+ function sameRefs(left, right) {
11781
+ if (left.length !== right.length) return false;
11782
+ const expected = new Map(right.map((ref) => [ref.name, ref.oid]));
11783
+ return left.every((ref) => expected.get(ref.name) === ref.oid);
11784
+ }
11785
+
11786
+ // src/cli/lib/identity-migration-api.ts
11787
+ async function getIdentityMigration(deployUrl, token, appId) {
11788
+ try {
11789
+ const result = await apiFetch(
11790
+ deployUrl,
11791
+ token,
11792
+ `/api/apps/${encodeURIComponent(appId)}/identity-migration`
11793
+ );
11794
+ return result.migration;
11795
+ } catch (error) {
11796
+ if (error instanceof ApiError && error.code === "migration_not_found") return null;
11797
+ throw error;
11798
+ }
11799
+ }
11800
+ async function prepareIdentityMigration(deployUrl, token, legacyAppId, input) {
11801
+ const result = await apiFetch(
11802
+ deployUrl,
11803
+ token,
11804
+ `/api/apps/${encodeURIComponent(legacyAppId)}/identity-migration/prepare`,
11805
+ { method: "POST", body: JSON.stringify(input) }
11806
+ );
11807
+ return result.migration;
11808
+ }
11809
+ async function inspectIdentityMigration(deployUrl, token, legacyAppId, input) {
11810
+ const result = await apiFetch(
11811
+ deployUrl,
11812
+ token,
11813
+ `/api/apps/${encodeURIComponent(legacyAppId)}/identity-migration/inspect`,
11814
+ { method: "POST", body: JSON.stringify(input) }
11815
+ );
11816
+ return result.inventory;
11817
+ }
11818
+ async function commitIdentityMigration(deployUrl, token, appId, input) {
11819
+ const result = await apiFetch(
11820
+ deployUrl,
11821
+ token,
11822
+ `/api/apps/${encodeURIComponent(appId)}/identity-migration/commit`,
11823
+ { method: "POST", body: JSON.stringify(input) }
11824
+ );
11825
+ return result.migration;
11826
+ }
11827
+ async function cancelIdentityMigration(deployUrl, token, appId) {
11828
+ await apiFetch(
11829
+ deployUrl,
11830
+ token,
11831
+ `/api/apps/${encodeURIComponent(appId)}/identity-migration`,
11832
+ { method: "DELETE" }
11833
+ );
11834
+ }
11835
+ async function rollbackIdentityMigration(deployUrl, token, appId) {
11836
+ const result = await apiFetch(
11837
+ deployUrl,
11838
+ token,
11839
+ `/api/apps/${encodeURIComponent(appId)}/identity-migration/rollback`,
11840
+ { method: "POST" }
11841
+ );
11842
+ return result.migration;
11843
+ }
11844
+
11845
+ // src/cli/commands/migrate.ts
11846
+ var STRICT_APP_ID_RE2 = /^app_[0-9A-HJKMNP-TV-Z]{26}$/;
11847
+ var migrate_default = defineDeepspaceCommand({
11848
+ meta: {
11849
+ name: "migrate",
11850
+ description: "Upgrade a legacy GitHub app to an immutable app id without moving its data"
11851
+ },
11852
+ args: {
11853
+ remote: {
11854
+ type: "string",
11855
+ description: "GitHub remote name when it cannot be inferred (default: origin)",
11856
+ required: false
11857
+ },
11858
+ env: {
11859
+ type: "string",
11860
+ alias: "e",
11861
+ description: "wrangler.toml environment whose app id should be migrated",
11862
+ required: false
11863
+ },
11864
+ "dry-run": {
11865
+ type: "boolean",
11866
+ description: "Inspect prerequisites and show the migration without changing anything",
11867
+ default: false
11868
+ },
11869
+ cancel: {
11870
+ type: "boolean",
11871
+ description: "Cancel a prepared migration after restoring and publishing the legacy id",
11872
+ default: false
11873
+ },
11874
+ rollback: {
11875
+ type: "boolean",
11876
+ description: "Roll back a committed migration before any canonical deploy starts",
11877
+ default: false
11878
+ }
11879
+ },
11880
+ async run({ args }) {
11881
+ const recoveryMode = args.cancel === true ? "cancel" : args.rollback === true ? "rollback" : null;
11882
+ const exclusiveModes = [args["dry-run"], args.cancel, args.rollback].filter(Boolean).length;
11883
+ if (exclusiveModes > 1) {
11884
+ throw new Refusal(
11885
+ "Use only one of --dry-run, --cancel, or --rollback.",
11886
+ "conflicting_migration_mode"
11887
+ );
11888
+ }
11889
+ const appDir = findAppDir();
11890
+ if (!appDir) {
11891
+ throw new Refusal("Run this command from a DeepSpace app Git checkout.", "not_in_app_repo");
11892
+ }
11893
+ assertSyncableRepo(appDir);
11894
+ const envName = typeof args.env === "string" && args.env ? args.env : void 0;
11895
+ const currentAppId = readAppId(appDir, envName) ?? readLegacyAppId(appDir, envName);
11896
+ if (!currentAppId) {
11897
+ throw new Refusal(
11898
+ "wrangler.toml has no unambiguous DeepSpace app identity for this environment.",
11899
+ "missing_app_id"
11900
+ );
11901
+ }
11902
+ if (!STRICT_APP_ID_RE2.test(currentAppId)) requireTrackedWrangler(appDir);
11903
+ const token = await ensureToken();
11904
+ const deployUrl = deployBaseUrl();
11905
+ const existingMigration = await getIdentityMigration(deployUrl, token, currentAppId);
11906
+ if (recoveryMode) {
11907
+ if (!existingMigration) {
11908
+ if (!args.json) console.log("No reversible identity migration is recorded.");
11909
+ return { data: { status: "no_migration", appId: currentAppId } };
11910
+ }
11911
+ return recoverMigration({
11912
+ mode: recoveryMode,
11913
+ appDir,
11914
+ envName,
11915
+ remoteName: typeof args.remote === "string" ? args.remote : void 0,
11916
+ currentAppId,
11917
+ token,
11918
+ deployUrl,
11919
+ migration: existingMigration,
11920
+ json: args.json
11921
+ });
11922
+ }
11923
+ if (STRICT_APP_ID_RE2.test(currentAppId)) {
11924
+ if (!existingMigration) {
11925
+ reportAlreadyModern(currentAppId, args.json);
11926
+ return { data: { status: "already_modern", appId: currentAppId } };
11927
+ }
11928
+ if (args["dry-run"]) {
11929
+ reportMigration(existingMigration, args.json);
11930
+ return { data: { status: existingMigration.status, migration: existingMigration } };
11931
+ }
11932
+ return continuePreparedMigration({
11933
+ appDir,
11934
+ envName,
11935
+ remoteName: typeof args.remote === "string" ? args.remote : void 0,
11936
+ token,
11937
+ deployUrl,
11938
+ migration: existingMigration,
11939
+ json: args.json
11940
+ });
11941
+ }
11942
+ if (existingMigration && existingMigration.status !== "prepared" && existingMigration.status !== "rolled_back") {
11943
+ throw new Refusal(
11944
+ `The registry already uses ${existingMigration.appId}, but wrangler.toml still uses ${currentAppId}. Restore the migration commit before deploying.`,
11945
+ "migration_config_stale",
11946
+ { extra: { migration: existingMigration } }
11947
+ );
11948
+ }
11949
+ const remote = requireGitHubRemote2(
11950
+ appDir,
11951
+ typeof args.remote === "string" ? args.remote : void 0
11952
+ );
11953
+ const head = requirePublishedHead2(appDir, remote);
11954
+ const proposedAppId = existingMigration?.appId ?? mintAppId();
11955
+ const inventory = await inspectIdentityMigration(deployUrl, token, currentAppId, {
11956
+ destinationAppId: proposedAppId,
11957
+ repository: remote.repository
11958
+ });
11959
+ if (!inventory.ready) {
11960
+ throw new Refusal(
11961
+ `Migration preflight is blocked: ${inventory.blockers.map((blocker) => blocker.message).join("; ")}`,
11962
+ "migration_preflight_blocked",
11963
+ { extra: { inventory } }
11964
+ );
11965
+ }
11966
+ if (args["dry-run"]) {
11967
+ if (!args.json) {
11968
+ console.log(`Legacy app: ${currentAppId}`);
11969
+ console.log(`GitHub: ${remote.repository} (${head.branch} @ ${head.oid.slice(0, 10)})`);
11970
+ console.log(
11971
+ `Immutable app id: ${proposedAppId}${existingMigration ? "" : " (not reserved)"}`
11972
+ );
11973
+ console.log(
11974
+ `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}`
11975
+ );
11976
+ console.log(
11977
+ `Physical stores retained at ${inventory.app.resourceId}: ${inventory.retainedPhysicalStores.map((store) => store.kind).join(", ")}`
11978
+ );
11979
+ console.log("No changes made.");
11980
+ }
11981
+ return {
11982
+ data: {
11983
+ status: "ready",
11984
+ legacyAppId: currentAppId,
11985
+ proposedAppId,
11986
+ resourceId: inventory.app.resourceId,
11987
+ repository: remote.repository,
11988
+ branch: head.branch,
11989
+ commitOid: head.oid,
11990
+ sourceClaimRequired: inventory.app.sourceClaimRequired,
11991
+ inventory
11992
+ }
11993
+ };
11994
+ }
11995
+ const migration = existingMigration?.status === "prepared" ? existingMigration : await prepareIdentityMigration(deployUrl, token, currentAppId, {
11996
+ destinationAppId: proposedAppId,
11997
+ repository: remote.repository,
11998
+ expectedSourceRevision: inventory.app.sourceRevision
11999
+ });
12000
+ writeAppId(appDir, migration.appId, { wranglerEnv: envName, force: true });
12001
+ throw commitRequired(appDir, migration, envName);
12002
+ }
12003
+ });
12004
+ async function recoverMigration(input) {
12005
+ const { mode, appDir, envName, remoteName, currentAppId, token, deployUrl, migration, json } = input;
12006
+ if (migration.status === "verified" || migration.deployStartedAt) {
12007
+ throw new Refusal(
12008
+ "A canonical deployment has started. Registry rollback is no longer safe; use the forward-recovery procedure to repair or redeploy the canonical app.",
12009
+ "forward_recovery_required",
12010
+ { extra: { migration } }
12011
+ );
12012
+ }
12013
+ if (migration.status === "rolled_back") {
12014
+ if (currentAppId !== migration.legacyAppId) {
12015
+ writeAppId(appDir, migration.legacyAppId, { wranglerEnv: envName, force: true });
12016
+ throw restoreLegacyCommitRequired(appDir, migration, envName);
12017
+ }
12018
+ reportMigration(migration, json);
12019
+ return { data: { status: "rolled_back", migration } };
12020
+ }
12021
+ if (mode === "cancel" && migration.status !== "prepared") {
12022
+ throw new Refusal(
12023
+ "This migration is committed. Use --rollback before any canonical deploy starts.",
12024
+ "migration_rollback_required",
12025
+ { extra: { migration } }
12026
+ );
12027
+ }
12028
+ if (mode === "rollback" && migration.status !== "committed") {
12029
+ throw new Refusal(
12030
+ "This migration is only prepared. Use --cancel instead.",
12031
+ "migration_cancel_required",
12032
+ { extra: { migration } }
12033
+ );
12034
+ }
12035
+ if (currentAppId !== migration.legacyAppId) {
12036
+ writeAppId(appDir, migration.legacyAppId, { wranglerEnv: envName, force: true });
12037
+ throw restoreLegacyCommitRequired(appDir, migration, envName);
12038
+ }
12039
+ const remote = requireGitHubRemote2(appDir, remoteName, migration.repository);
12040
+ requirePublishedHead2(appDir, remote);
12041
+ if (mode === "cancel") {
12042
+ await cancelIdentityMigration(deployUrl, token, migration.legacyAppId);
12043
+ if (!json) console.log(`Canceled ${migration.legacyAppId} \u2192 ${migration.appId}.`);
12044
+ return { data: { status: "cancelled", legacyAppId: migration.legacyAppId } };
12045
+ }
12046
+ const rolledBack = await rollbackIdentityMigration(deployUrl, token, migration.appId);
12047
+ reportMigration(rolledBack, json);
12048
+ return { data: { status: "rolled_back", migration: rolledBack } };
12049
+ }
12050
+ async function continuePreparedMigration(input) {
12051
+ const { appDir, envName, remoteName, token, deployUrl, migration, json } = input;
12052
+ if (migration.status === "rolled_back") {
12053
+ throw new Refusal(
12054
+ `This migration was rolled back. Restore DEEPSPACE_APP_ID to ${migration.legacyAppId} before continuing.`,
12055
+ "migration_rolled_back"
12056
+ );
12057
+ }
12058
+ if (migration.status === "verified") {
12059
+ reportMigration(migration, json);
12060
+ return { data: { status: "verified", migration } };
12061
+ }
12062
+ if (migration.status === "committed") {
12063
+ throw deployRequired(appDir, migration, envName);
12064
+ }
12065
+ if (!isWorkTreeClean(appDir)) {
12066
+ const wranglerPath = wranglerRepoPath(appDir);
12067
+ if (statusFiles(repoToplevel(appDir)).includes(wranglerPath)) {
12068
+ throw commitRequired(appDir, migration, envName);
12069
+ }
12070
+ throw new Refusal(
12071
+ "Commit or discard unrelated working-tree changes before completing the migration.",
12072
+ "dirty_worktree"
12073
+ );
12074
+ }
12075
+ const remote = requireGitHubRemote2(appDir, remoteName, migration.repository);
12076
+ const head = requireLocalHead2(appDir);
12077
+ const remoteOid = remoteBranchOid(appDir, remote.name, head.branch);
12078
+ if (remoteOid !== head.oid) {
12079
+ throw new Refusal(
12080
+ `GitHub ${remote.repository} does not have ${head.branch} at ${head.oid.slice(0, 10)}. Push the migration commit, then rerun.`,
12081
+ "github_push_required",
12082
+ {
12083
+ actionRequired: true,
12084
+ action: { cwd: appDir, argv: ["git", "push", remote.name, head.branch] },
12085
+ extra: { appId: migration.appId, repository: remote.repository, ...head }
12086
+ }
12087
+ );
12088
+ }
12089
+ const committed = await commitIdentityMigration(deployUrl, token, migration.appId, {
12090
+ commitOid: head.oid,
12091
+ branch: head.branch
12092
+ });
12093
+ throw deployRequired(appDir, committed, envName);
12094
+ }
12095
+ function requireGitHubRemote2(appDir, remoteName, repository) {
12096
+ const selected = selectGitHubRemote(appDir, { name: remoteName, repository });
12097
+ if (selected) return selected;
12098
+ throw new Refusal(
12099
+ 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>.",
12100
+ "github_remote_required"
12101
+ );
12102
+ }
12103
+ function requireLocalHead2(appDir) {
12104
+ const branch = currentBranch(appDir);
12105
+ if (!branch) throw new Refusal("HEAD is detached; switch to a branch first.", "detached_head");
12106
+ const oid = resolveCommit(appDir, "HEAD");
12107
+ if (!oid) throw new Refusal("The repository has no commit to publish.", "no_commits");
12108
+ return { branch, oid };
12109
+ }
12110
+ function requirePublishedHead2(appDir, remote) {
12111
+ if (!isWorkTreeClean(appDir)) {
12112
+ throw new Refusal(
12113
+ "Commit or discard local changes before preparing migration.",
12114
+ "dirty_worktree"
12115
+ );
12116
+ }
12117
+ const head = requireLocalHead2(appDir);
12118
+ if (remoteBranchOid(appDir, remote.name, head.branch) !== head.oid) {
12119
+ throw new Refusal(
12120
+ `GitHub ${remote.repository} does not have ${head.branch} at ${head.oid.slice(0, 10)}. Push it, then rerun.`,
12121
+ "github_push_required",
12122
+ {
12123
+ actionRequired: true,
12124
+ action: { cwd: appDir, argv: ["git", "push", remote.name, head.branch] },
12125
+ extra: { repository: remote.repository, ...head }
12126
+ }
12127
+ );
12128
+ }
12129
+ return head;
12130
+ }
12131
+ function commitRequired(appDir, migration, envName) {
12132
+ const wranglerPath = wranglerRepoPath(appDir);
12133
+ return new Refusal(
12134
+ `Prepared ${migration.legacyAppId} \u2192 ${migration.appId}. Commit the deterministic wrangler.toml change, then rerun.`,
12135
+ "migration_commit_required",
12136
+ {
12137
+ actionRequired: true,
12138
+ action: {
12139
+ cwd: repoToplevel(appDir),
12140
+ argv: [
12141
+ "git",
12142
+ "commit",
12143
+ "--only",
12144
+ "--message",
12145
+ `Migrate DeepSpace app identity to ${migration.appId}`,
12146
+ "--",
12147
+ wranglerPath
12148
+ ]
12149
+ },
12150
+ extra: { migration, env: envName ?? null }
12151
+ }
12152
+ );
12153
+ }
12154
+ function restoreLegacyCommitRequired(appDir, migration, envName) {
12155
+ const wranglerPath = wranglerRepoPath(appDir);
12156
+ return new Refusal(
12157
+ `Restored ${migration.legacyAppId} in wrangler.toml. Commit and push this recovery change, then rerun.`,
12158
+ "migration_restore_commit_required",
12159
+ {
12160
+ actionRequired: true,
12161
+ action: {
12162
+ cwd: repoToplevel(appDir),
12163
+ argv: [
12164
+ "git",
12165
+ "commit",
12166
+ "--only",
12167
+ "--message",
12168
+ `Restore DeepSpace app identity to ${migration.legacyAppId}`,
12169
+ "--",
12170
+ wranglerPath
12171
+ ]
12172
+ },
12173
+ extra: { migration, env: envName ?? null }
12174
+ }
12175
+ );
12176
+ }
12177
+ function wranglerRepoPath(appDir) {
12178
+ return `${gitLine(appDir, ["rev-parse", "--show-prefix"])}wrangler.toml`;
12179
+ }
12180
+ function requireTrackedWrangler(appDir) {
12181
+ const path = wranglerRepoPath(appDir);
12182
+ if (runGit(repoToplevel(appDir), ["ls-files", "--error-unmatch", "--", path], { allowFail: true }).status !== 0) {
12183
+ throw new Refusal(
12184
+ "Track, commit, and push wrangler.toml before migrating; GitHub must already contain the deployment configuration.",
12185
+ "wrangler_not_tracked"
12186
+ );
12187
+ }
12188
+ }
12189
+ function deployRequired(appDir, migration, envName) {
12190
+ return new Refusal(
12191
+ `Identity cutover committed; deploy once to verify ${migration.appId} on the existing physical resources.`,
12192
+ "migration_deploy_required",
12193
+ {
12194
+ actionRequired: true,
12195
+ action: {
12196
+ cwd: appDir,
12197
+ argv: ["deepspace", "deploy", ...envName ? ["--env", envName] : []]
12198
+ },
12199
+ extra: { migration, env: envName ?? null }
12200
+ }
12201
+ );
12202
+ }
12203
+ function reportAlreadyModern(appId, json) {
12204
+ if (!json) console.log(`${appId} already uses an immutable DeepSpace app id.`);
12205
+ }
12206
+ function reportMigration(migration, json) {
12207
+ if (json) return;
12208
+ console.log(`Migration: ${migration.legacyAppId} \u2192 ${migration.appId}`);
12209
+ console.log(`Status: ${migration.status}`);
12210
+ console.log(`Physical resources: ${migration.resourceId}`);
12211
+ console.log(`GitHub: ${migration.repository}`);
12212
+ }
12213
+
11167
12214
  // src/cli/cli.ts
11168
12215
  var __dirname = dirname10(fileURLToPath3(import.meta.url));
11169
12216
  var pkg = JSON.parse(readFileSync25(join23(__dirname, "..", "package.json"), "utf-8"));
@@ -11216,7 +12263,7 @@ var auth = defineCommand14({
11216
12263
  var app = defineCommand14({
11217
12264
  meta: {
11218
12265
  name: "app",
11219
- description: "The app itself: create, init, list, undeploy, transfer, collaborators, domain, library, usage"
12266
+ description: "The app itself: create, init, list, source, migrate, undeploy, transfer, collaborators, domain, library, usage"
11220
12267
  },
11221
12268
  subCommands: {
11222
12269
  create: create2,
@@ -11227,7 +12274,9 @@ var app = defineCommand14({
11227
12274
  collaborators: collaborators_default,
11228
12275
  domain: domain_default,
11229
12276
  library: library_default,
11230
- usage: usage_default
12277
+ usage: usage_default,
12278
+ source: source_default,
12279
+ migrate: migrate_default
11231
12280
  }
11232
12281
  });
11233
12282
  var devGroup = defineCommand14({
@@ -11278,55 +12327,21 @@ var main = defineCommand14({
11278
12327
  // command. Citty's default behavior when no subcommand matches is to print
11279
12328
  // help, which is what we want for `deepspace` with no args.
11280
12329
  });
11281
- function editDistance2(a, b) {
11282
- const dp = Array.from({ length: a.length + 1 }, (_, i) => [
11283
- i,
11284
- ...new Array(b.length).fill(0)
11285
- ]);
11286
- for (let j = 1; j <= b.length; j++) dp[0][j] = j;
11287
- for (let i = 1; i <= a.length; i++) {
11288
- for (let j = 1; j <= b.length; j++) {
11289
- dp[i][j] = Math.min(
11290
- dp[i - 1][j] + 1,
11291
- dp[i][j - 1] + 1,
11292
- dp[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)
11293
- );
11294
- }
11295
- }
11296
- return dp[a.length][b.length];
12330
+ function commandTree() {
12331
+ return main.subCommands ?? {};
11297
12332
  }
11298
- function closestCommand(input, candidates) {
11299
- let best = null;
11300
- let bestDist = 4;
11301
- for (const c of candidates) {
11302
- const d = editDistance2(input.toLowerCase(), c);
11303
- if (d < bestDist) {
11304
- best = c;
11305
- bestDist = d;
11306
- }
11307
- }
11308
- return best;
12333
+ function unknownCommandMessage(unknown) {
12334
+ return `Unknown command: ${unknown.attemptedPath.join(" ")} \u2014 did you mean \`${unknown.suggestion.join(" ")}\`?`;
11309
12335
  }
11310
12336
  function assertKnownCommandPath(argv) {
11311
- let table = main.subCommands;
11312
- let path = "deepspace";
11313
- for (const tok of argv) {
11314
- if (!table || tok.startsWith("-")) return;
11315
- const entry = table[tok];
11316
- if (!entry) {
11317
- const hint = closestCommand(tok, Object.keys(table));
11318
- console.error(
11319
- `Unknown command: ${path} ${tok}${hint ? ` \u2014 did you mean \`${path} ${hint}\`?` : ""}
11320
- Run \`${path} --help\` for the command list.`
11321
- );
11322
- if (hint) {
11323
- printAction({ cwd: process.cwd(), argv: [...path.split(" "), hint] });
11324
- }
11325
- process.exit(1);
11326
- }
11327
- path += ` ${tok}`;
11328
- table = entry.subCommands;
11329
- }
12337
+ const unknown = findUnknownCommand(argv, commandTree());
12338
+ if (!unknown) return;
12339
+ console.error(
12340
+ `${unknownCommandMessage(unknown)}
12341
+ Run \`${unknown.helpPath.join(" ")} --help\` for the command list.`
12342
+ );
12343
+ printAction({ cwd: process.cwd(), argv: unknown.suggestion });
12344
+ process.exit(1);
11330
12345
  }
11331
12346
  var rawArgs = process.argv.slice(2);
11332
12347
  var wantsHelp = rawArgs.some((arg) => arg === "--help" || arg === "-h");
@@ -11340,15 +12355,14 @@ if (DEEPSPACE_ENV === "invalid") {
11340
12355
  process.exit(1);
11341
12356
  }
11342
12357
  if (rawArgs.includes("--json") && !wantsHelp) {
11343
- const first = rawArgs.find((a) => !a.startsWith("-"));
11344
- if (first && !Object.prototype.hasOwnProperty.call(main.subCommands ?? {}, first)) {
11345
- const hint = closestCommand(first, Object.keys(main.subCommands ?? {}));
12358
+ const unknown = findUnknownCommand(rawArgs, commandTree());
12359
+ if (unknown) {
11346
12360
  console.log(
11347
12361
  JSON.stringify({
11348
12362
  ok: false,
11349
12363
  code: "unknown_command",
11350
- error: `Unknown command: deepspace ${first}${hint ? ` \u2014 did you mean \`deepspace ${hint}\`?` : ""}`,
11351
- ...hint ? { action: { cwd: process.cwd(), argv: ["deepspace", hint] } } : {}
12364
+ error: unknownCommandMessage(unknown),
12365
+ action: { cwd: process.cwd(), argv: unknown.suggestion }
11352
12366
  })
11353
12367
  );
11354
12368
  process.exit(1);
@@ -11366,7 +12380,7 @@ if (rawArgs.includes("--json") && !wantsHelp) {
11366
12380
  process.exit(1);
11367
12381
  });
11368
12382
  } else {
11369
- if (!wantsHelp) assertKnownCommandPath(rawArgs);
12383
+ assertKnownCommandPath(rawArgs);
11370
12384
  runMain(wrapCommandErrors(main));
11371
12385
  }
11372
12386
  //# sourceMappingURL=cli.js.map