deepspace 0.9.1 → 0.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // src/cli/cli.ts
4
4
  import { defineCommand as defineCommand14, runMain, runCommand } from "citty";
5
5
  import { sync as spawnSync8 } from "cross-spawn";
6
- import { readFileSync as readFileSync26, existsSync as existsSync24 } from "fs";
6
+ import { readFileSync as readFileSync26, existsSync as existsSync25 } from "fs";
7
7
  import { dirname as dirname10, join as join24 } from "path";
8
8
  import { fileURLToPath as fileURLToPath3 } from "url";
9
9
 
@@ -788,7 +788,6 @@ import { parse as parseToml } from "smol-toml";
788
788
  // src/server/utils/registry-client.ts
789
789
  var APP_ID_RE = /^app_[0-9A-HJKMNP-TV-Z]{26}$/;
790
790
  var STRICT_APP_ID_RE = APP_ID_RE;
791
- var LEGACY_APP_ID_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/;
792
791
 
793
792
  // src/cli/lib/app-identity.ts
794
793
  var CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
@@ -817,7 +816,7 @@ function mintUlid(now = Date.now()) {
817
816
  function mintAppId(now = Date.now()) {
818
817
  return `app_${mintUlid(now)}`;
819
818
  }
820
- function readWranglerConfig(cwd) {
819
+ function readWranglerIdentityConfig(cwd) {
821
820
  const wranglerPath = join3(resolve(cwd), "wrangler.toml");
822
821
  if (!existsSync3(wranglerPath)) return null;
823
822
  try {
@@ -830,46 +829,12 @@ function readWranglerConfig(cwd) {
830
829
  }
831
830
  }
832
831
  function readAppId(cwd = process.cwd(), wranglerEnv) {
833
- const cfg = readWranglerConfig(cwd);
832
+ const cfg = readWranglerIdentityConfig(cwd);
834
833
  if (!cfg) return null;
835
834
  const vars = wranglerEnv ? cfg.env?.[wranglerEnv]?.vars : cfg.vars;
836
835
  const id = vars?.DEEPSPACE_APP_ID;
837
836
  return typeof id === "string" && APP_ID_RE.test(id) ? id : null;
838
837
  }
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
- }
873
838
  function writeAppId(cwd, appId, opts = {}) {
874
839
  const wranglerPath = join3(resolve(cwd), "wrangler.toml");
875
840
  if (!existsSync3(wranglerPath)) {
@@ -1986,7 +1951,7 @@ function prepareWranglerEnvConfig(appDir, envName, opts = {}) {
1986
1951
  if (!opts.sharedDevVarsCache) return { wranglerEnv: envName, cleanup() {
1987
1952
  } };
1988
1953
  warnIfLegacyEnvDevVarsExists(appDir, envName, opts.warn);
1989
- const config = readWranglerConfig2(appDir);
1954
+ const config = readWranglerConfig(appDir);
1990
1955
  const resolved = resolveAppNameForEnv(config, envName);
1991
1956
  if (!resolved.ok) throw new Error(resolved.reason);
1992
1957
  const flattened = flattenWranglerEnvConfig(config, envName);
@@ -2050,7 +2015,7 @@ var WranglerConfigError = class extends Error {
2050
2015
  this.name = "WranglerConfigError";
2051
2016
  }
2052
2017
  };
2053
- function readWranglerConfig2(appDir) {
2018
+ function readWranglerConfig(appDir) {
2054
2019
  const path = join7(appDir, "wrangler.toml");
2055
2020
  const raw = readFileSync8(path, "utf-8");
2056
2021
  try {
@@ -2465,9 +2430,12 @@ function lintSchema(schema) {
2465
2430
  }
2466
2431
  if (schema.ownerField) {
2467
2432
  const col = schema.columns.find((c) => c.name === schema.ownerField);
2468
- if (col && !col.userBound) {
2433
+ const spoofableRoles = Object.entries(schema.permissions).filter(
2434
+ ([role, permissions]) => role !== "owner" && role !== "admin" && permissions.create === true
2435
+ ).map(([role]) => role);
2436
+ if (col && !col.userBound && spoofableRoles.length > 0) {
2469
2437
  warnings.push(
2470
- `[${schema.name}] ownerField is '${schema.ownerField}' but that column is not marked userBound: true. A client can create a row with someone else's id in this field, bypassing 'own' permission checks. Add userBound: true (and ideally immutable: true) to the column.`
2438
+ `[${schema.name}] ownerField is '${schema.ownerField}' but that column is not marked userBound: true. Client role(s) ${spoofableRoles.join(", ")} can create a row with someone else's id in this field, bypassing 'own' permission checks. Add userBound: true (and ideally immutable: true) to the column.`
2471
2439
  );
2472
2440
  }
2473
2441
  }
@@ -3594,8 +3562,298 @@ import { resolve as resolve12 } from "path";
3594
3562
  // src/cli/commands/deploy/build.ts
3595
3563
  import * as p6 from "@clack/prompts";
3596
3564
  import { execSync as execSync2 } from "child_process";
3597
- import { existsSync as existsSync13, readFileSync as readFileSync14, readdirSync as readdirSync4 } from "fs";
3598
- import { dirname as dirname5, join as join14, resolve as resolve10 } from "path";
3565
+ import { existsSync as existsSync14, readFileSync as readFileSync15, readdirSync as readdirSync4 } from "fs";
3566
+ import { dirname as dirname5, join as join15, resolve as resolve10 } from "path";
3567
+
3568
+ // src/cli/commands/migrate/app-migrations.ts
3569
+ import {
3570
+ closeSync,
3571
+ constants,
3572
+ existsSync as existsSync13,
3573
+ fstatSync,
3574
+ ftruncateSync,
3575
+ lstatSync,
3576
+ openSync,
3577
+ readFileSync as readFileSync14,
3578
+ writeSync
3579
+ } from "fs";
3580
+ import { join as join14 } from "path";
3581
+
3582
+ // src/shared/protocol/app-migrations.ts
3583
+ var APP_MIGRATION_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/;
3584
+ var MAX_APP_MIGRATION_IDS = 256;
3585
+ var CANONICAL_APP_IDENTITY_MIGRATION_ID = "2026-08-canonical-app-identity-wire";
3586
+ function validateAppMigrationIds(value) {
3587
+ if (!Array.isArray(value)) {
3588
+ return { valid: false, reason: "app migrations must be a JSON string array" };
3589
+ }
3590
+ if (value.length > MAX_APP_MIGRATION_IDS) {
3591
+ return {
3592
+ valid: false,
3593
+ reason: `app migrations cannot contain more than ${MAX_APP_MIGRATION_IDS} entries`
3594
+ };
3595
+ }
3596
+ const ids = [];
3597
+ const seen = /* @__PURE__ */ new Set();
3598
+ for (const valueId of value) {
3599
+ if (typeof valueId !== "string" || !APP_MIGRATION_ID_RE.test(valueId)) {
3600
+ return { valid: false, reason: `invalid app migration id: ${String(valueId)}` };
3601
+ }
3602
+ if (seen.has(valueId)) {
3603
+ return { valid: false, reason: `duplicate app migration id: ${valueId}` };
3604
+ }
3605
+ seen.add(valueId);
3606
+ ids.push(valueId);
3607
+ }
3608
+ return { valid: true, ids };
3609
+ }
3610
+
3611
+ // src/cli/commands/migrate/app-migrations.ts
3612
+ var APP_MIGRATIONS_MANIFEST = "deepspace.migrations.json";
3613
+ var SOURCE_MIGRATIONS = [
3614
+ {
3615
+ id: CANONICAL_APP_IDENTITY_MIGRATION_ID,
3616
+ description: "Use the immutable app id for authenticated platform requests",
3617
+ transform: migrateCanonicalIdentityWire,
3618
+ findBlockers: findLegacyIdentityWireBlockers
3619
+ }
3620
+ ];
3621
+ var APP_MIGRATION_DEFINITIONS = SOURCE_MIGRATIONS.map(
3622
+ ({ id, description }) => ({ id, description })
3623
+ );
3624
+ function readAppliedAppMigrations(appDir) {
3625
+ const path = join14(appDir, APP_MIGRATIONS_MANIFEST);
3626
+ if (!existsSync13(path)) return [];
3627
+ const source = readRegularFile(path);
3628
+ let parsed;
3629
+ try {
3630
+ parsed = JSON.parse(source);
3631
+ } catch {
3632
+ throw new Error(`${APP_MIGRATIONS_MANIFEST} must contain valid JSON`);
3633
+ }
3634
+ const validation = validateAppMigrationIds(parsed);
3635
+ if (!validation.valid) {
3636
+ throw new Error(`${APP_MIGRATIONS_MANIFEST}: ${validation.reason}`);
3637
+ }
3638
+ return validation.ids;
3639
+ }
3640
+ function planAppMigrations(appDir) {
3641
+ const repo = repoToplevel(appDir);
3642
+ const appPrefix = gitLine(appDir, ["rev-parse", "--show-prefix"]);
3643
+ const appliedIds = readAppliedAppMigrations(appDir);
3644
+ const applied = new Set(appliedIds);
3645
+ const pendingMigrations = SOURCE_MIGRATIONS.filter((migration) => !applied.has(migration.id));
3646
+ const tracked = splitNulFields(runGit(repo, ["ls-files", "-z"]).stdout);
3647
+ const perMigration = /* @__PURE__ */ new Map();
3648
+ const blockers = [];
3649
+ const edits = [];
3650
+ for (const repoPath of tracked) {
3651
+ if (!isSourcePath(repoPath, appPrefix)) continue;
3652
+ const absolutePath = join14(repo, repoPath);
3653
+ let stat;
3654
+ try {
3655
+ stat = lstatSync(absolutePath);
3656
+ } catch {
3657
+ continue;
3658
+ }
3659
+ if (!stat.isFile() || stat.isSymbolicLink()) continue;
3660
+ const before = readRegularFile(absolutePath);
3661
+ let after = before;
3662
+ for (const migration of pendingMigrations) {
3663
+ const result = migration.transform(after);
3664
+ after = result.source;
3665
+ if (result.replacements > 0) {
3666
+ const planned = perMigration.get(migration.id) ?? {
3667
+ id: migration.id,
3668
+ description: migration.description,
3669
+ files: [],
3670
+ replacements: 0
3671
+ };
3672
+ planned.files.push(repoPath);
3673
+ planned.replacements += result.replacements;
3674
+ perMigration.set(migration.id, planned);
3675
+ }
3676
+ blockers.push(...migration.findBlockers(after, repoPath));
3677
+ }
3678
+ if (after !== before) edits.push({ path: repoPath, before, after });
3679
+ }
3680
+ const pending = pendingMigrations.map((migration) => {
3681
+ const planned = perMigration.get(migration.id);
3682
+ return planned ? { ...planned, files: [...new Set(planned.files)].sort() } : { id: migration.id, description: migration.description, files: [], replacements: 0 };
3683
+ });
3684
+ if (pending.length > 0 && blockers.length === 0) {
3685
+ const manifestPath = `${appPrefix}${APP_MIGRATIONS_MANIFEST}`;
3686
+ const absoluteManifestPath = join14(repo, manifestPath);
3687
+ const before = existsSync13(absoluteManifestPath) ? readRegularFile(absoluteManifestPath) : null;
3688
+ const after = `${JSON.stringify([...appliedIds, ...pending.map(({ id }) => id)], null, 2)}
3689
+ `;
3690
+ if (before !== after) edits.push({ path: manifestPath, before, after });
3691
+ }
3692
+ return {
3693
+ pending,
3694
+ changedFiles: edits.map((edit) => edit.path).sort(),
3695
+ blockers,
3696
+ edits
3697
+ };
3698
+ }
3699
+ function applyAppMigrationPlan(appDir, plan) {
3700
+ if (plan.blockers.length > 0) {
3701
+ throw new Error("Cannot apply an app migration plan with unresolved blockers.");
3702
+ }
3703
+ const repo = repoToplevel(appDir);
3704
+ for (const edit of plan.edits) {
3705
+ const absolutePath = join14(repo, edit.path);
3706
+ const current = existsSync13(absolutePath) ? readRegularFile(absolutePath) : null;
3707
+ if (current !== edit.before) {
3708
+ throw new Error(`Migration input changed after planning: ${edit.path}`);
3709
+ }
3710
+ }
3711
+ for (const edit of plan.edits) writeRegularFile(join14(repo, edit.path), edit.before, edit.after);
3712
+ }
3713
+ function readRegularFile(path) {
3714
+ const before = lstatSync(path);
3715
+ if (!before.isFile() || before.isSymbolicLink()) {
3716
+ throw new Error(`Migration input must be a regular file, not a symlink: ${path}`);
3717
+ }
3718
+ const fd = openSync(path, constants.O_RDONLY);
3719
+ try {
3720
+ const opened = fstatSync(fd);
3721
+ const after = lstatSync(path);
3722
+ if (!opened.isFile() || opened.dev !== after.dev || opened.ino !== after.ino) {
3723
+ throw new Error(`Migration input changed while opening: ${path}`);
3724
+ }
3725
+ return readFileSync14(fd, "utf-8");
3726
+ } finally {
3727
+ closeSync(fd);
3728
+ }
3729
+ }
3730
+ function writeRegularFile(path, before, after) {
3731
+ if (before === null) {
3732
+ const fd2 = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 420);
3733
+ try {
3734
+ writeSync(fd2, after, 0, "utf-8");
3735
+ } finally {
3736
+ closeSync(fd2);
3737
+ }
3738
+ return;
3739
+ }
3740
+ const pathStat = lstatSync(path);
3741
+ if (!pathStat.isFile() || pathStat.isSymbolicLink()) {
3742
+ throw new Error(`Migration input must be a regular file, not a symlink: ${path}`);
3743
+ }
3744
+ const fd = openSync(path, constants.O_RDWR);
3745
+ try {
3746
+ const opened = fstatSync(fd);
3747
+ const currentPath = lstatSync(path);
3748
+ if (!opened.isFile() || opened.dev !== currentPath.dev || opened.ino !== currentPath.ino) {
3749
+ throw new Error(`Migration input changed while opening: ${path}`);
3750
+ }
3751
+ if (readFileSync14(fd, "utf-8") !== before) {
3752
+ throw new Error(`Migration input changed after planning: ${path}`);
3753
+ }
3754
+ ftruncateSync(fd, 0);
3755
+ writeSync(fd, after, 0, "utf-8");
3756
+ } finally {
3757
+ closeSync(fd);
3758
+ }
3759
+ }
3760
+ function isSourcePath(repoPath, appPrefix) {
3761
+ if (appPrefix && !repoPath.startsWith(appPrefix)) return false;
3762
+ return /\.(?:[cm]?[jt]sx?)$/.test(repoPath);
3763
+ }
3764
+ function migrateCanonicalIdentityWire(source) {
3765
+ let replacements = 0;
3766
+ const lines = source.split("\n");
3767
+ for (let index = 0; index < lines.length; index++) {
3768
+ let line = lines[index];
3769
+ line = line.replace(
3770
+ /(\.set\(\s*)(['"])x-app-name\2(\s*,\s*)([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\.APP_NAME(\s*\))/g,
3771
+ (_match, open, quote, separator, receiver, close) => {
3772
+ replacements++;
3773
+ return `${open}${quote}x-app-id${quote}${separator}${receiver}.DEEPSPACE_APP_ID${close}`;
3774
+ }
3775
+ );
3776
+ line = line.replace(
3777
+ /(['"])x-app-name\1(\s*:\s*)([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\.APP_NAME/g,
3778
+ (_match, quote, separator, receiver) => {
3779
+ replacements++;
3780
+ return `${quote}x-app-id${quote}${separator}${receiver}.DEEPSPACE_APP_ID`;
3781
+ }
3782
+ );
3783
+ line = line.replace(
3784
+ /(\[\s*)(['"])x-app-name\2(\s*\]\s*=\s*)([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\.APP_NAME/g,
3785
+ (_match, open, quote, separator, receiver) => {
3786
+ replacements++;
3787
+ return `${open}${quote}x-app-id${quote}${separator}${receiver}.DEEPSPACE_APP_ID`;
3788
+ }
3789
+ );
3790
+ line = line.replace(
3791
+ /(forwardedParams\.set\(\s*)(['"])appName\2(\s*,\s*)([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\.APP_NAME(\s*\))/g,
3792
+ (_match, open, quote, separator, receiver, close) => {
3793
+ replacements++;
3794
+ return `${open}${quote}appId${quote}${separator}${receiver}.DEEPSPACE_APP_ID${close}`;
3795
+ }
3796
+ );
3797
+ lines[index] = line;
3798
+ }
3799
+ if (replacements === 0) return { source, replacements };
3800
+ let migrated = lines.join("\n");
3801
+ if (/forwardedParams\.set\(\s*['"]appId['"]/.test(migrated)) {
3802
+ migrated = migrated.replaceAll("injectAppName", "injectAppId").replaceAll("`?appName=...`", "`?appId=...`").replaceAll("Inject appName into the query string", "Inject appId into the query string").replaceAll("caller-supplied appName", "caller-supplied appId").replaceAll(
3803
+ "/_deepspace/subscriptions/plans?appName=",
3804
+ "/_deepspace/subscriptions/plans?appId="
3805
+ );
3806
+ }
3807
+ migrated = migrated.replaceAll(
3808
+ "APP_IDENTITY_TOKEN + APP_NAME",
3809
+ "APP_IDENTITY_TOKEN + DEEPSPACE_APP_ID"
3810
+ );
3811
+ return {
3812
+ source: ensureCanonicalIdentityProperties(migrated),
3813
+ replacements
3814
+ };
3815
+ }
3816
+ function ensureCanonicalIdentityProperties(source) {
3817
+ const lines = source.split("\n");
3818
+ for (let index = lines.length - 1; index >= 0; index--) {
3819
+ const match = /^(\s*)(?:readonly\s+)?APP_NAME\??\s*:\s*string\s*([;,]?)\s*$/.exec(lines[index]);
3820
+ if (!match || blockAlreadyDeclaresCanonicalId(lines, index, match[1].length)) continue;
3821
+ const suffix = match[2] ?? "";
3822
+ lines.splice(index + 1, 0, `${match[1]}DEEPSPACE_APP_ID: string${suffix}`);
3823
+ }
3824
+ return lines.join("\n");
3825
+ }
3826
+ function blockAlreadyDeclaresCanonicalId(lines, propertyIndex, propertyIndent) {
3827
+ for (const direction of [-1, 1]) {
3828
+ for (let index = propertyIndex + direction; index >= 0 && index < lines.length; index += direction) {
3829
+ const line = lines[index];
3830
+ if (/^\s*(?:readonly\s+)?DEEPSPACE_APP_ID\??\s*:/.test(line)) return true;
3831
+ const trimmed = line.trimStart();
3832
+ if (!trimmed) continue;
3833
+ const indent = line.length - trimmed.length;
3834
+ if (indent < propertyIndent && (trimmed.includes("{") || trimmed.startsWith("}"))) break;
3835
+ }
3836
+ }
3837
+ return false;
3838
+ }
3839
+ function findLegacyIdentityWireBlockers(source, file) {
3840
+ const blockers = [];
3841
+ for (const [index, line] of source.split("\n").entries()) {
3842
+ const trimmed = line.trimStart();
3843
+ if (!/['"]x-app-name['"]/.test(line) || /\.delete\(\s*['"]x-app-name['"]/.test(line) || trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*")) {
3844
+ continue;
3845
+ }
3846
+ blockers.push({
3847
+ migrationId: CANONICAL_APP_IDENTITY_MIGRATION_ID,
3848
+ file,
3849
+ line: index + 1,
3850
+ message: "This app still sends the removed x-app-name identity header in a form the safe migration cannot rewrite."
3851
+ });
3852
+ }
3853
+ return blockers;
3854
+ }
3855
+
3856
+ // src/cli/commands/deploy/build.ts
3599
3857
  var RESERVED_RUN_WORKER_FIRST = /* @__PURE__ */ new Set([
3600
3858
  "/api/*",
3601
3859
  "/ws/*",
@@ -3631,19 +3889,19 @@ async function buildDeployBundle(options) {
3631
3889
  preparedWranglerConfig?.cleanup();
3632
3890
  }
3633
3891
  spinner2.stop("Built");
3634
- const deployConfigPath = join14(appDir, ".wrangler", "deploy", "config.json");
3635
- if (!existsSync13(deployConfigPath)) {
3892
+ const deployConfigPath = join15(appDir, ".wrangler", "deploy", "config.json");
3893
+ if (!existsSync14(deployConfigPath)) {
3636
3894
  output.die(
3637
3895
  "Build output config not found at .wrangler/deploy/config.json",
3638
3896
  "build_output_missing"
3639
3897
  );
3640
3898
  }
3641
- const deployConfig = JSON.parse(readFileSync14(deployConfigPath, "utf-8"));
3899
+ const deployConfig = JSON.parse(readFileSync15(deployConfigPath, "utf-8"));
3642
3900
  const outputWranglerPath = resolve10(dirname5(deployConfigPath), deployConfig.configPath);
3643
- if (!existsSync13(outputWranglerPath)) {
3901
+ if (!existsSync14(outputWranglerPath)) {
3644
3902
  output.die(`Output wrangler.json not found at ${outputWranglerPath}`, "build_output_missing");
3645
3903
  }
3646
- const outputConfig = JSON.parse(readFileSync14(outputWranglerPath, "utf-8"));
3904
+ const outputConfig = JSON.parse(readFileSync15(outputWranglerPath, "utf-8"));
3647
3905
  if (outputConfig.name && outputConfig.name !== appName) {
3648
3906
  const envHint = envName ? ` (--env ${envName})` : "";
3649
3907
  output.die(
@@ -3652,18 +3910,18 @@ async function buildDeployBundle(options) {
3652
3910
  );
3653
3911
  }
3654
3912
  const workerDir = dirname5(outputWranglerPath);
3655
- const workerBundlePath = join14(workerDir, outputConfig.main);
3913
+ const workerBundlePath = join15(workerDir, outputConfig.main);
3656
3914
  const clientDir = outputConfig.assets?.directory ? resolve10(workerDir, outputConfig.assets.directory) : null;
3657
- if (!existsSync13(workerBundlePath)) {
3915
+ if (!existsSync14(workerBundlePath)) {
3658
3916
  output.die(`Worker bundle not found at ${workerBundlePath}`, "build_output_missing");
3659
3917
  }
3660
- if (!clientDir || !existsSync13(clientDir)) {
3918
+ if (!clientDir || !existsSync14(clientDir)) {
3661
3919
  return output.die(`Client assets not found at ${clientDir}`, "build_output_missing");
3662
3920
  }
3663
3921
  spinner2.start("Collecting assets...");
3664
3922
  const assets = collectAssets(clientDir);
3665
3923
  spinner2.stop(`Collected ${assets.length} assets`);
3666
- const workerJs = readFileSync14(workerBundlePath, "utf-8");
3924
+ const workerJs = readFileSync15(workerBundlePath, "utf-8");
3667
3925
  const doBindings = outputConfig.durable_objects?.bindings;
3668
3926
  const sqliteClasses = new Set(
3669
3927
  outputConfig.migrations?.flatMap((migration) => migration.new_sqlite_classes ?? []) ?? []
@@ -3689,9 +3947,16 @@ async function buildDeployBundle(options) {
3689
3947
  );
3690
3948
  }
3691
3949
  const extraRoutes = extractRunWorkerFirst(wranglerConfig);
3950
+ let appMigrations = [];
3951
+ try {
3952
+ appMigrations = readAppliedAppMigrations(appDir);
3953
+ } catch (error) {
3954
+ output.die(errorMessage(error), "invalid_migration_manifest");
3955
+ }
3692
3956
  return {
3693
3957
  assets,
3694
3958
  workerJs,
3959
+ appMigrations,
3695
3960
  doManifest,
3696
3961
  customBindings,
3697
3962
  extraRoutes
@@ -3716,14 +3981,14 @@ function collectAssets(dir) {
3716
3981
  const walk = (currentDir, prefix) => {
3717
3982
  for (const entry of readdirSync4(currentDir, { withFileTypes: true })) {
3718
3983
  if (entry.name === ".assetsignore") continue;
3719
- const fullPath = join14(currentDir, entry.name);
3984
+ const fullPath = join15(currentDir, entry.name);
3720
3985
  const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
3721
3986
  if (entry.isDirectory()) {
3722
3987
  walk(fullPath, relativePath);
3723
3988
  } else {
3724
3989
  assets.push({
3725
3990
  path: `/${relativePath}`,
3726
- contentBase64: readFileSync14(fullPath).toString("base64")
3991
+ contentBase64: readFileSync15(fullPath).toString("base64")
3727
3992
  });
3728
3993
  }
3729
3994
  }
@@ -3737,18 +4002,18 @@ function errorMessage(error) {
3737
4002
 
3738
4003
  // src/cli/commands/deploy/commerce.ts
3739
4004
  import * as p7 from "@clack/prompts";
3740
- import { existsSync as existsSync14 } from "fs";
3741
- import { join as join15 } from "path";
4005
+ import { existsSync as existsSync15 } from "fs";
4006
+ import { join as join16 } from "path";
3742
4007
  import { pathToFileURL as pathToFileURL2 } from "url";
3743
4008
  var API_URL2 = process.env.DEEPSPACE_API_URL ?? PLATFORM_URLS.api;
3744
4009
  function errorMessage2(error) {
3745
4010
  return error instanceof Error ? error.message : String(error);
3746
4011
  }
3747
4012
  async function loadDeclaration(appDir, sourceFile, outputFile, exportName) {
3748
- const sourcePath = join15(appDir, "src", sourceFile);
3749
- if (!existsSync14(sourcePath)) return void 0;
4013
+ const sourcePath = join16(appDir, "src", sourceFile);
4014
+ if (!existsSync15(sourcePath)) return void 0;
3750
4015
  const esbuild = await import("esbuild");
3751
- const outputPath = join15(appDir, ".wrangler", "deploy", outputFile);
4016
+ const outputPath = join16(appDir, ".wrangler", "deploy", outputFile);
3752
4017
  await esbuild.build({
3753
4018
  entryPoints: [sourcePath],
3754
4019
  bundle: true,
@@ -3841,7 +4106,7 @@ Stripe will keep charging them until you cancel. Cancel at the end of their curr
3841
4106
  }
3842
4107
  }
3843
4108
  async function syncSubscriptionPlans(appDir, appName, token, nonInteractive) {
3844
- if (!existsSync14(join15(appDir, "src", "subscriptions.ts"))) return;
4109
+ if (!existsSync15(join16(appDir, "src", "subscriptions.ts"))) return;
3845
4110
  let plans;
3846
4111
  try {
3847
4112
  plans = await loadDeclaration(
@@ -3896,7 +4161,7 @@ async function syncSubscriptionPlans(appDir, appName, token, nonInteractive) {
3896
4161
  }
3897
4162
  }
3898
4163
  async function syncOneTimeProducts(appDir, appName, token) {
3899
- if (!existsSync14(join15(appDir, "src", "products.ts"))) return;
4164
+ if (!existsSync15(join16(appDir, "src", "products.ts"))) return;
3900
4165
  let products;
3901
4166
  try {
3902
4167
  products = await loadDeclaration(
@@ -4188,7 +4453,7 @@ function repoApi(deployUrl, token, appId) {
4188
4453
  }
4189
4454
 
4190
4455
  // src/cli/lib/vc-remote.ts
4191
- import { existsSync as existsSync15 } from "fs";
4456
+ import { existsSync as existsSync16 } from "fs";
4192
4457
  import { tmpdir } from "os";
4193
4458
  import { isAbsolute as isAbsolute3, relative, resolve as resolve11 } from "path";
4194
4459
  var SPACE_REMOTE = "space";
@@ -4229,7 +4494,7 @@ function entryIsTransient(cwd) {
4229
4494
  }
4230
4495
  function helperEntryUsable(command) {
4231
4496
  const match = /^!'([^']*)' '([^']*)' git-credential$/.exec(command);
4232
- return match !== null && existsSync15(match[1]) && existsSync15(match[2]);
4497
+ return match !== null && existsSync16(match[1]) && existsSync16(match[2]);
4233
4498
  }
4234
4499
  function credentialHelperCommand() {
4235
4500
  const entry = process.argv[1];
@@ -4347,7 +4612,7 @@ function classifyPushTransportFailure(error) {
4347
4612
  if (/(?:HTTP |error: )422\b/i.test(message)) {
4348
4613
  return {
4349
4614
  code: "source_managed_by_github",
4350
- error: "This app uses GitHub source. Push with normal Git/GitHub, then run `deepspace deploy`."
4615
+ error: "This app uses GitHub source. Manage commits with normal Git/GitHub; `deepspace deploy` ships the local working tree without changing Git."
4351
4616
  };
4352
4617
  }
4353
4618
  if (/(?:HTTP |error: )429\b/i.test(message)) {
@@ -4527,7 +4792,10 @@ async function syncDeployRepository(options) {
4527
4792
  let source = sourceState.source;
4528
4793
  let sourceRevision = sourceState.revision;
4529
4794
  const baseReleaseId = null;
4530
- if (!push && source?.provider !== "github") {
4795
+ if (source?.provider === "github") {
4796
+ return { commitOid, recoverable, deployKey, source, sourceRevision, baseReleaseId };
4797
+ }
4798
+ if (!push) {
4531
4799
  try {
4532
4800
  assertSyncableRepo(appDir);
4533
4801
  commitOid = resolveCommit(appDir, "HEAD");
@@ -4537,23 +4805,6 @@ async function syncDeployRepository(options) {
4537
4805
  }
4538
4806
  try {
4539
4807
  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
4808
  if (!source && listGitHubRemotes(appDir).length > 0) {
4558
4809
  output.die(
4559
4810
  "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.",
@@ -4662,71 +4913,6 @@ async function syncDeployRepository(options) {
4662
4913
  }
4663
4914
  return { commitOid, recoverable, deployKey, source, sourceRevision, baseReleaseId };
4664
4915
  }
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 };
4729
- }
4730
4916
  function classifyRemoteState(appDir, token, branch, localTip) {
4731
4917
  let remoteTip = null;
4732
4918
  try {
@@ -4882,6 +5068,7 @@ async function deployBuiltBundle(options) {
4882
5068
  );
4883
5069
  form.append("uploadId", uploadId);
4884
5070
  form.append("totalGroups", String(totalGroups));
5071
+ form.append("appMigrations", JSON.stringify(bundle.appMigrations));
4885
5072
  if (bundle.doManifest) form.append("doManifest", JSON.stringify(bundle.doManifest));
4886
5073
  if (bundle.customBindings.length) {
4887
5074
  form.append("bindingManifest", JSON.stringify(bundle.customBindings));
@@ -5047,7 +5234,7 @@ async function postWithRetry(url, makeInit, {
5047
5234
 
5048
5235
  // src/cli/commands/deploy/secrets.ts
5049
5236
  import * as p11 from "@clack/prompts";
5050
- import { existsSync as existsSync16, readFileSync as readFileSync15 } from "fs";
5237
+ import { existsSync as existsSync17, readFileSync as readFileSync16 } from "fs";
5051
5238
  function classifyDevVarsSecrets(options) {
5052
5239
  const store = new Set(options.storeSecretNames);
5053
5240
  const strayKeys = options.handEditedDevVarKeys.filter((key) => !store.has(key));
@@ -5141,21 +5328,61 @@ function prepareDeploySecrets(options) {
5141
5328
  );
5142
5329
  }
5143
5330
  }
5144
- if (names.length) p11.log.info(`App secrets: ${names.join(", ")}`);
5145
- return { values, names };
5146
- }
5147
- function readCustomDevVars(path, output) {
5148
- if (!existsSync16(path)) return {};
5149
- const section = extractCustomDevVars(stripGeneratedSecretsCache(readFileSync15(path, "utf-8")));
5150
- if (!section) return {};
5151
- try {
5152
- return parseDevVars(section);
5153
- } catch (error) {
5154
- output.die(`.dev.vars: ${errorMessage5(error)}`, "invalid_dev_vars");
5331
+ if (names.length) p11.log.info(`App secrets: ${names.join(", ")}`);
5332
+ return { values, names };
5333
+ }
5334
+ function readCustomDevVars(path, output) {
5335
+ if (!existsSync17(path)) return {};
5336
+ const section = extractCustomDevVars(stripGeneratedSecretsCache(readFileSync16(path, "utf-8")));
5337
+ if (!section) return {};
5338
+ try {
5339
+ return parseDevVars(section);
5340
+ } catch (error) {
5341
+ output.die(`.dev.vars: ${errorMessage5(error)}`, "invalid_dev_vars");
5342
+ }
5343
+ }
5344
+ function errorMessage5(error) {
5345
+ return error instanceof Error ? error.message : String(error);
5346
+ }
5347
+
5348
+ // src/cli/commands/migrate/legacy-app-id.ts
5349
+ var LEGACY_APP_ID_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/;
5350
+ function readLegacyAppId(cwd = process.cwd(), wranglerEnv) {
5351
+ const cfg = readWranglerIdentityConfig(cwd);
5352
+ if (!cfg) return null;
5353
+ const slot = wranglerEnv ? cfg.env?.[wranglerEnv] : cfg;
5354
+ if (!slot) return null;
5355
+ const declaredId = slot.vars?.DEEPSPACE_APP_ID;
5356
+ if (declaredId !== void 0) {
5357
+ if (typeof declaredId === "string" && LEGACY_APP_ID_RE.test(declaredId)) return declaredId;
5358
+ throw new InputError(
5359
+ "DEEPSPACE_APP_ID is present but invalid; correct it before migrating.",
5360
+ "invalid_app_id"
5361
+ );
5362
+ }
5363
+ const appName = typeof slot.vars?.APP_NAME === "string" ? slot.vars.APP_NAME : null;
5364
+ const workerName = typeof slot.name === "string" ? slot.name : null;
5365
+ if (!appName) return null;
5366
+ if (!workerName) {
5367
+ throw new InputError(
5368
+ "Legacy migration requires both Worker name and APP_NAME to be present and identical.",
5369
+ "ambiguous_legacy_app_id"
5370
+ );
5371
+ }
5372
+ if (appName !== workerName) {
5373
+ throw new InputError(
5374
+ `Legacy APP_NAME "${appName}" does not match Worker name "${workerName}"; migration cannot choose safely.`,
5375
+ "ambiguous_legacy_app_id"
5376
+ );
5155
5377
  }
5378
+ return !APP_ID_RE.test(appName) && LEGACY_APP_ID_RE.test(appName) ? appName : null;
5156
5379
  }
5157
- function errorMessage5(error) {
5158
- return error instanceof Error ? error.message : String(error);
5380
+ function legacyDeployRefusal(appDir, wranglerEnv) {
5381
+ const legacyAppId = readLegacyAppId(appDir, wranglerEnv);
5382
+ return legacyAppId ? {
5383
+ error: `This checkout still uses legacy app identity ${legacyAppId}. Run \`deepspace app migrate --dry-run\`, then complete \`deepspace app migrate\` before deploying.`,
5384
+ code: "legacy_app_migration_required"
5385
+ } : null;
5159
5386
  }
5160
5387
 
5161
5388
  // src/cli/commands/deploy.ts
@@ -5190,7 +5417,7 @@ var deploy_default = defineCommand3({
5190
5417
  // Citty maps --no-push to the negation of an argument named `push`.
5191
5418
  push: {
5192
5419
  type: "boolean",
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.",
5420
+ description: "Sync DeepSpace source before deploying. GitHub source always deploys the local working tree without Git operations; --no-push opts DeepSpace source out of sync.",
5194
5421
  default: true
5195
5422
  },
5196
5423
  "ignore-stale": {
@@ -5224,7 +5451,7 @@ var deploy_default = defineCommand3({
5224
5451
  }
5225
5452
  p12.intro("Deploying DeepSpace app");
5226
5453
  output.showIntro();
5227
- const wranglerConfig = readWranglerConfig2(appDir);
5454
+ const wranglerConfig = readWranglerConfig(appDir);
5228
5455
  const nameResult = resolveAppNameForEnv(wranglerConfig, envName);
5229
5456
  if (!nameResult.ok) return output.die(nameResult.reason, "invalid_app_name");
5230
5457
  const declaredName = envName ? wranglerConfig.env?.[envName]?.name : wranglerConfig.name;
@@ -5238,13 +5465,8 @@ var deploy_default = defineCommand3({
5238
5465
  const appName = nameResult.name;
5239
5466
  let appId = readAppId(appDir, envName);
5240
5467
  if (!appId) {
5241
- const legacyAppId = readLegacyAppId(appDir, envName);
5242
- if (legacyAppId) {
5243
- output.die(
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"
5246
- );
5247
- }
5468
+ const refusal = legacyDeployRefusal(appDir, envName);
5469
+ if (refusal) output.die(refusal.error, refusal.code);
5248
5470
  }
5249
5471
  p12.log.info(envName ? `App: ${appName} (env: ${envName})` : `App: ${appName}`);
5250
5472
  const { token, ownerId } = await authenticate(appDir, output);
@@ -6026,20 +6248,20 @@ var usage_default = defineDeepspaceCommand({
6026
6248
  });
6027
6249
 
6028
6250
  // src/cli/commands/add.ts
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";
6251
+ import { existsSync as existsSync19, readFileSync as readFileSync18, realpathSync as realpathSync3 } from "fs";
6252
+ import { dirname as dirname7, join as join18, resolve as resolve14 } from "path";
6031
6253
  import { pathToFileURL as pathToFileURL3 } from "url";
6032
6254
 
6033
6255
  // src/cli/commands/feature-installer.ts
6034
6256
  import {
6035
- closeSync,
6257
+ closeSync as closeSync2,
6036
6258
  copyFileSync,
6037
- existsSync as existsSync17,
6038
- fstatSync,
6259
+ existsSync as existsSync18,
6260
+ fstatSync as fstatSync2,
6039
6261
  mkdirSync as mkdirSync6,
6040
6262
  mkdtempSync,
6041
- openSync,
6042
- readFileSync as readFileSync16,
6263
+ openSync as openSync2,
6264
+ readFileSync as readFileSync17,
6043
6265
  readSync,
6044
6266
  readdirSync as readdirSync5,
6045
6267
  rmSync as rmSync4,
@@ -6047,16 +6269,16 @@ import {
6047
6269
  } from "fs";
6048
6270
  import { spawnSync as spawnSync6 } from "child_process";
6049
6271
  import { tmpdir as tmpdir2 } from "os";
6050
- import { dirname as dirname6, isAbsolute as isAbsolute4, join as join16, resolve as resolve13 } from "path";
6272
+ import { dirname as dirname6, isAbsolute as isAbsolute4, join as join17, resolve as resolve13 } from "path";
6051
6273
  import { fileURLToPath } from "url";
6052
6274
  var NAV_MARKER = "// \u2500\u2500 Features add nav items below this line \u2500\u2500";
6053
6275
  function featurePackageRoot(moduleUrl = import.meta.url) {
6054
6276
  let dir = dirname6(fileURLToPath(moduleUrl));
6055
6277
  for (; ; ) {
6056
- const packagePath = join16(dir, "package.json");
6057
- if (existsSync17(packagePath)) {
6278
+ const packagePath = join17(dir, "package.json");
6279
+ if (existsSync18(packagePath)) {
6058
6280
  try {
6059
- const pkg2 = JSON.parse(readFileSync16(packagePath, "utf-8"));
6281
+ const pkg2 = JSON.parse(readFileSync17(packagePath, "utf-8"));
6060
6282
  if (pkg2.name === "deepspace") return dir;
6061
6283
  } catch {
6062
6284
  }
@@ -6067,13 +6289,13 @@ function featurePackageRoot(moduleUrl = import.meta.url) {
6067
6289
  }
6068
6290
  }
6069
6291
  function hasFeatureCatalog(packageRoot = featurePackageRoot()) {
6070
- return existsSync17(join16(packageRoot, "features"));
6292
+ return existsSync18(join17(packageRoot, "features"));
6071
6293
  }
6072
6294
  function readFeatureConfig(id, packageRoot = featurePackageRoot()) {
6073
- const configPath = join16(packageRoot, "features", id, "feature.json");
6074
- if (!existsSync17(configPath)) return null;
6295
+ const configPath = join17(packageRoot, "features", id, "feature.json");
6296
+ if (!existsSync18(configPath)) return null;
6075
6297
  try {
6076
- const config = JSON.parse(readFileSync16(configPath, "utf-8"));
6298
+ const config = JSON.parse(readFileSync17(configPath, "utf-8"));
6077
6299
  if (typeof config.id !== "string" || typeof config.name !== "string" || !Array.isArray(config.files)) {
6078
6300
  return null;
6079
6301
  }
@@ -6083,8 +6305,8 @@ function readFeatureConfig(id, packageRoot = featurePackageRoot()) {
6083
6305
  }
6084
6306
  }
6085
6307
  function listFeatureConfigs(packageRoot = featurePackageRoot()) {
6086
- const featuresDir = join16(packageRoot, "features");
6087
- if (!existsSync17(featuresDir)) return [];
6308
+ const featuresDir = join17(packageRoot, "features");
6309
+ if (!existsSync18(featuresDir)) return [];
6088
6310
  return readdirSync5(featuresDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => readFeatureConfig(entry.name, packageRoot)).filter((config) => config !== null && !config.internal).sort((a, b) => a.id.localeCompare(b.id));
6089
6311
  }
6090
6312
  function featureCatalog(packageRoot = featurePackageRoot()) {
@@ -6138,7 +6360,7 @@ function copyFile(src, dest) {
6138
6360
  function copyFileWithImportShift(src, dest, levels) {
6139
6361
  mkdirSync6(dirname6(dest), { recursive: true });
6140
6362
  const prefix = "../".repeat(levels);
6141
- const shifted = readFileSync16(src, "utf-8").replace(
6363
+ const shifted = readFileSync17(src, "utf-8").replace(
6142
6364
  /(from\s+|import\s+|import\()(['"])(\.\.\/)/g,
6143
6365
  `$1$2${prefix}$3`
6144
6366
  );
@@ -6151,12 +6373,12 @@ function installFiles(packageRoot, config, targetDir, emit) {
6151
6373
  const preexisting = [];
6152
6374
  const missing = [];
6153
6375
  for (const file of config.files) {
6154
- const srcPath = join16(packageRoot, "features", config.id, file.src);
6376
+ const srcPath = join17(packageRoot, "features", config.id, file.src);
6155
6377
  const resolvedDest = resolveFeatureDestPath(file.dest, config.route);
6156
- const destPath = join16(targetDir, resolvedDest);
6157
- const existed = existsSync17(destPath);
6378
+ const destPath = join17(targetDir, resolvedDest);
6379
+ const existed = existsSync18(destPath);
6158
6380
  if (existed) preexisting.push(resolvedDest);
6159
- if (!existsSync17(srcPath)) {
6381
+ if (!existsSync18(srcPath)) {
6160
6382
  emit(` Warning: source not found: ${file.src}`, "stderr");
6161
6383
  if (!existed) missing.push(resolvedDest);
6162
6384
  continue;
@@ -6193,11 +6415,11 @@ function unresolvedSchema(schema, reason, emit) {
6193
6415
  }
6194
6416
  function integrateSchema(config, targetDir, emit) {
6195
6417
  if (!config.schema) return [];
6196
- const schemasPath = join16(targetDir, "src", "schemas.ts");
6197
- if (!existsSync17(schemasPath)) {
6418
+ const schemasPath = join17(targetDir, "src", "schemas.ts");
6419
+ if (!existsSync18(schemasPath)) {
6198
6420
  return [unresolvedSchema(config.schema, "src/schemas.ts was not found.", emit)];
6199
6421
  }
6200
- let content = readFileSync16(schemasPath, "utf-8");
6422
+ let content = readFileSync17(schemasPath, "utf-8");
6201
6423
  const { exportName, importPath, spreadOperator } = config.schema;
6202
6424
  const importPattern = new RegExp(`^import\\s+\\{[^}]*\\b${exportName}\\b`, "m");
6203
6425
  if (importPattern.test(content)) {
@@ -6250,11 +6472,11 @@ function unresolvedActions(actions, reason, emit) {
6250
6472
  }
6251
6473
  function integrateActions(config, targetDir, emit) {
6252
6474
  if (!config.actions?.length) return [];
6253
- const actionsPath = join16(targetDir, "src", "actions", "index.ts");
6254
- if (!existsSync17(actionsPath)) {
6475
+ const actionsPath = join17(targetDir, "src", "actions", "index.ts");
6476
+ if (!existsSync18(actionsPath)) {
6255
6477
  return [unresolvedActions(config.actions, "src/actions/index.ts was not found.", emit)];
6256
6478
  }
6257
- let content = readFileSync16(actionsPath, "utf-8");
6479
+ let content = readFileSync17(actionsPath, "utf-8");
6258
6480
  const registryPattern = /export const actions:\s*Record<string,\s*ActionHandler(?:<[^>]*>)?>\s*=\s*\{/;
6259
6481
  if (!registryPattern.test(content)) {
6260
6482
  const reason = "src/actions/index.ts has an unexpected structure.";
@@ -6299,8 +6521,8 @@ ${registryEntry}`);
6299
6521
  }
6300
6522
  function integrateCss(packageRoot, config, targetDir, emit) {
6301
6523
  if (!config.css?.length) return [];
6302
- const stylesPath = join16(targetDir, "src", "styles.css");
6303
- if (!existsSync17(stylesPath)) {
6524
+ const stylesPath = join17(targetDir, "src", "styles.css");
6525
+ if (!existsSync18(stylesPath)) {
6304
6526
  emit(" Warning: Cannot integrate CSS \u2014 src/styles.css not found");
6305
6527
  return [
6306
6528
  {
@@ -6312,12 +6534,12 @@ function integrateCss(packageRoot, config, targetDir, emit) {
6312
6534
  }
6313
6535
  ];
6314
6536
  }
6315
- let styles = readFileSync16(stylesPath, "utf-8");
6537
+ let styles = readFileSync17(stylesPath, "utf-8");
6316
6538
  let integrated = 0;
6317
6539
  const unresolved = [];
6318
6540
  for (const cssFile of config.css) {
6319
- const srcPath = join16(packageRoot, "features", config.id, cssFile);
6320
- if (!existsSync17(srcPath)) {
6541
+ const srcPath = join17(packageRoot, "features", config.id, cssFile);
6542
+ if (!existsSync18(srcPath)) {
6321
6543
  emit(` Warning: CSS source not found: ${cssFile}`, "stderr");
6322
6544
  unresolved.push({
6323
6545
  area: "css",
@@ -6326,7 +6548,7 @@ function integrateCss(packageRoot, config, targetDir, emit) {
6326
6548
  });
6327
6549
  continue;
6328
6550
  }
6329
- const css = readFileSync16(srcPath, "utf-8");
6551
+ const css = readFileSync17(srcPath, "utf-8");
6330
6552
  const fingerprint = css.match(/\.([\w-]+)\s*\{|@keyframes\s+([\w-]+)/);
6331
6553
  const marker = fingerprint?.[1] ?? fingerprint?.[2];
6332
6554
  if (marker && styles.includes(marker)) {
@@ -6353,11 +6575,11 @@ function unresolvedRoute(route, config, reason, emit) {
6353
6575
  }
6354
6576
  function integrateRoute(config, targetDir, emit) {
6355
6577
  if (!config.route) return [];
6356
- const navPath = join16(targetDir, "src", "nav.ts");
6357
- if (!existsSync17(navPath)) {
6578
+ const navPath = join17(targetDir, "src", "nav.ts");
6579
+ if (!existsSync18(navPath)) {
6358
6580
  return [unresolvedRoute(config.route, config, "src/nav.ts was not found.", emit)];
6359
6581
  }
6360
- let content = readFileSync16(navPath, "utf-8");
6582
+ let content = readFileSync17(navPath, "utf-8");
6361
6583
  const routePath = config.route.path;
6362
6584
  if (content.includes(`'${routePath}'`)) {
6363
6585
  emit(` Nav already present: ${routePath}`);
@@ -6385,7 +6607,7 @@ ${entry}`);
6385
6607
  }
6386
6608
  var INSTALL_DIAGNOSTIC_BYTES = 16 * 1024;
6387
6609
  function readDiagnosticTail(outputFd) {
6388
- const size = fstatSync(outputFd).size;
6610
+ const size = fstatSync2(outputFd).size;
6389
6611
  if (size === 0) return void 0;
6390
6612
  const length = Math.min(size, INSTALL_DIAGNOSTIC_BYTES);
6391
6613
  const buffer = Buffer.alloc(length);
@@ -6404,10 +6626,10 @@ function runPackageInstall(packageManager, targetDir, outputVisible) {
6404
6626
  const child = spawnSync6(packageManager, ["install"], { ...options, stdio: "inherit" });
6405
6627
  return { error: child.error, status: child.status };
6406
6628
  }
6407
- const captureDir = mkdtempSync(join16(tmpdir2(), "deepspace-add-install-"));
6629
+ const captureDir = mkdtempSync(join17(tmpdir2(), "deepspace-add-install-"));
6408
6630
  let outputFd;
6409
6631
  try {
6410
- outputFd = openSync(join16(captureDir, "output.log"), "w+");
6632
+ outputFd = openSync2(join17(captureDir, "output.log"), "w+");
6411
6633
  const child = spawnSync6(packageManager, ["install"], {
6412
6634
  ...options,
6413
6635
  stdio: ["ignore", outputFd, outputFd]
@@ -6418,7 +6640,7 @@ function runPackageInstall(packageManager, targetDir, outputVisible) {
6418
6640
  diagnosticTail: readDiagnosticTail(outputFd)
6419
6641
  };
6420
6642
  } finally {
6421
- if (outputFd !== void 0) closeSync(outputFd);
6643
+ if (outputFd !== void 0) closeSync2(outputFd);
6422
6644
  rmSync4(captureDir, { recursive: true, force: true });
6423
6645
  }
6424
6646
  }
@@ -6431,14 +6653,14 @@ function integrateDependencies(config, targetDir, runInstall, outputVisible, emi
6431
6653
  if (declarations.every(({ values }) => !values || Object.keys(values).length === 0)) {
6432
6654
  return { added: [], installRan: false, packageManager };
6433
6655
  }
6434
- const packagePath = join16(targetDir, "package.json");
6435
- if (!existsSync17(packagePath)) {
6656
+ const packagePath = join17(targetDir, "package.json");
6657
+ if (!existsSync18(packagePath)) {
6436
6658
  emit(" Could not patch package.json (file not found)");
6437
6659
  return { added: [], installRan: false, packageManager };
6438
6660
  }
6439
6661
  let pkg2;
6440
6662
  try {
6441
- pkg2 = JSON.parse(readFileSync16(packagePath, "utf-8"));
6663
+ pkg2 = JSON.parse(readFileSync17(packagePath, "utf-8"));
6442
6664
  } catch (error) {
6443
6665
  emit(
6444
6666
  ` Could not patch package.json (parse error: ${error instanceof Error ? error.message : String(error)})`
@@ -6498,7 +6720,7 @@ ${child.diagnosticTail}` : "";
6498
6720
  }
6499
6721
  function dependencySnapshot(targetDir) {
6500
6722
  try {
6501
- const pkg2 = JSON.parse(readFileSync16(join16(targetDir, "package.json"), "utf-8"));
6723
+ const pkg2 = JSON.parse(readFileSync17(join17(targetDir, "package.json"), "utf-8"));
6502
6724
  return {
6503
6725
  dependencies: pkg2.dependencies ?? {},
6504
6726
  devDependencies: pkg2.devDependencies ?? {}
@@ -6510,7 +6732,7 @@ function dependencySnapshot(targetDir) {
6510
6732
  function installedNavFeatures(packageRoot, targetDir) {
6511
6733
  return listFeatureConfigs(packageRoot).filter((feature) => feature.category === "nav" && feature.files.length > 0).filter((feature) => {
6512
6734
  const signature = resolveFeatureDestPath(feature.files[0].dest, feature.route);
6513
- return existsSync17(join16(targetDir, signature));
6735
+ return existsSync18(join17(targetDir, signature));
6514
6736
  }).map((feature) => feature.id);
6515
6737
  }
6516
6738
  function warnNavConflicts(packageRoot, config, targetDir, emit) {
@@ -6739,11 +6961,11 @@ function renderFeatureInfo(config) {
6739
6961
  return lines.join("\n");
6740
6962
  }
6741
6963
  function isDeepSpaceApp(dir) {
6742
- if (existsSync18(join17(dir, "wrangler.toml"))) return true;
6743
- const packagePath = join17(dir, "package.json");
6744
- if (!existsSync18(packagePath)) return false;
6964
+ if (existsSync19(join18(dir, "wrangler.toml"))) return true;
6965
+ const packagePath = join18(dir, "package.json");
6966
+ if (!existsSync19(packagePath)) return false;
6745
6967
  try {
6746
- const pkg2 = JSON.parse(readFileSync17(packagePath, "utf-8"));
6968
+ const pkg2 = JSON.parse(readFileSync18(packagePath, "utf-8"));
6747
6969
  return Boolean(pkg2.dependencies?.deepspace || pkg2.devDependencies?.deepspace);
6748
6970
  } catch {
6749
6971
  return false;
@@ -6752,8 +6974,8 @@ function isDeepSpaceApp(dir) {
6752
6974
  function installedPackageRoot(fromDir) {
6753
6975
  let dir = fromDir;
6754
6976
  for (; ; ) {
6755
- const candidate = join17(dir, "node_modules", "deepspace");
6756
- if (existsSync18(join17(candidate, "package.json"))) return candidate;
6977
+ const candidate = join18(dir, "node_modules", "deepspace");
6978
+ if (existsSync19(join18(candidate, "package.json"))) return candidate;
6757
6979
  const parent = dirname7(dir);
6758
6980
  if (parent === dir) return null;
6759
6981
  dir = parent;
@@ -6770,9 +6992,9 @@ async function targetInstaller(appDir) {
6770
6992
  if (realpathSync3(installedRoot) === realpathSync3(featurePackageRoot())) {
6771
6993
  return { installFeature };
6772
6994
  }
6773
- const entry = join17(installedRoot, "dist", "feature-installer.js");
6774
- const catalog = join17(installedRoot, "features");
6775
- if (!existsSync18(entry) || !existsSync18(catalog)) {
6995
+ const entry = join18(installedRoot, "dist", "feature-installer.js");
6996
+ const catalog = join18(installedRoot, "features");
6997
+ if (!existsSync19(entry) || !existsSync19(catalog)) {
6776
6998
  throw new Refusal(
6777
6999
  `The app's installed deepspace package does not contain the typed feature installer.
6778
7000
  Expected: ${entry}
@@ -6865,7 +7087,7 @@ var add_default = defineDeepspaceCommand({
6865
7087
  return { data: { features: featureCatalog(root) } };
6866
7088
  }
6867
7089
  const appDir = resolve14(args.dir ?? ".");
6868
- if (!existsSync18(appDir)) {
7090
+ if (!existsSync19(appDir)) {
6869
7091
  throw new Refusal(
6870
7092
  `Target directory not found: ${appDir}
6871
7093
  Create a new app first: deepspace app create <name>`,
@@ -7526,7 +7748,7 @@ var transfer_default = defineCommand7({
7526
7748
  import { defineCommand as defineCommand8 } from "citty";
7527
7749
 
7528
7750
  // src/cli/commands/_invoke-impl.ts
7529
- import { readFileSync as readFileSync18 } from "fs";
7751
+ import { readFileSync as readFileSync19 } from "fs";
7530
7752
  import * as p15 from "@clack/prompts";
7531
7753
  var API_URL7 = process.env.DEEPSPACE_API_URL ?? PLATFORM_URLS.api;
7532
7754
  var DEFAULT_TIMEOUT_MS = 12e4;
@@ -7567,7 +7789,7 @@ function resolveBody(opts) {
7567
7789
  if (opts.body != null) {
7568
7790
  raw = opts.body;
7569
7791
  } else if (opts.bodyFile != null) {
7570
- raw = opts.bodyFile === "-" ? readFileSync18(0, "utf-8") : readFileSync18(opts.bodyFile, "utf-8");
7792
+ raw = opts.bodyFile === "-" ? readFileSync19(0, "utf-8") : readFileSync19(opts.bodyFile, "utf-8");
7571
7793
  } else {
7572
7794
  return "{}";
7573
7795
  }
@@ -7846,8 +8068,8 @@ var integrations_default = defineCommand8({
7846
8068
 
7847
8069
  // src/cli/commands/library.ts
7848
8070
  import { defineCommand as defineCommand9 } from "citty";
7849
- import { existsSync as existsSync19, readFileSync as readFileSync19 } from "fs";
7850
- import { join as join18, resolve as resolve15 } from "path";
8071
+ import { existsSync as existsSync20, readFileSync as readFileSync20 } from "fs";
8072
+ import { join as join19, resolve as resolve15 } from "path";
7851
8073
  import { parse as parseToml4 } from "smol-toml";
7852
8074
  import * as p16 from "@clack/prompts";
7853
8075
  var DEFAULT_LIBRARY_APP = "deepdotspace-site";
@@ -7918,14 +8140,14 @@ var publish = defineDeepspaceCommand({
7918
8140
  let appName = str(args.app)?.trim();
7919
8141
  if (!appName) {
7920
8142
  const appDir = resolve15(str(args.dir) ?? ".");
7921
- const wranglerPath = join18(appDir, "wrangler.toml");
7922
- if (!existsSync19(wranglerPath)) {
8143
+ const wranglerPath = join19(appDir, "wrangler.toml");
8144
+ if (!existsSync20(wranglerPath)) {
7923
8145
  throw new Refusal(
7924
8146
  "No --app provided and no wrangler.toml found. Either pass --app <name> or run from your app's directory.",
7925
8147
  "not_in_app_repo"
7926
8148
  );
7927
8149
  }
7928
- const cfg = parseToml4(readFileSync19(wranglerPath, "utf-8"));
8150
+ const cfg = parseToml4(readFileSync20(wranglerPath, "utf-8"));
7929
8151
  const nameRes = resolveAppName(cfg.name);
7930
8152
  if (!nameRes.ok) throw new Refusal(`wrangler.toml: ${nameRes.reason}`, "invalid_config");
7931
8153
  appName = nameRes.name;
@@ -8063,8 +8285,8 @@ var library_default = defineCommand9({
8063
8285
 
8064
8286
  // src/cli/commands/feedback.ts
8065
8287
  import * as p17 from "@clack/prompts";
8066
- import { existsSync as existsSync20, readFileSync as readFileSync20 } from "fs";
8067
- import { dirname as dirname8, join as join19 } from "path";
8288
+ import { existsSync as existsSync21, readFileSync as readFileSync21 } from "fs";
8289
+ import { dirname as dirname8, join as join20 } from "path";
8068
8290
  import { fileURLToPath as fileURLToPath2 } from "url";
8069
8291
  import { release } from "os";
8070
8292
  var API_URL8 = process.env.DEEPSPACE_API_URL ?? PLATFORM_URLS.api;
@@ -8087,12 +8309,12 @@ function readCliVersion() {
8087
8309
  try {
8088
8310
  const here = dirname8(fileURLToPath2(import.meta.url));
8089
8311
  const candidates = [
8090
- join19(here, "..", "package.json"),
8091
- join19(here, "..", "..", "..", "package.json")
8312
+ join20(here, "..", "package.json"),
8313
+ join20(here, "..", "..", "..", "package.json")
8092
8314
  ];
8093
8315
  for (const path of candidates) {
8094
- if (!existsSync20(path)) continue;
8095
- const pkg2 = JSON.parse(readFileSync20(path, "utf-8"));
8316
+ if (!existsSync21(path)) continue;
8317
+ const pkg2 = JSON.parse(readFileSync21(path, "utf-8"));
8096
8318
  if (pkg2.name === "deepspace" && pkg2.version) return pkg2.version;
8097
8319
  }
8098
8320
  } catch {
@@ -8103,7 +8325,7 @@ function detectAppName2() {
8103
8325
  try {
8104
8326
  const cwd = process.cwd();
8105
8327
  if (!hasWranglerConfig(cwd)) return void 0;
8106
- const config = readWranglerConfig2(cwd);
8328
+ const config = readWranglerConfig(cwd);
8107
8329
  return typeof config.name === "string" ? config.name : void 0;
8108
8330
  } catch {
8109
8331
  return void 0;
@@ -8288,7 +8510,7 @@ Context: deepspace ${payload.cliVersion} \xB7 ${payload.nodeVersion} \xB7 ${payl
8288
8510
 
8289
8511
  // src/cli/commands/secrets.ts
8290
8512
  import { defineCommand as defineCommand10 } from "citty";
8291
- import { readFileSync as readFileSync21 } from "fs";
8513
+ import { readFileSync as readFileSync22 } from "fs";
8292
8514
 
8293
8515
  // src/cli/lib/citty-args.ts
8294
8516
  function dedupePositionals(bound, rest) {
@@ -8558,7 +8780,7 @@ var upload = defineCommand10({
8558
8780
  async run({ args }) {
8559
8781
  try {
8560
8782
  const t = await resolveTarget(args);
8561
- const content = args.file === "-" ? readFileSync21(0, "utf-8") : readFileSync21(args.file, "utf-8");
8783
+ const content = args.file === "-" ? readFileSync22(0, "utf-8") : readFileSync22(args.file, "utf-8");
8562
8784
  const secrets = parseSecretsUpload(content);
8563
8785
  if (Object.keys(secrets).length === 0)
8564
8786
  fail(new InputError("No secrets found in the input.", "empty_input"));
@@ -8814,7 +9036,7 @@ var init_default = defineDeepspaceCommand({
8814
9036
  // src/cli/commands/status.ts
8815
9037
  import { defineCommand as defineCommand11 } from "citty";
8816
9038
  import * as p18 from "@clack/prompts";
8817
- import { existsSync as existsSync21, readFileSync as readFileSync22 } from "fs";
9039
+ import { existsSync as existsSync22, readFileSync as readFileSync23 } from "fs";
8818
9040
  function timeAgo(iso) {
8819
9041
  const ms = Date.now() - Date.parse(iso);
8820
9042
  if (!Number.isFinite(ms) || ms < 0) return iso;
@@ -8883,7 +9105,7 @@ var status_default = defineCommand11({
8883
9105
  json.services = urls;
8884
9106
  if (urlOverrides.length > 0) json.urlOverrides = urlOverrides;
8885
9107
  }
8886
- facts.loggedIn = existsSync21(SESSION_PATH);
9108
+ facts.loggedIn = existsSync22(SESSION_PATH);
8887
9109
  let userId = null;
8888
9110
  if (!facts.loggedIn) {
8889
9111
  lines.push(["Session", "not logged in"]);
@@ -8892,7 +9114,7 @@ var status_default = defineCommand11({
8892
9114
  let who = "logged in";
8893
9115
  try {
8894
9116
  const payload = decodeJwtPayload(
8895
- readFileSync22(TOKEN_PATH, "utf-8").trim()
9117
+ readFileSync23(TOKEN_PATH, "utf-8").trim()
8896
9118
  );
8897
9119
  if (payload.email) who = payload.email;
8898
9120
  userId = payload.sub ?? null;
@@ -9555,18 +9777,18 @@ var pull_default = defineDeepspaceCommand({
9555
9777
 
9556
9778
  // src/cli/commands/clone.ts
9557
9779
  import * as p21 from "@clack/prompts";
9558
- import { existsSync as existsSync22, readdirSync as readdirSync6, rmSync as rmSync5 } from "fs";
9559
- import { resolve as resolve16, basename as basename3, join as join20 } from "path";
9780
+ import { existsSync as existsSync23, readdirSync as readdirSync6, rmSync as rmSync5 } from "fs";
9781
+ import { resolve as resolve16, basename as basename3, join as join21 } from "path";
9560
9782
  function cleanFailedCloneDir(dir) {
9561
9783
  let entries;
9562
9784
  try {
9563
9785
  entries = readdirSync6(dir);
9564
9786
  } catch {
9565
- return existsSync22(dir) ? ["<unreadable>"] : [];
9787
+ return existsSync23(dir) ? ["<unreadable>"] : [];
9566
9788
  }
9567
9789
  if (entries.length === 1 && entries[0] === ".git") {
9568
9790
  try {
9569
- rmSync5(join20(dir, ".git"), { recursive: true, force: true });
9791
+ rmSync5(join21(dir, ".git"), { recursive: true, force: true });
9570
9792
  return [];
9571
9793
  } catch {
9572
9794
  return entries;
@@ -9575,7 +9797,7 @@ function cleanFailedCloneDir(dir) {
9575
9797
  return entries;
9576
9798
  }
9577
9799
  function validateCloneTarget(dir) {
9578
- if (!existsSync22(dir)) return false;
9800
+ if (!existsSync23(dir)) return false;
9579
9801
  let entries;
9580
9802
  try {
9581
9803
  entries = readdirSync6(dir);
@@ -9989,8 +10211,8 @@ function printOverlaps(overlaps, json) {
9989
10211
 
9990
10212
  // src/cli/commands/workspace/local.ts
9991
10213
  import * as p25 from "@clack/prompts";
9992
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync23, realpathSync as realpathSync5, renameSync as renameSync2, statSync, writeFileSync as writeFileSync9 } from "fs";
9993
- import { dirname as dirname9, isAbsolute as isAbsolute5, join as join21, relative as relative2, sep as sep2 } from "path";
10214
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync24, realpathSync as realpathSync5, renameSync as renameSync2, statSync, writeFileSync as writeFileSync9 } from "fs";
10215
+ import { dirname as dirname9, isAbsolute as isAbsolute5, join as join22, relative as relative2, sep as sep2 } from "path";
9994
10216
  var WORKSPACE_MARKER = "deepspace-workspace.json";
9995
10217
  function excludeWorktreeDir(appDir, dir) {
9996
10218
  try {
@@ -10011,7 +10233,7 @@ function worktreeMarkerPath(worktreeRoot) {
10011
10233
  const result = runGit(worktreeRoot, ["rev-parse", "--absolute-git-dir"], { allowFail: true });
10012
10234
  if (result.status !== 0) return null;
10013
10235
  const gitDir = result.stdout.toString("utf-8").trim();
10014
- return isAbsolute5(gitDir) ? join21(gitDir, WORKSPACE_MARKER) : null;
10236
+ return isAbsolute5(gitDir) ? join22(gitDir, WORKSPACE_MARKER) : null;
10015
10237
  } catch {
10016
10238
  return null;
10017
10239
  }
@@ -10047,7 +10269,7 @@ function isManagedWorkspaceWorktree(worktreeRoot, workspaceId) {
10047
10269
  const markerPath = worktreeMarkerPath(worktreeRoot);
10048
10270
  if (!markerPath) return false;
10049
10271
  try {
10050
- const marker = JSON.parse(readFileSync23(markerPath, "utf-8"));
10272
+ const marker = JSON.parse(readFileSync24(markerPath, "utf-8"));
10051
10273
  return marker.version === 1 && marker.workspaceId === workspaceId && marker.mode === "managed-linked";
10052
10274
  } catch {
10053
10275
  return false;
@@ -10056,11 +10278,11 @@ function isManagedWorkspaceWorktree(worktreeRoot, workspaceId) {
10056
10278
  function defaultWorkspaceRoot(appDir, workspaceId) {
10057
10279
  const primary = listWorktrees(appDir)[0];
10058
10280
  if (!primary) throw new Error("Git did not report a primary worktree");
10059
- return join21(primary.path, ".deepspace", "ws", workspaceId.slice(3).toLowerCase());
10281
+ return join22(primary.path, ".deepspace", "ws", workspaceId.slice(3).toLowerCase());
10060
10282
  }
10061
10283
  function appDirInWorktree(appDir, worktreeRoot) {
10062
10284
  const appRelative = relative2(realpathSync5(repoToplevel(appDir)), realpathSync5(appDir));
10063
- return appRelative ? join21(worktreeRoot, appRelative) : worktreeRoot;
10285
+ return appRelative ? join22(worktreeRoot, appRelative) : worktreeRoot;
10064
10286
  }
10065
10287
  function materializeWorkspaceWorktree(appDir, worktreeRoot, branch, startPoint, workspaceId, options = {}) {
10066
10288
  const createdOid = resolveCommit(appDir, startPoint);
@@ -10078,11 +10300,41 @@ function materializeWorkspaceWorktree(appDir, worktreeRoot, branch, startPoint,
10078
10300
  function inOwnLinkedWorktree(appDir, id) {
10079
10301
  if (workspaceIdFromBranch(currentBranch(appDir)) !== id) return false;
10080
10302
  try {
10081
- return statSync(join21(repoToplevel(appDir), ".git")).isFile();
10303
+ return statSync(join22(repoToplevel(appDir), ".git")).isFile();
10082
10304
  } catch {
10083
10305
  return false;
10084
10306
  }
10085
10307
  }
10308
+ function inspectWorkspaceCleanup(appDir, id) {
10309
+ const branch = workspaceBranchName(id);
10310
+ const branchOid = resolveCommit(appDir, `refs/heads/${branch}`);
10311
+ const worktrees = listWorktrees(appDir);
10312
+ const checkoutIndex = worktrees.findIndex((worktree) => worktree.branch === branch);
10313
+ if (checkoutIndex < 0) {
10314
+ return {
10315
+ branch,
10316
+ branchOid,
10317
+ checkout: { kind: "none", dir: null },
10318
+ willDeleteBranch: branchOid !== null
10319
+ };
10320
+ }
10321
+ const dir = worktrees[checkoutIndex].path;
10322
+ if (checkoutIndex === 0) {
10323
+ return {
10324
+ branch,
10325
+ branchOid,
10326
+ checkout: { kind: "primary", dir },
10327
+ willDeleteBranch: branchOid !== null
10328
+ };
10329
+ }
10330
+ const managed = isManagedWorkspaceWorktree(dir, id);
10331
+ return {
10332
+ branch,
10333
+ branchOid,
10334
+ checkout: { kind: managed ? "managed-linked" : "external-linked", dir },
10335
+ willDeleteBranch: branchOid !== null && managed
10336
+ };
10337
+ }
10086
10338
  function cleanupJson(outcome) {
10087
10339
  return {
10088
10340
  worktreeRemoved: outcome.worktreeRemoved,
@@ -10092,11 +10344,27 @@ function cleanupJson(outcome) {
10092
10344
  ...outcome.error ? { error: outcome.error } : {}
10093
10345
  };
10094
10346
  }
10095
- function deleteWsBranch(cwd, branch, out) {
10096
- const del3 = runGit(cwd, ["branch", "-D", branch], { allowFail: true });
10097
- if (del3.status === 0) out.branchDeleted = branch;
10098
- else
10099
- out.error ??= `git branch -D ${branch} failed: ${del3.stderr.toString("utf-8").trim() || "unknown git error"}`;
10347
+ function deleteWsBranch(cwd, branch, expectedOid, out) {
10348
+ const ref = `refs/heads/${branch}`;
10349
+ const currentOid = resolveCommit(cwd, ref);
10350
+ if (!currentOid) return;
10351
+ const approvedOid = expectedOid ?? currentOid;
10352
+ if (currentOid !== approvedOid) {
10353
+ out.error ??= `branch ${branch} advanced from ${approvedOid.slice(0, 10)} to ${currentOid.slice(0, 10)} during cleanup; retained the branch`;
10354
+ return;
10355
+ }
10356
+ const del3 = runGit(cwd, ["update-ref", "-d", ref, approvedOid], { allowFail: true });
10357
+ if (del3.status === 0) {
10358
+ out.branchDeleted = branch;
10359
+ runGit(cwd, ["config", "--remove-section", `branch.${branch}`], { allowFail: true });
10360
+ return;
10361
+ }
10362
+ const afterOid = resolveCommit(cwd, ref);
10363
+ if (afterOid && afterOid !== approvedOid) {
10364
+ out.error ??= `branch ${branch} advanced from ${approvedOid.slice(0, 10)} to ${afterOid.slice(0, 10)} during cleanup; retained the branch`;
10365
+ } else if (afterOid) {
10366
+ out.error ??= `could not delete branch ${branch}: ${del3.stderr.toString("utf-8").trim() || "unknown git error"}`;
10367
+ }
10100
10368
  }
10101
10369
  function switchOffWsBranch(mainDir, trunkBranch) {
10102
10370
  if (trunkBranch) {
@@ -10110,8 +10378,10 @@ function switchOffWsBranch(mainDir, trunkBranch) {
10110
10378
  }
10111
10379
  return runGit(mainDir, ["switch", "--quiet", "--detach"], { allowFail: true }).status === 0;
10112
10380
  }
10113
- function cleanupWorkspaceLocal(appDir, id, trunkBranch) {
10114
- const branch = workspaceBranchName(id);
10381
+ function cleanupWorkspaceLocal(appDir, id, trunkBranch, options = {}) {
10382
+ const inspection = inspectWorkspaceCleanup(appDir, id);
10383
+ const { branch } = inspection;
10384
+ const expectedBranchOid = options.expectedBranchOid ?? inspection.branchOid;
10115
10385
  const out = {
10116
10386
  worktreeRemoved: null,
10117
10387
  worktreeRetained: null,
@@ -10119,6 +10389,10 @@ function cleanupWorkspaceLocal(appDir, id, trunkBranch) {
10119
10389
  worktreeDir: null,
10120
10390
  branch
10121
10391
  };
10392
+ if (options.retainLocal) {
10393
+ out.worktreeRetained = inspection.checkout.dir;
10394
+ return out;
10395
+ }
10122
10396
  try {
10123
10397
  const worktrees = listWorktrees(appDir);
10124
10398
  const mainDir = worktrees[0]?.path ?? repoToplevel(appDir);
@@ -10145,14 +10419,15 @@ function cleanupWorkspaceLocal(appDir, id, trunkBranch) {
10145
10419
  return out;
10146
10420
  }
10147
10421
  out.worktreeRemoved = linked.path;
10148
- deleteWsBranch(mainDir, branch, out);
10422
+ out.worktreeDir = null;
10423
+ deleteWsBranch(mainDir, branch, expectedBranchOid, out);
10149
10424
  return out;
10150
10425
  }
10151
10426
  if (currentBranch(mainDir) === branch && !switchOffWsBranch(mainDir, trunkBranch)) {
10152
10427
  out.error = `could not switch off ${branch} to delete it`;
10153
10428
  return out;
10154
10429
  }
10155
- if (resolveCommit(mainDir, `refs/heads/${branch}`)) deleteWsBranch(mainDir, branch, out);
10430
+ deleteWsBranch(mainDir, branch, expectedBranchOid, out);
10156
10431
  return out;
10157
10432
  } catch (err) {
10158
10433
  out.error = err instanceof Error ? err.message : String(err);
@@ -10409,7 +10684,7 @@ var newWorkspaceCommand = defineDeepspaceCommand({
10409
10684
 
10410
10685
  // src/cli/commands/workspace/attach.ts
10411
10686
  import * as p27 from "@clack/prompts";
10412
- import { existsSync as existsSync23, readdirSync as readdirSync7, rmSync as rmSync6 } from "fs";
10687
+ import { existsSync as existsSync24, readdirSync as readdirSync7, rmSync as rmSync6 } from "fs";
10413
10688
  import { isAbsolute as isAbsolute7, resolve as resolve18 } from "path";
10414
10689
  function finishedWorkspaceMessage(id, view) {
10415
10690
  const workspace = view.workspace;
@@ -10419,7 +10694,7 @@ function finishedWorkspaceMessage(id, view) {
10419
10694
  return `Workspace ${id} is ${workspace.status} \u2014 its ref is retained briefly (fetch ${workspace.ref} to inspect), but attach only works on active workspaces.`;
10420
10695
  }
10421
10696
  function cleanFailedFreshAttachDir(dir) {
10422
- if (!existsSync23(dir)) return [];
10697
+ if (!existsSync24(dir)) return [];
10423
10698
  let entries;
10424
10699
  try {
10425
10700
  entries = readdirSync7(dir);
@@ -10537,7 +10812,7 @@ var attachWorkspaceCommand = defineDeepspaceCommand({
10537
10812
  process.cwd(),
10538
10813
  (typeof args.dir === "string" ? args.dir.trim() : "") || `ws-${id.slice(3).toLowerCase()}`
10539
10814
  );
10540
- if (existsSync23(dir)) {
10815
+ if (existsSync24(dir)) {
10541
10816
  spinner2?.stop("Target exists.");
10542
10817
  throw new Refusal(`${dir} already exists \u2014 pass a different directory.`, "dir_exists");
10543
10818
  }
@@ -10854,13 +11129,13 @@ import * as p31 from "@clack/prompts";
10854
11129
 
10855
11130
  // src/cli/lib/validation.ts
10856
11131
  import { sync as spawnSync7 } from "cross-spawn";
10857
- import { readFileSync as readFileSync24 } from "fs";
10858
- import { join as join22 } from "path";
11132
+ import { readFileSync as readFileSync25 } from "fs";
11133
+ import { join as join23 } from "path";
10859
11134
  var MAX_SUMMARY_CHARS = 4e3;
10860
11135
  var MAX_OUTPUT_BUFFER = 64 * 1024 * 1024;
10861
11136
  function configuredCommand(appDir) {
10862
11137
  try {
10863
- const pkg2 = JSON.parse(readFileSync24(join22(appDir, "package.json"), "utf-8"));
11138
+ const pkg2 = JSON.parse(readFileSync25(join23(appDir, "package.json"), "utf-8"));
10864
11139
  if (pkg2.scripts?.validate) return `${detectPackageManager(appDir)} run --silent validate`;
10865
11140
  } catch {
10866
11141
  }
@@ -11122,7 +11397,7 @@ var landWorkspaceCommand = defineDeepspaceCommand({
11122
11397
  }
11123
11398
  await api4.landWorkspace(id, { landedOid, intoRef });
11124
11399
  spinner2?.stop(`Landed ${id} into ${intoBranch} at ${landedOid.slice(0, 10)}.`);
11125
- const cleanup = keepWorktree ? null : cleanupWorkspaceLocal(appDir, id, intoBranch);
11400
+ const cleanup = keepWorktree ? null : cleanupWorkspaceLocal(appDir, id, intoBranch, { expectedBranchOid: landedOid });
11126
11401
  const inOwnWorktree = !cleanup && inOwnLinkedWorktree(appDir, id);
11127
11402
  const retainedWorktree = cleanup?.worktreeRetained ?? (keepWorktree || inOwnWorktree ? appDir : null);
11128
11403
  const data = {
@@ -11159,6 +11434,24 @@ var landWorkspaceCommand = defineDeepspaceCommand({
11159
11434
 
11160
11435
  // src/cli/commands/workspace/drop.ts
11161
11436
  import * as p32 from "@clack/prompts";
11437
+ function isWorkspaceTipPublished(appDir, localTip, publishedOids) {
11438
+ return publishedOids.some(
11439
+ (publishedOid) => publishedOid === localTip || isAncestor(appDir, localTip, publishedOid)
11440
+ );
11441
+ }
11442
+ function workspaceUnsyncedRefusal(args) {
11443
+ const syncArgv = ["deepspace", "workspace", "sync", "--app", args.appId, "--workspace", args.id];
11444
+ const dropArgv = ["deepspace", "workspace", "drop", args.id, "--app", args.appId];
11445
+ const publish2 = args.workspaceDir ? `Publish them first with \`${humanCommand(syncArgv)}\`, then re-run \`${humanCommand(dropArgv)}\`.` : `Recreate a worktree from the branch (\`git worktree add <dir> ${args.branch}\`), publish from there with \`${humanCommand(syncArgv)}\`, then re-run \`${humanCommand(dropArgv)}\`.`;
11446
+ return new Refusal(
11447
+ `${args.id} has committed work that isn't published yet. ${publish2} Alternatively, pass \`--keep-worktree\` to drop the cloud workspace while retaining the local branch/worktree for manual disposal. Nothing was dropped.`,
11448
+ "workspace_unsynced",
11449
+ {
11450
+ ...args.workspaceDir ? { action: { cwd: args.workspaceDir, argv: syncArgv } } : {},
11451
+ extra: { workspaceId: args.id }
11452
+ }
11453
+ );
11454
+ }
11162
11455
  var dropWorkspaceCommand = defineDeepspaceCommand({
11163
11456
  meta: { name: "drop", description: "Abandon a workspace (ref retained briefly, then GC)" },
11164
11457
  args: {
@@ -11192,24 +11485,44 @@ var dropWorkspaceCommand = defineDeepspaceCommand({
11192
11485
  }
11193
11486
  return { data: { workspaceId: explicitId, status: view2.workspace.status } };
11194
11487
  }
11195
- const { appDir, api: api4 } = await resolveTarget2(appArg);
11488
+ const { appDir, appId, api: api4 } = await resolveTarget2(appArg);
11196
11489
  const id = inferWorkspaceId(appDir, explicitId);
11197
- const ownLinkedWorktree = inOwnLinkedWorktree(appDir, id);
11198
- const cleanupOwnsCheckout = !ownLinkedWorktree || isManagedWorkspaceWorktree(appDir, id);
11199
- if (!keepWorktree && cleanupOwnsCheckout && !isWorkTreeClean(appDir)) {
11200
- const dropArgv = ["deepspace", "workspace", "drop", ...explicitId ? [explicitId] : []];
11490
+ const inspection = inspectWorkspaceCleanup(appDir, id);
11491
+ let approvedBranchOid;
11492
+ if (!keepWorktree && inspection.willDeleteBranch && inspection.branchOid) {
11493
+ const { view: before } = await api4.getWorkspace(id);
11494
+ const publishedOids = [
11495
+ before.tipOid,
11496
+ before.workspace.landedOid,
11497
+ before.workspace.baseOid
11498
+ ].filter((oid) => oid !== null);
11499
+ if (!isWorkspaceTipPublished(appDir, inspection.branchOid, publishedOids)) {
11500
+ throw workspaceUnsyncedRefusal({
11501
+ appId,
11502
+ id,
11503
+ branch: inspection.branch,
11504
+ workspaceDir: inspection.checkout.dir
11505
+ });
11506
+ }
11507
+ approvedBranchOid = inspection.branchOid;
11508
+ }
11509
+ const cleanupDir = inspection.willDeleteBranch ? inspection.checkout.dir : null;
11510
+ if (!keepWorktree && cleanupDir && !isWorkTreeClean(cleanupDir)) {
11511
+ const dropArgv = ["deepspace", "workspace", "drop", id, "--app", appId];
11201
11512
  const dropNext = humanCommand(dropArgv);
11202
11513
  throw new Refusal(
11203
- `The worktree has uncommitted changes and cleanup would remove it. Commit them to the workspace branch and re-run \`${dropNext}\`, or pass \`--keep-worktree\` to drop while keeping the worktree. Nothing was dropped.`,
11514
+ `The workspace checkout has uncommitted changes and cleanup would remove it or switch it off ${inspection.branch}. Commit them and re-run \`${dropNext}\`, or pass \`--keep-worktree\` to drop while retaining the checkout. Nothing was dropped.`,
11204
11515
  "dirty_worktree",
11205
11516
  { extra: { workspaceId: id } }
11206
11517
  );
11207
11518
  }
11208
11519
  const { view } = await api4.dropWorkspace(id);
11209
11520
  spinner2?.message(`Cleaning up ${id} locally\u2026`);
11210
- const cleanup = keepWorktree ? null : cleanupWorkspaceLocal(appDir, id, null);
11211
- const inOwnWorktree = !cleanup && ownLinkedWorktree;
11212
- const retainedWorktree = cleanup?.worktreeRetained ?? (keepWorktree || inOwnWorktree ? appDir : null);
11521
+ const cleanup = keepWorktree ? null : cleanupWorkspaceLocal(appDir, id, null, {
11522
+ expectedBranchOid: approvedBranchOid,
11523
+ retainLocal: !inspection.willDeleteBranch
11524
+ });
11525
+ const retainedWorktree = cleanup?.worktreeRetained ?? (keepWorktree ? inspection.checkout.dir : null);
11213
11526
  const data = {
11214
11527
  workspaceId: id,
11215
11528
  status: view.workspace.status,
@@ -11783,178 +12096,7 @@ function sameRefs(left, right) {
11783
12096
  return left.every((ref) => expected.get(ref.name) === ref.oid);
11784
12097
  }
11785
12098
 
11786
- // src/cli/lib/app-migrations.ts
11787
- import { lstatSync, readFileSync as readFileSync25, writeFileSync as writeFileSync10 } from "fs";
11788
- import { join as join23 } from "path";
11789
- var CANONICAL_IDENTITY_WIRE_ID = "2026-08-canonical-app-identity-wire";
11790
- var SOURCE_MIGRATIONS = [
11791
- {
11792
- id: CANONICAL_IDENTITY_WIRE_ID,
11793
- description: "Use the immutable app id for authenticated platform requests",
11794
- transform: migrateCanonicalIdentityWire,
11795
- findBlockers: findLegacyIdentityWireBlockers
11796
- }
11797
- ];
11798
- var APP_MIGRATION_DEFINITIONS = SOURCE_MIGRATIONS.map(
11799
- ({ id, description }) => ({ id, description })
11800
- );
11801
- function planAppMigrations(appDir) {
11802
- const repo = repoToplevel(appDir);
11803
- const appPrefix = gitLine(appDir, ["rev-parse", "--show-prefix"]);
11804
- const tracked = splitNulFields(runGit(repo, ["ls-files", "-z"]).stdout);
11805
- const perMigration = /* @__PURE__ */ new Map();
11806
- const blockers = [];
11807
- const edits = [];
11808
- for (const repoPath of tracked) {
11809
- if (!isSourcePath(repoPath, appPrefix)) continue;
11810
- const absolutePath = join23(repo, repoPath);
11811
- let stat;
11812
- try {
11813
- stat = lstatSync(absolutePath);
11814
- } catch {
11815
- continue;
11816
- }
11817
- if (!stat.isFile() || stat.isSymbolicLink()) continue;
11818
- const before = readFileSync25(absolutePath, "utf-8");
11819
- let after = before;
11820
- for (const migration of SOURCE_MIGRATIONS) {
11821
- const result = migration.transform(after);
11822
- after = result.source;
11823
- if (result.replacements > 0) {
11824
- const planned = perMigration.get(migration.id) ?? {
11825
- id: migration.id,
11826
- description: migration.description,
11827
- files: [],
11828
- replacements: 0
11829
- };
11830
- planned.files.push(repoPath);
11831
- planned.replacements += result.replacements;
11832
- perMigration.set(migration.id, planned);
11833
- }
11834
- blockers.push(...migration.findBlockers(after, repoPath));
11835
- }
11836
- if (after !== before) edits.push({ path: repoPath, before, after });
11837
- }
11838
- return {
11839
- pending: SOURCE_MIGRATIONS.flatMap((migration) => {
11840
- const planned = perMigration.get(migration.id);
11841
- return planned ? [{ ...planned, files: [...new Set(planned.files)].sort() }] : [];
11842
- }),
11843
- changedFiles: edits.map((edit) => edit.path).sort(),
11844
- blockers,
11845
- edits
11846
- };
11847
- }
11848
- function applyAppMigrationPlan(appDir, plan) {
11849
- if (plan.blockers.length > 0) {
11850
- throw new Error("Cannot apply an app migration plan with unresolved blockers.");
11851
- }
11852
- const repo = repoToplevel(appDir);
11853
- for (const edit of plan.edits) {
11854
- const absolutePath = join23(repo, edit.path);
11855
- if (readFileSync25(absolutePath, "utf-8") !== edit.before) {
11856
- throw new Error(`Migration input changed after planning: ${edit.path}`);
11857
- }
11858
- }
11859
- for (const edit of plan.edits) writeFileSync10(join23(repo, edit.path), edit.after);
11860
- }
11861
- function isSourcePath(repoPath, appPrefix) {
11862
- if (appPrefix && !repoPath.startsWith(appPrefix)) return false;
11863
- return /\.(?:[cm]?[jt]sx?)$/.test(repoPath);
11864
- }
11865
- function migrateCanonicalIdentityWire(source) {
11866
- let replacements = 0;
11867
- const lines = source.split("\n");
11868
- for (let index = 0; index < lines.length; index++) {
11869
- let line = lines[index];
11870
- line = line.replace(
11871
- /(\.set\(\s*)(['"])x-app-name\2(\s*,\s*)([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\.APP_NAME(\s*\))/g,
11872
- (_match, open, quote, separator, receiver, close) => {
11873
- replacements++;
11874
- return `${open}${quote}x-app-id${quote}${separator}${receiver}.DEEPSPACE_APP_ID${close}`;
11875
- }
11876
- );
11877
- line = line.replace(
11878
- /(['"])x-app-name\1(\s*:\s*)([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\.APP_NAME/g,
11879
- (_match, quote, separator, receiver) => {
11880
- replacements++;
11881
- return `${quote}x-app-id${quote}${separator}${receiver}.DEEPSPACE_APP_ID`;
11882
- }
11883
- );
11884
- line = line.replace(
11885
- /(\[\s*)(['"])x-app-name\2(\s*\]\s*=\s*)([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\.APP_NAME/g,
11886
- (_match, open, quote, separator, receiver) => {
11887
- replacements++;
11888
- return `${open}${quote}x-app-id${quote}${separator}${receiver}.DEEPSPACE_APP_ID`;
11889
- }
11890
- );
11891
- line = line.replace(
11892
- /(forwardedParams\.set\(\s*)(['"])appName\2(\s*,\s*)([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\.APP_NAME(\s*\))/g,
11893
- (_match, open, quote, separator, receiver, close) => {
11894
- replacements++;
11895
- return `${open}${quote}appId${quote}${separator}${receiver}.DEEPSPACE_APP_ID${close}`;
11896
- }
11897
- );
11898
- lines[index] = line;
11899
- }
11900
- if (replacements === 0) return { source, replacements };
11901
- let migrated = lines.join("\n");
11902
- if (/forwardedParams\.set\(\s*['"]appId['"]/.test(migrated)) {
11903
- migrated = migrated.replaceAll("injectAppName", "injectAppId").replaceAll("`?appName=...`", "`?appId=...`").replaceAll("Inject appName into the query string", "Inject appId into the query string").replaceAll("caller-supplied appName", "caller-supplied appId").replaceAll(
11904
- "/_deepspace/subscriptions/plans?appName=",
11905
- "/_deepspace/subscriptions/plans?appId="
11906
- );
11907
- }
11908
- migrated = migrated.replaceAll(
11909
- "APP_IDENTITY_TOKEN + APP_NAME",
11910
- "APP_IDENTITY_TOKEN + DEEPSPACE_APP_ID"
11911
- );
11912
- return {
11913
- source: ensureCanonicalIdentityProperties(migrated),
11914
- replacements
11915
- };
11916
- }
11917
- function ensureCanonicalIdentityProperties(source) {
11918
- const lines = source.split("\n");
11919
- for (let index = lines.length - 1; index >= 0; index--) {
11920
- const match = /^(\s*)(?:readonly\s+)?APP_NAME\??\s*:\s*string\s*([;,]?)\s*$/.exec(lines[index]);
11921
- if (!match || blockAlreadyDeclaresCanonicalId(lines, index, match[1].length)) continue;
11922
- const suffix = match[2] ?? "";
11923
- lines.splice(index + 1, 0, `${match[1]}DEEPSPACE_APP_ID: string${suffix}`);
11924
- }
11925
- return lines.join("\n");
11926
- }
11927
- function blockAlreadyDeclaresCanonicalId(lines, propertyIndex, propertyIndent) {
11928
- for (const direction of [-1, 1]) {
11929
- for (let index = propertyIndex + direction; index >= 0 && index < lines.length; index += direction) {
11930
- const line = lines[index];
11931
- if (/^\s*(?:readonly\s+)?DEEPSPACE_APP_ID\??\s*:/.test(line)) return true;
11932
- const trimmed = line.trimStart();
11933
- if (!trimmed) continue;
11934
- const indent = line.length - trimmed.length;
11935
- if (indent < propertyIndent && (trimmed.includes("{") || trimmed.startsWith("}"))) break;
11936
- }
11937
- }
11938
- return false;
11939
- }
11940
- function findLegacyIdentityWireBlockers(source, file) {
11941
- const blockers = [];
11942
- for (const [index, line] of source.split("\n").entries()) {
11943
- const trimmed = line.trimStart();
11944
- if (!/['"]x-app-name['"]/.test(line) || /\.delete\(\s*['"]x-app-name['"]/.test(line) || trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*")) {
11945
- continue;
11946
- }
11947
- blockers.push({
11948
- migrationId: CANONICAL_IDENTITY_WIRE_ID,
11949
- file,
11950
- line: index + 1,
11951
- message: "This app still sends the removed x-app-name identity header in a form the safe migration cannot rewrite."
11952
- });
11953
- }
11954
- return blockers;
11955
- }
11956
-
11957
- // src/cli/lib/identity-migration-api.ts
12099
+ // src/cli/commands/migrate/identity-api.ts
11958
12100
  async function getIdentityMigration(deployUrl, token, appId) {
11959
12101
  try {
11960
12102
  const result = await apiFetch(
@@ -12003,11 +12145,11 @@ async function cancelIdentityMigration(deployUrl, token, appId) {
12003
12145
  { method: "DELETE" }
12004
12146
  );
12005
12147
  }
12006
- async function rollbackIdentityMigration(deployUrl, token, appId) {
12148
+ async function verifyIdentityMigration(deployUrl, token, appId) {
12007
12149
  const result = await apiFetch(
12008
12150
  deployUrl,
12009
12151
  token,
12010
- `/api/apps/${encodeURIComponent(appId)}/identity-migration/rollback`,
12152
+ `/api/apps/${encodeURIComponent(appId)}/identity-migration/verify`,
12011
12153
  { method: "POST" }
12012
12154
  );
12013
12155
  return result.migration;
@@ -12041,21 +12183,13 @@ var migrate_default = defineDeepspaceCommand({
12041
12183
  type: "boolean",
12042
12184
  description: "Cancel a prepared migration after restoring and publishing the legacy id",
12043
12185
  default: false
12044
- },
12045
- rollback: {
12046
- type: "boolean",
12047
- description: "Roll back a committed migration before any canonical deploy starts",
12048
- default: false
12049
12186
  }
12050
12187
  },
12051
12188
  async run({ args }) {
12052
- const recoveryMode = args.cancel === true ? "cancel" : args.rollback === true ? "rollback" : null;
12053
- const exclusiveModes = [args["dry-run"], args.cancel, args.rollback].filter(Boolean).length;
12189
+ const recoveryMode = args.cancel === true ? "cancel" : null;
12190
+ const exclusiveModes = [args["dry-run"], args.cancel].filter(Boolean).length;
12054
12191
  if (exclusiveModes > 1) {
12055
- throw new Refusal(
12056
- "Use only one of --dry-run, --cancel, or --rollback.",
12057
- "conflicting_migration_mode"
12058
- );
12192
+ throw new Refusal("Use only one of --dry-run or --cancel.", "conflicting_migration_mode");
12059
12193
  }
12060
12194
  const appDir = findAppDir();
12061
12195
  if (!appDir) {
@@ -12117,12 +12251,18 @@ var migrate_default = defineDeepspaceCommand({
12117
12251
  applyAppMigrationPlan(appDir, sourcePlan);
12118
12252
  throw sourceMigrationCommitRequired(appDir, sourcePlan);
12119
12253
  }
12120
- await requireSourceMigrationDeploy(appDir, currentAppId, deployUrl, token);
12254
+ await requireSourceMigrationDeploy(appDir, currentAppId, deployUrl, token, {
12255
+ envName,
12256
+ remoteName: typeof args.remote === "string" ? args.remote : void 0
12257
+ });
12121
12258
  reportUpToDate(currentAppId, args.json);
12122
12259
  return { data: { status: "up_to_date", appId: currentAppId } };
12123
12260
  }
12124
12261
  if (sourcePlan.pending.length === 0 && existingMigration.status === "verified") {
12125
- await requireSourceMigrationDeploy(appDir, currentAppId, deployUrl, token);
12262
+ await requireSourceMigrationDeploy(appDir, currentAppId, deployUrl, token, {
12263
+ envName,
12264
+ remoteName: typeof args.remote === "string" ? args.remote : void 0
12265
+ });
12126
12266
  }
12127
12267
  if (args["dry-run"]) {
12128
12268
  reportMigration(existingMigration, args.json);
@@ -12216,14 +12356,7 @@ var migrate_default = defineDeepspaceCommand({
12216
12356
  }
12217
12357
  });
12218
12358
  async function recoverMigration(input) {
12219
- const { mode, appDir, envName, remoteName, currentAppId, token, deployUrl, migration, json } = input;
12220
- if (migration.status === "verified" || migration.deployStartedAt) {
12221
- throw new Refusal(
12222
- "A canonical deployment has started. Registry rollback is no longer safe; use the forward-recovery procedure to repair or redeploy the canonical app.",
12223
- "forward_recovery_required",
12224
- { extra: { migration } }
12225
- );
12226
- }
12359
+ const { appDir, envName, remoteName, currentAppId, token, deployUrl, migration, json } = input;
12227
12360
  if (migration.status === "rolled_back") {
12228
12361
  if (currentAppId !== migration.legacyAppId) {
12229
12362
  writeAppId(appDir, migration.legacyAppId, { wranglerEnv: envName, force: true });
@@ -12232,17 +12365,10 @@ async function recoverMigration(input) {
12232
12365
  reportMigration(migration, json);
12233
12366
  return { data: { status: "rolled_back", migration } };
12234
12367
  }
12235
- if (mode === "cancel" && migration.status !== "prepared") {
12236
- throw new Refusal(
12237
- "This migration is committed. Use --rollback before any canonical deploy starts.",
12238
- "migration_rollback_required",
12239
- { extra: { migration } }
12240
- );
12241
- }
12242
- if (mode === "rollback" && migration.status !== "committed") {
12368
+ if (migration.status !== "prepared") {
12243
12369
  throw new Refusal(
12244
- "This migration is only prepared. Use --cancel instead.",
12245
- "migration_cancel_required",
12370
+ "This migration is already committed. Complete it by deploying the canonical app.",
12371
+ "forward_recovery_required",
12246
12372
  { extra: { migration } }
12247
12373
  );
12248
12374
  }
@@ -12252,14 +12378,9 @@ async function recoverMigration(input) {
12252
12378
  }
12253
12379
  const remote = requireGitHubRemote2(appDir, remoteName, migration.repository);
12254
12380
  requirePublishedHead2(appDir, remote);
12255
- if (mode === "cancel") {
12256
- await cancelIdentityMigration(deployUrl, token, migration.legacyAppId);
12257
- if (!json) console.log(`Canceled ${migration.legacyAppId} \u2192 ${migration.appId}.`);
12258
- return { data: { status: "cancelled", legacyAppId: migration.legacyAppId } };
12259
- }
12260
- const rolledBack = await rollbackIdentityMigration(deployUrl, token, migration.appId);
12261
- reportMigration(rolledBack, json);
12262
- return { data: { status: "rolled_back", migration: rolledBack } };
12381
+ await cancelIdentityMigration(deployUrl, token, migration.legacyAppId);
12382
+ if (!json) console.log(`Canceled ${migration.legacyAppId} \u2192 ${migration.appId}.`);
12383
+ return { data: { status: "cancelled", legacyAppId: migration.legacyAppId } };
12263
12384
  }
12264
12385
  async function continuePreparedMigration(input) {
12265
12386
  const { appDir, envName, remoteName, token, deployUrl, migration, json } = input;
@@ -12274,7 +12395,14 @@ async function continuePreparedMigration(input) {
12274
12395
  return { data: { status: "verified", migration } };
12275
12396
  }
12276
12397
  if (migration.status === "committed") {
12277
- throw deployRequired(appDir, migration, envName);
12398
+ await requireSourceMigrationDeploy(appDir, migration.appId, deployUrl, token, {
12399
+ envName,
12400
+ remoteName,
12401
+ after: migration.committedAt ?? void 0
12402
+ });
12403
+ const verified = await verifyIdentityMigration(deployUrl, token, migration.appId);
12404
+ reportMigration(verified, json);
12405
+ return { data: { status: "verified", migration: verified } };
12278
12406
  }
12279
12407
  if (!isWorkTreeClean(appDir)) {
12280
12408
  const wranglerPath = wranglerRepoPath(appDir);
@@ -12381,8 +12509,6 @@ function sourceMigrationCommitRequired(appDir, plan) {
12381
12509
  "--only",
12382
12510
  "--message",
12383
12511
  "Apply DeepSpace app migrations",
12384
- "--message",
12385
- `DeepSpace-Migrations: ${migrationIds.join(",")}`,
12386
12512
  "--",
12387
12513
  ...plan.changedFiles
12388
12514
  ]
@@ -12391,61 +12517,39 @@ function sourceMigrationCommitRequired(appDir, plan) {
12391
12517
  }
12392
12518
  );
12393
12519
  }
12394
- async function requireSourceMigrationDeploy(appDir, appId, deployUrl, token) {
12395
- if (!isWorkTreeClean(appDir)) {
12396
- throw new Refusal(
12397
- "Commit or discard local changes before checking app migration deployment state.",
12398
- "dirty_worktree"
12399
- );
12400
- }
12401
- const head = resolveCommit(appDir, "HEAD");
12402
- if (!head) return;
12403
- const repo = repoToplevel(appDir);
12404
- const knownIds = new Set(APP_MIGRATION_DEFINITIONS.map((migration) => migration.id));
12405
- const migrationCommits = splitNulFields(
12406
- runGit(repo, [
12407
- "log",
12408
- "--format=%H%x00",
12409
- "--extended-regexp",
12410
- "--grep",
12411
- `^DeepSpace-Migrations:.*(${[...knownIds].join("|")})`
12412
- ]).stdout
12413
- ).map((oid) => oid.trim()).filter((oid) => /^[0-9a-f]{40}$/.test(oid));
12414
- if (migrationCommits.length === 0) return;
12520
+ async function requireSourceMigrationDeploy(appDir, appId, deployUrl, token, options = {}) {
12415
12521
  const { release: release2 } = await repoApi(deployUrl, token, appId).latestRelease();
12416
- if (release2?.commitOid === head) return;
12417
- const liveCommit = release2?.commitOid ?? null;
12418
- if (liveCommit && (resolveCommit(appDir, liveCommit) === null || !isAncestor(appDir, liveCommit, head))) {
12419
- throw new Refusal(
12420
- `The live release commit ${liveCommit.slice(0, 10)} is not an ancestor of this checkout, so migration deployment state cannot be proven. Fetch and integrate the live source history first.`,
12421
- "migration_lineage_unknown",
12422
- { extra: { appId, liveCommitOid: liveCommit, commitOid: head } }
12423
- );
12424
- }
12425
- const undeployedMigrationCommits = migrationCommits.filter(
12426
- (oid) => !liveCommit || oid !== liveCommit && isAncestor(appDir, liveCommit, oid)
12522
+ const deployedIds = releaseMigrationIds(release2?.config);
12523
+ const pendingIds = APP_MIGRATION_DEFINITIONS.map(({ id }) => id).filter(
12524
+ (id) => !deployedIds.has(id)
12427
12525
  );
12428
- const pendingIds = [
12429
- ...new Set(
12430
- undeployedMigrationCommits.flatMap((oid) => {
12431
- const message = runGit(repo, ["show", "-s", "--format=%B", oid]).stdout.toString("utf-8");
12432
- return [...message.matchAll(/^DeepSpace-Migrations:\s*(.+)$/gm)].flatMap(
12433
- (match) => (match[1] ?? "").split(",").map((id) => id.trim()).filter((id) => knownIds.has(id))
12434
- );
12435
- })
12436
- )
12437
- ];
12438
- if (pendingIds.length === 0) return;
12526
+ const releaseAfterCutover = options.after === void 0 || typeof release2?.createdAt === "string" && Number.isFinite(Date.parse(release2.createdAt)) && Date.parse(release2.createdAt) > Date.parse(options.after);
12527
+ if (pendingIds.length === 0 && releaseAfterCutover) return;
12528
+ const sourceState = await getAppSource(deployUrl, token, appId);
12529
+ if (sourceState.source?.provider === "github") {
12530
+ const remote = requireGitHubRemote2(appDir, options.remoteName, sourceState.source.repository);
12531
+ requirePublishedHead2(appDir, remote);
12532
+ }
12439
12533
  throw new Refusal(
12440
- `App migrations ${pendingIds.join(", ")} are committed but are not in the live release. Deploy them, then rerun.`,
12534
+ pendingIds.length > 0 ? `App migrations ${pendingIds.join(", ")} are not in the live release. Deploy them, then rerun.` : "The canonical app has not been deployed since its identity cutover. Deploy it, then rerun.",
12441
12535
  "migration_deploy_required",
12442
12536
  {
12443
12537
  actionRequired: true,
12444
- action: { cwd: appDir, argv: ["deepspace", "deploy"] },
12445
- extra: { appId, sourceMigrations: pendingIds, commitOid: head }
12538
+ action: {
12539
+ cwd: appDir,
12540
+ argv: ["deepspace", "deploy", ...options.envName ? ["--env", options.envName] : []]
12541
+ },
12542
+ extra: { appId, sourceMigrations: pendingIds, after: options.after ?? null }
12446
12543
  }
12447
12544
  );
12448
12545
  }
12546
+ function releaseMigrationIds(config) {
12547
+ if (config === null || typeof config !== "object" || Array.isArray(config)) return /* @__PURE__ */ new Set();
12548
+ const validation = validateAppMigrationIds(
12549
+ config.appMigrations ?? []
12550
+ );
12551
+ return new Set(validation.valid ? validation.ids : []);
12552
+ }
12449
12553
  function requireSourceMigrationWorktree(appDir, allowDirtyWrangler = false) {
12450
12554
  if (isWorkTreeClean(appDir)) return;
12451
12555
  const changed = statusFiles(repoToplevel(appDir));
@@ -12526,7 +12630,7 @@ function reportMigration(migration, json) {
12526
12630
  // src/cli/cli.ts
12527
12631
  var __dirname = dirname10(fileURLToPath3(import.meta.url));
12528
12632
  var pkg = JSON.parse(readFileSync26(join24(__dirname, "..", "package.json"), "utf-8"));
12529
- if (process.platform === "linux" && !process.env.SSL_CERT_FILE && existsSync24("/etc/ssl/certs/ca-certificates.crt")) {
12633
+ if (process.platform === "linux" && !process.env.SSL_CERT_FILE && existsSync25("/etc/ssl/certs/ca-certificates.crt")) {
12530
12634
  process.env.SSL_CERT_FILE = "/etc/ssl/certs/ca-certificates.crt";
12531
12635
  }
12532
12636
  var create2 = defineCommand14({