deepspace 0.16.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,10 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli/cli.ts
4
- import { defineCommand as defineCommand14, runMain, runCommand, renderUsage } from "citty";
4
+ import {
5
+ defineCommand as defineCommand14,
6
+ runMain,
7
+ runCommand,
8
+ renderUsage
9
+ } from "citty";
5
10
  import { sync as spawnSync8 } from "cross-spawn";
6
- import { readFileSync as readFileSync29, existsSync as existsSync27 } from "fs";
7
- import { dirname as dirname10, join as join25 } from "path";
11
+ import { readFileSync as readFileSync28, existsSync as existsSync25 } from "fs";
12
+ import { dirname as dirname11, join as join25 } from "path";
8
13
  import { fileURLToPath as fileURLToPath3 } from "url";
9
14
 
10
15
  // src/cli/lib/spinner.ts
@@ -41,7 +46,8 @@ function stopActiveSpinner() {
41
46
 
42
47
  // src/cli/lib/output.ts
43
48
  import * as p2 from "@clack/prompts";
44
- import { isAbsolute } from "path";
49
+ import { readFileSync, realpathSync } from "fs";
50
+ import { dirname, isAbsolute, resolve } from "path";
45
51
 
46
52
  // src/cli/lib/cli-format.ts
47
53
  var ANSI_RE = /\[[0-9;]*m/g;
@@ -59,10 +65,35 @@ function humanCommand(argv) {
59
65
  }
60
66
 
61
67
  // src/cli/lib/output.ts
68
+ function resolveActionArgv(argv) {
69
+ if (argv[0] !== "deepspace") return argv;
70
+ const entry = cliEntryPath();
71
+ return entry ? [process.execPath, entry, ...argv.slice(1)] : argv;
72
+ }
73
+ function cliEntryPath() {
74
+ const entry = process.argv[1];
75
+ if (!entry || !isAbsolute(entry)) return null;
76
+ let real;
77
+ try {
78
+ real = realpathSync(entry);
79
+ } catch {
80
+ return null;
81
+ }
82
+ if (!/(^|[\\/])cli\.[cm]?js$/.test(real)) return null;
83
+ try {
84
+ const pkg2 = JSON.parse(readFileSync(resolve(dirname(real), "..", "package.json"), "utf-8"));
85
+ return pkg2.name === "deepspace" ? real : null;
86
+ } catch {
87
+ return null;
88
+ }
89
+ }
90
+ function executableAction(action) {
91
+ const pinned = { ...action, argv: resolveActionArgv(action.argv) };
92
+ assertExecutableAction(pinned);
93
+ return pinned;
94
+ }
62
95
  function cliAction(...argv) {
63
- const action = { cwd: process.cwd(), argv };
64
- assertExecutableAction(action);
65
- return action;
96
+ return executableAction({ cwd: process.cwd(), argv });
66
97
  }
67
98
  function assertExecutableAction(action) {
68
99
  if (!isAbsolute(action.cwd)) throw new TypeError("CLI action cwd must be absolute");
@@ -78,7 +109,9 @@ function withSlug(msg, code) {
78
109
  return msg.trimEnd().endsWith(tag) ? msg : `${msg.trimEnd()} ${tag}`;
79
110
  }
80
111
  function displayArg(arg) {
81
- return /^[A-Za-z0-9_./:@%+=,-]+$/.test(arg) ? arg : shQuote(arg);
112
+ if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(arg)) return arg;
113
+ if (process.platform === "win32") return `"${arg.replaceAll('"', '""')}"`;
114
+ return shQuote(arg);
82
115
  }
83
116
  function printAction(action) {
84
117
  assertExecutableAction(action);
@@ -232,6 +265,146 @@ function wrapCommandErrors(cmd) {
232
265
  return cmd;
233
266
  }
234
267
 
268
+ // src/cli/lib/command.ts
269
+ import { defineCommand, parseArgs } from "citty";
270
+ import * as p3 from "@clack/prompts";
271
+ var Refusal = class extends Error {
272
+ code;
273
+ action;
274
+ actionRequired;
275
+ extra;
276
+ constructor(message, code, opts = {}) {
277
+ super(message);
278
+ this.name = "Refusal";
279
+ this.code = code;
280
+ this.action = opts.action ? executableAction(opts.action) : void 0;
281
+ this.actionRequired = opts.actionRequired ?? false;
282
+ this.extra = opts.extra ?? {};
283
+ }
284
+ };
285
+ function apiErrorDetails(err) {
286
+ return err instanceof ApiError ? err.details : void 0;
287
+ }
288
+ function withoutReservedKeys(payload) {
289
+ return Object.fromEntries(
290
+ Object.entries(payload).filter(
291
+ ([key]) => ![
292
+ "ok",
293
+ "code",
294
+ "error",
295
+ "action",
296
+ "actionRequired",
297
+ "next",
298
+ "nextAction",
299
+ "resume"
300
+ ].includes(key)
301
+ )
302
+ );
303
+ }
304
+ function exitWhenFlushed(code) {
305
+ if (process.stdout.writableLength === 0) process.exit(code);
306
+ process.stdout.write("", () => process.exit(code));
307
+ return void 0;
308
+ }
309
+ function nameVariants(name) {
310
+ return [
311
+ name,
312
+ name.replace(/-([a-z])/g, (_, c) => c.toUpperCase()),
313
+ name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)
314
+ ];
315
+ }
316
+ function unknownOptionNames(parsed, args) {
317
+ const known = /* @__PURE__ */ new Set(["_"]);
318
+ for (const [name, def] of Object.entries(args)) {
319
+ for (const variant of nameVariants(name)) known.add(variant);
320
+ const alias = def.alias;
321
+ for (const a of Array.isArray(alias) ? alias : alias ? [alias] : []) {
322
+ for (const variant of nameVariants(a)) known.add(variant);
323
+ }
324
+ }
325
+ return Object.keys(parsed).filter((key) => !known.has(key));
326
+ }
327
+ function unknownOptionNamesFromRaw(rawArgs2, args) {
328
+ const relaxedArgs = Object.fromEntries(
329
+ Object.entries(args).map(([name, definition]) => [
330
+ name,
331
+ definition.type === "enum" ? { ...definition, type: "string", required: false, options: void 0 } : { ...definition, required: false }
332
+ ])
333
+ );
334
+ return unknownOptionNames(parseArgs(rawArgs2, relaxedArgs), args);
335
+ }
336
+ function unknownOptionMessage(commandName, unknown, args) {
337
+ const valid = Object.entries(args).filter(([, definition]) => definition.type !== "positional").map(([name]) => name).sort().join(", ");
338
+ return `Unknown ${unknown.length === 1 ? "option" : "options"}: ${unknown.map((name) => `--${name}`).join(", ")}. \`${commandName}\` accepts: ${valid || "(none)"}.`;
339
+ }
340
+ function defineDeepspaceCommand(def) {
341
+ const declaredArgs = {
342
+ ...def.args ?? {},
343
+ json: {
344
+ type: "boolean",
345
+ description: "Emit a single-line JSON result for scripts/agents",
346
+ default: false
347
+ }
348
+ };
349
+ return defineCommand({
350
+ meta: def.meta,
351
+ args: declaredArgs,
352
+ async run({ args }) {
353
+ const json = Boolean(args.json);
354
+ let succeeded = false;
355
+ try {
356
+ const unknown = unknownOptionNames(args, declaredArgs);
357
+ if (unknown.length > 0) {
358
+ throw new Refusal(
359
+ unknownOptionMessage(def.meta.name, unknown, declaredArgs),
360
+ "unknown_option"
361
+ );
362
+ }
363
+ const out = await def.run({
364
+ args
365
+ }) ?? {};
366
+ if (out.action) out.action = executableAction(out.action);
367
+ if (json) {
368
+ console.log(
369
+ JSON.stringify({
370
+ ok: true,
371
+ ...withoutReservedKeys(out.data ?? {}),
372
+ ...out.action ? { action: out.action } : {}
373
+ })
374
+ );
375
+ } else if (out.action) {
376
+ printAction(out.action);
377
+ }
378
+ succeeded = true;
379
+ } catch (err) {
380
+ stopActiveSpinner();
381
+ const refusal = err instanceof Refusal ? err : null;
382
+ const code = refusal?.code ?? errorCode(err);
383
+ const message = err instanceof Error ? err.message : String(err);
384
+ if (json) {
385
+ console.log(
386
+ JSON.stringify({
387
+ ok: false,
388
+ ...code ? { code } : {},
389
+ ...refusal?.actionRequired ? { actionRequired: true } : {},
390
+ error: message,
391
+ ...refusal?.action ? { action: refusal.action } : {},
392
+ ...withoutReservedKeys(refusal?.extra ?? apiErrorDetails(err) ?? {})
393
+ })
394
+ );
395
+ } else {
396
+ const line = code ? withSlug(message, code) : message;
397
+ if (refusal?.actionRequired) p3.log.warn(line);
398
+ else p3.log.error(line);
399
+ if (refusal?.action) printAction(refusal.action);
400
+ }
401
+ exitWhenFlushed(refusal?.actionRequired ? 2 : 1);
402
+ }
403
+ if (succeeded) exitWhenFlushed(0);
404
+ }
405
+ });
406
+ }
407
+
235
408
  // src/cli/lib/command-suggestions.ts
236
409
  function splitTokenPath(token, table) {
237
410
  const parts = token.trim().split(/\s+/);
@@ -342,7 +515,7 @@ function effectivePlatformUrls(env = process.env) {
342
515
  var DASHBOARD_URL = DEEPSPACE_ENV === "production" ? "https://dashboard.deep.space" : DEEPSPACE_ENV === "staging" ? "https://dashboard.deepspacesites.com" : "http://127.0.0.1:9";
343
516
 
344
517
  // src/cli/auth.ts
345
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
518
+ import { readFileSync as readFileSync2, writeFileSync, existsSync, mkdirSync } from "fs";
346
519
  import { join } from "path";
347
520
  import { homedir } from "os";
348
521
 
@@ -370,106 +543,6 @@ async function exchangeSession(authUrl, sessionToken) {
370
543
  return data.token ?? null;
371
544
  }
372
545
 
373
- // src/cli/lib/command.ts
374
- import { defineCommand } from "citty";
375
- import * as p3 from "@clack/prompts";
376
- var Refusal = class extends Error {
377
- code;
378
- action;
379
- actionRequired;
380
- extra;
381
- constructor(message, code, opts = {}) {
382
- super(message);
383
- this.name = "Refusal";
384
- this.code = code;
385
- if (opts.action) assertExecutableAction(opts.action);
386
- this.action = opts.action;
387
- this.actionRequired = opts.actionRequired ?? false;
388
- this.extra = opts.extra ?? {};
389
- }
390
- };
391
- function apiErrorDetails(err) {
392
- return err instanceof ApiError ? err.details : void 0;
393
- }
394
- function withoutReservedKeys(payload) {
395
- return Object.fromEntries(
396
- Object.entries(payload).filter(
397
- ([key]) => ![
398
- "ok",
399
- "code",
400
- "error",
401
- "action",
402
- "actionRequired",
403
- "next",
404
- "nextAction",
405
- "resume"
406
- ].includes(key)
407
- )
408
- );
409
- }
410
- function exitWhenFlushed(code) {
411
- if (process.stdout.writableLength === 0) process.exit(code);
412
- process.stdout.write("", () => process.exit(code));
413
- return void 0;
414
- }
415
- function defineDeepspaceCommand(def) {
416
- return defineCommand({
417
- meta: def.meta,
418
- args: {
419
- ...def.args ?? {},
420
- json: {
421
- type: "boolean",
422
- description: "Emit a single-line JSON result for scripts/agents",
423
- default: false
424
- }
425
- },
426
- async run({ args }) {
427
- const json = Boolean(args.json);
428
- let succeeded = false;
429
- try {
430
- const out = await def.run({ args }) ?? {};
431
- if (out.action) assertExecutableAction(out.action);
432
- if (json) {
433
- console.log(
434
- JSON.stringify({
435
- ok: true,
436
- ...withoutReservedKeys(out.data ?? {}),
437
- ...out.action ? { action: out.action } : {}
438
- })
439
- );
440
- } else if (out.action) {
441
- printAction(out.action);
442
- }
443
- succeeded = true;
444
- } catch (err) {
445
- stopActiveSpinner();
446
- const refusal = err instanceof Refusal ? err : null;
447
- const code = refusal?.code ?? errorCode(err);
448
- const message = err instanceof Error ? err.message : String(err);
449
- if (json) {
450
- console.log(
451
- JSON.stringify({
452
- ok: false,
453
- ...code ? { code } : {},
454
- ...refusal?.actionRequired ? { actionRequired: true } : {},
455
- error: message,
456
- ...refusal?.action ? { action: refusal.action } : {},
457
- ...withoutReservedKeys(refusal?.extra ?? apiErrorDetails(err) ?? {})
458
- })
459
- );
460
- } else {
461
- const line = code ? withSlug(message, code) : message;
462
- if (refusal?.actionRequired) p3.log.warn(line);
463
- else p3.log.error(line);
464
- if (refusal?.action) printAction(refusal.action);
465
- }
466
- exitWhenFlushed(refusal?.actionRequired ? 2 : 1);
467
- }
468
- if (succeeded) exitWhenFlushed(0);
469
- }
470
- });
471
- }
472
-
473
546
  // src/cli/auth.ts
474
547
  var AUTH_URL = process.env.DEEPSPACE_AUTH_URL ?? PLATFORM_URLS.auth;
475
548
  var PROD_AUTH_URL = "https://auth.deep.space";
@@ -492,7 +565,7 @@ function credentialPaths(authUrl = AUTH_URL) {
492
565
  var { sessionPath: SESSION_PATH, tokenPath: TOKEN_PATH } = credentialPaths();
493
566
  async function ensureToken() {
494
567
  if (existsSync(TOKEN_PATH)) {
495
- const existing = readFileSync(TOKEN_PATH, "utf-8").trim();
568
+ const existing = readFileSync2(TOKEN_PATH, "utf-8").trim();
496
569
  if (isTokenValid(existing)) {
497
570
  return existing;
498
571
  }
@@ -502,7 +575,7 @@ async function ensureToken() {
502
575
  action: cliAction("deepspace", "auth", "login")
503
576
  });
504
577
  }
505
- const sessionToken = readFileSync(SESSION_PATH, "utf-8").trim();
578
+ const sessionToken = readFileSync2(SESSION_PATH, "utf-8").trim();
506
579
  const token = await exchangeSession(AUTH_URL, sessionToken);
507
580
  if (!token) {
508
581
  throw new Refusal(
@@ -526,7 +599,7 @@ function isTokenValid(jwt) {
526
599
  registerAuthRefresh(async () => {
527
600
  try {
528
601
  if (!existsSync(SESSION_PATH)) return null;
529
- const sessionToken = readFileSync(SESSION_PATH, "utf-8").trim();
602
+ const sessionToken = readFileSync2(SESSION_PATH, "utf-8").trim();
530
603
  const token = await exchangeSession(AUTH_URL, sessionToken);
531
604
  if (!token) return null;
532
605
  mkdirSync(DIR, { recursive: true, mode: 448 });
@@ -538,7 +611,7 @@ registerAuthRefresh(async () => {
538
611
  });
539
612
 
540
613
  // src/cli/commands/login.ts
541
- import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2 } from "fs";
614
+ import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3 } from "fs";
542
615
  import { join as join2 } from "path";
543
616
  import { homedir as homedir2 } from "os";
544
617
  import { randomBytes, createHash } from "crypto";
@@ -697,7 +770,7 @@ function readPasswordFromStdin() {
697
770
  "password_stdin_needs_pipe"
698
771
  );
699
772
  }
700
- return readFileSync2(0, "utf-8").replace(/\r?\n$/, "");
773
+ return readFileSync3(0, "utf-8").replace(/\r?\n$/, "");
701
774
  }
702
775
  async function pollForCompletion(sessionId, codeVerifier) {
703
776
  const maxAttempts = 120;
@@ -738,7 +811,7 @@ async function provisionProfile(jwt) {
738
811
  }
739
812
  }
740
813
  function sleep(ms) {
741
- return new Promise((resolve20) => setTimeout(resolve20, ms));
814
+ return new Promise((resolve21) => setTimeout(resolve21, ms));
742
815
  }
743
816
  function base64url(bytes) {
744
817
  return bytes.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
@@ -775,7 +848,7 @@ async function doEmailLogin(email, password) {
775
848
  }
776
849
 
777
850
  // src/cli/commands/logout.ts
778
- import { existsSync as existsSync2, readFileSync as readFileSync3, rmSync } from "fs";
851
+ import { existsSync as existsSync2, readFileSync as readFileSync4, rmSync } from "fs";
779
852
  var AUTH_URL3 = process.env.DEEPSPACE_AUTH_URL ?? PLATFORM_URLS.auth;
780
853
  var { sessionPath: SESSION_PATH2, tokenPath: TOKEN_PATH2 } = credentialPaths(AUTH_URL3);
781
854
  var logout_default = defineDeepspaceCommand({
@@ -784,7 +857,7 @@ var logout_default = defineDeepspaceCommand({
784
857
  description: "Sign out and remove cached credentials"
785
858
  },
786
859
  async run({ args }) {
787
- const sessionToken = existsSync2(SESSION_PATH2) ? readFileSync3(SESSION_PATH2, "utf-8").trim() : null;
860
+ const sessionToken = existsSync2(SESSION_PATH2) ? readFileSync4(SESSION_PATH2, "utf-8").trim() : null;
788
861
  if (sessionToken) {
789
862
  try {
790
863
  await fetch(`${AUTH_URL3}/api/auth/sign-out`, {
@@ -810,8 +883,8 @@ var logout_default = defineDeepspaceCommand({
810
883
  });
811
884
 
812
885
  // src/cli/lib/app-identity.ts
813
- import { existsSync as existsSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
814
- import { join as join3, resolve } from "path";
886
+ import { existsSync as existsSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
887
+ import { join as join3, resolve as resolve2 } from "path";
815
888
  import { parse as parseToml } from "smol-toml";
816
889
 
817
890
  // src/server/utils/registry-client.ts
@@ -846,10 +919,10 @@ function mintAppId(now = Date.now()) {
846
919
  return `app_${mintUlid(now)}`;
847
920
  }
848
921
  function readWranglerIdentityConfig(cwd) {
849
- const wranglerPath = join3(resolve(cwd), "wrangler.toml");
922
+ const wranglerPath = join3(resolve2(cwd), "wrangler.toml");
850
923
  if (!existsSync3(wranglerPath)) return null;
851
924
  try {
852
- return parseToml(readFileSync4(wranglerPath, "utf-8"));
925
+ return parseToml(readFileSync5(wranglerPath, "utf-8"));
853
926
  } catch (err) {
854
927
  throw new InputError(
855
928
  `Could not parse ${wranglerPath}: ${err instanceof Error ? err.message : String(err)}`,
@@ -865,9 +938,9 @@ function readAppId(cwd = process.cwd(), wranglerEnv) {
865
938
  return typeof id === "string" && APP_ID_RE.test(id) ? id : null;
866
939
  }
867
940
  function writeAppId(cwd, appId, opts = {}) {
868
- const wranglerPath = join3(resolve(cwd), "wrangler.toml");
941
+ const wranglerPath = join3(resolve2(cwd), "wrangler.toml");
869
942
  if (!existsSync3(wranglerPath)) {
870
- throw new Error(`No wrangler.toml in ${resolve(cwd)}`);
943
+ throw new Error(`No wrangler.toml in ${resolve2(cwd)}`);
871
944
  }
872
945
  const existing = readAppId(cwd, opts.wranglerEnv);
873
946
  if (existing && !opts.force) {
@@ -876,7 +949,7 @@ function writeAppId(cwd, appId, opts = {}) {
876
949
  `wrangler.toml already carries ${existing}. The app id is immutable \u2014 use --new-id only to fork this repo as a separate app.`
877
950
  );
878
951
  }
879
- let src = readFileSync4(wranglerPath, "utf-8");
952
+ let src = readFileSync5(wranglerPath, "utf-8");
880
953
  const header = opts.wranglerEnv ? `[env.${opts.wranglerEnv}.vars]` : "[vars]";
881
954
  const line = `DEEPSPACE_APP_ID = "${appId}"`;
882
955
  const headerRe = new RegExp(`^\\${header.replace(/\./g, "\\.")}[ \\t]*$`, "m");
@@ -902,18 +975,18 @@ ${line}
902
975
  }
903
976
 
904
977
  // src/cli/commands/dev.ts
905
- import { resolve as resolve7, basename as basename2, join as join12 } from "path";
978
+ import { resolve as resolve8, basename as basename2, join as join12 } from "path";
906
979
  import spawn from "cross-spawn";
907
980
 
908
981
  // src/cli/lib/app-context.ts
909
- import { existsSync as existsSync4, readFileSync as readFileSync5, readdirSync } from "fs";
910
- import { dirname, join as join4, resolve as resolve2 } from "path";
982
+ import { existsSync as existsSync4, readFileSync as readFileSync6, readdirSync } from "fs";
983
+ import { dirname as dirname2, join as join4, resolve as resolve3 } from "path";
911
984
  import { parse as parseToml2 } from "smol-toml";
912
985
  function detectAppName(cwd = process.cwd()) {
913
- const wranglerPath = join4(resolve2(cwd), "wrangler.toml");
986
+ const wranglerPath = join4(resolve3(cwd), "wrangler.toml");
914
987
  if (!existsSync4(wranglerPath)) return null;
915
988
  try {
916
- const config = parseToml2(readFileSync5(wranglerPath, "utf-8"));
989
+ const config = parseToml2(readFileSync6(wranglerPath, "utf-8"));
917
990
  return typeof config.name === "string" && config.name.length > 0 ? config.name : null;
918
991
  } catch (error) {
919
992
  throw new InputError(
@@ -923,10 +996,10 @@ function detectAppName(cwd = process.cwd()) {
923
996
  }
924
997
  }
925
998
  function findAppDir(start = process.cwd()) {
926
- let dir = resolve2(start);
999
+ let dir = resolve3(start);
927
1000
  for (; ; ) {
928
1001
  if (existsSync4(join4(dir, "wrangler.toml"))) return dir;
929
- const parent = dirname(dir);
1002
+ const parent = dirname2(dir);
930
1003
  if (parent === dir) return null;
931
1004
  dir = parent;
932
1005
  }
@@ -942,18 +1015,18 @@ function findChildApps(dir) {
942
1015
  // src/cli/lib/launch-config.ts
943
1016
  import {
944
1017
  existsSync as existsSync5,
945
- readFileSync as readFileSync7,
946
- realpathSync,
1018
+ readFileSync as readFileSync8,
1019
+ realpathSync as realpathSync2,
947
1020
  writeFileSync as writeFileSync4,
948
1021
  mkdirSync as mkdirSync4,
949
1022
  renameSync,
950
1023
  unlinkSync
951
1024
  } from "fs";
952
- import { join as join6, resolve as resolve4, dirname as dirname3, basename, isAbsolute as isAbsolute2, sep } from "path";
1025
+ import { join as join6, resolve as resolve5, dirname as dirname4, basename, isAbsolute as isAbsolute2, sep } from "path";
953
1026
 
954
1027
  // src/cli/lib/git/repository.ts
955
- import { appendFileSync, mkdirSync as mkdirSync3, readFileSync as readFileSync6 } from "fs";
956
- import { dirname as dirname2, join as join5, resolve as resolve3 } from "path";
1028
+ import { appendFileSync, mkdirSync as mkdirSync3, readFileSync as readFileSync7 } from "fs";
1029
+ import { dirname as dirname3, join as join5, resolve as resolve4 } from "path";
957
1030
 
958
1031
  // src/cli/lib/git/process.ts
959
1032
  import { sync as spawnSync } from "cross-spawn";
@@ -1156,16 +1229,16 @@ function listWorktrees(cwd) {
1156
1229
  return worktrees;
1157
1230
  }
1158
1231
  function ensureLocalExclude(cwd, pattern) {
1159
- const commonDir = resolve3(cwd, gitLine(cwd, ["rev-parse", "--git-common-dir"]));
1232
+ const commonDir = resolve4(cwd, gitLine(cwd, ["rev-parse", "--git-common-dir"]));
1160
1233
  const excludePath = join5(commonDir, "info", "exclude");
1161
1234
  let current = "";
1162
1235
  try {
1163
- current = readFileSync6(excludePath, "utf-8");
1236
+ current = readFileSync7(excludePath, "utf-8");
1164
1237
  } catch {
1165
1238
  }
1166
1239
  if (current.split("\n").some((line) => line.trim() === pattern)) return;
1167
1240
  try {
1168
- mkdirSync3(dirname2(excludePath), { recursive: true });
1241
+ mkdirSync3(dirname3(excludePath), { recursive: true });
1169
1242
  appendFileSync(
1170
1243
  excludePath,
1171
1244
  `${current.endsWith("\n") || current === "" ? "" : "\n"}${pattern}
@@ -1177,7 +1250,7 @@ function ensureLocalExclude(cwd, pattern) {
1177
1250
 
1178
1251
  // src/cli/lib/launch-config.ts
1179
1252
  function writeLaunchFile(launchPath, config) {
1180
- mkdirSync4(dirname3(launchPath), { recursive: true });
1253
+ mkdirSync4(dirname4(launchPath), { recursive: true });
1181
1254
  const tmpPath = `${launchPath}.${process.pid}.tmp`;
1182
1255
  writeFileSync4(tmpPath, JSON.stringify(config, null, 2) + "\n");
1183
1256
  try {
@@ -1193,7 +1266,7 @@ function writeLaunchFile(launchPath, config) {
1193
1266
  function readLaunchFile(launchPath) {
1194
1267
  const empty = { version: "0.0.1", configurations: [] };
1195
1268
  if (!existsSync5(launchPath)) return empty;
1196
- const raw = readFileSync7(launchPath, "utf-8").trim();
1269
+ const raw = readFileSync8(launchPath, "utf-8").trim();
1197
1270
  if (!raw) return empty;
1198
1271
  const parsed = JSON.parse(raw);
1199
1272
  if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.configurations)) return null;
@@ -1258,10 +1331,10 @@ function detectClaudeWorktree(appDir) {
1258
1331
  listWorktrees(appDir).map((worktree) => canonicalPath(worktree.path))
1259
1332
  );
1260
1333
  if (!registered.has(worktreeRoot)) return null;
1261
- const worktreesDir = dirname3(worktreeRoot);
1262
- const claudeDir = dirname3(worktreesDir);
1334
+ const worktreesDir = dirname4(worktreeRoot);
1335
+ const claudeDir = dirname4(worktreesDir);
1263
1336
  if (basename(worktreesDir) !== "worktrees" || basename(claudeDir) !== ".claude") return null;
1264
- const ownerRoot = canonicalPath(dirname3(claudeDir));
1337
+ const ownerRoot = canonicalPath(dirname4(claudeDir));
1265
1338
  if (!registered.has(ownerRoot)) return null;
1266
1339
  return { mainRepoRoot: ownerRoot, worktreeName: basename(worktreeRoot) };
1267
1340
  } catch {
@@ -1270,9 +1343,9 @@ function detectClaudeWorktree(appDir) {
1270
1343
  }
1271
1344
  function canonicalPath(path) {
1272
1345
  try {
1273
- return realpathSync(path);
1346
+ return realpathSync2(path);
1274
1347
  } catch {
1275
- return resolve4(path);
1348
+ return resolve5(path);
1276
1349
  }
1277
1350
  }
1278
1351
  function detectLinkedWorktree(appDir) {
@@ -1324,7 +1397,7 @@ function upsertWorktreeLaunchConfig(mainRepoRoot, worktreeName, appDir, opts) {
1324
1397
  // the Claude Code desktop launch.json reference alongside `port`) and
1325
1398
  // verified end-to-end: with it set, the preview server builds from the
1326
1399
  // worktree instead of the session's main repo.
1327
- cwd: resolve4(appDir)
1400
+ cwd: resolve5(appDir)
1328
1401
  });
1329
1402
  writeLaunchFile(launchPath, config);
1330
1403
  return { entryName, port };
@@ -1350,9 +1423,6 @@ function resolveWorktreePort(appDir) {
1350
1423
  return deriveWorktreePort(linked.worktreeRoot);
1351
1424
  }
1352
1425
 
1353
- // src/cli/lib/dev-vars.ts
1354
- import { existsSync as existsSync7, readFileSync as readFileSync9 } from "fs";
1355
-
1356
1426
  // src/cli/lib/app-tokens.ts
1357
1427
  async function fetchPublicKey(authUrl) {
1358
1428
  const response = await fetch(`${authUrl}/api/auth/jwks`);
@@ -1402,501 +1472,18 @@ async function fetchAppIdentityToken(deployUrl, callerJwt, appId) {
1402
1472
  return body.token;
1403
1473
  }
1404
1474
 
1405
- // src/server/rooms/binding-manifest.ts
1406
- var AUTO_PROVISION_SENTINEL = "auto";
1407
- var VECTORIZE_METRICS = /* @__PURE__ */ new Set(["cosine", "euclidean", "dot-product"]);
1408
- var ALLOWED_BINDING_TYPES = /* @__PURE__ */ new Set([
1409
- "vectorize",
1410
- "ai",
1411
- "r2_bucket",
1412
- "kv_namespace",
1413
- "d1",
1414
- "queue",
1415
- "browser_rendering",
1416
- "analytics_engine",
1417
- "hyperdrive",
1418
- "ratelimit"
1419
- ]);
1420
- var RESERVED_BINDING_NAMES = /* @__PURE__ */ new Set([
1421
- "ASSETS",
1422
- "PLATFORM_WORKER",
1423
- "API_WORKER",
1424
- "DEEPSPACE_APP_ID",
1425
- "DEEPSPACE_RESOURCE_ID",
1426
- "DEEPSPACE_RELEASE_OPERATION_ID",
1427
- "APP_NAME",
1428
- "OWNER_USER_ID",
1429
- "AUTH_JWT_PUBLIC_KEY",
1430
- "AUTH_JWT_ISSUER",
1431
- "AUTH_WORKER_URL",
1432
- "APP_IDENTITY_TOKEN",
1433
- "APP_OWNER_JWT",
1434
- "USAGE_EVENTS"
1435
- ]);
1436
- function validateBindingManifest(manifest) {
1437
- if (!Array.isArray(manifest)) {
1438
- return { valid: false, errors: [{ reason: "Manifest must be an array" }] };
1439
- }
1440
- const errors = [];
1441
- const seenNames = /* @__PURE__ */ new Set();
1442
- for (const entry of manifest) {
1443
- if (!entry || typeof entry !== "object") {
1444
- errors.push({
1445
- reason: `Entry must be an object (got ${entry === null ? "null" : typeof entry})`
1446
- });
1447
- continue;
1448
- }
1449
- const e = entry;
1450
- const type = e.type;
1451
- const name = e.name;
1452
- if (typeof type !== "string" || !ALLOWED_BINDING_TYPES.has(type)) {
1453
- errors.push({
1454
- reason: `Disallowed binding type: ${typeof type === "string" ? type : `(${typeof type})`}. Allowed: ${[...ALLOWED_BINDING_TYPES].sort().join(", ")}`
1455
- });
1456
- continue;
1457
- }
1458
- if (typeof name !== "string" || !name) {
1459
- errors.push({ reason: `Binding of type "${type}" missing 'name' (string)` });
1460
- continue;
1461
- }
1462
- if (RESERVED_BINDING_NAMES.has(name)) {
1463
- errors.push({ reason: `Binding name "${name}" is reserved by the SDK` });
1464
- continue;
1465
- }
1466
- if (seenNames.has(name)) {
1467
- errors.push({ reason: `Duplicate binding name "${name}"` });
1468
- continue;
1469
- }
1470
- seenNames.add(name);
1471
- const binding = entry;
1472
- const fieldErr = requiredFieldError(binding);
1473
- if (fieldErr) errors.push({ binding, reason: fieldErr });
1474
- }
1475
- if (errors.length) return { valid: false, errors };
1476
- return { valid: true, bindings: manifest };
1477
- }
1478
- function requiredFieldError(b) {
1479
- switch (b.type) {
1480
- case "vectorize": {
1481
- if (!b.index_name) return `vectorize binding "${b.name}" missing index_name (or "auto")`;
1482
- if (b.index_name === AUTO_PROVISION_SENTINEL) {
1483
- if (typeof b.dimensions !== "number" || b.dimensions <= 0) {
1484
- return `vectorize binding "${b.name}" with index_name="auto" requires "dimensions" (positive number)`;
1485
- }
1486
- if (!b.metric || !VECTORIZE_METRICS.has(b.metric)) {
1487
- return `vectorize binding "${b.name}" with index_name="auto" requires "metric" (one of: ${[...VECTORIZE_METRICS].join(", ")})`;
1488
- }
1489
- }
1490
- return null;
1491
- }
1492
- case "r2_bucket":
1493
- return b.bucket_name ? null : `r2_bucket binding "${b.name}" missing bucket_name (or "auto")`;
1494
- case "kv_namespace": {
1495
- if (!b.namespace_id)
1496
- return `kv_namespace binding "${b.name}" missing namespace_id (or "auto")`;
1497
- if (b.namespace_id === AUTO_PROVISION_SENTINEL && !b.title) {
1498
- return `kv_namespace binding "${b.name}" with namespace_id="auto" requires "title"`;
1499
- }
1500
- return null;
1501
- }
1502
- case "d1": {
1503
- if (!b.id) return `d1 binding "${b.name}" missing id (or "auto")`;
1504
- if (b.id === AUTO_PROVISION_SENTINEL && !b.database_name) {
1505
- return `d1 binding "${b.name}" with id="auto" requires "database_name"`;
1506
- }
1507
- return null;
1508
- }
1509
- case "queue":
1510
- return b.queue_name ? null : `queue binding "${b.name}" missing queue_name (or "auto")`;
1511
- case "hyperdrive":
1512
- if (!b.id) return `hyperdrive binding "${b.name}" missing id`;
1513
- if (b.id === AUTO_PROVISION_SENTINEL) {
1514
- return `hyperdrive binding "${b.name}": auto-provisioning not yet supported (provide a real id)`;
1515
- }
1516
- return null;
1517
- case "ratelimit":
1518
- if (!/^\d+$/u.test(b.namespace_id) || Number(b.namespace_id) <= 0) {
1519
- return `ratelimit binding "${b.name}" requires a positive integer namespace_id string`;
1520
- }
1521
- if (!Number.isInteger(b.simple?.limit) || b.simple.limit <= 0) {
1522
- return `ratelimit binding "${b.name}" requires simple.limit (positive integer)`;
1523
- }
1524
- if (b.simple.period !== 10 && b.simple.period !== 60) {
1525
- return `ratelimit binding "${b.name}" requires simple.period (10 or 60)`;
1526
- }
1527
- return null;
1528
- case "ai":
1529
- case "browser_rendering":
1530
- case "analytics_engine":
1531
- return null;
1532
- // no required fields beyond `name`
1533
- default: {
1534
- const _exhaustive = b;
1535
- void _exhaustive;
1536
- return null;
1537
- }
1538
- }
1539
- }
1540
- function bindingManifestFromOutputConfig(outputConfig) {
1541
- const out = [];
1542
- const aiBinding = pluckString(outputConfig.ai, "binding");
1543
- if (aiBinding) out.push({ type: "ai", name: aiBinding });
1544
- for (const v of asObjectArray(outputConfig.vectorize)) {
1545
- const name = pluckString(v, "binding");
1546
- const index_name = pluckString(v, "index_name");
1547
- if (!name || !index_name) continue;
1548
- const dimensions = pluckNumber(v, "dimensions");
1549
- const metric = pluckString(v, "metric");
1550
- out.push({
1551
- type: "vectorize",
1552
- name,
1553
- index_name,
1554
- ...dimensions != null && { dimensions },
1555
- ...metric && (metric === "cosine" || metric === "euclidean" || metric === "dot-product") && { metric }
1556
- });
1557
- }
1558
- for (const r of asObjectArray(outputConfig.r2_buckets)) {
1559
- const name = pluckString(r, "binding");
1560
- const bucket_name = pluckString(r, "bucket_name");
1561
- if (name && bucket_name) out.push({ type: "r2_bucket", name, bucket_name });
1562
- }
1563
- for (const k of asObjectArray(outputConfig.kv_namespaces)) {
1564
- const name = pluckString(k, "binding");
1565
- const namespace_id = pluckString(k, "id");
1566
- if (!name || !namespace_id) continue;
1567
- const title = pluckString(k, "title");
1568
- out.push({
1569
- type: "kv_namespace",
1570
- name,
1571
- namespace_id,
1572
- ...title && { title }
1573
- });
1574
- }
1575
- for (const d of asObjectArray(outputConfig.d1_databases)) {
1576
- const name = pluckString(d, "binding");
1577
- const id = pluckString(d, "database_id");
1578
- if (!name || !id) continue;
1579
- const database_name = pluckString(d, "database_name");
1580
- out.push({
1581
- type: "d1",
1582
- name,
1583
- id,
1584
- ...database_name && { database_name }
1585
- });
1586
- }
1587
- const queues = isObject(outputConfig.queues) ? outputConfig.queues : null;
1588
- for (const q of asObjectArray(queues?.producers)) {
1589
- const name = pluckString(q, "binding");
1590
- const queue_name = pluckString(q, "queue");
1591
- if (name && queue_name) out.push({ type: "queue", name, queue_name });
1592
- }
1593
- const browserBinding = pluckString(outputConfig.browser, "binding");
1594
- if (browserBinding) out.push({ type: "browser_rendering", name: browserBinding });
1595
- for (const a of asObjectArray(outputConfig.analytics_engine_datasets)) {
1596
- const name = pluckString(a, "binding");
1597
- const dataset = pluckString(a, "dataset");
1598
- if (name) out.push({ type: "analytics_engine", name, dataset: dataset ?? void 0 });
1599
- }
1600
- for (const h of asObjectArray(outputConfig.hyperdrive)) {
1601
- const name = pluckString(h, "binding");
1602
- const id = pluckString(h, "id");
1603
- if (name && id) out.push({ type: "hyperdrive", name, id });
1604
- }
1605
- for (const r of asObjectArray(outputConfig.ratelimits)) {
1606
- const name = pluckString(r, "name");
1607
- const namespace_id = pluckString(r, "namespace_id");
1608
- const simple = isObject(r.simple) ? r.simple : null;
1609
- const limit = pluckNumber(simple, "limit");
1610
- const period = pluckNumber(simple, "period");
1611
- if (!name || !namespace_id || limit === null || period !== 10 && period !== 60) continue;
1612
- out.push({ type: "ratelimit", name, namespace_id, simple: { limit, period } });
1613
- }
1614
- return out;
1615
- }
1616
- function isObject(v) {
1617
- return v !== null && typeof v === "object" && !Array.isArray(v);
1618
- }
1619
- function pluckString(v, key) {
1620
- if (!isObject(v)) return null;
1621
- const val = v[key];
1622
- return typeof val === "string" && val.length > 0 ? val : null;
1623
- }
1624
- function pluckNumber(v, key) {
1625
- if (!isObject(v)) return null;
1626
- const val = v[key];
1627
- return typeof val === "number" && Number.isFinite(val) ? val : null;
1628
- }
1629
- function asObjectArray(v) {
1630
- if (!Array.isArray(v)) return [];
1631
- return v.filter(isObject);
1632
- }
1633
-
1634
- // src/cli/lib/secrets.ts
1635
- var GENERATED_SECRETS_DIVIDER = "# --- DeepSpace secrets cache: generated by `npx deepspace secrets`; do not edit manually ---";
1636
- var RESERVED_SECRET_NAMES = /* @__PURE__ */ new Set([
1637
- ...RESERVED_BINDING_NAMES,
1638
- "API_WORKER_URL",
1639
- "PLATFORM_WORKER_URL"
1640
- ]);
1641
- var SECRET_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
1642
- var CONFIG_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
1643
- var MAX_SECRET_VALUE_BYTES = 32 * 1024;
1644
- function validateConfigName(configName) {
1645
- if (!CONFIG_NAME_RE.test(configName)) {
1646
- throw new Error(
1647
- `Invalid config "${configName}". Use letters, numbers, underscores, or hyphens; start with a letter or number.`
1648
- );
1649
- }
1650
- return configName;
1651
- }
1652
- function validateSecretName(name) {
1653
- if (!SECRET_NAME_RE.test(name)) {
1654
- throw new Error(
1655
- `Invalid secret name "${name}". Use letters, numbers, and underscores; start with a letter or underscore.`
1656
- );
1657
- }
1658
- if (RESERVED_SECRET_NAMES.has(name)) {
1659
- throw new Error(
1660
- `"${name}" is managed by the DeepSpace SDK and can't be set as an app secret \u2014 the platform injects it at deploy time.`
1661
- );
1662
- }
1663
- return name;
1664
- }
1665
- function validateSecretValue(key, value) {
1666
- if (new TextEncoder().encode(value).byteLength > MAX_SECRET_VALUE_BYTES) {
1667
- throw new Error(`Secret "${key}" exceeds ${MAX_SECRET_VALUE_BYTES} bytes`);
1668
- }
1669
- }
1670
- function defaultConfigNameForEnv(wranglerEnv) {
1671
- return wranglerEnv ?? "prd";
1672
- }
1673
- async function secretsApi(deployUrl, token, path, init = {}) {
1674
- const res = await fetch(`${deployUrl}/api/secrets${path}`, {
1675
- ...init,
1676
- headers: {
1677
- Authorization: `Bearer ${token}`,
1678
- "Content-Type": "application/json",
1679
- ...init.headers ?? {}
1680
- }
1681
- });
1682
- const body = await res.json().catch(() => ({}));
1683
- if (!res.ok) {
1684
- const err = new Error(body.error ?? `Secrets request failed (${res.status})`);
1685
- err.status = res.status;
1686
- err.apiPath = path;
1687
- throw err;
1688
- }
1689
- return body;
1690
- }
1691
- function listSecrets(deployUrl, token, appId, configName) {
1692
- return secretsApi(deployUrl, token, `/${appId}/configs/${encodeURIComponent(configName)}`);
1693
- }
1694
- function fetchSecretsValues(deployUrl, token, appId, configName) {
1695
- return secretsApi(deployUrl, token, `/${appId}/configs/${encodeURIComponent(configName)}/values`);
1696
- }
1697
- function getSecretPlain(deployUrl, token, appId, configName, key) {
1698
- return secretsApi(
1699
- deployUrl,
1700
- token,
1701
- `/${appId}/configs/${encodeURIComponent(configName)}/secrets/${encodeURIComponent(key)}?plain=true`
1702
- );
1703
- }
1704
- function setSecret(deployUrl, token, appId, configName, key, value) {
1705
- return secretsApi(
1706
- deployUrl,
1707
- token,
1708
- `/${appId}/configs/${encodeURIComponent(configName)}/secrets/${encodeURIComponent(key)}`,
1709
- { method: "PUT", body: JSON.stringify({ value }) }
1710
- );
1711
- }
1712
- function deleteSecret(deployUrl, token, appId, configName, key) {
1713
- return secretsApi(
1714
- deployUrl,
1715
- token,
1716
- `/${appId}/configs/${encodeURIComponent(configName)}/secrets/${encodeURIComponent(key)}`,
1717
- { method: "DELETE" }
1718
- );
1719
- }
1720
- function uploadSecrets(deployUrl, token, appId, configName, secrets, replace) {
1721
- return secretsApi(
1722
- deployUrl,
1723
- token,
1724
- `/${appId}/configs/${encodeURIComponent(configName)}/secrets`,
1725
- { method: "PUT", body: JSON.stringify({ secrets, replace }) }
1726
- );
1727
- }
1728
- function listConfigs(deployUrl, token, appId) {
1729
- return secretsApi(deployUrl, token, `/${appId}/configs`);
1730
- }
1731
- function createConfig(deployUrl, token, appId, configName, copyFrom) {
1732
- return secretsApi(deployUrl, token, `/${appId}/configs`, {
1733
- method: "POST",
1734
- body: JSON.stringify({ name: configName, ...copyFrom ? { copyFrom } : {} })
1735
- });
1736
- }
1737
- function deleteConfig(deployUrl, token, appId, configName) {
1738
- return secretsApi(deployUrl, token, `/${appId}/configs/${encodeURIComponent(configName)}`, {
1739
- method: "DELETE"
1740
- });
1741
- }
1742
- async function pullAppSecretsCache(deployUrl, token, appId, configName) {
1743
- try {
1744
- const { secrets } = await fetchSecretsValues(deployUrl, token, appId, configName);
1745
- if (secrets === null || typeof secrets !== "object" || Array.isArray(secrets)) {
1746
- throw new ApiError(
1747
- `The secrets service at ${deployUrl} returned an unexpected response shape \u2014 check DEEPSPACE_DEPLOY_URL (is this the service the app lives on?).`,
1748
- 200,
1749
- "invalid_response",
1750
- `/${appId}/configs/${configName}/values`
1751
- );
1752
- }
1753
- return { appId, configName, values: secrets };
1754
- } catch (err) {
1755
- if (err?.status === 404) return null;
1756
- throw err;
1757
- }
1758
- }
1759
- function quoteDotenvValue(value) {
1760
- if (/^[A-Za-z0-9_./:@+-]*$/.test(value) && value !== "") return value;
1761
- const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r");
1762
- return `"${escaped}"`;
1763
- }
1764
- function renderSecretsCache(secrets, source) {
1765
- const keys = Object.keys(secrets).sort();
1766
- const lines = [
1767
- GENERATED_SECRETS_DIVIDER,
1768
- `# app ${source.appId} \xB7 config ${source.configName}`,
1769
- "# Use `npx deepspace secrets` to change these values."
1770
- ];
1771
- for (const key of keys) {
1772
- validateSecretValue(key, secrets[key]);
1773
- lines.push(`${key}=${quoteDotenvValue(secrets[key])}`);
1774
- }
1775
- return lines.join("\n");
1776
- }
1777
- function stripGeneratedSecretsCache(content) {
1778
- const idx = content.indexOf(GENERATED_SECRETS_DIVIDER);
1779
- if (idx === -1) return content;
1780
- return content.slice(0, idx).trimEnd() + "\n";
1781
- }
1782
- function parseSecretsUpload(content) {
1783
- const trimmed = content.trim();
1784
- if (!trimmed) return {};
1785
- if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
1786
- const parsed = JSON.parse(trimmed);
1787
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1788
- throw new Error("JSON upload must be an object of KEY=value strings.");
1789
- }
1790
- const out2 = {};
1791
- for (const [key, value] of Object.entries(parsed)) {
1792
- validateSecretName(key);
1793
- if (typeof value !== "string") throw new Error(`Secret "${key}" must be a string`);
1794
- validateSecretValue(key, value);
1795
- out2[key] = value;
1796
- }
1797
- return out2;
1798
- }
1799
- const out = {};
1800
- const lines = trimmed.split(/\r?\n/);
1801
- for (let i = 0; i < lines.length; i++) {
1802
- const line = lines[i].trim();
1803
- if (!line || line.startsWith("#")) continue;
1804
- const eq = line.indexOf("=");
1805
- if (eq <= 0) continue;
1806
- const key = line.slice(0, eq).trim();
1807
- if (!SECRET_NAME_RE.test(key)) continue;
1808
- validateSecretName(key);
1809
- const raw = line.slice(eq + 1).trim();
1810
- const quote = raw[0] === '"' || raw[0] === "'" ? raw[0] : null;
1811
- let value;
1812
- if (!quote) {
1813
- value = raw;
1814
- } else if (endsWithUnescapedQuote(raw.slice(1), quote)) {
1815
- const inner = raw.slice(1, -1);
1816
- value = quote === '"' ? unescapeDotenv(inner) : inner;
1817
- } else {
1818
- const startLine = i + 1;
1819
- const parts = [raw.slice(1)];
1820
- let closed = false;
1821
- while (++i < lines.length) {
1822
- const cont = lines[i].trimEnd();
1823
- if (endsWithUnescapedQuote(cont, quote)) {
1824
- parts.push(cont.slice(0, -1));
1825
- closed = true;
1826
- break;
1827
- }
1828
- parts.push(lines[i]);
1829
- }
1830
- if (!closed) {
1831
- throw new Error(
1832
- `Unterminated quoted value for "${key}" (opens on line ${startLine}). Close the ${quote === '"' ? "double" : "single"} quote, or escape newlines as \\n on one line.`
1833
- );
1834
- }
1835
- value = parts.join("\n");
1836
- if (quote === '"') value = unescapeDotenv(value);
1837
- }
1838
- validateSecretValue(key, value);
1839
- out[key] = value;
1840
- }
1841
- return out;
1842
- }
1843
- function endsWithUnescapedQuote(s, q) {
1844
- if (!s.endsWith(q)) return false;
1845
- if (q === "'") return true;
1846
- let backslashes = 0;
1847
- for (let j = s.length - 2; j >= 0 && s[j] === "\\"; j--) backslashes++;
1848
- return backslashes % 2 === 0;
1849
- }
1850
- function unescapeDotenv(v) {
1851
- let out = "";
1852
- for (let i = 0; i < v.length; i++) {
1853
- if (v[i] === "\\" && i + 1 < v.length) {
1854
- const c = v[++i];
1855
- out += c === "n" ? "\n" : c === "r" ? "\r" : c;
1856
- } else {
1857
- out += v[i];
1858
- }
1859
- }
1860
- return out;
1861
- }
1862
- function shellSingleQuote(value) {
1863
- return `'${value.replace(/'/g, "'\\''")}'`;
1864
- }
1865
- function formatSecretsDownload(secrets, format) {
1866
- const entries = Object.entries(secrets).sort(([a], [b]) => a.localeCompare(b));
1867
- if (format === "json") {
1868
- return JSON.stringify(Object.fromEntries(entries), null, 2) + "\n";
1869
- }
1870
- if (format === "shell") {
1871
- return entries.map(([k, v]) => `export ${k}=${shellSingleQuote(v)}`).join("\n") + "\n";
1872
- }
1873
- return entries.map(([k, v]) => `${k}=${quoteDotenvValue(v)}`).join("\n") + "\n";
1874
- }
1875
- async function refreshSecretsCache(deployUrl, token, appId, wranglerEnv) {
1876
- const configName = defaultConfigNameForEnv(wranglerEnv);
1877
- const pulled = await pullAppSecretsCache(deployUrl, token, appId, configName);
1878
- if (!pulled) return null;
1879
- const count = Object.keys(pulled.values).length;
1880
- return {
1881
- pulled,
1882
- rendered: renderSecretsCache(pulled.values, pulled),
1883
- // An empty store is the common case and not worth a status line.
1884
- summary: count ? `Secrets: ${pulled.configName} (${count} secret${count === 1 ? "" : "s"})` : null
1885
- };
1886
- }
1887
-
1888
- // src/cli/lib/secure-file.ts
1889
- import { chmodSync, writeFileSync as writeFileSync5 } from "fs";
1890
- function writeSecretFileSync(path, content) {
1891
- writeFileSync5(path, content, { mode: 384 });
1892
- try {
1893
- chmodSync(path, 384);
1894
- } catch {
1475
+ // src/cli/lib/secure-file.ts
1476
+ import { chmodSync, writeFileSync as writeFileSync5 } from "fs";
1477
+ function writeSecretFileSync(path, content) {
1478
+ writeFileSync5(path, content, { mode: 384 });
1479
+ try {
1480
+ chmodSync(path, 384);
1481
+ } catch {
1895
1482
  }
1896
1483
  }
1897
1484
 
1898
1485
  // src/cli/lib/wrangler-env.ts
1899
- import { existsSync as existsSync6, readFileSync as readFileSync8, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
1486
+ import { existsSync as existsSync6, readFileSync as readFileSync9, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
1900
1487
  import { join as join7 } from "path";
1901
1488
  import { parse as parseToml3, stringify as stringifyToml } from "smol-toml";
1902
1489
 
@@ -1997,26 +1584,12 @@ function resolveAppNameForEnv(config, envName) {
1997
1584
  }
1998
1585
  return resolveAppName(envBlock.name);
1999
1586
  }
2000
- function devVarsPathFor(appDir, envName, opts = {}) {
2001
- if (opts.sharedDevVarsCache || !envName) return join7(appDir, ".dev.vars");
2002
- return join7(appDir, `.dev.vars.${envName}`);
2003
- }
2004
- function legacyEnvDevVarsPathFor(appDir, envName) {
2005
- return envName ? join7(appDir, `.dev.vars.${envName}`) : void 0;
2006
- }
2007
- function warnIfLegacyEnvDevVarsExists(appDir, envName, warn = console.warn) {
2008
- const legacyPath = legacyEnvDevVarsPathFor(appDir, envName);
2009
- if (!legacyPath || !existsSync6(legacyPath)) return;
2010
- warn(
2011
- `Warning: ${legacyPath} exists, but DeepSpace now uses ${devVarsPathFor(appDir, envName, { sharedDevVarsCache: true })} for every environment. This run will ignore ${legacyPath}; move any secrets you still need into .dev.vars, then remove the env-specific file when you're ready.`
2012
- );
1587
+ function devVarsPathFor(appDir) {
1588
+ return join7(appDir, ".dev.vars");
2013
1589
  }
2014
- function prepareWranglerEnvConfig(appDir, envName, opts = {}) {
1590
+ function prepareWranglerEnvConfig(appDir, envName) {
2015
1591
  if (!envName) return { cleanup() {
2016
1592
  } };
2017
- if (!opts.sharedDevVarsCache) return { wranglerEnv: envName, cleanup() {
2018
- } };
2019
- warnIfLegacyEnvDevVarsExists(appDir, envName, opts.warn);
2020
1593
  const config = readWranglerConfig(appDir);
2021
1594
  const resolved = resolveAppNameForEnv(config, envName);
2022
1595
  if (!resolved.ok) throw new Error(resolved.reason);
@@ -2045,8 +1618,6 @@ function wranglerViteEnv(baseEnv, prepared, extraEnv = {}) {
2045
1618
  if (prepared.configPath) {
2046
1619
  delete env.CLOUDFLARE_ENV;
2047
1620
  env.CLOUDFLARE_VITE_WRANGLER_CONFIG_PATH = prepared.configPath;
2048
- } else if (prepared.wranglerEnv) {
2049
- env.CLOUDFLARE_ENV = prepared.wranglerEnv;
2050
1621
  }
2051
1622
  return env;
2052
1623
  }
@@ -2090,7 +1661,7 @@ var WranglerConfigError = class extends Error {
2090
1661
  };
2091
1662
  function readWranglerConfig(appDir) {
2092
1663
  const path = join7(appDir, "wrangler.toml");
2093
- const raw = readFileSync8(path, "utf-8");
1664
+ const raw = readFileSync9(path, "utf-8");
2094
1665
  try {
2095
1666
  return parseToml3(raw);
2096
1667
  } catch (err) {
@@ -2102,369 +1673,706 @@ function hasWranglerConfig(appDir) {
2102
1673
  }
2103
1674
 
2104
1675
  // src/cli/lib/dev-vars.ts
2105
- var SDK_MANAGED_KEYS = /* @__PURE__ */ new Set([
1676
+ var HEADER = [
1677
+ "# Generated by deepspace \u2014 do not edit; this file is rewritten on every",
1678
+ "# dev, test, deploy, and secrets pull run. App values live in the secret store:",
1679
+ "# npx deepspace secrets set KEY=value"
1680
+ ].join("\n");
1681
+ function requireAppIdFor(appDir, wranglerEnv) {
1682
+ const id = readAppId(appDir, wranglerEnv);
1683
+ if (id) return id;
1684
+ throw new Error(
1685
+ "wrangler.toml has no DEEPSPACE_APP_ID" + (wranglerEnv ? ` for [env.${wranglerEnv}]` : "") + ". Run `deepspace app init` (or `deepspace app init --env <name>`) and retry."
1686
+ );
1687
+ }
1688
+ async function writeDevVars(appDir, ownerId, callerJwt, wranglerEnv, opts = {}) {
1689
+ const urls = PLATFORM_URLS;
1690
+ const publicKey = await fetchPublicKey(urls.auth);
1691
+ const appId = opts.appId ?? requireAppIdFor(appDir, wranglerEnv);
1692
+ const appOwnerJwt = await mintAppOwnerJwt(urls.auth, callerJwt, appId);
1693
+ const appIdentityToken = await fetchAppIdentityToken(urls.deploy, callerJwt, appId);
1694
+ const sdkVars = [
1695
+ `AUTH_JWT_PUBLIC_KEY="${publicKey}"`,
1696
+ `AUTH_JWT_ISSUER=${urls.auth}/api/auth`,
1697
+ `AUTH_WORKER_URL=${urls.auth}`,
1698
+ `API_WORKER_URL=${urls.api}`,
1699
+ `PLATFORM_WORKER_URL=${urls.platform}`,
1700
+ `OWNER_USER_ID=${ownerId}`,
1701
+ `APP_OWNER_JWT=${appOwnerJwt}`,
1702
+ ...appIdentityToken ? [`APP_IDENTITY_TOKEN=${appIdentityToken}`] : [],
1703
+ `ALLOW_DEBUG_ROUTES=true`
1704
+ ].join("\n");
1705
+ const devVarsPath = devVarsPathFor(appDir);
1706
+ const cache = opts.generatedSecretsCache;
1707
+ writeSecretFileSync(
1708
+ devVarsPath,
1709
+ [HEADER, "", sdkVars, ...cache ? ["", cache] : [], ""].join("\n")
1710
+ );
1711
+ }
1712
+
1713
+ // src/cli/lib/install-status.ts
1714
+ import { existsSync as existsSync8, readFileSync as readFileSync11, realpathSync as realpathSync3 } from "fs";
1715
+ import { dirname as dirname5, join as join9, resolve as resolve6 } from "path";
1716
+
1717
+ // src/cli/lib/package-manager.ts
1718
+ import { existsSync as existsSync7, readFileSync as readFileSync10 } from "fs";
1719
+ import { join as join8 } from "path";
1720
+ function detectPackageManager(appDir) {
1721
+ try {
1722
+ const pkg2 = JSON.parse(readFileSync10(join8(appDir, "package.json"), "utf-8"));
1723
+ const name = typeof pkg2.packageManager === "string" ? pkg2.packageManager.split("@")[0] : "";
1724
+ if (name === "npm" || name === "pnpm" || name === "yarn" || name === "bun") return name;
1725
+ } catch {
1726
+ }
1727
+ if (existsSync7(join8(appDir, "pnpm-lock.yaml"))) return "pnpm";
1728
+ if (existsSync7(join8(appDir, "yarn.lock"))) return "yarn";
1729
+ if (existsSync7(join8(appDir, "bun.lock")) || existsSync7(join8(appDir, "bun.lockb"))) return "bun";
1730
+ return "npm";
1731
+ }
1732
+ function installCommand(appDir) {
1733
+ return `${detectPackageManager(appDir)} install`;
1734
+ }
1735
+
1736
+ // src/cli/lib/install-status.ts
1737
+ function installState(appDir) {
1738
+ if (resolvesDeepspace(appDir)) return "ready";
1739
+ const sentinelDir = join9(appDir, ".deepspace");
1740
+ if (existsSync8(join9(sentinelDir, "install.err"))) return "failed";
1741
+ if (existsSync8(join9(sentinelDir, "install.started"))) {
1742
+ if (!existsSync8(join9(sentinelDir, "install.done")) && installWorkerAlive(sentinelDir)) {
1743
+ return "installing";
1744
+ }
1745
+ return "failed";
1746
+ }
1747
+ return "missing";
1748
+ }
1749
+ function tailHint(logPath, platform = process.platform) {
1750
+ return platform === "win32" ? `Get-Content -Wait ${logPath}` : `tail -f ${logPath}`;
1751
+ }
1752
+ function ensureInstallReady(appDir) {
1753
+ if (resolvesDeepspace(appDir)) return;
1754
+ const sentinelDir = join9(appDir, ".deepspace");
1755
+ const errPath = join9(sentinelDir, "install.err");
1756
+ const startedPath = join9(sentinelDir, "install.started");
1757
+ const donePath = join9(sentinelDir, "install.done");
1758
+ const logPath = join9(sentinelDir, "install.log");
1759
+ if (existsSync8(errPath)) {
1760
+ const detail = readFileSync11(errPath, "utf-8").trimEnd();
1761
+ const log29 = existsSync8(logPath) ? ` Full log: ${logPath}.` : "";
1762
+ throw new Refusal(
1763
+ `Dependency install failed: ${detail}.${log29} Run \`npm install\` (or \`bun install\`) manually, then retry.`,
1764
+ "install_failed",
1765
+ { action: { cwd: appDir, argv: [detectPackageManager(appDir), "install"] } }
1766
+ );
1767
+ }
1768
+ if (existsSync8(startedPath) && !existsSync8(donePath)) {
1769
+ if (!installWorkerAlive(sentinelDir)) {
1770
+ const log29 = existsSync8(logPath) ? ` See what happened: ${logPath}.` : "";
1771
+ throw new Refusal(
1772
+ `The dependency install is no longer running and never finished.${log29} Run \`npm install\` (or \`bun install\`) manually, then retry.`,
1773
+ "install_failed",
1774
+ { action: { cwd: appDir, argv: [detectPackageManager(appDir), "install"] } }
1775
+ );
1776
+ }
1777
+ const tail = existsSync8(logPath) ? ` Tail progress: ${tailHint(logPath)}.` : "";
1778
+ throw new Refusal(
1779
+ `Dependencies are still installing.${tail} Retry once it finishes.`,
1780
+ "install_in_progress"
1781
+ );
1782
+ }
1783
+ throw new Refusal(
1784
+ "Dependencies not installed. Run `npm install` (or `bun install`) first.",
1785
+ "deps_missing",
1786
+ {
1787
+ action: { cwd: appDir, argv: [detectPackageManager(appDir), "install"] }
1788
+ }
1789
+ );
1790
+ }
1791
+ function resolvesDeepspace(fromDir) {
1792
+ let dir = canonicalPath2(fromDir);
1793
+ let checkoutRoot = null;
1794
+ try {
1795
+ checkoutRoot = canonicalPath2(repoToplevel(dir));
1796
+ } catch {
1797
+ }
1798
+ for (; ; ) {
1799
+ if (existsSync8(join9(dir, "node_modules", "deepspace", "package.json"))) return true;
1800
+ if (dir === checkoutRoot) return false;
1801
+ const parent = dirname5(dir);
1802
+ if (parent === dir) return false;
1803
+ dir = parent;
1804
+ }
1805
+ }
1806
+ function canonicalPath2(path) {
1807
+ try {
1808
+ return realpathSync3(path);
1809
+ } catch {
1810
+ return resolve6(path);
1811
+ }
1812
+ }
1813
+ function installWorkerAlive(sentinelDir) {
1814
+ const pidPath = join9(sentinelDir, "install.pid");
1815
+ if (!existsSync8(pidPath)) return false;
1816
+ const pid = Number(readFileSync11(pidPath, "utf-8").trim());
1817
+ if (!Number.isSafeInteger(pid) || pid <= 0) return false;
1818
+ try {
1819
+ process.kill(pid, 0);
1820
+ return true;
1821
+ } catch (err) {
1822
+ return err.code === "EPERM";
1823
+ }
1824
+ }
1825
+
1826
+ // src/cli/lib/preflight.ts
1827
+ import { execFileSync } from "child_process";
1828
+ import { existsSync as existsSync9 } from "fs";
1829
+ import * as nodeModule from "module";
1830
+ import { resolve as resolve7 } from "path";
1831
+ function preflightNodeVersion(command, hasRegisterHooks = typeof nodeModule.registerHooks === "function", version = process.versions.node) {
1832
+ const [major, minor] = version.split(".").map(Number);
1833
+ const supportedLine = major === 22 && minor >= 15 || major === 24 || major === 26;
1834
+ if (hasRegisterHooks && supportedLine) return;
1835
+ throw new Refusal(
1836
+ `deepspace ${command} requires a supported Node release: 22.15+, 24, or 26 (found v${version}). Install the current LTS from https://nodejs.org and re-run.`,
1837
+ supportedLine ? "node_capability_missing" : "node_unsupported"
1838
+ );
1839
+ }
1840
+ var DLL_NOT_FOUND = -1073741515;
1841
+ function preflightWindowsWorkerd(appDir) {
1842
+ if (process.platform !== "win32") return;
1843
+ const workerdExe = resolve7(
1844
+ appDir,
1845
+ "node_modules",
1846
+ "@cloudflare",
1847
+ "workerd-windows-64",
1848
+ "bin",
1849
+ "workerd.exe"
1850
+ );
1851
+ if (!existsSync9(workerdExe)) return;
1852
+ try {
1853
+ execFileSync(workerdExe, ["--help"], { stdio: "pipe", timeout: 1e4 });
1854
+ return;
1855
+ } catch (err) {
1856
+ const status3 = err.status;
1857
+ if (status3 !== DLL_NOT_FOUND) return;
1858
+ throw new Refusal(
1859
+ "workerd (Cloudflare Workers runtime) cannot start on this system: it needs the Microsoft Visual C++ Redistributable, which is not installed. Install it with `winget install Microsoft.VCRedist.2015+.x64` (or choco install -y vcredist140, or https://aka.ms/vs/17/release/vc_redist.x64.exe), then restart this terminal and re-run.",
1860
+ "vcredist_missing"
1861
+ );
1862
+ }
1863
+ }
1864
+
1865
+ // src/cli/lib/macos-junk.ts
1866
+ import { readdirSync as readdirSync2, rmSync as rmSync3 } from "fs";
1867
+ import { join as join10 } from "path";
1868
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".wrangler", ".vite"]);
1869
+ function removeMacosJunk(dir) {
1870
+ let removed = 0;
1871
+ let entries;
1872
+ try {
1873
+ entries = readdirSync2(dir, { withFileTypes: true });
1874
+ } catch {
1875
+ return removed;
1876
+ }
1877
+ for (const entry of entries) {
1878
+ const path = join10(dir, entry.name);
1879
+ if (entry.isDirectory()) {
1880
+ if (!SKIP_DIRS.has(entry.name)) removed += removeMacosJunk(path);
1881
+ continue;
1882
+ }
1883
+ if (entry.name === ".DS_Store" || entry.name.startsWith("._")) {
1884
+ try {
1885
+ rmSync3(path, { force: true });
1886
+ removed++;
1887
+ } catch {
1888
+ }
1889
+ }
1890
+ }
1891
+ return removed;
1892
+ }
1893
+
1894
+ // src/server/rooms/binding-manifest.ts
1895
+ var AUTO_PROVISION_SENTINEL = "auto";
1896
+ var VECTORIZE_METRICS = /* @__PURE__ */ new Set(["cosine", "euclidean", "dot-product"]);
1897
+ var ALLOWED_BINDING_TYPES = /* @__PURE__ */ new Set([
1898
+ "vectorize",
1899
+ "ai",
1900
+ "r2_bucket",
1901
+ "kv_namespace",
1902
+ "d1",
1903
+ "queue",
1904
+ "browser_rendering",
1905
+ "analytics_engine",
1906
+ "hyperdrive",
1907
+ "ratelimit"
1908
+ ]);
1909
+ var RESERVED_BINDING_NAMES = /* @__PURE__ */ new Set([
1910
+ "ASSETS",
1911
+ "PLATFORM_WORKER",
1912
+ "API_WORKER",
1913
+ "DEEPSPACE_APP_ID",
1914
+ "DEEPSPACE_RESOURCE_ID",
1915
+ "DEEPSPACE_RELEASE_OPERATION_ID",
1916
+ "APP_NAME",
1917
+ "OWNER_USER_ID",
2106
1918
  "AUTH_JWT_PUBLIC_KEY",
2107
1919
  "AUTH_JWT_ISSUER",
2108
1920
  "AUTH_WORKER_URL",
2109
- "API_WORKER_URL",
2110
- "PLATFORM_WORKER_URL",
2111
- "OWNER_USER_ID",
2112
- "APP_OWNER_JWT",
2113
1921
  "APP_IDENTITY_TOKEN",
2114
- "ALLOW_DEBUG_ROUTES"
1922
+ "APP_OWNER_JWT",
1923
+ "USAGE_EVENTS"
2115
1924
  ]);
2116
- var DEV_VARS_DIVIDER = "# --- not managed by the SDK; preserved across dev/test runs ---";
2117
- function extractCustomDevVars(content) {
2118
- const lines = content.split("\n");
2119
- const out = [];
2120
- let belowDivider = false;
2121
- let i = 0;
2122
- while (i < lines.length) {
2123
- const line = lines[i];
2124
- if (line.trim() === DEV_VARS_DIVIDER.trim()) {
2125
- belowDivider = true;
2126
- i++;
2127
- continue;
2128
- }
2129
- const eq = line.indexOf("=");
2130
- const keyMatch = eq >= 0 ? line.slice(0, eq).match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*$/) : null;
2131
- if (!keyMatch) {
2132
- out.push(line);
2133
- i++;
2134
- continue;
2135
- }
2136
- const key = keyMatch[1];
2137
- const valueStart = line.slice(eq + 1);
2138
- const block = [line];
2139
- if (valueStart.startsWith('"') && !hasUnescapedQuote(valueStart.slice(1))) {
2140
- i++;
2141
- while (i < lines.length) {
2142
- block.push(lines[i]);
2143
- const closed = hasUnescapedQuote(lines[i]);
2144
- i++;
2145
- if (closed) break;
2146
- }
2147
- } else {
2148
- i++;
2149
- }
2150
- if (belowDivider || !SDK_MANAGED_KEYS.has(key)) out.push(...block);
1925
+ function validateBindingManifest(manifest) {
1926
+ if (!Array.isArray(manifest)) {
1927
+ return { valid: false, errors: [{ reason: "Manifest must be an array" }] };
2151
1928
  }
2152
- while (out.length && out[out.length - 1].trim() === "") out.pop();
2153
- return out.join("\n");
2154
- }
2155
- function hasUnescapedQuote(value) {
2156
- let escaped = false;
2157
- for (const char of value) {
2158
- if (escaped) {
2159
- escaped = false;
1929
+ const errors = [];
1930
+ const seenNames = /* @__PURE__ */ new Set();
1931
+ for (const entry of manifest) {
1932
+ if (!entry || typeof entry !== "object") {
1933
+ errors.push({
1934
+ reason: `Entry must be an object (got ${entry === null ? "null" : typeof entry})`
1935
+ });
2160
1936
  continue;
2161
1937
  }
2162
- if (char === "\\") {
2163
- escaped = true;
1938
+ const e = entry;
1939
+ const type = e.type;
1940
+ const name = e.name;
1941
+ if (typeof type !== "string" || !ALLOWED_BINDING_TYPES.has(type)) {
1942
+ errors.push({
1943
+ reason: `Disallowed binding type: ${typeof type === "string" ? type : `(${typeof type})`}. Allowed: ${[...ALLOWED_BINDING_TYPES].sort().join(", ")}`
1944
+ });
2164
1945
  continue;
2165
1946
  }
2166
- if (char === '"') return true;
2167
- }
2168
- return false;
2169
- }
2170
- function parseDevVars(content) {
2171
- const result = {};
2172
- const lines = content.split("\n");
2173
- let i = 0;
2174
- while (i < lines.length) {
2175
- const line = lines[i];
2176
- const trimmed = line.trim();
2177
- if (!trimmed || trimmed.startsWith("#")) {
2178
- i++;
1947
+ if (typeof name !== "string" || !name) {
1948
+ errors.push({ reason: `Binding of type "${type}" missing 'name' (string)` });
2179
1949
  continue;
2180
1950
  }
2181
- const eq = line.indexOf("=");
2182
- if (eq < 0) {
2183
- i++;
1951
+ if (RESERVED_BINDING_NAMES.has(name)) {
1952
+ errors.push({ reason: `Binding name "${name}" is reserved by the SDK` });
2184
1953
  continue;
2185
1954
  }
2186
- const keyMatch = line.slice(0, eq).match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*$/);
2187
- if (!keyMatch) {
2188
- i++;
1955
+ if (seenNames.has(name)) {
1956
+ errors.push({ reason: `Duplicate binding name "${name}"` });
2189
1957
  continue;
2190
1958
  }
2191
- const key = keyMatch[1];
2192
- let value = line.slice(eq + 1);
2193
- let closed = !value.startsWith('"') || hasUnescapedQuote(value.slice(1));
2194
- if (!closed) {
2195
- i++;
2196
- while (i < lines.length) {
2197
- value += "\n" + lines[i];
2198
- if (hasUnescapedQuote(lines[i])) {
2199
- closed = true;
2200
- i++;
2201
- break;
1959
+ seenNames.add(name);
1960
+ const binding = entry;
1961
+ const fieldErr = requiredFieldError(binding);
1962
+ if (fieldErr) errors.push({ binding, reason: fieldErr });
1963
+ }
1964
+ if (errors.length) return { valid: false, errors };
1965
+ return { valid: true, bindings: manifest };
1966
+ }
1967
+ function requiredFieldError(b) {
1968
+ switch (b.type) {
1969
+ case "vectorize": {
1970
+ if (!b.index_name) return `vectorize binding "${b.name}" missing index_name (or "auto")`;
1971
+ if (b.index_name === AUTO_PROVISION_SENTINEL) {
1972
+ if (typeof b.dimensions !== "number" || b.dimensions <= 0) {
1973
+ return `vectorize binding "${b.name}" with index_name="auto" requires "dimensions" (positive number)`;
1974
+ }
1975
+ if (!b.metric || !VECTORIZE_METRICS.has(b.metric)) {
1976
+ return `vectorize binding "${b.name}" with index_name="auto" requires "metric" (one of: ${[...VECTORIZE_METRICS].join(", ")})`;
2202
1977
  }
2203
- i++;
2204
1978
  }
2205
- } else {
2206
- i++;
1979
+ return null;
2207
1980
  }
2208
- if (value.startsWith('"') && !closed) {
2209
- throw new Error(
2210
- `parseDevVars: unterminated quoted value for key "${key}" \u2014 check your .dev.vars file for a missing closing quote.`
2211
- );
1981
+ case "r2_bucket":
1982
+ return b.bucket_name ? null : `r2_bucket binding "${b.name}" missing bucket_name (or "auto")`;
1983
+ case "kv_namespace": {
1984
+ if (!b.namespace_id)
1985
+ return `kv_namespace binding "${b.name}" missing namespace_id (or "auto")`;
1986
+ if (b.namespace_id === AUTO_PROVISION_SENTINEL && !b.title) {
1987
+ return `kv_namespace binding "${b.name}" with namespace_id="auto" requires "title"`;
1988
+ }
1989
+ return null;
2212
1990
  }
2213
- if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
2214
- value = unescapeQuotedDevVar(value.slice(1, -1));
1991
+ case "d1": {
1992
+ if (!b.id) return `d1 binding "${b.name}" missing id (or "auto")`;
1993
+ if (b.id === AUTO_PROVISION_SENTINEL && !b.database_name) {
1994
+ return `d1 binding "${b.name}" with id="auto" requires "database_name"`;
1995
+ }
1996
+ return null;
1997
+ }
1998
+ case "queue":
1999
+ return b.queue_name ? null : `queue binding "${b.name}" missing queue_name (or "auto")`;
2000
+ case "hyperdrive":
2001
+ if (!b.id) return `hyperdrive binding "${b.name}" missing id`;
2002
+ if (b.id === AUTO_PROVISION_SENTINEL) {
2003
+ return `hyperdrive binding "${b.name}": auto-provisioning not yet supported (provide a real id)`;
2004
+ }
2005
+ return null;
2006
+ case "ratelimit":
2007
+ if (!/^\d+$/u.test(b.namespace_id) || Number(b.namespace_id) <= 0) {
2008
+ return `ratelimit binding "${b.name}" requires a positive integer namespace_id string`;
2009
+ }
2010
+ if (!Number.isInteger(b.simple?.limit) || b.simple.limit <= 0) {
2011
+ return `ratelimit binding "${b.name}" requires simple.limit (positive integer)`;
2012
+ }
2013
+ if (b.simple.period !== 10 && b.simple.period !== 60) {
2014
+ return `ratelimit binding "${b.name}" requires simple.period (10 or 60)`;
2015
+ }
2016
+ return null;
2017
+ case "ai":
2018
+ case "browser_rendering":
2019
+ case "analytics_engine":
2020
+ return null;
2021
+ // no required fields beyond `name`
2022
+ default: {
2023
+ const _exhaustive = b;
2024
+ void _exhaustive;
2025
+ return null;
2215
2026
  }
2216
- result[key] = value;
2217
2027
  }
2218
- return result;
2219
2028
  }
2220
- function unescapeQuotedDevVar(value) {
2221
- let out = "";
2222
- for (let i = 0; i < value.length; i++) {
2223
- const char = value[i];
2224
- if (char !== "\\" || i === value.length - 1) {
2225
- out += char;
2226
- continue;
2227
- }
2228
- const next = value[i + 1];
2229
- if (next === "\\" || next === '"') {
2230
- out += next;
2231
- i++;
2232
- continue;
2233
- }
2234
- out += char;
2029
+ function bindingManifestFromOutputConfig(outputConfig) {
2030
+ const out = [];
2031
+ const aiBinding = pluckString(outputConfig.ai, "binding");
2032
+ if (aiBinding) out.push({ type: "ai", name: aiBinding });
2033
+ for (const v of asObjectArray(outputConfig.vectorize)) {
2034
+ const name = pluckString(v, "binding");
2035
+ const index_name = pluckString(v, "index_name");
2036
+ if (!name || !index_name) continue;
2037
+ const dimensions = pluckNumber(v, "dimensions");
2038
+ const metric = pluckString(v, "metric");
2039
+ out.push({
2040
+ type: "vectorize",
2041
+ name,
2042
+ index_name,
2043
+ ...dimensions != null && { dimensions },
2044
+ ...metric && (metric === "cosine" || metric === "euclidean" || metric === "dot-product") && { metric }
2045
+ });
2046
+ }
2047
+ for (const r of asObjectArray(outputConfig.r2_buckets)) {
2048
+ const name = pluckString(r, "binding");
2049
+ const bucket_name = pluckString(r, "bucket_name");
2050
+ if (name && bucket_name) out.push({ type: "r2_bucket", name, bucket_name });
2051
+ }
2052
+ for (const k of asObjectArray(outputConfig.kv_namespaces)) {
2053
+ const name = pluckString(k, "binding");
2054
+ const namespace_id = pluckString(k, "id");
2055
+ if (!name || !namespace_id) continue;
2056
+ const title = pluckString(k, "title");
2057
+ out.push({
2058
+ type: "kv_namespace",
2059
+ name,
2060
+ namespace_id,
2061
+ ...title && { title }
2062
+ });
2063
+ }
2064
+ for (const d of asObjectArray(outputConfig.d1_databases)) {
2065
+ const name = pluckString(d, "binding");
2066
+ const id = pluckString(d, "database_id");
2067
+ if (!name || !id) continue;
2068
+ const database_name = pluckString(d, "database_name");
2069
+ out.push({
2070
+ type: "d1",
2071
+ name,
2072
+ id,
2073
+ ...database_name && { database_name }
2074
+ });
2075
+ }
2076
+ const queues = isObject(outputConfig.queues) ? outputConfig.queues : null;
2077
+ for (const q of asObjectArray(queues?.producers)) {
2078
+ const name = pluckString(q, "binding");
2079
+ const queue_name = pluckString(q, "queue");
2080
+ if (name && queue_name) out.push({ type: "queue", name, queue_name });
2081
+ }
2082
+ const browserBinding = pluckString(outputConfig.browser, "binding");
2083
+ if (browserBinding) out.push({ type: "browser_rendering", name: browserBinding });
2084
+ for (const a of asObjectArray(outputConfig.analytics_engine_datasets)) {
2085
+ const name = pluckString(a, "binding");
2086
+ const dataset = pluckString(a, "dataset");
2087
+ if (name) out.push({ type: "analytics_engine", name, dataset: dataset ?? void 0 });
2088
+ }
2089
+ for (const h of asObjectArray(outputConfig.hyperdrive)) {
2090
+ const name = pluckString(h, "binding");
2091
+ const id = pluckString(h, "id");
2092
+ if (name && id) out.push({ type: "hyperdrive", name, id });
2093
+ }
2094
+ for (const r of asObjectArray(outputConfig.ratelimits)) {
2095
+ const name = pluckString(r, "name");
2096
+ const namespace_id = pluckString(r, "namespace_id");
2097
+ const simple = isObject(r.simple) ? r.simple : null;
2098
+ const limit = pluckNumber(simple, "limit");
2099
+ const period = pluckNumber(simple, "period");
2100
+ if (!name || !namespace_id || limit === null || period !== 10 && period !== 60) continue;
2101
+ out.push({ type: "ratelimit", name, namespace_id, simple: { limit, period } });
2235
2102
  }
2236
2103
  return out;
2237
2104
  }
2238
- function requireAppIdFor(appDir, wranglerEnv) {
2239
- const id = readAppId(appDir, wranglerEnv);
2240
- if (id) return id;
2241
- throw new Error(
2242
- "wrangler.toml has no DEEPSPACE_APP_ID" + (wranglerEnv ? ` for [env.${wranglerEnv}]` : "") + ". Run `deepspace app init` (or `deepspace app init --env <name>`) and retry."
2243
- );
2105
+ function isObject(v) {
2106
+ return v !== null && typeof v === "object" && !Array.isArray(v);
2244
2107
  }
2245
- async function writeDevVars(appDir, ownerId, callerJwt, wranglerEnv, opts = {}) {
2246
- const urls = PLATFORM_URLS;
2247
- const publicKey = await fetchPublicKey(urls.auth);
2248
- const appId = opts.appId ?? requireAppIdFor(appDir, wranglerEnv);
2249
- const appOwnerJwt = await mintAppOwnerJwt(urls.auth, callerJwt, appId);
2250
- const appIdentityToken = await fetchAppIdentityToken(urls.deploy, callerJwt, appId);
2251
- const sdkVars = [
2252
- `AUTH_JWT_PUBLIC_KEY="${publicKey}"`,
2253
- `AUTH_JWT_ISSUER=${urls.auth}/api/auth`,
2254
- `AUTH_WORKER_URL=${urls.auth}`,
2255
- `API_WORKER_URL=${urls.api}`,
2256
- `PLATFORM_WORKER_URL=${urls.platform}`,
2257
- `OWNER_USER_ID=${ownerId}`,
2258
- `APP_OWNER_JWT=${appOwnerJwt}`,
2259
- ...appIdentityToken ? [`APP_IDENTITY_TOKEN=${appIdentityToken}`] : [],
2260
- `ALLOW_DEBUG_ROUTES=true`
2261
- ].join("\n");
2262
- const useSharedDevVarsCache = opts.sharedDevVarsCache ?? opts.generatedSecretsCache !== void 0;
2263
- const devVarsPath = devVarsPathFor(appDir, wranglerEnv, {
2264
- sharedDevVarsCache: useSharedDevVarsCache
2265
- });
2266
- const existing = existsSync7(devVarsPath) ? readFileSync9(devVarsPath, "utf-8") : "";
2267
- const custom = opts.generatedSecretsCache ?? extractCustomDevVars(stripGeneratedSecretsCache(existing).trimEnd());
2268
- let body;
2269
- if (!custom) {
2270
- body = `${sdkVars}
2271
- `;
2272
- } else if (opts.generatedSecretsCache) {
2273
- body = `${sdkVars}
2274
-
2275
- ${custom}
2276
- `;
2277
- } else {
2278
- body = `${sdkVars}
2279
-
2280
- ${DEV_VARS_DIVIDER}
2281
- ${custom}
2282
- `;
2283
- }
2284
- writeSecretFileSync(devVarsPath, body);
2108
+ function pluckString(v, key) {
2109
+ if (!isObject(v)) return null;
2110
+ const val = v[key];
2111
+ return typeof val === "string" && val.length > 0 ? val : null;
2285
2112
  }
2286
-
2287
- // src/cli/lib/install-status.ts
2288
- import { existsSync as existsSync9, readFileSync as readFileSync11, realpathSync as realpathSync2 } from "fs";
2289
- import { dirname as dirname4, join as join9, resolve as resolve5 } from "path";
2290
-
2291
- // src/cli/lib/package-manager.ts
2292
- import { existsSync as existsSync8, readFileSync as readFileSync10 } from "fs";
2293
- import { join as join8 } from "path";
2294
- function detectPackageManager(appDir) {
2295
- try {
2296
- const pkg2 = JSON.parse(readFileSync10(join8(appDir, "package.json"), "utf-8"));
2297
- const name = typeof pkg2.packageManager === "string" ? pkg2.packageManager.split("@")[0] : "";
2298
- if (name === "npm" || name === "pnpm" || name === "yarn" || name === "bun") return name;
2299
- } catch {
2300
- }
2301
- if (existsSync8(join8(appDir, "pnpm-lock.yaml"))) return "pnpm";
2302
- if (existsSync8(join8(appDir, "yarn.lock"))) return "yarn";
2303
- if (existsSync8(join8(appDir, "bun.lock")) || existsSync8(join8(appDir, "bun.lockb"))) return "bun";
2304
- return "npm";
2113
+ function pluckNumber(v, key) {
2114
+ if (!isObject(v)) return null;
2115
+ const val = v[key];
2116
+ return typeof val === "number" && Number.isFinite(val) ? val : null;
2305
2117
  }
2306
- function installCommand(appDir) {
2307
- return `${detectPackageManager(appDir)} install`;
2118
+ function asObjectArray(v) {
2119
+ if (!Array.isArray(v)) return [];
2120
+ return v.filter(isObject);
2308
2121
  }
2309
2122
 
2310
- // src/cli/lib/install-status.ts
2311
- function installState(appDir) {
2312
- if (resolvesDeepspace(appDir)) return "ready";
2313
- const sentinelDir = join9(appDir, ".deepspace");
2314
- if (existsSync9(join9(sentinelDir, "install.err"))) return "failed";
2315
- if (existsSync9(join9(sentinelDir, "install.started"))) {
2316
- if (!existsSync9(join9(sentinelDir, "install.done")) && installWorkerAlive(sentinelDir)) {
2317
- return "installing";
2318
- }
2319
- return "failed";
2123
+ // src/cli/lib/secrets.ts
2124
+ var RESERVED_SECRET_NAMES = /* @__PURE__ */ new Set([
2125
+ ...RESERVED_BINDING_NAMES,
2126
+ "API_WORKER_URL",
2127
+ "PLATFORM_WORKER_URL"
2128
+ ]);
2129
+ var SECRET_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
2130
+ var CONFIG_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
2131
+ var MAX_SECRET_VALUE_BYTES = 32 * 1024;
2132
+ function validateConfigName(configName) {
2133
+ if (!CONFIG_NAME_RE.test(configName)) {
2134
+ throw new Error(
2135
+ `Invalid config "${configName}". Use letters, numbers, underscores, or hyphens; start with a letter or number.`
2136
+ );
2320
2137
  }
2321
- return "missing";
2322
- }
2323
- function tailHint(logPath, platform = process.platform) {
2324
- return platform === "win32" ? `Get-Content -Wait ${logPath}` : `tail -f ${logPath}`;
2138
+ return configName;
2325
2139
  }
2326
- function ensureInstallReady(appDir) {
2327
- if (resolvesDeepspace(appDir)) return;
2328
- const sentinelDir = join9(appDir, ".deepspace");
2329
- const errPath = join9(sentinelDir, "install.err");
2330
- const startedPath = join9(sentinelDir, "install.started");
2331
- const donePath = join9(sentinelDir, "install.done");
2332
- const logPath = join9(sentinelDir, "install.log");
2333
- if (existsSync9(errPath)) {
2334
- const detail = readFileSync11(errPath, "utf-8").trimEnd();
2335
- const log29 = existsSync9(logPath) ? ` Full log: ${logPath}.` : "";
2336
- throw new Refusal(
2337
- `Dependency install failed: ${detail}.${log29} Run \`npm install\` (or \`bun install\`) manually, then retry.`,
2338
- "install_failed",
2339
- { action: { cwd: appDir, argv: [detectPackageManager(appDir), "install"] } }
2140
+ function validateSecretName(name) {
2141
+ if (!SECRET_NAME_RE.test(name)) {
2142
+ throw new Error(
2143
+ `Invalid secret name "${name}". Use letters, numbers, and underscores; start with a letter or underscore.`
2340
2144
  );
2341
2145
  }
2342
- if (existsSync9(startedPath) && !existsSync9(donePath)) {
2343
- if (!installWorkerAlive(sentinelDir)) {
2344
- const log29 = existsSync9(logPath) ? ` See what happened: ${logPath}.` : "";
2345
- throw new Refusal(
2346
- `The dependency install is no longer running and never finished.${log29} Run \`npm install\` (or \`bun install\`) manually, then retry.`,
2347
- "install_failed",
2348
- { action: { cwd: appDir, argv: [detectPackageManager(appDir), "install"] } }
2349
- );
2350
- }
2351
- const tail = existsSync9(logPath) ? ` Tail progress: ${tailHint(logPath)}.` : "";
2352
- throw new Refusal(
2353
- `Dependencies are still installing.${tail} Retry once it finishes.`,
2354
- "install_in_progress"
2146
+ if (RESERVED_SECRET_NAMES.has(name)) {
2147
+ throw new Error(
2148
+ `"${name}" is managed by the DeepSpace SDK and can't be set as an app secret \u2014 the platform injects it at deploy time.`
2355
2149
  );
2356
2150
  }
2357
- throw new Refusal(
2358
- "Dependencies not installed. Run `npm install` (or `bun install`) first.",
2359
- "deps_missing",
2360
- {
2361
- action: { cwd: appDir, argv: [detectPackageManager(appDir), "install"] }
2362
- }
2363
- );
2151
+ return name;
2364
2152
  }
2365
- function resolvesDeepspace(fromDir) {
2366
- let dir = canonicalPath2(fromDir);
2367
- let checkoutRoot = null;
2368
- try {
2369
- checkoutRoot = canonicalPath2(repoToplevel(dir));
2370
- } catch {
2371
- }
2372
- for (; ; ) {
2373
- if (existsSync9(join9(dir, "node_modules", "deepspace", "package.json"))) return true;
2374
- if (dir === checkoutRoot) return false;
2375
- const parent = dirname4(dir);
2376
- if (parent === dir) return false;
2377
- dir = parent;
2153
+ function validateSecretValue(key, value) {
2154
+ if (new TextEncoder().encode(value).byteLength > MAX_SECRET_VALUE_BYTES) {
2155
+ throw new Error(`Secret "${key}" exceeds ${MAX_SECRET_VALUE_BYTES} bytes`);
2378
2156
  }
2379
2157
  }
2380
- function canonicalPath2(path) {
2381
- try {
2382
- return realpathSync2(path);
2383
- } catch {
2384
- return resolve5(path);
2158
+ function defaultConfigNameForEnv(wranglerEnv) {
2159
+ return wranglerEnv ?? "prd";
2160
+ }
2161
+ async function secretsApi(deployUrl, token, path, init = {}) {
2162
+ const res = await fetch(`${deployUrl}/api/secrets${path}`, {
2163
+ ...init,
2164
+ headers: {
2165
+ Authorization: `Bearer ${token}`,
2166
+ "Content-Type": "application/json",
2167
+ ...init.headers ?? {}
2168
+ }
2169
+ });
2170
+ const body = await res.json().catch(() => ({}));
2171
+ if (!res.ok) {
2172
+ const err = new Error(body.error ?? `Secrets request failed (${res.status})`);
2173
+ err.status = res.status;
2174
+ err.apiPath = path;
2175
+ throw err;
2385
2176
  }
2177
+ return body;
2386
2178
  }
2387
- function installWorkerAlive(sentinelDir) {
2388
- const pidPath = join9(sentinelDir, "install.pid");
2389
- if (!existsSync9(pidPath)) return false;
2390
- const pid = Number(readFileSync11(pidPath, "utf-8").trim());
2391
- if (!Number.isSafeInteger(pid) || pid <= 0) return false;
2179
+ function listSecrets(deployUrl, token, appId, configName) {
2180
+ return secretsApi(deployUrl, token, `/${appId}/configs/${encodeURIComponent(configName)}`);
2181
+ }
2182
+ function fetchSecretsValues(deployUrl, token, appId, configName) {
2183
+ return secretsApi(deployUrl, token, `/${appId}/configs/${encodeURIComponent(configName)}/values`);
2184
+ }
2185
+ function getSecretPlain(deployUrl, token, appId, configName, key) {
2186
+ return secretsApi(
2187
+ deployUrl,
2188
+ token,
2189
+ `/${appId}/configs/${encodeURIComponent(configName)}/secrets/${encodeURIComponent(key)}?plain=true`
2190
+ );
2191
+ }
2192
+ function setSecret(deployUrl, token, appId, configName, key, value) {
2193
+ return secretsApi(
2194
+ deployUrl,
2195
+ token,
2196
+ `/${appId}/configs/${encodeURIComponent(configName)}/secrets/${encodeURIComponent(key)}`,
2197
+ { method: "PUT", body: JSON.stringify({ value }) }
2198
+ );
2199
+ }
2200
+ function deleteSecret(deployUrl, token, appId, configName, key) {
2201
+ return secretsApi(
2202
+ deployUrl,
2203
+ token,
2204
+ `/${appId}/configs/${encodeURIComponent(configName)}/secrets/${encodeURIComponent(key)}`,
2205
+ { method: "DELETE" }
2206
+ );
2207
+ }
2208
+ function uploadSecrets(deployUrl, token, appId, configName, secrets, replace) {
2209
+ return secretsApi(
2210
+ deployUrl,
2211
+ token,
2212
+ `/${appId}/configs/${encodeURIComponent(configName)}/secrets`,
2213
+ { method: "PUT", body: JSON.stringify({ secrets, replace }) }
2214
+ );
2215
+ }
2216
+ function listConfigs(deployUrl, token, appId) {
2217
+ return secretsApi(deployUrl, token, `/${appId}/configs`);
2218
+ }
2219
+ function createConfig(deployUrl, token, appId, configName, copyFrom) {
2220
+ return secretsApi(deployUrl, token, `/${appId}/configs`, {
2221
+ method: "POST",
2222
+ body: JSON.stringify({ name: configName, ...copyFrom ? { copyFrom } : {} })
2223
+ });
2224
+ }
2225
+ function deleteConfig(deployUrl, token, appId, configName) {
2226
+ return secretsApi(deployUrl, token, `/${appId}/configs/${encodeURIComponent(configName)}`, {
2227
+ method: "DELETE"
2228
+ });
2229
+ }
2230
+ async function pullAppSecretsCache(deployUrl, token, appId, configName) {
2392
2231
  try {
2393
- process.kill(pid, 0);
2394
- return true;
2232
+ const { secrets } = await fetchSecretsValues(deployUrl, token, appId, configName);
2233
+ if (secrets === null || typeof secrets !== "object" || Array.isArray(secrets)) {
2234
+ throw new ApiError(
2235
+ `The secrets service at ${deployUrl} returned an unexpected response shape \u2014 check DEEPSPACE_DEPLOY_URL (is this the service the app lives on?).`,
2236
+ 200,
2237
+ "invalid_response",
2238
+ `/${appId}/configs/${configName}/values`
2239
+ );
2240
+ }
2241
+ return { appId, configName, values: secrets };
2395
2242
  } catch (err) {
2396
- return err.code === "EPERM";
2243
+ if (err?.status === 404) return null;
2244
+ throw err;
2397
2245
  }
2398
2246
  }
2399
-
2400
- // src/cli/lib/preflight.ts
2401
- import { execFileSync } from "child_process";
2402
- import { existsSync as existsSync10 } from "fs";
2403
- import * as nodeModule from "module";
2404
- import { resolve as resolve6 } from "path";
2405
- function preflightNodeVersion(command, hasRegisterHooks = typeof nodeModule.registerHooks === "function") {
2406
- if (hasRegisterHooks) return;
2407
- throw new Refusal(
2408
- `deepspace ${command} requires Node 22.15 or newer (found v${process.versions.node}). Install the current LTS from https://nodejs.org and re-run.`,
2409
- "node_too_old"
2410
- );
2247
+ function quoteDotenvValue(value) {
2248
+ if (/^[A-Za-z0-9_./:@+-]*$/.test(value) && value !== "") return value;
2249
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r");
2250
+ return `"${escaped}"`;
2251
+ }
2252
+ function renderSecretsCache(secrets, source) {
2253
+ const keys = Object.keys(secrets).sort();
2254
+ const lines = [`# App secrets \xB7 config ${source.configName} \xB7 app ${source.appId}`];
2255
+ for (const key of keys) {
2256
+ validateSecretValue(key, secrets[key]);
2257
+ lines.push(`${key}=${quoteDotenvValue(secrets[key])}`);
2258
+ }
2259
+ return lines.join("\n");
2260
+ }
2261
+ function parseSecretsUpload(content) {
2262
+ const trimmed = content.trim();
2263
+ if (!trimmed) return {};
2264
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
2265
+ const parsed = JSON.parse(trimmed);
2266
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
2267
+ throw new Error("JSON upload must be an object of KEY=value strings.");
2268
+ }
2269
+ const out2 = {};
2270
+ for (const [key, value] of Object.entries(parsed)) {
2271
+ validateSecretName(key);
2272
+ if (typeof value !== "string") throw new Error(`Secret "${key}" must be a string`);
2273
+ validateSecretValue(key, value);
2274
+ out2[key] = value;
2275
+ }
2276
+ return out2;
2277
+ }
2278
+ const out = {};
2279
+ const lines = trimmed.split(/\r?\n/);
2280
+ for (let i = 0; i < lines.length; i++) {
2281
+ const line = lines[i].trim();
2282
+ if (!line || line.startsWith("#")) continue;
2283
+ const lineNumber = i + 1;
2284
+ const eq = line.indexOf("=");
2285
+ if (eq <= 0) {
2286
+ throw new Error(`Invalid dotenv assignment on line ${lineNumber}: expected KEY=value.`);
2287
+ }
2288
+ const key = line.slice(0, eq).trim();
2289
+ if (!SECRET_NAME_RE.test(key)) {
2290
+ throw new Error(
2291
+ `Invalid secret name on line ${lineNumber}: use letters, numbers, and underscores; start with a letter or underscore.`
2292
+ );
2293
+ }
2294
+ validateSecretName(key);
2295
+ const raw = line.slice(eq + 1).trim();
2296
+ const quote = raw[0] === '"' || raw[0] === "'" ? raw[0] : null;
2297
+ let value;
2298
+ if (!quote) {
2299
+ value = raw;
2300
+ } else if (endsWithUnescapedQuote(raw.slice(1), quote)) {
2301
+ const inner = raw.slice(1, -1);
2302
+ value = quote === '"' ? unescapeDotenv(inner) : inner;
2303
+ } else {
2304
+ const startLine = i + 1;
2305
+ const parts = [raw.slice(1)];
2306
+ let closed = false;
2307
+ while (++i < lines.length) {
2308
+ const cont = lines[i].trimEnd();
2309
+ if (endsWithUnescapedQuote(cont, quote)) {
2310
+ parts.push(cont.slice(0, -1));
2311
+ closed = true;
2312
+ break;
2313
+ }
2314
+ parts.push(lines[i]);
2315
+ }
2316
+ if (!closed) {
2317
+ throw new Error(
2318
+ `Unterminated quoted value for "${key}" (opens on line ${startLine}). Close the ${quote === '"' ? "double" : "single"} quote, or escape newlines as \\n on one line.`
2319
+ );
2320
+ }
2321
+ value = parts.join("\n");
2322
+ if (quote === '"') value = unescapeDotenv(value);
2323
+ }
2324
+ validateSecretValue(key, value);
2325
+ out[key] = value;
2326
+ }
2327
+ return out;
2411
2328
  }
2412
- var DLL_NOT_FOUND = -1073741515;
2413
- function preflightWindowsWorkerd(appDir) {
2414
- if (process.platform !== "win32") return;
2415
- const workerdExe = resolve6(
2416
- appDir,
2417
- "node_modules",
2418
- "@cloudflare",
2419
- "workerd-windows-64",
2420
- "bin",
2421
- "workerd.exe"
2422
- );
2423
- if (!existsSync10(workerdExe)) return;
2424
- try {
2425
- execFileSync(workerdExe, ["--help"], { stdio: "pipe", timeout: 1e4 });
2426
- return;
2427
- } catch (err) {
2428
- const status3 = err.status;
2429
- if (status3 !== DLL_NOT_FOUND) return;
2430
- throw new Refusal(
2431
- "workerd (Cloudflare Workers runtime) cannot start on this system: it needs the Microsoft Visual C++ Redistributable, which is not installed. Install it with `winget install Microsoft.VCRedist.2015+.x64` (or choco install -y vcredist140, or https://aka.ms/vs/17/release/vc_redist.x64.exe), then restart this terminal and re-run.",
2432
- "vcredist_missing"
2433
- );
2329
+ function endsWithUnescapedQuote(s, q) {
2330
+ if (!s.endsWith(q)) return false;
2331
+ if (q === "'") return true;
2332
+ let backslashes = 0;
2333
+ for (let j = s.length - 2; j >= 0 && s[j] === "\\"; j--) backslashes++;
2334
+ return backslashes % 2 === 0;
2335
+ }
2336
+ function unescapeDotenv(v) {
2337
+ let out = "";
2338
+ for (let i = 0; i < v.length; i++) {
2339
+ if (v[i] === "\\" && i + 1 < v.length) {
2340
+ const c = v[++i];
2341
+ out += c === "n" ? "\n" : c === "r" ? "\r" : c;
2342
+ } else {
2343
+ out += v[i];
2344
+ }
2434
2345
  }
2346
+ return out;
2435
2347
  }
2436
-
2437
- // src/cli/lib/macos-junk.ts
2438
- import { readdirSync as readdirSync2, rmSync as rmSync3 } from "fs";
2439
- import { join as join10 } from "path";
2440
- var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".wrangler", ".vite"]);
2441
- function removeMacosJunk(dir) {
2442
- let removed = 0;
2443
- let entries;
2444
- try {
2445
- entries = readdirSync2(dir, { withFileTypes: true });
2446
- } catch {
2447
- return removed;
2348
+ function shellSingleQuote(value) {
2349
+ return `'${value.replace(/'/g, "'\\''")}'`;
2350
+ }
2351
+ function formatSecretsDownload(secrets, format) {
2352
+ const entries = Object.entries(secrets).sort(([a], [b]) => a.localeCompare(b));
2353
+ if (format === "json") {
2354
+ return JSON.stringify(Object.fromEntries(entries), null, 2) + "\n";
2448
2355
  }
2449
- for (const entry of entries) {
2450
- const path = join10(dir, entry.name);
2451
- if (entry.isDirectory()) {
2452
- if (!SKIP_DIRS.has(entry.name)) removed += removeMacosJunk(path);
2453
- continue;
2454
- }
2455
- if (entry.name === ".DS_Store" || entry.name.startsWith("._")) {
2456
- try {
2457
- rmSync3(path, { force: true });
2458
- removed++;
2459
- } catch {
2460
- }
2461
- }
2356
+ if (format === "shell") {
2357
+ return entries.map(([k, v]) => `export ${k}=${shellSingleQuote(v)}`).join("\n") + "\n";
2462
2358
  }
2463
- return removed;
2359
+ return entries.map(([k, v]) => `${k}=${quoteDotenvValue(v)}`).join("\n") + "\n";
2360
+ }
2361
+ async function refreshSecretsCache(deployUrl, token, appId, wranglerEnv, selectedConfigName = defaultConfigNameForEnv(wranglerEnv)) {
2362
+ const configName = selectedConfigName;
2363
+ const pulled = await pullAppSecretsCache(deployUrl, token, appId, configName);
2364
+ const values = pulled?.values ?? {};
2365
+ const count = Object.keys(values).length;
2366
+ return {
2367
+ pulled,
2368
+ rendered: renderSecretsCache(values, { appId, configName }),
2369
+ // An empty store is the common case and not worth a status line.
2370
+ summary: count ? `Secrets: ${configName} (${count} secret${count === 1 ? "" : "s"})` : null
2371
+ };
2464
2372
  }
2465
2373
 
2466
2374
  // src/cli/lib/schema-lint.ts
2467
- import { existsSync as existsSync11 } from "fs";
2375
+ import { existsSync as existsSync10 } from "fs";
2468
2376
  import { join as join11 } from "path";
2469
2377
  import { pathToFileURL } from "url";
2470
2378
 
@@ -2525,7 +2433,7 @@ function lintSchema(schema) {
2525
2433
  // src/cli/lib/schema-lint.ts
2526
2434
  async function lintProjectSchemas(appDir) {
2527
2435
  const schemasPath = join11(appDir, "src", "schemas.ts");
2528
- if (!existsSync11(schemasPath)) return null;
2436
+ if (!existsSync10(schemasPath)) return null;
2529
2437
  let schemas;
2530
2438
  try {
2531
2439
  const esbuild = await import("esbuild");
@@ -2577,11 +2485,11 @@ import { createServer, Socket } from "net";
2577
2485
  var DEFAULT_PORT = 5173;
2578
2486
  function isPortListening(port, host, timeoutMs = 500) {
2579
2487
  const target2 = host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host;
2580
- return new Promise((resolve20) => {
2488
+ return new Promise((resolve21) => {
2581
2489
  const socket = new Socket();
2582
2490
  const done = (listening) => {
2583
2491
  socket.destroy();
2584
- resolve20(listening);
2492
+ resolve21(listening);
2585
2493
  };
2586
2494
  socket.setTimeout(timeoutMs);
2587
2495
  socket.once("connect", () => done(true));
@@ -2594,7 +2502,7 @@ async function waitForPortListening(port, host, timeoutMs) {
2594
2502
  const deadline = Date.now() + timeoutMs;
2595
2503
  while (Date.now() < deadline) {
2596
2504
  if (await isPortListening(port, host)) return true;
2597
- await new Promise((resolve20) => setTimeout(resolve20, 250));
2505
+ await new Promise((resolve21) => setTimeout(resolve21, 250));
2598
2506
  }
2599
2507
  return false;
2600
2508
  }
@@ -2660,7 +2568,7 @@ var dev_default = defineDeepspaceCommand({
2660
2568
  preflightNodeVersion("dev start");
2661
2569
  const wranglerEnv = typeof args.env === "string" && args.env.trim() ? args.env.trim() : void 0;
2662
2570
  const host = typeof args.host === "string" && args.host.trim() ? args.host.trim() : DEFAULT_DEV_HOST;
2663
- const start = resolve7(args.dir ?? ".");
2571
+ const start = resolve8(args.dir ?? ".");
2664
2572
  const appDir = findAppDir(start);
2665
2573
  if (!appDir) {
2666
2574
  throw new Refusal(noAppDirMessage(start, findChildApps(start)), "no_app_dir");
@@ -2729,10 +2637,8 @@ var dev_default = defineDeepspaceCommand({
2729
2637
  if (appIdForSecrets) {
2730
2638
  try {
2731
2639
  const refreshed = await refreshSecretsCache(DEPLOY_URL, token, appIdForSecrets, wranglerEnv);
2732
- if (refreshed) {
2733
- generatedSecretsCache = refreshed.rendered;
2734
- if (refreshed.summary) say(refreshed.summary);
2735
- }
2640
+ generatedSecretsCache = refreshed.rendered;
2641
+ if (refreshed.summary) say(refreshed.summary);
2736
2642
  } catch (err) {
2737
2643
  throw new Refusal(
2738
2644
  `Failed to refresh app secrets: ${err instanceof Error ? err.message : String(err)}`,
@@ -2740,16 +2646,10 @@ var dev_default = defineDeepspaceCommand({
2740
2646
  );
2741
2647
  }
2742
2648
  }
2743
- const sharedDevVarsCache = generatedSecretsCache !== void 0;
2744
2649
  await writeDevVars(appDir, payload.sub, token, wranglerEnv, {
2745
- generatedSecretsCache,
2746
- sharedDevVarsCache
2650
+ generatedSecretsCache
2747
2651
  });
2748
- const wranglerConfig = prepareWranglerEnvConfig(
2749
- appDir,
2750
- wranglerEnv,
2751
- { sharedDevVarsCache }
2752
- );
2652
+ const wranglerConfig = prepareWranglerEnvConfig(appDir, wranglerEnv);
2753
2653
  preflightWindowsWorkerd(appDir);
2754
2654
  if (await isPortListening(port, host)) {
2755
2655
  const killArgv = port === DEFAULT_PORT ? ["deepspace", "dev", "kill"] : ["deepspace", "dev", "kill", "--port", String(port)];
@@ -2764,15 +2664,11 @@ Free it with \`${killCmd}\`, or start on another port: \`deepspace dev start --p
2764
2664
  }
2765
2665
  const url = `http://${host === DEFAULT_DEV_HOST ? "localhost" : host}:${port}`;
2766
2666
  say("Starting dev server...\n");
2767
- const vite = spawn(
2768
- "npx",
2769
- ["vite", "--port", String(port), "--strictPort", "--host", host],
2770
- {
2771
- cwd: appDir,
2772
- stdio: "inherit",
2773
- env: wranglerViteEnv(process.env, wranglerConfig, { DEEPSPACE_PORT: String(port) })
2774
- }
2775
- );
2667
+ const vite = spawn("npx", ["vite", "--port", String(port), "--strictPort", "--host", host], {
2668
+ cwd: appDir,
2669
+ stdio: "inherit",
2670
+ env: wranglerViteEnv(process.env, wranglerConfig, { DEEPSPACE_PORT: String(port) })
2671
+ });
2776
2672
  if (args.json) {
2777
2673
  void waitForPortListening(port, host, READINESS_TIMEOUT_MS).then((ready) => {
2778
2674
  console.log(
@@ -3098,7 +2994,7 @@ function isAlive(pid) {
3098
2994
  }
3099
2995
 
3100
2996
  // src/cli/commands/test.ts
3101
- import { resolve as resolve8 } from "path";
2997
+ import { resolve as resolve9 } from "path";
3102
2998
  import { sync as spawnSync4 } from "cross-spawn";
3103
2999
 
3104
3000
  // src/cli/lib/playwright.ts
@@ -3130,7 +3026,7 @@ function ensurePlaywright(appDir) {
3130
3026
  import { readFileSync as readFileSync14 } from "fs";
3131
3027
 
3132
3028
  // src/testing/accounts.ts
3133
- import { existsSync as existsSync12, mkdirSync as mkdirSync5, readFileSync as readFileSync13, writeFileSync as writeFileSync7 } from "fs";
3029
+ import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync13, writeFileSync as writeFileSync7 } from "fs";
3134
3030
  import { homedir as homedir3 } from "os";
3135
3031
  import { join as join13 } from "path";
3136
3032
  var TEST_ACCOUNTS_PATH = join13(homedir3(), ".deepspace", "test-accounts.json");
@@ -3204,7 +3100,7 @@ function removeTestAccounts(ids, emails = [], scope = currentTestAccountScope())
3204
3100
  });
3205
3101
  }
3206
3102
  function readTestAccountStore() {
3207
- if (!existsSync12(TEST_ACCOUNTS_PATH)) return { version: 2, scopes: {}, unscoped: [] };
3103
+ if (!existsSync11(TEST_ACCOUNTS_PATH)) return { version: 2, scopes: {}, unscoped: [] };
3208
3104
  try {
3209
3105
  const raw = JSON.parse(readFileSync13(TEST_ACCOUNTS_PATH, "utf-8"));
3210
3106
  if (Array.isArray(raw)) return { version: 2, scopes: {}, unscoped: validTestAccounts(raw) };
@@ -3321,7 +3217,7 @@ var test_default = defineDeepspaceCommand({
3321
3217
  preflightNodeVersion("test run");
3322
3218
  const suite = args.suite ?? "default";
3323
3219
  const wranglerEnv = typeof args.env === "string" && args.env.trim() ? args.env.trim() : void 0;
3324
- const start = resolve8(".");
3220
+ const start = resolve9(".");
3325
3221
  const appDir = findAppDir(start);
3326
3222
  if (!appDir) {
3327
3223
  throw new Refusal(noAppDirMessage(start, findChildApps(start)), "no_app_dir");
@@ -3348,10 +3244,8 @@ var test_default = defineDeepspaceCommand({
3348
3244
  if (appIdForSecrets) {
3349
3245
  try {
3350
3246
  const refreshed = await refreshSecretsCache(DEPLOY_URL2, token, appIdForSecrets, wranglerEnv);
3351
- if (refreshed) {
3352
- generatedSecretsCache = refreshed.rendered;
3353
- if (refreshed.summary) say(refreshed.summary);
3354
- }
3247
+ generatedSecretsCache = refreshed.rendered;
3248
+ if (refreshed.summary) say(refreshed.summary);
3355
3249
  } catch (err) {
3356
3250
  throw new Refusal(
3357
3251
  `Failed to refresh app secrets: ${err instanceof Error ? err.message : String(err)}`,
@@ -3359,17 +3253,17 @@ var test_default = defineDeepspaceCommand({
3359
3253
  );
3360
3254
  }
3361
3255
  }
3362
- const sharedDevVarsCache = generatedSecretsCache !== void 0;
3363
3256
  await writeDevVars(appDir, ownerId, token, wranglerEnv, {
3364
- generatedSecretsCache,
3365
- sharedDevVarsCache
3257
+ generatedSecretsCache
3366
3258
  });
3367
3259
  if (suite !== "unit") {
3368
3260
  try {
3369
3261
  const { removed } = await syncTestAccountStore();
3370
3262
  if (removed > 0) say(`Removed ${removed} stale test account credential(s).`);
3371
3263
  } catch (error) {
3372
- say(`Warning: could not reconcile test accounts: ${error instanceof Error ? error.message : String(error)}`);
3264
+ say(
3265
+ `Warning: could not reconcile test accounts: ${error instanceof Error ? error.message : String(error)}`
3266
+ );
3373
3267
  }
3374
3268
  }
3375
3269
  if (suite !== "unit") {
@@ -3379,46 +3273,32 @@ var test_default = defineDeepspaceCommand({
3379
3273
  let exitCode = 0;
3380
3274
  switch (suite) {
3381
3275
  case "smoke":
3382
- exitCode = runPlaywright(
3383
- appDir,
3384
- ["tests/smoke.spec.ts"],
3385
- port,
3386
- wranglerEnv,
3387
- sharedDevVarsCache
3388
- );
3276
+ exitCode = runPlaywright(appDir, ["tests/smoke.spec.ts"], port, wranglerEnv);
3389
3277
  break;
3390
3278
  case "api":
3391
- exitCode = runPlaywright(
3392
- appDir,
3393
- ["tests/api.spec.ts"],
3394
- port,
3395
- wranglerEnv,
3396
- sharedDevVarsCache
3397
- );
3279
+ exitCode = runPlaywright(appDir, ["tests/api.spec.ts"], port, wranglerEnv);
3398
3280
  break;
3399
3281
  case "e2e":
3400
- exitCode = runPlaywright(appDir, [], port, wranglerEnv, sharedDevVarsCache);
3282
+ exitCode = runPlaywright(appDir, [], port, wranglerEnv);
3401
3283
  break;
3402
3284
  case "unit":
3403
3285
  exitCode = runVitest(appDir);
3404
3286
  break;
3405
3287
  case "all":
3406
3288
  exitCode = runVitest(appDir);
3407
- if (exitCode === 0)
3408
- exitCode = runPlaywright(appDir, [], port, wranglerEnv, sharedDevVarsCache);
3289
+ if (exitCode === 0) exitCode = runPlaywright(appDir, [], port, wranglerEnv);
3409
3290
  break;
3410
3291
  case "default":
3411
3292
  exitCode = runPlaywright(
3412
3293
  appDir,
3413
3294
  ["tests/smoke.spec.ts", "tests/api.spec.ts"],
3414
3295
  port,
3415
- wranglerEnv,
3416
- sharedDevVarsCache
3296
+ wranglerEnv
3417
3297
  );
3418
3298
  break;
3419
3299
  default:
3420
3300
  if (suite.endsWith(".spec.ts")) {
3421
- exitCode = runPlaywright(appDir, [suite], port, wranglerEnv, sharedDevVarsCache);
3301
+ exitCode = runPlaywright(appDir, [suite], port, wranglerEnv);
3422
3302
  } else {
3423
3303
  throw new Refusal(
3424
3304
  `Unknown test suite: ${suite}
@@ -3435,10 +3315,10 @@ Available: smoke, api, e2e, unit, all`,
3435
3315
  return { data: { suite, port, appDir, wranglerEnv: wranglerEnv ?? null } };
3436
3316
  }
3437
3317
  });
3438
- function runPlaywright(appDir, testFiles, port, wranglerEnv, sharedDevVarsCache = false) {
3318
+ function runPlaywright(appDir, testFiles, port, wranglerEnv) {
3439
3319
  let wranglerConfig;
3440
3320
  try {
3441
- wranglerConfig = prepareWranglerEnvConfig(appDir, wranglerEnv, { sharedDevVarsCache });
3321
+ wranglerConfig = prepareWranglerEnvConfig(appDir, wranglerEnv);
3442
3322
  } catch (err) {
3443
3323
  console.error(err instanceof Error ? err.message : String(err));
3444
3324
  return 1;
@@ -3468,7 +3348,7 @@ function runVitest(appDir) {
3468
3348
 
3469
3349
  // src/cli/commands/screenshot.ts
3470
3350
  import { sync as spawnSync5 } from "cross-spawn";
3471
- import { resolve as resolve9 } from "path";
3351
+ import { resolve as resolve10 } from "path";
3472
3352
  var screenshot_default = defineDeepspaceCommand({
3473
3353
  meta: {
3474
3354
  name: "screenshot",
@@ -3497,7 +3377,7 @@ var screenshot_default = defineDeepspaceCommand({
3497
3377
  }
3498
3378
  },
3499
3379
  async run({ args }) {
3500
- const appDir = resolve9(".");
3380
+ const appDir = resolve10(".");
3501
3381
  ensurePlaywright(appDir);
3502
3382
  const url = String(args.url);
3503
3383
  const output = String(args.output);
@@ -3798,7 +3678,7 @@ var test_accounts_default = defineCommand2({
3798
3678
  // src/cli/commands/deploy.ts
3799
3679
  import * as p12 from "@clack/prompts";
3800
3680
  import { defineCommand as defineCommand3 } from "citty";
3801
- import { resolve as resolve12 } from "path";
3681
+ import { resolve as resolve13 } from "path";
3802
3682
 
3803
3683
  // src/cli/lib/edge-propagation.ts
3804
3684
  import { request as httpRequest } from "http";
@@ -3863,14 +3743,14 @@ async function servesRelease(probe, releaseStamp) {
3863
3743
  }
3864
3744
  function getOnFreshConnection(url, timeoutMs = 5e3) {
3865
3745
  const send = url.protocol === "http:" ? httpRequest : httpsRequest;
3866
- return new Promise((resolve20) => {
3746
+ return new Promise((resolve21) => {
3867
3747
  const request2 = send(
3868
3748
  url,
3869
3749
  { method: "GET", agent: false, timeout: timeoutMs, headers: { "cache-control": "no-cache" } },
3870
3750
  (response) => {
3871
3751
  if (response.statusCode !== 200) {
3872
3752
  response.resume();
3873
- resolve20(null);
3753
+ resolve21(null);
3874
3754
  return;
3875
3755
  }
3876
3756
  let body = "";
@@ -3878,20 +3758,20 @@ function getOnFreshConnection(url, timeoutMs = 5e3) {
3878
3758
  response.on("data", (chunk) => {
3879
3759
  if (body.length < 4096) body += chunk;
3880
3760
  });
3881
- response.on("end", () => resolve20(body));
3882
- response.on("error", () => resolve20(null));
3761
+ response.on("end", () => resolve21(body));
3762
+ response.on("error", () => resolve21(null));
3883
3763
  }
3884
3764
  );
3885
3765
  request2.on("timeout", () => {
3886
3766
  request2.destroy();
3887
- resolve20(null);
3767
+ resolve21(null);
3888
3768
  });
3889
- request2.on("error", () => resolve20(null));
3769
+ request2.on("error", () => resolve21(null));
3890
3770
  request2.end();
3891
3771
  });
3892
3772
  }
3893
3773
  function sleep2(ms) {
3894
- return new Promise((resolve20) => setTimeout(resolve20, ms));
3774
+ return new Promise((resolve21) => setTimeout(resolve21, ms));
3895
3775
  }
3896
3776
 
3897
3777
  // src/shared/app-files.ts
@@ -3970,16 +3850,16 @@ function describeFilesFailure(status3, body) {
3970
3850
  import * as p6 from "@clack/prompts";
3971
3851
  import { execSync as execSync2 } from "child_process";
3972
3852
  import { createHash as createHash2 } from "crypto";
3973
- import { existsSync as existsSync15, readFileSync as readFileSync17, readdirSync as readdirSync4 } from "fs";
3974
- import { dirname as dirname5, join as join16, resolve as resolve10 } from "path";
3853
+ import { existsSync as existsSync14, readFileSync as readFileSync17, readdirSync as readdirSync4 } from "fs";
3854
+ import { dirname as dirname6, join as join16, resolve as resolve11 } from "path";
3975
3855
 
3976
3856
  // src/documentation/deploy.ts
3977
- import { existsSync as existsSync13, readFileSync as readFileSync15 } from "fs";
3857
+ import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
3978
3858
  import { join as join14 } from "path";
3979
3859
  function readDocumentationDeployManifest(appDir, clientDir) {
3980
- if (!existsSync13(join14(appDir, "documentation.json"))) return void 0;
3860
+ if (!existsSync12(join14(appDir, "documentation.json"))) return void 0;
3981
3861
  const manifestPath = join14(clientDir, "_documentation", "manifest.json");
3982
- if (!existsSync13(manifestPath)) {
3862
+ if (!existsSync12(manifestPath)) {
3983
3863
  throw new Error(
3984
3864
  "documentation.json exists but the build produced no documentation manifest. Ensure vite.config.ts includes deepSpaceDocumentation() from deepspace/documentation."
3985
3865
  );
@@ -3991,7 +3871,7 @@ function readDocumentationDeployManifest(appDir, clientDir) {
3991
3871
  import {
3992
3872
  closeSync,
3993
3873
  constants,
3994
- existsSync as existsSync14,
3874
+ existsSync as existsSync13,
3995
3875
  fstatSync,
3996
3876
  ftruncateSync,
3997
3877
  lstatSync,
@@ -4045,7 +3925,7 @@ var APP_MIGRATION_DEFINITIONS = SOURCE_MIGRATIONS.map(
4045
3925
  );
4046
3926
  function readAppliedAppMigrations(appDir) {
4047
3927
  const path = join15(appDir, APP_MIGRATIONS_MANIFEST);
4048
- if (!existsSync14(path)) return [];
3928
+ if (!existsSync13(path)) return [];
4049
3929
  const source = readRegularFile(path);
4050
3930
  let parsed;
4051
3931
  try {
@@ -4106,7 +3986,7 @@ function planAppMigrations(appDir) {
4106
3986
  if (pending.length > 0 && blockers.length === 0) {
4107
3987
  const manifestPath = `${appPrefix}${APP_MIGRATIONS_MANIFEST}`;
4108
3988
  const absoluteManifestPath = join15(repo, manifestPath);
4109
- const before = existsSync14(absoluteManifestPath) ? readRegularFile(absoluteManifestPath) : null;
3989
+ const before = existsSync13(absoluteManifestPath) ? readRegularFile(absoluteManifestPath) : null;
4110
3990
  const after = `${JSON.stringify([...appliedIds, ...pending.map(({ id }) => id)], null, 2)}
4111
3991
  `;
4112
3992
  if (before !== after) edits.push({ path: manifestPath, before, after });
@@ -4122,7 +4002,7 @@ function applyAppMigrationPlan(appDir, plan) {
4122
4002
  const repo = repoToplevel(appDir);
4123
4003
  for (const edit of plan.edits) {
4124
4004
  const absolutePath = join15(repo, edit.path);
4125
- const current = existsSync14(absolutePath) ? readRegularFile(absolutePath) : null;
4005
+ const current = existsSync13(absolutePath) ? readRegularFile(absolutePath) : null;
4126
4006
  if (current !== edit.before) {
4127
4007
  throw new Error(`Migration input changed after planning: ${edit.path}`);
4128
4008
  }
@@ -4251,7 +4131,7 @@ function declaredCompatibilityDate(config) {
4251
4131
  return typeof declared === "string" && /^\d{4}-\d{2}-\d{2}$/.test(declared) ? declared : null;
4252
4132
  }
4253
4133
  async function buildDeployBundle(options) {
4254
- const { appDir, appName, envName, sharedDevVarsCache, output, spinner: spinner2 } = options;
4134
+ const { appDir, appName, envName, output, spinner: spinner2 } = options;
4255
4135
  const junk = removeMacosJunk(appDir);
4256
4136
  if (junk > 0) p6.log.info(`Removed ${junk} macOS metadata file(s) (._*, .DS_Store)`);
4257
4137
  const lintFindings = await lintProjectSchemas(appDir);
@@ -4261,9 +4141,7 @@ async function buildDeployBundle(options) {
4261
4141
  spinner2.start("Building...");
4262
4142
  let preparedWranglerConfig;
4263
4143
  try {
4264
- preparedWranglerConfig = prepareWranglerEnvConfig(appDir, envName, {
4265
- sharedDevVarsCache
4266
- });
4144
+ preparedWranglerConfig = prepareWranglerEnvConfig(appDir, envName);
4267
4145
  const appDomain = DEEPSPACE_ENV === "staging" ? "spacestest.com" : "app.space";
4268
4146
  execSync2("npx vite build", {
4269
4147
  cwd: appDir,
@@ -4284,15 +4162,15 @@ async function buildDeployBundle(options) {
4284
4162
  }
4285
4163
  spinner2.stop("Built");
4286
4164
  const deployConfigPath = join16(appDir, ".wrangler", "deploy", "config.json");
4287
- if (!existsSync15(deployConfigPath)) {
4165
+ if (!existsSync14(deployConfigPath)) {
4288
4166
  output.die(
4289
4167
  "Build output config not found at .wrangler/deploy/config.json",
4290
4168
  "build_output_missing"
4291
4169
  );
4292
4170
  }
4293
4171
  const deployConfig = JSON.parse(readFileSync17(deployConfigPath, "utf-8"));
4294
- const outputWranglerPath = resolve10(dirname5(deployConfigPath), deployConfig.configPath);
4295
- if (!existsSync15(outputWranglerPath)) {
4172
+ const outputWranglerPath = resolve11(dirname6(deployConfigPath), deployConfig.configPath);
4173
+ if (!existsSync14(outputWranglerPath)) {
4296
4174
  output.die(`Output wrangler.json not found at ${outputWranglerPath}`, "build_output_missing");
4297
4175
  }
4298
4176
  const outputConfig = JSON.parse(readFileSync17(outputWranglerPath, "utf-8"));
@@ -4303,13 +4181,13 @@ async function buildDeployBundle(options) {
4303
4181
  "build_output_mismatch"
4304
4182
  );
4305
4183
  }
4306
- const workerDir = dirname5(outputWranglerPath);
4184
+ const workerDir = dirname6(outputWranglerPath);
4307
4185
  const workerBundlePath = join16(workerDir, outputConfig.main);
4308
- const clientDir = outputConfig.assets?.directory ? resolve10(workerDir, outputConfig.assets.directory) : null;
4309
- if (!existsSync15(workerBundlePath)) {
4186
+ const clientDir = outputConfig.assets?.directory ? resolve11(workerDir, outputConfig.assets.directory) : null;
4187
+ if (!existsSync14(workerBundlePath)) {
4310
4188
  output.die(`Worker bundle not found at ${workerBundlePath}`, "build_output_missing");
4311
4189
  }
4312
- if (!clientDir || !existsSync15(clientDir)) {
4190
+ if (!clientDir || !existsSync14(clientDir)) {
4313
4191
  return output.die(`Client assets not found at ${clientDir}`, "build_output_missing");
4314
4192
  }
4315
4193
  try {
@@ -4416,9 +4294,12 @@ function collectAssets(dir) {
4416
4294
  }
4417
4295
  function readDeployAssetConfig(clientDir) {
4418
4296
  const config = {};
4419
- for (const [file, key] of [["_headers", "_headers"], ["_redirects", "_redirects"]]) {
4297
+ for (const [file, key] of [
4298
+ ["_headers", "_headers"],
4299
+ ["_redirects", "_redirects"]
4300
+ ]) {
4420
4301
  const filePath = join16(clientDir, file);
4421
- if (!existsSync15(filePath)) continue;
4302
+ if (!existsSync14(filePath)) continue;
4422
4303
  const content = readFileSync17(filePath, "utf8");
4423
4304
  if (content.trim()) config[key] = content;
4424
4305
  }
@@ -4433,7 +4314,7 @@ function errorMessage(error) {
4433
4314
 
4434
4315
  // src/cli/commands/deploy/commerce.ts
4435
4316
  import * as p7 from "@clack/prompts";
4436
- import { existsSync as existsSync16 } from "fs";
4317
+ import { existsSync as existsSync15 } from "fs";
4437
4318
  import { join as join17 } from "path";
4438
4319
  import { pathToFileURL as pathToFileURL2 } from "url";
4439
4320
  var API_URL2 = process.env.DEEPSPACE_API_URL ?? PLATFORM_URLS.api;
@@ -4442,7 +4323,7 @@ function errorMessage2(error) {
4442
4323
  }
4443
4324
  async function loadDeclaration(appDir, sourceFile, outputFile, exportName) {
4444
4325
  const sourcePath = join17(appDir, "src", sourceFile);
4445
- if (!existsSync16(sourcePath)) return void 0;
4326
+ if (!existsSync15(sourcePath)) return void 0;
4446
4327
  const esbuild = await import("esbuild");
4447
4328
  const outputPath = join17(appDir, ".wrangler", "deploy", outputFile);
4448
4329
  await esbuild.build({
@@ -4537,7 +4418,7 @@ Stripe will keep charging them until you cancel. Cancel at the end of their curr
4537
4418
  }
4538
4419
  }
4539
4420
  async function syncSubscriptionPlans(appDir, appName, token, nonInteractive) {
4540
- if (!existsSync16(join17(appDir, "src", "subscriptions.ts"))) return;
4421
+ if (!existsSync15(join17(appDir, "src", "subscriptions.ts"))) return;
4541
4422
  let plans;
4542
4423
  try {
4543
4424
  plans = await loadDeclaration(
@@ -4592,7 +4473,7 @@ async function syncSubscriptionPlans(appDir, appName, token, nonInteractive) {
4592
4473
  }
4593
4474
  }
4594
4475
  async function syncOneTimeProducts(appDir, appName, token) {
4595
- if (!existsSync16(join17(appDir, "src", "products.ts"))) return;
4476
+ if (!existsSync15(join17(appDir, "src", "products.ts"))) return;
4596
4477
  let products;
4597
4478
  try {
4598
4479
  products = await loadDeclaration(
@@ -4672,19 +4553,20 @@ function createDeployOutput(json) {
4672
4553
  introShown = true;
4673
4554
  },
4674
4555
  die(message, code, opts = {}) {
4556
+ const action = opts.action ? executableAction(opts.action) : void 0;
4675
4557
  if (json) {
4676
4558
  emitJson({
4677
4559
  ok: false,
4678
4560
  code,
4679
4561
  error: message,
4680
4562
  ...opts.actionRequired ? { actionRequired: true } : {},
4681
- ...opts.action ? { action: opts.action } : {}
4563
+ ...action ? { action } : {}
4682
4564
  });
4683
4565
  } else {
4684
4566
  if (introShown && opts.actionRequired) p8.log.warn(withSlug(message, code));
4685
4567
  else if (introShown) p8.cancel(withSlug(message, code));
4686
4568
  else console.error(withSlug(message, code));
4687
- if (opts.action) printAction(opts.action);
4569
+ if (action) printAction(action);
4688
4570
  }
4689
4571
  process.exit(opts.actionRequired ? 2 : 1);
4690
4572
  }
@@ -4884,9 +4766,9 @@ function repoApi(deployUrl, token, appId) {
4884
4766
  }
4885
4767
 
4886
4768
  // src/cli/lib/vc-remote.ts
4887
- import { existsSync as existsSync17 } from "fs";
4769
+ import { existsSync as existsSync16 } from "fs";
4888
4770
  import { tmpdir } from "os";
4889
- import { isAbsolute as isAbsolute3, relative, resolve as resolve11 } from "path";
4771
+ import { isAbsolute as isAbsolute3, relative, resolve as resolve12 } from "path";
4890
4772
  function spaceRemoteName(environment = DEEPSPACE_ENV) {
4891
4773
  if (environment === "staging") return "space-staging";
4892
4774
  if (environment === "production") return "space";
@@ -4932,14 +4814,14 @@ function entryIsTransient(cwd) {
4932
4814
  const entry = process.argv[1];
4933
4815
  if (!entry) return true;
4934
4816
  if (/[\\/]_npx[\\/]/.test(entry) || entry.startsWith(tmpdir())) return true;
4935
- const rel = relative(resolve11(cwd), resolve11(entry));
4817
+ const rel = relative(resolve12(cwd), resolve12(entry));
4936
4818
  return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel);
4937
4819
  }
4938
4820
  function helperEntryUsable(command) {
4939
4821
  const match = /^!(?:DEEPSPACE_ENV=(production|staging) )?'([^']*)' '([^']*)' git-credential$/.exec(command);
4940
4822
  const expectedEnvironment = DEEPSPACE_ENV === "production" ? void 0 : DEEPSPACE_ENV;
4941
4823
  if (!match || match[1] !== expectedEnvironment) return false;
4942
- return existsSync17(match[2]) && existsSync17(match[3]);
4824
+ return existsSync16(match[2]) && existsSync16(match[3]);
4943
4825
  }
4944
4826
  function credentialHelperCommand(environment = DEEPSPACE_ENV) {
4945
4827
  const entry = process.argv[1];
@@ -5005,6 +4887,25 @@ function ensureSpaceRemote(cwd, appId, remote = SPACE_REMOTE) {
5005
4887
  installCredentialHelper(cwd, url);
5006
4888
  return url;
5007
4889
  }
4890
+ function ensureGitIdentity(cwd, token) {
4891
+ try {
4892
+ const configured = (key) => runGit(cwd, ["config", "--get", key], { allowFail: true }).stdout.toString("utf-8").trim() !== "";
4893
+ const missingName = !configured("user.name");
4894
+ const missingEmail = !configured("user.email");
4895
+ if (!missingName && !missingEmail) return;
4896
+ const payload = decodeJwtPayload(token);
4897
+ if (!payload.email) return;
4898
+ if (missingName) {
4899
+ runGit(cwd, ["config", "--local", "user.name", payload.name || payload.email], {
4900
+ allowFail: true
4901
+ });
4902
+ }
4903
+ if (missingEmail) {
4904
+ runGit(cwd, ["config", "--local", "user.email", payload.email], { allowFail: true });
4905
+ }
4906
+ } catch {
4907
+ }
4908
+ }
5008
4909
  function appIdFromRepoUrl(url) {
5009
4910
  const match = /\/api\/repo\/([^/]+)\/?$/.exec(url);
5010
4911
  return match ? decodeURIComponent(match[1]) : null;
@@ -5420,7 +5321,7 @@ async function pushWithTransientRetry(push) {
5420
5321
  const message = errorMessage3(error);
5421
5322
  const transient = /(?:HTTP |error: )(?:429|503)\b/i.test(message);
5422
5323
  if (transient && attempt < backoffMs.length && Date.now() + backoffMs[attempt] < deadline) {
5423
- await new Promise((resolve20) => setTimeout(resolve20, backoffMs[attempt]));
5324
+ await new Promise((resolve21) => setTimeout(resolve21, backoffMs[attempt]));
5424
5325
  continue;
5425
5326
  }
5426
5327
  throw error;
@@ -5460,6 +5361,7 @@ async function deployBuiltBundle(options) {
5460
5361
  appId,
5461
5362
  appName,
5462
5363
  token,
5364
+ claimReleased,
5463
5365
  ignoreStale,
5464
5366
  bundle,
5465
5367
  secrets,
@@ -5477,7 +5379,8 @@ async function deployBuiltBundle(options) {
5477
5379
  await uploadDeployAssets({ deployUrl, appId, token, assets: bundle.assets, output, spinner: spinner2 });
5478
5380
  spinner2.message(`Deploying ${appName}...`);
5479
5381
  let confirmRename = options.rename;
5480
- const bail = async (message, stopLabel = "Deploy failed", code, actionRequired = false, action) => {
5382
+ const bail = async (message, stopLabel = "Deploy failed", code, actionRequired = false, rawAction) => {
5383
+ const action = rawAction ? executableAction(rawAction) : void 0;
5481
5384
  if (stopLabel !== null) spinner2.stop(stopLabel);
5482
5385
  p10.cancel(code ? withSlug(message, code) : message);
5483
5386
  if (action) printAction(action);
@@ -5505,7 +5408,7 @@ async function deployBuiltBundle(options) {
5505
5408
  if (bundle.customBindings.length) {
5506
5409
  form.append("bindingManifest", JSON.stringify(bundle.customBindings));
5507
5410
  }
5508
- if (secrets.names.length) form.append("userSecrets", JSON.stringify(secrets.values));
5411
+ if (secrets.authoritative) form.append("userSecrets", JSON.stringify(secrets.values));
5509
5412
  if (bundle.extraRoutes === true || bundle.extraRoutes.length) {
5510
5413
  form.append("extraRunWorkerFirst", JSON.stringify(bundle.extraRoutes));
5511
5414
  }
@@ -5523,6 +5426,7 @@ async function deployBuiltBundle(options) {
5523
5426
  }
5524
5427
  form.append("name", appName);
5525
5428
  if (confirmRename) form.append("confirmRename", "true");
5429
+ if (claimReleased) form.append("claimReleased", "true");
5526
5430
  if (shouldSendLineage(repository.commitOid, repository.recoverable)) {
5527
5431
  form.append("commitOid", repository.commitOid);
5528
5432
  }
@@ -5712,9 +5616,7 @@ async function uploadDeployAssets(options) {
5712
5616
  }
5713
5617
  };
5714
5618
  try {
5715
- await Promise.all(
5716
- Array.from({ length: Math.min(UPLOAD_CONCURRENCY, missing.length) }, worker)
5717
- );
5619
+ await Promise.all(Array.from({ length: Math.min(UPLOAD_CONCURRENCY, missing.length) }, worker));
5718
5620
  } catch (error) {
5719
5621
  spinner2.stop("Deploy failed");
5720
5622
  if (error instanceof AssetUploadError) {
@@ -5774,7 +5676,7 @@ async function postWithRetry(url, makeInit, {
5774
5676
  lastError = error;
5775
5677
  }
5776
5678
  if (attempt < attempts) {
5777
- await new Promise((resolve20) => setTimeout(resolve20, Math.min(8e3, 500 * 2 ** (attempt - 1))));
5679
+ await new Promise((resolve21) => setTimeout(resolve21, Math.min(8e3, 500 * 2 ** (attempt - 1))));
5778
5680
  }
5779
5681
  }
5780
5682
  if (lastResponse) return lastResponse;
@@ -5783,35 +5685,17 @@ async function postWithRetry(url, makeInit, {
5783
5685
 
5784
5686
  // src/cli/commands/deploy/secrets.ts
5785
5687
  import * as p11 from "@clack/prompts";
5786
- import { existsSync as existsSync18, readFileSync as readFileSync19 } from "fs";
5787
- function classifyDevVarsSecrets(options) {
5788
- const store = new Set(options.storeSecretNames);
5789
- const strayKeys = options.handEditedDevVarKeys.filter((key) => !store.has(key));
5790
- if (strayKeys.length === 0) return { kind: "ok" };
5791
- if (options.storeSecretNames.length === 0 && !options.allowMissing) {
5792
- return { kind: "block", strayKeys };
5793
- }
5794
- return { kind: "warn", strayKeys };
5795
- }
5796
5688
  async function loadDeploySecrets(options) {
5797
5689
  const { deployUrl, appDir, appId, envName, ownerId, token, output } = options;
5798
5690
  let linked = null;
5799
- let localOnlyDevVars = {};
5800
5691
  try {
5801
5692
  const refreshed = await refreshSecretsCache(deployUrl, token, appId, envName);
5802
- if (refreshed) {
5803
- linked = refreshed.pulled;
5804
- localOnlyDevVars = readCustomDevVars(
5805
- devVarsPathFor(appDir, envName, { sharedDevVarsCache: true }),
5806
- output
5807
- );
5808
- await writeDevVars(appDir, ownerId, token, envName, {
5809
- appId,
5810
- generatedSecretsCache: refreshed.rendered,
5811
- sharedDevVarsCache: true
5812
- });
5813
- if (refreshed.summary) p11.log.info(refreshed.summary);
5814
- }
5693
+ linked = refreshed.pulled;
5694
+ await writeDevVars(appDir, ownerId, token, envName, {
5695
+ appId,
5696
+ generatedSecretsCache: refreshed.rendered
5697
+ });
5698
+ if (refreshed.summary) p11.log.info(refreshed.summary);
5815
5699
  } catch (error) {
5816
5700
  if (error?.status === 403) {
5817
5701
  output.die(
@@ -5824,40 +5708,31 @@ async function loadDeploySecrets(options) {
5824
5708
  "secrets_refresh_failed"
5825
5709
  );
5826
5710
  }
5827
- return { linked, localOnlyDevVars };
5828
- }
5829
- function prepareDeploySecrets(options) {
5830
- const { appDir, envName, cache, allowMissing, customBindings, doManifest, output } = options;
5831
- const values = cache.linked ? { ...cache.linked.values } : {};
5832
- const devVarsPath = devVarsPathFor(appDir, envName, {
5833
- sharedDevVarsCache: cache.linked !== null
5834
- });
5835
- const handEditedDevVars = cache.linked ? cache.localOnlyDevVars : readCustomDevVars(devVarsPath, output);
5836
- const decision = classifyDevVarsSecrets({
5837
- storeSecretNames: Object.keys(values),
5838
- handEditedDevVarKeys: Object.keys(handEditedDevVars),
5839
- allowMissing
5840
- });
5841
- if (decision.kind === "block") {
5842
- const uploadArgv = [
5843
- "deepspace",
5844
- "secrets",
5845
- "upload",
5846
- ".dev.vars",
5847
- ...envName ? ["-e", envName] : []
5848
- ];
5711
+ if (!linked) {
5712
+ const configName = defaultConfigNameForEnv(envName);
5849
5713
  output.die(
5850
- `Refusing to deploy: the app store has no secrets, but .dev.vars has ${decision.strayKeys.join(", ")}. Deploying now would ship the app with no secrets \u2014 replacing any secrets a previous deploy set (they would be dropped from production). Upload them first, then redeploy. (Pass --allow-missing-secrets to deploy without them.)`,
5851
- "secrets_not_uploaded",
5852
- { action: { cwd: appDir, argv: uploadArgv } }
5853
- );
5854
- }
5855
- if (decision.kind === "warn") {
5856
- const stray = decision.strayKeys;
5857
- p11.log.warn(
5858
- `.dev.vars has ${stray.join(", ")} not in the app store \u2014 ${stray.length === 1 ? "it is" : "they are"} NOT deployed. \`.dev.vars\` is a local dev cache, not deploy config. Move ${stray.length === 1 ? "it" : "them"} into the store with \`deepspace secrets set KEY=value\` (or \`deepspace secrets upload .dev.vars\`), then redeploy.`
5714
+ `Secrets config "${configName}" does not exist. Create it explicitly before deploying; this distinguishes an intentional empty config from an uninitialized app and protects live Worker secrets during the platform rollout.`,
5715
+ "secrets_config_missing",
5716
+ {
5717
+ action: {
5718
+ cwd: appDir,
5719
+ argv: [
5720
+ "deepspace",
5721
+ "secrets",
5722
+ "configs",
5723
+ "create",
5724
+ configName,
5725
+ ...envName ? ["--env", envName] : []
5726
+ ]
5727
+ }
5728
+ }
5859
5729
  );
5860
5730
  }
5731
+ return { linked };
5732
+ }
5733
+ function prepareDeploySecrets(options) {
5734
+ const { cache, customBindings, doManifest, output } = options;
5735
+ const values = cache.linked ? { ...cache.linked.values } : {};
5861
5736
  const names = Object.keys(values);
5862
5737
  const declaredBindingNames = /* @__PURE__ */ new Set([
5863
5738
  ...customBindings.map((binding) => binding.name),
@@ -5878,17 +5753,7 @@ function prepareDeploySecrets(options) {
5878
5753
  }
5879
5754
  }
5880
5755
  if (names.length) p11.log.info(`App secrets: ${names.join(", ")}`);
5881
- return { values, names };
5882
- }
5883
- function readCustomDevVars(path, output) {
5884
- if (!existsSync18(path)) return {};
5885
- const section = extractCustomDevVars(stripGeneratedSecretsCache(readFileSync19(path, "utf-8")));
5886
- if (!section) return {};
5887
- try {
5888
- return parseDevVars(section);
5889
- } catch (error) {
5890
- output.die(`.dev.vars: ${errorMessage5(error)}`, "invalid_dev_vars");
5891
- }
5756
+ return { values, names, authoritative: cache.linked !== null };
5892
5757
  }
5893
5758
  function errorMessage5(error) {
5894
5759
  return error instanceof Error ? error.message : String(error);
@@ -5922,9 +5787,9 @@ var deploy_default = defineCommand3({
5922
5787
  description: "Confirm that a changed wrangler `name` renames this app (its URL moves; data, secrets, and collaborators travel). Without this flag an interactive prompt asks.",
5923
5788
  default: false
5924
5789
  },
5925
- "allow-missing-secrets": {
5790
+ "claim-released": {
5926
5791
  type: "boolean",
5927
- description: "Deploy even though hand-edited .dev.vars secrets are absent from the store (they will NOT be deployed, and any a previous deploy set are dropped).",
5792
+ description: "Platform admins only: claim an app name still inside its 30-day release cooldown. The previous owner permanently loses their reserved reclaim. Non-admin accounts are refused.",
5928
5793
  default: false
5929
5794
  },
5930
5795
  // Citty maps --no-push to the negation of an argument named `push`.
@@ -5947,7 +5812,7 @@ var deploy_default = defineCommand3({
5947
5812
  async run({ args }) {
5948
5813
  const output = createDeployOutput(args.json === true);
5949
5814
  preflightNodeVersion("deploy");
5950
- const appDir = resolve12(args.dir ?? ".");
5815
+ const appDir = resolve13(args.dir ?? ".");
5951
5816
  const selectorRefusal = blankSelectorRefusal(args);
5952
5817
  if (selectorRefusal) output.die(selectorRefusal.error, selectorRefusal.code);
5953
5818
  const envName = typeof args.env === "string" && args.env.trim() ? args.env.trim() : void 0;
@@ -6000,7 +5865,6 @@ var deploy_default = defineCommand3({
6000
5865
  appDir,
6001
5866
  appName,
6002
5867
  envName,
6003
- sharedDevVarsCache: secretsCache.linked !== null,
6004
5868
  output,
6005
5869
  spinner: spinner2
6006
5870
  });
@@ -6020,10 +5884,7 @@ var deploy_default = defineCommand3({
6020
5884
  sourceState
6021
5885
  });
6022
5886
  const secrets = prepareDeploySecrets({
6023
- appDir,
6024
- envName,
6025
5887
  cache: secretsCache,
6026
- allowMissing: Boolean(args["allow-missing-secrets"]),
6027
5888
  customBindings: bundle.customBindings,
6028
5889
  doManifest: bundle.doManifest,
6029
5890
  output
@@ -6035,6 +5896,7 @@ var deploy_default = defineCommand3({
6035
5896
  appName,
6036
5897
  token,
6037
5898
  rename: args.rename === true,
5899
+ claimReleased: args["claim-released"] === true,
6038
5900
  ignoreStale: Boolean(args["ignore-stale"]),
6039
5901
  bundle,
6040
5902
  secrets,
@@ -6063,10 +5925,7 @@ var deploy_default = defineCommand3({
6063
5925
  url: body.url,
6064
5926
  releaseId: body.releaseId ?? null,
6065
5927
  bundleRetained: body.bundleRetained ?? null,
6066
- // `serving` says what was actually established; `edgePropagating`
6067
- // stays for callers written against the old shape.
6068
5928
  serving,
6069
- edgePropagating: serving === "unconfirmed",
6070
5929
  recoverable: repository.recoverable,
6071
5930
  ...staleBaseGuardFields(body)
6072
5931
  });
@@ -6085,7 +5944,7 @@ var deploy_default = defineCommand3({
6085
5944
  appName,
6086
5945
  url: body.url ?? null,
6087
5946
  // Always present, so a caller branches on one field instead of
6088
- // inferring success from the absence of `edgePropagating`.
5947
+ // inferring success from an absent one.
6089
5948
  serving,
6090
5949
  releaseId: body.releaseId ?? null,
6091
5950
  bundleRetained: body.bundleRetained ?? null,
@@ -6765,20 +6624,20 @@ var usage_default = defineDeepspaceCommand({
6765
6624
  });
6766
6625
 
6767
6626
  // src/cli/commands/add.ts
6768
- import { existsSync as existsSync20, readFileSync as readFileSync21, realpathSync as realpathSync3 } from "fs";
6769
- import { dirname as dirname7, join as join19, resolve as resolve14 } from "path";
6627
+ import { existsSync as existsSync18, readFileSync as readFileSync20, realpathSync as realpathSync4 } from "fs";
6628
+ import { dirname as dirname8, join as join19, resolve as resolve15 } from "path";
6770
6629
  import { pathToFileURL as pathToFileURL3 } from "url";
6771
6630
 
6772
6631
  // src/cli/commands/feature-installer.ts
6773
6632
  import {
6774
6633
  closeSync as closeSync2,
6775
6634
  copyFileSync,
6776
- existsSync as existsSync19,
6635
+ existsSync as existsSync17,
6777
6636
  fstatSync as fstatSync2,
6778
6637
  mkdirSync as mkdirSync6,
6779
6638
  mkdtempSync,
6780
6639
  openSync as openSync2,
6781
- readFileSync as readFileSync20,
6640
+ readFileSync as readFileSync19,
6782
6641
  readSync,
6783
6642
  readdirSync as readdirSync5,
6784
6643
  rmSync as rmSync4,
@@ -6786,33 +6645,33 @@ import {
6786
6645
  } from "fs";
6787
6646
  import { spawnSync as spawnSync6 } from "child_process";
6788
6647
  import { tmpdir as tmpdir2 } from "os";
6789
- import { dirname as dirname6, isAbsolute as isAbsolute4, join as join18, resolve as resolve13 } from "path";
6648
+ import { dirname as dirname7, isAbsolute as isAbsolute4, join as join18, resolve as resolve14 } from "path";
6790
6649
  import { fileURLToPath } from "url";
6791
6650
  var NAV_MARKER = "// \u2500\u2500 Features add nav items below this line \u2500\u2500";
6792
6651
  function featurePackageRoot(moduleUrl = import.meta.url) {
6793
- let dir = dirname6(fileURLToPath(moduleUrl));
6652
+ let dir = dirname7(fileURLToPath(moduleUrl));
6794
6653
  for (; ; ) {
6795
6654
  const packagePath = join18(dir, "package.json");
6796
- if (existsSync19(packagePath)) {
6655
+ if (existsSync17(packagePath)) {
6797
6656
  try {
6798
- const pkg2 = JSON.parse(readFileSync20(packagePath, "utf-8"));
6657
+ const pkg2 = JSON.parse(readFileSync19(packagePath, "utf-8"));
6799
6658
  if (pkg2.name === "deepspace") return dir;
6800
6659
  } catch {
6801
6660
  }
6802
6661
  }
6803
- const parent = dirname6(dir);
6662
+ const parent = dirname7(dir);
6804
6663
  if (parent === dir) throw new Error("Could not locate the deepspace package root");
6805
6664
  dir = parent;
6806
6665
  }
6807
6666
  }
6808
6667
  function hasFeatureCatalog(packageRoot = featurePackageRoot()) {
6809
- return existsSync19(join18(packageRoot, "features"));
6668
+ return existsSync17(join18(packageRoot, "features"));
6810
6669
  }
6811
6670
  function readFeatureConfig(id, packageRoot = featurePackageRoot()) {
6812
6671
  const configPath = join18(packageRoot, "features", id, "feature.json");
6813
- if (!existsSync19(configPath)) return null;
6672
+ if (!existsSync17(configPath)) return null;
6814
6673
  try {
6815
- const config = JSON.parse(readFileSync20(configPath, "utf-8"));
6674
+ const config = JSON.parse(readFileSync19(configPath, "utf-8"));
6816
6675
  if (typeof config.id !== "string" || typeof config.name !== "string" || !Array.isArray(config.files)) {
6817
6676
  return null;
6818
6677
  }
@@ -6823,7 +6682,7 @@ function readFeatureConfig(id, packageRoot = featurePackageRoot()) {
6823
6682
  }
6824
6683
  function listFeatureConfigs(packageRoot = featurePackageRoot()) {
6825
6684
  const featuresDir = join18(packageRoot, "features");
6826
- if (!existsSync19(featuresDir)) return [];
6685
+ if (!existsSync17(featuresDir)) return [];
6827
6686
  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));
6828
6687
  }
6829
6688
  function featureCatalog(packageRoot = featurePackageRoot()) {
@@ -6861,7 +6720,7 @@ function suggestFeature(featureId, packageRoot = featurePackageRoot()) {
6861
6720
  return closest !== null && closestDistance <= threshold ? closest : null;
6862
6721
  }
6863
6722
  function resolveTargetDir(raw) {
6864
- return isAbsolute4(raw) ? raw : resolve13(raw);
6723
+ return isAbsolute4(raw) ? raw : resolve14(raw);
6865
6724
  }
6866
6725
  function resolveFeatureDestPath(fileDest, route) {
6867
6726
  const pages = "src/pages/";
@@ -6871,13 +6730,13 @@ function resolveFeatureDestPath(fileDest, route) {
6871
6730
  return route?.protected === false ? `${appPages}${rest}` : `${appPages}(protected)/${rest}`;
6872
6731
  }
6873
6732
  function copyFile(src, dest) {
6874
- mkdirSync6(dirname6(dest), { recursive: true });
6733
+ mkdirSync6(dirname7(dest), { recursive: true });
6875
6734
  copyFileSync(src, dest);
6876
6735
  }
6877
6736
  function copyFileWithImportShift(src, dest, levels) {
6878
- mkdirSync6(dirname6(dest), { recursive: true });
6737
+ mkdirSync6(dirname7(dest), { recursive: true });
6879
6738
  const prefix = "../".repeat(levels);
6880
- const shifted = readFileSync20(src, "utf-8").replace(
6739
+ const shifted = readFileSync19(src, "utf-8").replace(
6881
6740
  /(from\s+|import\s+|import\()(['"])(\.\.\/)/g,
6882
6741
  `$1$2${prefix}$3`
6883
6742
  );
@@ -6893,9 +6752,9 @@ function installFiles(packageRoot, config, targetDir, emit) {
6893
6752
  const srcPath = join18(packageRoot, "features", config.id, file.src);
6894
6753
  const resolvedDest = resolveFeatureDestPath(file.dest, config.route);
6895
6754
  const destPath = join18(targetDir, resolvedDest);
6896
- const existed = existsSync19(destPath);
6755
+ const existed = existsSync17(destPath);
6897
6756
  if (existed) preexisting.push(resolvedDest);
6898
- if (!existsSync19(srcPath)) {
6757
+ if (!existsSync17(srcPath)) {
6899
6758
  emit(` Warning: source not found: ${file.src}`, "stderr");
6900
6759
  if (!existed) missing.push(resolvedDest);
6901
6760
  continue;
@@ -6933,10 +6792,10 @@ function unresolvedSchema(schema, reason, emit) {
6933
6792
  function integrateSchema(config, targetDir, emit) {
6934
6793
  if (!config.schema) return [];
6935
6794
  const schemasPath = join18(targetDir, "src", "schemas.ts");
6936
- if (!existsSync19(schemasPath)) {
6795
+ if (!existsSync17(schemasPath)) {
6937
6796
  return [unresolvedSchema(config.schema, "src/schemas.ts was not found.", emit)];
6938
6797
  }
6939
- let content = readFileSync20(schemasPath, "utf-8");
6798
+ let content = readFileSync19(schemasPath, "utf-8");
6940
6799
  const { exportName, importPath, spreadOperator } = config.schema;
6941
6800
  const importPattern = new RegExp(`^import\\s+\\{[^}]*\\b${exportName}\\b`, "m");
6942
6801
  if (importPattern.test(content)) {
@@ -6990,10 +6849,10 @@ function unresolvedActions(actions, reason, emit) {
6990
6849
  function integrateActions(config, targetDir, emit) {
6991
6850
  if (!config.actions?.length) return [];
6992
6851
  const actionsPath = join18(targetDir, "src", "actions", "index.ts");
6993
- if (!existsSync19(actionsPath)) {
6852
+ if (!existsSync17(actionsPath)) {
6994
6853
  return [unresolvedActions(config.actions, "src/actions/index.ts was not found.", emit)];
6995
6854
  }
6996
- let content = readFileSync20(actionsPath, "utf-8");
6855
+ let content = readFileSync19(actionsPath, "utf-8");
6997
6856
  const registryPattern = /export const actions:\s*Record<string,\s*ActionHandler(?:<[^>]*>)?>\s*=\s*\{/;
6998
6857
  if (!registryPattern.test(content)) {
6999
6858
  const reason = "src/actions/index.ts has an unexpected structure.";
@@ -7040,7 +6899,7 @@ function integrateCode(config, targetDir, emit) {
7040
6899
  const unresolved = [];
7041
6900
  for (const insertion of config.code ?? []) {
7042
6901
  const targetPath = join18(targetDir, insertion.file);
7043
- if (!existsSync19(targetPath)) {
6902
+ if (!existsSync17(targetPath)) {
7044
6903
  unresolved.push({
7045
6904
  area: "code",
7046
6905
  reason: `${insertion.file} was not found.`,
@@ -7048,7 +6907,7 @@ function integrateCode(config, targetDir, emit) {
7048
6907
  });
7049
6908
  continue;
7050
6909
  }
7051
- const original = readFileSync20(targetPath, "utf-8");
6910
+ const original = readFileSync19(targetPath, "utf-8");
7052
6911
  if (original.includes(insertion.insert.trim())) {
7053
6912
  emit(` Integration already present: ${insertion.file}`);
7054
6913
  continue;
@@ -7084,7 +6943,7 @@ ${content.slice(importIndex < 0 ? 0 : importIndex)}`;
7084
6943
  function integrateIgnore(config, targetDir) {
7085
6944
  if (!config.ignore?.length) return;
7086
6945
  const targetPath = join18(targetDir, ".gitignore");
7087
- const current = existsSync19(targetPath) ? readFileSync20(targetPath, "utf-8") : "";
6946
+ const current = existsSync17(targetPath) ? readFileSync19(targetPath, "utf-8") : "";
7088
6947
  const present = new Set(current.split(/\r?\n/).map((line) => line.trim()));
7089
6948
  const missing = config.ignore.filter((pattern) => !present.has(pattern));
7090
6949
  if (missing.length === 0) return;
@@ -7101,10 +6960,10 @@ function integrateWrangler(config, targetDir, emit) {
7101
6960
  ...runWorkerFirst ? ["Set assets.run_worker_first = true in wrangler.toml."] : [],
7102
6961
  ...rateLimits.map((binding) => `Add the ${binding.name} rate-limit binding to wrangler.toml.`)
7103
6962
  ];
7104
- if (!existsSync19(targetPath)) {
6963
+ if (!existsSync17(targetPath)) {
7105
6964
  return [{ area: "wrangler", reason: "wrangler.toml was not found.", steps }];
7106
6965
  }
7107
- let source = readFileSync20(targetPath, "utf-8");
6966
+ let source = readFileSync19(targetPath, "utf-8");
7108
6967
  if (runWorkerFirst && !/^\[assets\]\s*$/m.test(source)) {
7109
6968
  return [{ area: "wrangler", reason: "wrangler.toml has no [assets] section.", steps }];
7110
6969
  }
@@ -7181,7 +7040,7 @@ function rateLimitBindingBlock(scope, binding) {
7181
7040
  function integrateCss(packageRoot, config, targetDir, emit) {
7182
7041
  if (!config.css?.length) return [];
7183
7042
  const stylesPath = join18(targetDir, "src", "styles.css");
7184
- if (!existsSync19(stylesPath)) {
7043
+ if (!existsSync17(stylesPath)) {
7185
7044
  emit(" Warning: Cannot integrate CSS \u2014 src/styles.css not found");
7186
7045
  return [
7187
7046
  {
@@ -7193,12 +7052,12 @@ function integrateCss(packageRoot, config, targetDir, emit) {
7193
7052
  }
7194
7053
  ];
7195
7054
  }
7196
- let styles = readFileSync20(stylesPath, "utf-8");
7055
+ let styles = readFileSync19(stylesPath, "utf-8");
7197
7056
  let integrated = 0;
7198
7057
  const unresolved = [];
7199
7058
  for (const cssFile of config.css) {
7200
7059
  const srcPath = join18(packageRoot, "features", config.id, cssFile);
7201
- if (!existsSync19(srcPath)) {
7060
+ if (!existsSync17(srcPath)) {
7202
7061
  emit(` Warning: CSS source not found: ${cssFile}`, "stderr");
7203
7062
  unresolved.push({
7204
7063
  area: "css",
@@ -7207,7 +7066,7 @@ function integrateCss(packageRoot, config, targetDir, emit) {
7207
7066
  });
7208
7067
  continue;
7209
7068
  }
7210
- const css = readFileSync20(srcPath, "utf-8");
7069
+ const css = readFileSync19(srcPath, "utf-8");
7211
7070
  const fingerprint = css.match(/\.([\w-]+)\s*\{|@keyframes\s+([\w-]+)/);
7212
7071
  const marker = fingerprint?.[1] ?? fingerprint?.[2];
7213
7072
  if (marker && styles.includes(marker)) {
@@ -7235,10 +7094,10 @@ function unresolvedRoute(route, config, reason, emit) {
7235
7094
  function integrateRoute(config, targetDir, emit) {
7236
7095
  if (!config.route) return [];
7237
7096
  const navPath = join18(targetDir, "src", "nav.ts");
7238
- if (!existsSync19(navPath)) {
7097
+ if (!existsSync17(navPath)) {
7239
7098
  return [unresolvedRoute(config.route, config, "src/nav.ts was not found.", emit)];
7240
7099
  }
7241
- let content = readFileSync20(navPath, "utf-8");
7100
+ let content = readFileSync19(navPath, "utf-8");
7242
7101
  const routePath = config.route.path;
7243
7102
  if (content.includes(`'${routePath}'`)) {
7244
7103
  emit(` Nav already present: ${routePath}`);
@@ -7313,13 +7172,13 @@ function integrateDependencies(config, targetDir, runInstall, outputVisible, emi
7313
7172
  return { added: [], installRan: false, packageManager };
7314
7173
  }
7315
7174
  const packagePath = join18(targetDir, "package.json");
7316
- if (!existsSync19(packagePath)) {
7175
+ if (!existsSync17(packagePath)) {
7317
7176
  emit(" Could not patch package.json (file not found)");
7318
7177
  return { added: [], installRan: false, packageManager };
7319
7178
  }
7320
7179
  let pkg2;
7321
7180
  try {
7322
- pkg2 = JSON.parse(readFileSync20(packagePath, "utf-8"));
7181
+ pkg2 = JSON.parse(readFileSync19(packagePath, "utf-8"));
7323
7182
  } catch (error) {
7324
7183
  emit(
7325
7184
  ` Could not patch package.json (parse error: ${error instanceof Error ? error.message : String(error)})`
@@ -7379,7 +7238,7 @@ ${child.diagnosticTail}` : "";
7379
7238
  }
7380
7239
  function dependencySnapshot(targetDir) {
7381
7240
  try {
7382
- const pkg2 = JSON.parse(readFileSync20(join18(targetDir, "package.json"), "utf-8"));
7241
+ const pkg2 = JSON.parse(readFileSync19(join18(targetDir, "package.json"), "utf-8"));
7383
7242
  return {
7384
7243
  dependencies: pkg2.dependencies ?? {},
7385
7244
  devDependencies: pkg2.devDependencies ?? {}
@@ -7391,7 +7250,7 @@ function dependencySnapshot(targetDir) {
7391
7250
  function installedNavFeatures(packageRoot, targetDir) {
7392
7251
  return listFeatureConfigs(packageRoot).filter((feature) => feature.category === "nav" && feature.files.length > 0).filter((feature) => {
7393
7252
  const signature = resolveFeatureDestPath(feature.files[0].dest, feature.route);
7394
- return existsSync19(join18(targetDir, signature));
7253
+ return existsSync17(join18(targetDir, signature));
7395
7254
  }).map((feature) => feature.id);
7396
7255
  }
7397
7256
  function warnNavConflicts(packageRoot, config, targetDir, emit) {
@@ -7624,11 +7483,11 @@ function renderFeatureInfo(config) {
7624
7483
  return lines.join("\n");
7625
7484
  }
7626
7485
  function isDeepSpaceApp(dir) {
7627
- if (existsSync20(join19(dir, "wrangler.toml"))) return true;
7486
+ if (existsSync18(join19(dir, "wrangler.toml"))) return true;
7628
7487
  const packagePath = join19(dir, "package.json");
7629
- if (!existsSync20(packagePath)) return false;
7488
+ if (!existsSync18(packagePath)) return false;
7630
7489
  try {
7631
- const pkg2 = JSON.parse(readFileSync21(packagePath, "utf-8"));
7490
+ const pkg2 = JSON.parse(readFileSync20(packagePath, "utf-8"));
7632
7491
  return Boolean(pkg2.dependencies?.deepspace || pkg2.devDependencies?.deepspace);
7633
7492
  } catch {
7634
7493
  return false;
@@ -7638,8 +7497,8 @@ function installedPackageRoot(fromDir) {
7638
7497
  let dir = fromDir;
7639
7498
  for (; ; ) {
7640
7499
  const candidate = join19(dir, "node_modules", "deepspace");
7641
- if (existsSync20(join19(candidate, "package.json"))) return candidate;
7642
- const parent = dirname7(dir);
7500
+ if (existsSync18(join19(candidate, "package.json"))) return candidate;
7501
+ const parent = dirname8(dir);
7643
7502
  if (parent === dir) return null;
7644
7503
  dir = parent;
7645
7504
  }
@@ -7652,12 +7511,12 @@ async function targetInstaller(appDir) {
7652
7511
  "feature_installer_missing"
7653
7512
  );
7654
7513
  }
7655
- if (realpathSync3(installedRoot) === realpathSync3(featurePackageRoot())) {
7514
+ if (realpathSync4(installedRoot) === realpathSync4(featurePackageRoot())) {
7656
7515
  return { installFeature };
7657
7516
  }
7658
7517
  const entry = join19(installedRoot, "dist", "feature-installer.js");
7659
7518
  const catalog = join19(installedRoot, "features");
7660
- if (!existsSync20(entry) || !existsSync20(catalog)) {
7519
+ if (!existsSync18(entry) || !existsSync18(catalog)) {
7661
7520
  throw new Refusal(
7662
7521
  `The app's installed deepspace package does not contain the typed feature installer.
7663
7522
  Expected: ${entry}
@@ -7749,8 +7608,8 @@ var add_default = defineDeepspaceCommand({
7749
7608
  }
7750
7609
  return { data: { features: featureCatalog(root) } };
7751
7610
  }
7752
- const appDir = resolve14(args.dir ?? ".");
7753
- if (!existsSync20(appDir)) {
7611
+ const appDir = resolve15(args.dir ?? ".");
7612
+ if (!existsSync18(appDir)) {
7754
7613
  throw new Refusal(
7755
7614
  `Target directory not found: ${appDir}
7756
7615
  Create a new app first: deepspace app create <name>`,
@@ -7831,11 +7690,11 @@ function fmtDate(iso) {
7831
7690
  }
7832
7691
  async function confirm4(message) {
7833
7692
  process.stdout.write(`${message} [y/N] `);
7834
- return new Promise((resolve20) => {
7693
+ return new Promise((resolve21) => {
7835
7694
  process.stdin.setEncoding("utf-8");
7836
7695
  process.stdin.once("data", (data) => {
7837
7696
  const answer = data.toString().trim().toLowerCase();
7838
- resolve20(answer === "y" || answer === "yes");
7697
+ resolve21(answer === "y" || answer === "yes");
7839
7698
  });
7840
7699
  });
7841
7700
  }
@@ -8125,8 +7984,8 @@ var domain_default = defineCommand5({
8125
7984
 
8126
7985
  // src/cli/commands/app-files.ts
8127
7986
  import { defineCommand as defineCommand6 } from "citty";
8128
- import { basename as basename4, resolve as resolve15 } from "path";
8129
- import { existsSync as existsSync21, statSync as statSync2 } from "fs";
7987
+ import { basename as basename4, resolve as resolve16 } from "path";
7988
+ import { existsSync as existsSync19, statSync as statSync2 } from "fs";
8130
7989
 
8131
7990
  // src/cli/lib/app-files-api.ts
8132
7991
  import { createWriteStream, openAsBlob, statSync } from "fs";
@@ -8326,7 +8185,7 @@ async function deleteAppFile(baseUrl, token, appId, key) {
8326
8185
  const path = filesPath(appId, `/${encodeKeyPath(key)}`);
8327
8186
  const res = await request(baseUrl, token, path, { method: "DELETE" });
8328
8187
  const body = await readJson(res, path);
8329
- return { existed: body.existed !== false };
8188
+ return { existed: body.existed };
8330
8189
  }
8331
8190
  async function downloadAppFile(baseUrl, token, appId, key, destination) {
8332
8191
  const path = filesPath(appId, `/${encodeKeyPath(key)}`);
@@ -8396,8 +8255,8 @@ var put = defineDeepspaceCommand({
8396
8255
  app: { type: "string", description: "App id or live name (defaults to ./wrangler.toml)" }
8397
8256
  },
8398
8257
  async run({ args }) {
8399
- const localPath = resolve15(String(args.file));
8400
- if (!existsSync21(localPath) || !statSync2(localPath).isFile()) {
8258
+ const localPath = resolve16(String(args.file));
8259
+ if (!existsSync19(localPath) || !statSync2(localPath).isFile()) {
8401
8260
  throw new Refusal(`No such file: ${args.file}`, "file_not_found");
8402
8261
  }
8403
8262
  const key = requireKey(args.key ?? basename4(localPath));
@@ -8497,7 +8356,7 @@ var get = defineDeepspaceCommand({
8497
8356
  },
8498
8357
  async run({ args }) {
8499
8358
  const key = requireKey(String(args.key));
8500
- const destination = resolve15(String(args.out ?? basename4(key)));
8359
+ const destination = resolve16(String(args.out ?? basename4(key)));
8501
8360
  const { token, appId } = await target(args.app);
8502
8361
  const result = await downloadAppFile(PLATFORM_URL, token, appId, key, destination);
8503
8362
  if (!args.json) console.log(`\u2713 ${key} \u2192 ${destination} (${formatBytes(result.bytes)})`);
@@ -8867,7 +8726,7 @@ var transfer_default = defineCommand8({
8867
8726
  import { defineCommand as defineCommand9 } from "citty";
8868
8727
 
8869
8728
  // src/cli/commands/_invoke-impl.ts
8870
- import { readFileSync as readFileSync22 } from "fs";
8729
+ import { readFileSync as readFileSync21 } from "fs";
8871
8730
  import * as p15 from "@clack/prompts";
8872
8731
  var API_URL7 = process.env.DEEPSPACE_API_URL ?? PLATFORM_URLS.api;
8873
8732
  var DEFAULT_TIMEOUT_MS = 12e4;
@@ -8908,7 +8767,7 @@ function resolveBody(opts) {
8908
8767
  if (opts.body != null) {
8909
8768
  raw = opts.body;
8910
8769
  } else if (opts.bodyFile != null) {
8911
- raw = opts.bodyFile === "-" ? readFileSync22(0, "utf-8") : readFileSync22(opts.bodyFile, "utf-8");
8770
+ raw = opts.bodyFile === "-" ? readFileSync21(0, "utf-8") : readFileSync21(opts.bodyFile, "utf-8");
8912
8771
  } else {
8913
8772
  return "{}";
8914
8773
  }
@@ -9187,8 +9046,8 @@ var integrations_default = defineCommand9({
9187
9046
 
9188
9047
  // src/cli/commands/feedback.ts
9189
9048
  import * as p16 from "@clack/prompts";
9190
- import { existsSync as existsSync22, readFileSync as readFileSync23 } from "fs";
9191
- import { dirname as dirname8, join as join20 } from "path";
9049
+ import { existsSync as existsSync20, readFileSync as readFileSync22 } from "fs";
9050
+ import { dirname as dirname9, join as join20 } from "path";
9192
9051
  import { fileURLToPath as fileURLToPath2 } from "url";
9193
9052
  import { release } from "os";
9194
9053
  var API_URL8 = process.env.DEEPSPACE_API_URL ?? PLATFORM_URLS.api;
@@ -9209,14 +9068,14 @@ function normalizeType(raw) {
9209
9068
  }
9210
9069
  function readCliVersion() {
9211
9070
  try {
9212
- const here = dirname8(fileURLToPath2(import.meta.url));
9071
+ const here = dirname9(fileURLToPath2(import.meta.url));
9213
9072
  const candidates = [
9214
9073
  join20(here, "..", "package.json"),
9215
9074
  join20(here, "..", "..", "..", "package.json")
9216
9075
  ];
9217
9076
  for (const path of candidates) {
9218
- if (!existsSync22(path)) continue;
9219
- const pkg2 = JSON.parse(readFileSync23(path, "utf-8"));
9077
+ if (!existsSync20(path)) continue;
9078
+ const pkg2 = JSON.parse(readFileSync22(path, "utf-8"));
9220
9079
  if (pkg2.name === "deepspace" && pkg2.version) return pkg2.version;
9221
9080
  }
9222
9081
  } catch {
@@ -9412,7 +9271,7 @@ Context: deepspace ${payload.cliVersion} \xB7 ${payload.nodeVersion} \xB7 ${payl
9412
9271
 
9413
9272
  // src/cli/commands/secrets.ts
9414
9273
  import { defineCommand as defineCommand10 } from "citty";
9415
- import { readFileSync as readFileSync24 } from "fs";
9274
+ import { readFileSync as readFileSync23 } from "fs";
9416
9275
 
9417
9276
  // src/cli/lib/citty-args.ts
9418
9277
  function dedupePositionals(bound, rest) {
@@ -9682,7 +9541,7 @@ var upload = defineCommand10({
9682
9541
  async run({ args }) {
9683
9542
  try {
9684
9543
  const t = await resolveTarget(args);
9685
- const content = args.file === "-" ? readFileSync24(0, "utf-8") : readFileSync24(args.file, "utf-8");
9544
+ const content = args.file === "-" ? readFileSync23(0, "utf-8") : readFileSync23(args.file, "utf-8");
9686
9545
  const secrets = parseSecretsUpload(content);
9687
9546
  if (Object.keys(secrets).length === 0)
9688
9547
  fail(new InputError("No secrets found in the input.", "empty_input"));
@@ -9746,27 +9605,24 @@ var pull = defineCommand10({
9746
9605
  );
9747
9606
  const t = await resolveTarget(args);
9748
9607
  const ownerId = decodeJwtPayload(t.token).sub;
9749
- const pulled = await pullAppSecretsCache(DEPLOY_URL11, t.token, t.appId, t.configName);
9750
- if (!pulled) {
9751
- ok(
9752
- args.json === true,
9753
- { appId: t.appId, config: t.configName, pulled: 0 },
9754
- () => console.log(
9755
- `No secrets in ${t.configName} yet \u2014 nothing to pull. Whoever has them adds them with \`deepspace secrets set KEY=value\` (or \`secrets upload <file>\`); if the app truly needs none, you're ready as-is.`
9756
- )
9757
- );
9758
- return;
9759
- }
9608
+ const refreshed = await refreshSecretsCache(
9609
+ DEPLOY_URL11,
9610
+ t.token,
9611
+ t.appId,
9612
+ wranglerEnv,
9613
+ t.configName
9614
+ );
9760
9615
  await writeDevVars(appDir, ownerId, t.token, wranglerEnv, {
9761
9616
  appId: t.appId,
9762
- generatedSecretsCache: renderSecretsCache(pulled.values, pulled),
9763
- sharedDevVarsCache: true
9617
+ generatedSecretsCache: refreshed.rendered
9764
9618
  });
9765
- const count = Object.keys(pulled.values).length;
9619
+ const count = Object.keys(refreshed.pulled?.values ?? {}).length;
9766
9620
  ok(
9767
9621
  args.json === true,
9768
9622
  { appId: t.appId, config: t.configName, pulled: count },
9769
- () => console.log(`Pulled ${count} secrets (${t.configName}) into .dev.vars.`)
9623
+ () => console.log(
9624
+ refreshed.pulled ? `Pulled ${count} secrets (${t.configName}) into .dev.vars.` : `Config ${t.configName} does not exist yet; regenerated .dev.vars without app secrets.`
9625
+ )
9770
9626
  );
9771
9627
  } catch (err) {
9772
9628
  fail(err);
@@ -9938,7 +9794,7 @@ var init_default = defineDeepspaceCommand({
9938
9794
  // src/cli/commands/status.ts
9939
9795
  import { defineCommand as defineCommand11 } from "citty";
9940
9796
  import * as p17 from "@clack/prompts";
9941
- import { existsSync as existsSync23, readFileSync as readFileSync25 } from "fs";
9797
+ import { existsSync as existsSync21, readFileSync as readFileSync24 } from "fs";
9942
9798
  function timeAgo(iso) {
9943
9799
  const ms = Date.now() - Date.parse(iso);
9944
9800
  if (!Number.isFinite(ms) || ms < 0) return iso;
@@ -10023,7 +9879,7 @@ var status_default = defineCommand11({
10023
9879
  json.services = urls;
10024
9880
  if (urlOverrides.length > 0) json.urlOverrides = urlOverrides;
10025
9881
  }
10026
- facts.loggedIn = existsSync23(SESSION_PATH);
9882
+ facts.loggedIn = existsSync21(SESSION_PATH);
10027
9883
  let userId = null;
10028
9884
  if (!facts.loggedIn) {
10029
9885
  lines.push(["Session", "not logged in"]);
@@ -10032,7 +9888,7 @@ var status_default = defineCommand11({
10032
9888
  let who = "logged in";
10033
9889
  try {
10034
9890
  const payload = decodeJwtPayload(
10035
- readFileSync25(TOKEN_PATH, "utf-8").trim()
9891
+ readFileSync24(TOKEN_PATH, "utf-8").trim()
10036
9892
  );
10037
9893
  if (payload.email) who = payload.email;
10038
9894
  userId = payload.sub ?? null;
@@ -10184,21 +10040,24 @@ var status_default = defineCommand11({
10184
10040
  }
10185
10041
  const refs = refsResult.ok ? refsResult.value : void 0;
10186
10042
  const trunkRef = refs?.refs.find((r) => `refs/heads/${branch}` === r.name);
10043
+ const defaultBranch = refs?.head && refs.head.startsWith("refs/heads/") ? refs.head.slice("refs/heads/".length) : null;
10187
10044
  if (!facts.workspaceId && branch) {
10045
+ const isTrunk = defaultBranch === null ? null : branch === defaultBranch;
10046
+ const label = isTrunk === false ? "Branch sync" : "Trunk";
10188
10047
  if (githubSource) {
10189
- lines.push(["Trunk", `${branch} is owned through normal Git/GitHub`]);
10190
- json.trunk = { state: "external", provider: "github", branch };
10191
- } else if (!refsResult.ok) reportRemoteFailure2("Trunk", "trunk", refsResult.error);
10048
+ lines.push([label, `${branch} is owned through normal Git/GitHub`]);
10049
+ json.trunk = { state: "external", provider: "github", branch, isTrunk };
10050
+ } else if (!refsResult.ok) reportRemoteFailure2(label, "trunk", refsResult.error);
10192
10051
  else {
10193
10052
  const remoteOid = trunkRef?.oid ?? null;
10194
10053
  const localOid = resolveCommit(appDir, `refs/heads/${branch}`);
10195
10054
  const remoteKnownLocally = remoteOid ? resolveCommit(appDir, remoteOid) !== null : false;
10196
10055
  facts.trunkState = remoteOid === null && localOid === null ? "unknown" : remoteOid === localOid ? "in_sync" : remoteOid && localOid && remoteKnownLocally && isAncestor(appDir, remoteOid, localOid) ? "local_ahead" : remoteOid && localOid && remoteKnownLocally && isAncestor(appDir, localOid, remoteOid) ? "local_behind" : remoteOid && localOid && remoteKnownLocally ? "diverged" : "differs";
10197
10056
  lines.push([
10198
- "Trunk",
10057
+ label,
10199
10058
  remoteOid === null ? `cloud repo has no ${branch} yet` : facts.trunkState === "in_sync" ? `${branch} in sync with the cloud repo (${remoteOid.slice(0, 10)})` : `${branch} ${facts.trunkState.replace("_", " ")} relative to the cloud repo (local ${localOid?.slice(0, 10) ?? "none"}, cloud ${remoteOid.slice(0, 10)})`
10200
10059
  ]);
10201
- json.trunk = { state: facts.trunkState, localOid, remoteOid };
10060
+ json.trunk = { state: facts.trunkState, localOid, remoteOid, branch, isTrunk };
10202
10061
  }
10203
10062
  }
10204
10063
  if (!releaseResult.ok) reportRemoteFailure2("Live", "live", releaseResult.error);
@@ -10425,7 +10284,7 @@ var push_default = defineDeepspaceCommand({
10425
10284
 
10426
10285
  // src/cli/commands/pull.ts
10427
10286
  import * as p19 from "@clack/prompts";
10428
- import { realpathSync as realpathSync4 } from "fs";
10287
+ import { realpathSync as realpathSync5 } from "fs";
10429
10288
  function targetedVcAction(command, cwd, appId, branch) {
10430
10289
  return {
10431
10290
  cwd,
@@ -10449,7 +10308,7 @@ function workspaceBranchPullRefusal(branch, trunkBranch) {
10449
10308
  function worktreeHoldingBranch(worktrees, branch, selfPath) {
10450
10309
  const real = (pth) => {
10451
10310
  try {
10452
- return realpathSync4(pth);
10311
+ return realpathSync5(pth);
10453
10312
  } catch {
10454
10313
  return pth;
10455
10314
  }
@@ -10578,12 +10437,7 @@ var pull_default = defineDeepspaceCommand({
10578
10437
  }
10579
10438
  spinner2?.message(`Fetching ${branch} from the cloud repo\u2026`);
10580
10439
  const trackingRef = spaceTrackingRef(branch);
10581
- runGitRemote(appDir, token, [
10582
- "fetch",
10583
- "--quiet",
10584
- SPACE_REMOTE,
10585
- `+${refName}:${trackingRef}`
10586
- ]);
10440
+ runGitRemote(appDir, token, ["fetch", "--quiet", SPACE_REMOTE, `+${refName}:${trackingRef}`]);
10587
10441
  const remoteOid = resolveCommit(appDir, trackingRef);
10588
10442
  if (!remoteOid) {
10589
10443
  spinner2?.stop("Fetch failed.");
@@ -10621,6 +10475,8 @@ var pull_default = defineDeepspaceCommand({
10621
10475
  integration = "fast_forwarded";
10622
10476
  }
10623
10477
  }
10478
+ } else if (isAncestor(appDir, remoteOid, localOid)) {
10479
+ integration = "local_ahead";
10624
10480
  } else {
10625
10481
  integration = "fetched_only_diverged";
10626
10482
  }
@@ -10634,10 +10490,15 @@ var pull_default = defineDeepspaceCommand({
10634
10490
  let line;
10635
10491
  let codeForState;
10636
10492
  let recoveryAction;
10493
+ let successAction;
10637
10494
  switch (integration) {
10638
10495
  case "up_to_date":
10639
10496
  line = `${branch} is already up to date.`;
10640
10497
  break;
10498
+ case "local_ahead":
10499
+ line = `${branch} already contains ${SPACE_REMOTE}/${branch} at ${remoteOid.slice(0, 10)} \u2014 nothing to pull. Local work is not on the cloud repo yet: run \`${pushCommand}\`.`;
10500
+ successAction = targetedVcAction("push", appDir, appId, branch);
10501
+ break;
10641
10502
  case "fast_forwarded":
10642
10503
  line = `Fast-forwarded ${branch} to ${remoteOid.slice(0, 10)}.`;
10643
10504
  break;
@@ -10686,7 +10547,8 @@ var pull_default = defineDeepspaceCommand({
10686
10547
  }
10687
10548
  const actionRequired = recoveryAction !== void 0;
10688
10549
  spinner2?.stop(codeForState ? `Fetched ${SPACE_REMOTE}/${branch}.` : line);
10689
- if (!args.json) p19.log.info(`App: ${appId} \xB7 ${SPACE_REMOTE}/${branch} at ${remoteOid.slice(0, 10)}`);
10550
+ if (!args.json)
10551
+ p19.log.info(`App: ${appId} \xB7 ${SPACE_REMOTE}/${branch} at ${remoteOid.slice(0, 10)}`);
10690
10552
  if (codeForState) {
10691
10553
  throw new Refusal(line, codeForState, {
10692
10554
  actionRequired,
@@ -10700,20 +10562,20 @@ var pull_default = defineDeepspaceCommand({
10700
10562
  }
10701
10563
  });
10702
10564
  }
10703
- return { data: { status: integration, appId, branch, remoteOid } };
10565
+ return { data: { status: integration, appId, branch, remoteOid }, action: successAction };
10704
10566
  }
10705
10567
  });
10706
10568
 
10707
10569
  // src/cli/commands/clone.ts
10708
10570
  import * as p20 from "@clack/prompts";
10709
- import { existsSync as existsSync24, readdirSync as readdirSync6, rmSync as rmSync5 } from "fs";
10710
- import { resolve as resolve16, basename as basename5, join as join21 } from "path";
10571
+ import { existsSync as existsSync22, readdirSync as readdirSync6, rmSync as rmSync5 } from "fs";
10572
+ import { resolve as resolve17, basename as basename5, join as join21 } from "path";
10711
10573
  function cleanFailedCloneDir(dir) {
10712
10574
  let entries;
10713
10575
  try {
10714
10576
  entries = readdirSync6(dir);
10715
10577
  } catch {
10716
- return existsSync24(dir) ? ["<unreadable>"] : [];
10578
+ return existsSync22(dir) ? ["<unreadable>"] : [];
10717
10579
  }
10718
10580
  if (entries.length === 1 && entries[0] === ".git") {
10719
10581
  try {
@@ -10726,7 +10588,7 @@ function cleanFailedCloneDir(dir) {
10726
10588
  return entries;
10727
10589
  }
10728
10590
  function validateCloneTarget(dir) {
10729
- if (!existsSync24(dir)) return false;
10591
+ if (!existsSync22(dir)) return false;
10730
10592
  let entries;
10731
10593
  try {
10732
10594
  entries = readdirSync6(dir);
@@ -10761,7 +10623,7 @@ var clone_default = defineDeepspaceCommand({
10761
10623
  async run({ args }) {
10762
10624
  assertAppTargetResolvable(String(args.app));
10763
10625
  const selector = String(args.app).trim();
10764
- const dir = resolve16(String(args.dir ?? selector));
10626
+ const dir = resolve17(String(args.dir ?? selector));
10765
10627
  const dirPreexisted = validateCloneTarget(dir);
10766
10628
  const spinner2 = args.json ? null : createSpinner();
10767
10629
  spinner2?.start(`Resolving ${selector}\u2026`);
@@ -10801,6 +10663,7 @@ var clone_default = defineDeepspaceCommand({
10801
10663
  throw err;
10802
10664
  }
10803
10665
  ensureSpaceRemote(dir, appId);
10666
+ ensureGitIdentity(dir, token);
10804
10667
  const head = resolveCommit(dir, "HEAD");
10805
10668
  const branch = currentBranch(dir);
10806
10669
  spinner2?.stop(
@@ -11023,14 +10886,13 @@ Re-run with --allow-do-deletion to accept the data loss.`,
11023
10886
  spinner2?.message("Waiting for the edge to serve this release...");
11024
10887
  serving = await waitForLiveRelease(result.url, result.releaseStamp, 9e4);
11025
10888
  }
11026
- const edgePropagating = serving === "unconfirmed";
11027
10889
  spinner2?.stop(
11028
10890
  serving === "confirmed" ? `Rolled back to ${result.rolledBackTo ?? releaseId}.` : serving === "unconfirmed" ? `Rolled back to ${result.rolledBackTo ?? releaseId} \u2014 the edge is still rolling it out.` : `Rolled back to ${result.rolledBackTo ?? releaseId} \u2014 serving could not be verified from here.`
11029
10891
  );
11030
10892
  const doClassGuard = result.doClassGuard === "unverified" ? "unverified" : void 0;
11031
10893
  if (!args.json) {
11032
10894
  if (result.url) {
11033
- if (edgePropagating) p22.log.warn(`URL: ${result.url} (accepted; serving not yet verified)`);
10895
+ if (serving === "unconfirmed") p22.log.warn(`URL: ${result.url} (accepted; serving not yet verified)`);
11034
10896
  else p22.log.success(`Live at: ${result.url}`);
11035
10897
  }
11036
10898
  if (result.bundleRetained === false) {
@@ -11058,7 +10920,6 @@ Re-run with --allow-do-deletion to accept the data loss.`,
11058
10920
  wranglerEnv: wranglerEnv ?? null,
11059
10921
  // Always present, matching `deploy`: one field to branch on.
11060
10922
  serving,
11061
- ...edgePropagating ? { edgePropagating: true } : {},
11062
10923
  ...doClassGuard ? { doClassGuard } : {}
11063
10924
  }
11064
10925
  };
@@ -11070,7 +10931,7 @@ import { defineCommand as defineCommand12 } from "citty";
11070
10931
 
11071
10932
  // src/cli/commands/workspace/new.ts
11072
10933
  import * as p25 from "@clack/prompts";
11073
- import { isAbsolute as isAbsolute6, resolve as resolve17 } from "path";
10934
+ import { isAbsolute as isAbsolute6, resolve as resolve18 } from "path";
11074
10935
 
11075
10936
  // src/cli/commands/workspace/analysis.ts
11076
10937
  import * as p23 from "@clack/prompts";
@@ -11079,6 +10940,14 @@ var PEER_REF_PREFIX = `${spacePrivateRef("peers")}/`;
11079
10940
  function peerWorkspaceRef(workspaceId) {
11080
10941
  return `${PEER_REF_PREFIX}${workspaceId}`;
11081
10942
  }
10943
+ function workspaceSyncRelation(dir, localOid, publishedOid) {
10944
+ if (localOid !== null && localOid === publishedOid) return "in_sync";
10945
+ if (localOid === null || publishedOid === null) return "unknown";
10946
+ if (resolveCommit(dir, publishedOid) === null) return "unknown";
10947
+ if (isAncestor(dir, localOid, publishedOid)) return "behind";
10948
+ if (isAncestor(dir, publishedOid, localOid)) return "ahead";
10949
+ return "diverged";
10950
+ }
11082
10951
  function formatCount(count) {
11083
10952
  if (!count) return "?";
11084
10953
  return count.capped ? `${count.count}+` : String(count.count);
@@ -11195,18 +11064,18 @@ function printOverlaps(overlaps, json) {
11195
11064
 
11196
11065
  // src/cli/commands/workspace/local.ts
11197
11066
  import * as p24 from "@clack/prompts";
11198
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync26, realpathSync as realpathSync5, renameSync as renameSync2, statSync as statSync3, writeFileSync as writeFileSync9 } from "fs";
11199
- import { dirname as dirname9, isAbsolute as isAbsolute5, join as join22, relative as relative2, sep as sep2 } from "path";
11067
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync25, realpathSync as realpathSync6, renameSync as renameSync2, statSync as statSync3, writeFileSync as writeFileSync9 } from "fs";
11068
+ import { dirname as dirname10, isAbsolute as isAbsolute5, join as join22, relative as relative2, sep as sep2 } from "path";
11200
11069
  var WORKSPACE_MARKER = "deepspace-workspace.json";
11201
11070
  function excludeWorktreeDir(appDir, dir) {
11202
11071
  try {
11203
11072
  ensureLocalExclude(appDir, ".deepspace/");
11204
- const realDir = realpathSync5(dir);
11205
- const rel = relative2(realpathSync5(repoToplevel(appDir)), realDir);
11073
+ const realDir = realpathSync6(dir);
11074
+ const rel = relative2(realpathSync6(repoToplevel(appDir)), realDir);
11206
11075
  if (!rel || rel === ".." || rel.startsWith(`..${sep2}`) || isAbsolute5(rel)) return;
11207
11076
  const pattern = rel.split(sep2).join("/");
11208
11077
  if (pattern === ".deepspace" || pattern.startsWith(".deepspace/")) return;
11209
- const relApp = relative2(realpathSync5(appDir), realDir).split(sep2).join("/");
11078
+ const relApp = relative2(realpathSync6(appDir), realDir).split(sep2).join("/");
11210
11079
  if (relApp === ".deepspace" || relApp.startsWith(".deepspace/")) return;
11211
11080
  ensureLocalExclude(appDir, `/${pattern}/`);
11212
11081
  } catch {
@@ -11228,7 +11097,7 @@ function markManagedWorkspace(worktreeRoot, workspaceId) {
11228
11097
  const marker = { version: 1, workspaceId, mode: "managed-linked" };
11229
11098
  const tempPath = `${markerPath}.${process.pid}.tmp`;
11230
11099
  try {
11231
- mkdirSync7(dirname9(markerPath), { recursive: true });
11100
+ mkdirSync7(dirname10(markerPath), { recursive: true });
11232
11101
  writeFileSync9(tempPath, JSON.stringify(marker) + "\n");
11233
11102
  renameSync2(tempPath, markerPath);
11234
11103
  return true;
@@ -11253,7 +11122,7 @@ function isManagedWorkspaceWorktree(worktreeRoot, workspaceId) {
11253
11122
  const markerPath = worktreeMarkerPath(worktreeRoot);
11254
11123
  if (!markerPath) return false;
11255
11124
  try {
11256
- const marker = JSON.parse(readFileSync26(markerPath, "utf-8"));
11125
+ const marker = JSON.parse(readFileSync25(markerPath, "utf-8"));
11257
11126
  return marker.version === 1 && marker.workspaceId === workspaceId && marker.mode === "managed-linked";
11258
11127
  } catch {
11259
11128
  return false;
@@ -11265,7 +11134,7 @@ function defaultWorkspaceRoot(appDir, workspaceId) {
11265
11134
  return join22(primary.path, ".deepspace", "ws", workspaceId.slice(3).toLowerCase());
11266
11135
  }
11267
11136
  function appDirInWorktree(appDir, worktreeRoot) {
11268
- const appRelative = relative2(realpathSync5(repoToplevel(appDir)), realpathSync5(appDir));
11137
+ const appRelative = relative2(realpathSync6(repoToplevel(appDir)), realpathSync6(appDir));
11269
11138
  return appRelative ? join22(worktreeRoot, appRelative) : worktreeRoot;
11270
11139
  }
11271
11140
  function materializeWorkspaceWorktree(appDir, worktreeRoot, branch, startPoint, workspaceId, options = {}) {
@@ -11281,6 +11150,18 @@ function materializeWorkspaceWorktree(appDir, worktreeRoot, branch, startPoint,
11281
11150
  }
11282
11151
  excludeWorktreeDir(appDir, worktreeRoot);
11283
11152
  }
11153
+ function rematerializeWorkspaceWorktree(appDir, worktreeRoot, branch, workspaceId, options = {}) {
11154
+ const added = runGit(appDir, ["worktree", "add", worktreeRoot, branch], { allowFail: true });
11155
+ if (added.status !== 0) {
11156
+ return added.stderr.toString("utf-8").trim() || "unknown git error";
11157
+ }
11158
+ if (!(options.markManaged ?? markManagedWorkspace)(worktreeRoot, workspaceId)) {
11159
+ runGit(appDir, ["worktree", "remove", worktreeRoot], { allowFail: true });
11160
+ return `could not record DeepSpace ownership for ${worktreeRoot}`;
11161
+ }
11162
+ excludeWorktreeDir(appDir, worktreeRoot);
11163
+ return null;
11164
+ }
11284
11165
  function inOwnLinkedWorktree(appDir, id) {
11285
11166
  if (workspaceIdFromBranch(currentBranch(appDir)) !== id) return false;
11286
11167
  try {
@@ -11389,7 +11270,7 @@ function cleanupWorkspaceLocal(appDir, id, trunkBranch, options = {}) {
11389
11270
  }
11390
11271
  let inside = false;
11391
11272
  try {
11392
- inside = realpathSync5(repoToplevel(appDir)) === realpathSync5(linked.path);
11273
+ inside = realpathSync6(repoToplevel(appDir)) === realpathSync6(linked.path);
11393
11274
  } catch {
11394
11275
  inside = false;
11395
11276
  }
@@ -11485,7 +11366,7 @@ function inferWorkspaceId(appDir, explicit) {
11485
11366
  const id = explicit?.trim() || workspaceIdFromBranch(currentBranch(appDir));
11486
11367
  if (!id) {
11487
11368
  throw new Refusal(
11488
- 'Not inside a workspace worktree (the branch is not ws/<id>). Pass the workspace id, or create one with `deepspace workspace new -t "\u2026"`.',
11369
+ 'Not inside a workspace worktree (the branch is not ws/<id>). Select one with `-w ws_\u2026` (for `workspace drop`, pass the id as the argument), or create one with `deepspace workspace new -t "\u2026"`.',
11489
11370
  "not_in_workspace"
11490
11371
  );
11491
11372
  }
@@ -11494,17 +11375,28 @@ function inferWorkspaceId(appDir, explicit) {
11494
11375
  }
11495
11376
  return id;
11496
11377
  }
11378
+ function workspaceNotActiveRefusal(id, status3, dropAction) {
11379
+ return new Refusal(
11380
+ `Workspace ${id} is already ${status3}.` + (dropAction ? " This checkout is a leftover \u2014 `deepspace workspace drop` cleans it up." : ""),
11381
+ "workspace_not_active",
11382
+ {
11383
+ ...dropAction ? { action: dropAction } : {},
11384
+ extra: { workspaceId: id, status: status3 }
11385
+ }
11386
+ );
11387
+ }
11497
11388
  function assertSelectedWorkspaceCheckout(appDir, id, resumeArgv) {
11498
11389
  const branch = currentBranch(appDir);
11499
11390
  if (isSelectedWorkspaceCheckout(branch, id)) return;
11500
11391
  const worktree = resolveWorkspaceWorktree(listWorktrees(appDir), id);
11501
- const action = worktree ? { cwd: worktree, argv: resumeArgv } : { cwd: appDir, argv: ["deepspace", "workspace", "attach", id] };
11392
+ const worktreeAppDir = worktree ? appDirInWorktree(appDir, worktree) : null;
11393
+ const action = worktreeAppDir ? { cwd: worktreeAppDir, argv: resumeArgv } : { cwd: appDir, argv: ["deepspace", "workspace", "attach", id] };
11502
11394
  throw new Refusal(
11503
- `Workspace ${id} is not checked out here (current branch: ${branch ?? "detached"}). ` + (worktree ? `Run this command from its checkout at ${worktree}.` : "Attach it in this clone before mutating its published line."),
11395
+ `Workspace ${id} is not checked out here (current branch: ${branch ?? "detached"}). ` + (worktreeAppDir ? `Run this command from its checkout at ${worktreeAppDir}.` : "Attach it in this clone before mutating its published line."),
11504
11396
  "workspace_checkout_mismatch",
11505
11397
  {
11506
11398
  action,
11507
- extra: worktree ? { workspaceDir: worktree } : void 0
11399
+ extra: worktreeAppDir ? { workspaceDir: worktreeAppDir } : void 0
11508
11400
  }
11509
11401
  );
11510
11402
  }
@@ -11528,10 +11420,11 @@ function pushWorkspaceRef(appDir, token, ref, headOid) {
11528
11420
  if (push.status === "committed" || push.status === "up_to_date") return;
11529
11421
  if (push.status === "ref_conflict" || push.status === "non_fast_forward") {
11530
11422
  throw new Refusal(
11531
- `Another checkout advanced this workspace's line, so publishing yours is not a fast-forward \u2014 the push was refused rather than drop that work. Integrate its tip (\`git pull ${SPACE_REMOTE} ${ref}\`, or re-attach the workspace in a fresh dir), resolve any conflicts, then re-run the command. (Amended or rebased your own commits? Same refusal \u2014 merge your old tip back in.)`,
11423
+ `Another checkout advanced this workspace's line, so publishing yours is not a fast-forward \u2014 the push was refused rather than drop that work. Integrate its tip (\`git pull --no-rebase ${SPACE_REMOTE} ${ref}\`, or re-attach the workspace in a fresh dir), resolve any conflicts, then re-run the command. (Amended or rebased your own commits? Same refusal \u2014 merge your old tip back in.)`,
11532
11424
  push.status,
11533
11425
  {
11534
- action: { cwd: appDir, argv: ["git", "pull", SPACE_REMOTE, ref] }
11426
+ actionRequired: true,
11427
+ action: { cwd: appDir, argv: ["git", "pull", "--no-rebase", SPACE_REMOTE, ref] }
11535
11428
  }
11536
11429
  );
11537
11430
  }
@@ -11587,6 +11480,7 @@ var newWorkspaceCommand = defineDeepspaceCommand({
11587
11480
  );
11588
11481
  spinner2?.message("Creating workspace\u2026");
11589
11482
  ensureSpaceRemote(appDir, appId);
11483
+ ensureGitIdentity(appDir, token);
11590
11484
  let baseOid = null;
11591
11485
  const refs = await api4.getRefs();
11592
11486
  if (baseArg) {
@@ -11630,7 +11524,7 @@ var newWorkspaceCommand = defineDeepspaceCommand({
11630
11524
  idempotencyKey: mintIdempotencyKey()
11631
11525
  });
11632
11526
  const dirInput = typeof args.dir === "string" ? args.dir.trim() : "";
11633
- const worktreeRoot = dirInput ? isAbsolute6(dirInput) ? dirInput : resolve17(appDir, dirInput) : defaultWorkspaceRoot(appDir, id);
11527
+ const worktreeRoot = dirInput ? isAbsolute6(dirInput) ? dirInput : resolve18(appDir, dirInput) : defaultWorkspaceRoot(appDir, id);
11634
11528
  const dir = appDirInWorktree(appDir, worktreeRoot);
11635
11529
  try {
11636
11530
  materializeWorkspaceWorktree(appDir, worktreeRoot, branch, baseOid, id);
@@ -11668,8 +11562,18 @@ var newWorkspaceCommand = defineDeepspaceCommand({
11668
11562
 
11669
11563
  // src/cli/commands/workspace/attach.ts
11670
11564
  import * as p26 from "@clack/prompts";
11671
- import { existsSync as existsSync25, readdirSync as readdirSync7, rmSync as rmSync6 } from "fs";
11672
- import { isAbsolute as isAbsolute7, resolve as resolve18 } from "path";
11565
+ import { existsSync as existsSync23, readdirSync as readdirSync7, realpathSync as realpathSync7, rmSync as rmSync6 } from "fs";
11566
+ import { isAbsolute as isAbsolute7, resolve as resolve19 } from "path";
11567
+ function samePath(a, b) {
11568
+ const real = (p35) => {
11569
+ try {
11570
+ return realpathSync7(p35);
11571
+ } catch {
11572
+ return resolve19(p35);
11573
+ }
11574
+ };
11575
+ return real(a) === real(b);
11576
+ }
11673
11577
  function finishedWorkspaceMessage(id, view) {
11674
11578
  const workspace = view.workspace;
11675
11579
  if (workspace.status === "landed") {
@@ -11678,7 +11582,7 @@ function finishedWorkspaceMessage(id, view) {
11678
11582
  return `Workspace ${id} is ${workspace.status} \u2014 its ref is retained briefly (fetch ${workspace.ref} to inspect), but attach only works on active workspaces.`;
11679
11583
  }
11680
11584
  function cleanFailedFreshAttachDir(dir) {
11681
- if (!existsSync25(dir)) return [];
11585
+ if (!existsSync23(dir)) return [];
11682
11586
  let entries;
11683
11587
  try {
11684
11588
  entries = readdirSync7(dir);
@@ -11738,6 +11642,7 @@ var attachWorkspaceCommand = defineDeepspaceCommand({
11738
11642
  });
11739
11643
  }
11740
11644
  ensureSpaceRemote(appDir, appId2);
11645
+ ensureGitIdentity(appDir, token2);
11741
11646
  const localWorkspaceRef = spacePrivateRef(`workspaces/${id}`);
11742
11647
  runGitRemote(appDir, token2, [
11743
11648
  "fetch",
@@ -11754,11 +11659,87 @@ var attachWorkspaceCommand = defineDeepspaceCommand({
11754
11659
  );
11755
11660
  }
11756
11661
  const requestedDir = typeof args.dir === "string" ? args.dir.trim() : "";
11757
- const worktreeRoot = requestedDir ? isAbsolute7(requestedDir) ? requestedDir : resolve18(appDir, requestedDir) : defaultWorkspaceRoot(appDir, id);
11662
+ const worktreeRoot = requestedDir ? isAbsolute7(requestedDir) ? requestedDir : resolve19(appDir, requestedDir) : defaultWorkspaceRoot(appDir, id);
11758
11663
  const dir2 = appDirInWorktree(appDir, worktreeRoot);
11759
- if (resolveCommit(appDir, `refs/heads/${branch}`)) {
11664
+ const localTip = resolveCommit(appDir, `refs/heads/${branch}`);
11665
+ if (localTip) {
11666
+ let existing = resolveWorkspaceWorktree(listWorktrees(appDir), id);
11667
+ if (existing && !existsSync23(existing)) {
11668
+ runGit(appDir, ["worktree", "prune"], { allowFail: true });
11669
+ existing = null;
11670
+ }
11671
+ const relation = workspaceSyncRelation(appDir, localTip, tip2);
11672
+ const inSync = relation === "in_sync";
11673
+ const behindOrDiverged = relation === "behind" || relation === "diverged";
11674
+ const syncAdvice = behindOrDiverged ? `integrate the published tip first (\`git pull --no-rebase ${SPACE_REMOTE} ${view2.workspace.ref}\`)` : `run \`deepspace workspace sync\` from it`;
11675
+ const syncAction = (cwd) => behindOrDiverged ? { cwd, argv: ["git", "pull", "--no-rebase", SPACE_REMOTE, view2.workspace.ref] } : { cwd, argv: ["deepspace", "workspace", "sync"] };
11676
+ if (existing && (!requestedDir || samePath(existing, worktreeRoot))) {
11677
+ const existingDir = appDirInWorktree(appDir, existing);
11678
+ spinner2?.stop(`Already attached: ${id}.`);
11679
+ if (!args.json) {
11680
+ p26.log.info(`Task: ${view2.workspace.task}`);
11681
+ p26.log.info(`Work in: cd ${existingDir}`);
11682
+ if (!inSync) {
11683
+ p26.log.warn(
11684
+ `The checkout is at ${localTip.slice(0, 10)} but the workspace's published tip is ${tip2.slice(0, 10)} \u2014 ${syncAdvice}.`
11685
+ );
11686
+ }
11687
+ }
11688
+ return {
11689
+ data: {
11690
+ workspaceId: id,
11691
+ mode: "worktree",
11692
+ dir: existingDir,
11693
+ worktreeRoot: existing,
11694
+ branch,
11695
+ tipOid: localTip,
11696
+ remoteTip: tip2,
11697
+ syncRelation: relation,
11698
+ task: view2.workspace.task,
11699
+ alreadyAttached: true
11700
+ },
11701
+ ...inSync ? {} : { action: syncAction(existingDir) }
11702
+ };
11703
+ }
11704
+ if (!existing) {
11705
+ const failure = rematerializeWorkspaceWorktree(appDir, worktreeRoot, branch, id);
11706
+ if (failure) {
11707
+ spinner2?.stop("Branch exists.");
11708
+ throw new Refusal(
11709
+ `Branch ${branch} exists locally with no worktree, and re-materializing it failed: ${failure}. Inspect it (\`git log ${branch}\`) and check it out yourself.`,
11710
+ "branch_exists"
11711
+ );
11712
+ }
11713
+ spinner2?.stop(`Re-attached ${id}.`);
11714
+ if (!args.json) {
11715
+ p26.log.info(`Task: ${view2.workspace.task}`);
11716
+ p26.log.info(`Work in: cd ${dir2}`);
11717
+ if (!inSync) {
11718
+ p26.log.warn(
11719
+ `The re-materialized checkout is at ${localTip.slice(0, 10)} but the workspace's published tip is ${tip2.slice(0, 10)} \u2014 after installing, ${syncAdvice}.`
11720
+ );
11721
+ }
11722
+ }
11723
+ const action3 = { cwd: dir2, argv: [detectPackageManager(dir2), "install"] };
11724
+ return {
11725
+ data: {
11726
+ workspaceId: id,
11727
+ mode: "worktree",
11728
+ dir: dir2,
11729
+ worktreeRoot,
11730
+ branch,
11731
+ tipOid: localTip,
11732
+ remoteTip: tip2,
11733
+ syncRelation: relation,
11734
+ task: view2.workspace.task,
11735
+ rematerialized: true
11736
+ },
11737
+ action: action3
11738
+ };
11739
+ }
11740
+ spinner2?.stop("Branch exists.");
11760
11741
  throw new Refusal(
11761
- `Branch ${branch} already exists locally (this workspace has a worktree here). Find it with \`git worktree list\` and continue there.`,
11742
+ `Workspace ${id} is already checked out at ${existing} \u2014 continue there (or drop that worktree before attaching it elsewhere).`,
11762
11743
  "branch_exists"
11763
11744
  );
11764
11745
  }
@@ -11777,6 +11758,10 @@ var attachWorkspaceCommand = defineDeepspaceCommand({
11777
11758
  worktreeRoot,
11778
11759
  branch,
11779
11760
  tipOid: tip2,
11761
+ remoteTip: tip2,
11762
+ // Freshly materialized at the published tip — trivially in sync;
11763
+ // every attach outcome carries the same shape.
11764
+ syncRelation: "in_sync",
11780
11765
  task: view2.workspace.task
11781
11766
  },
11782
11767
  action: action2
@@ -11793,11 +11778,11 @@ var attachWorkspaceCommand = defineDeepspaceCommand({
11793
11778
  extra: { status: view.workspace.status }
11794
11779
  });
11795
11780
  }
11796
- const dir = resolve18(
11781
+ const dir = resolve19(
11797
11782
  process.cwd(),
11798
11783
  (typeof args.dir === "string" ? args.dir.trim() : "") || `ws-${id.slice(3).toLowerCase()}`
11799
11784
  );
11800
- if (existsSync25(dir)) {
11785
+ if (existsSync23(dir)) {
11801
11786
  spinner2?.stop("Target exists.");
11802
11787
  throw new Refusal(`${dir} already exists \u2014 pass a different directory.`, "dir_exists");
11803
11788
  }
@@ -11812,6 +11797,7 @@ var attachWorkspaceCommand = defineDeepspaceCommand({
11812
11797
  try {
11813
11798
  runGit(process.cwd(), ["init", "--quiet", dir]);
11814
11799
  ensureSpaceRemote(dir, appId);
11800
+ ensureGitIdentity(dir, token);
11815
11801
  const localWorkspaceRef = spacePrivateRef(`workspaces/${id}`);
11816
11802
  runGitRemote(dir, token, [
11817
11803
  "fetch",
@@ -11848,6 +11834,8 @@ var attachWorkspaceCommand = defineDeepspaceCommand({
11848
11834
  dir,
11849
11835
  branch,
11850
11836
  tipOid: tip,
11837
+ remoteTip: tip,
11838
+ syncRelation: "in_sync",
11851
11839
  task: view.workspace.task
11852
11840
  },
11853
11841
  action
@@ -11880,7 +11868,19 @@ var syncWorkspaceCommand = defineDeepspaceCommand({
11880
11868
  typeof args.app === "string" ? args.app : void 0
11881
11869
  );
11882
11870
  const id = inferWorkspaceId(appDir, workspaceArg);
11883
- assertSelectedWorkspaceCheckout(appDir, id, ["deepspace", "workspace", "sync"]);
11871
+ try {
11872
+ assertSelectedWorkspaceCheckout(appDir, id, ["deepspace", "workspace", "sync"]);
11873
+ } catch (mismatch) {
11874
+ const current = await api4.getWorkspace(id).then((r) => r.view).catch(() => null);
11875
+ if (current && current.workspace.status !== "active") {
11876
+ spinner2?.stop("Workspace finished.");
11877
+ throw workspaceNotActiveRefusal(id, current.workspace.status, {
11878
+ cwd: appDir,
11879
+ argv: ["deepspace", "workspace", "drop", id, "--app", appId]
11880
+ });
11881
+ }
11882
+ throw mismatch;
11883
+ }
11884
11884
  const headOid = resolveCommit(appDir, "HEAD");
11885
11885
  if (!headOid) {
11886
11886
  throw new Refusal("The workspace has no commits yet \u2014 commit first, then sync.", "no_commits");
@@ -11899,6 +11899,7 @@ var syncWorkspaceCommand = defineDeepspaceCommand({
11899
11899
  );
11900
11900
  }
11901
11901
  ensureSpaceRemote(appDir, appId);
11902
+ ensureGitIdentity(appDir, token);
11902
11903
  pushWorkspaceRef(appDir, token, before.workspace.ref, headOid);
11903
11904
  try {
11904
11905
  fetchTrunk(appDir, token, refs?.head ?? null);
@@ -12039,6 +12040,7 @@ var workspaceStatusCommand = defineDeepspaceCommand({
12039
12040
  const aheadExact = headOid && baseLocal ? revListCount(appDir, workspace.baseOid, headOid) : null;
12040
12041
  const unsynced = headOid !== null && view.tipOid !== headOid;
12041
12042
  const dirty = workspaceDir ? statusFiles(workspaceDir) : [];
12043
+ const relation = !unsynced ? "in_sync" : workspace.status !== "active" || !workspaceDir ? "unknown" : workspaceSyncRelation(workspaceDir, headOid, view.tipOid);
12042
12044
  const lines = await loadWorkspaceLines(appDir, token, api4, trunkOid);
12043
12045
  const myTip = headOid ?? resolveCommit(appDir, peerWorkspaceRef(id)) ?? view.tipOid;
12044
12046
  const line = myTip ? lineChangedPaths(appDir, workspace.baseOid, myTip, trunkOid) : null;
@@ -12058,6 +12060,10 @@ var workspaceStatusCommand = defineDeepspaceCommand({
12058
12060
  ...attached ? {
12059
12061
  headOid,
12060
12062
  unsynced,
12063
+ // The human line distinguishes behind/diverged/ahead; the machine
12064
+ // mirror must too, or a JSON caller can't tell a fast-forward from
12065
+ // a real divergence.
12066
+ syncRelation: relation,
12061
12067
  uncommitted: dirty,
12062
12068
  trunkOverlap
12063
12069
  } : {},
@@ -12073,10 +12079,29 @@ var workspaceStatusCommand = defineDeepspaceCommand({
12073
12079
  `Not checked out in this clone \u2014 showing the published state (local HEAD/uncommitted unavailable). Attach it with \`deepspace workspace attach ${id} <dir>\`.`
12074
12080
  );
12075
12081
  }
12076
- if (unsynced)
12077
- p29.log.warn(
12078
- `Local HEAD ${headOid?.slice(0, 10)} is not synced (server has ${view.tipOid?.slice(0, 10) ?? "nothing"}) \u2014 run \`deepspace workspace sync\`.`
12079
- );
12082
+ if (unsynced) {
12083
+ if (workspace.status === "landed") {
12084
+ p29.log.info(
12085
+ `Already landed${workspace.landedOid ? ` at ${workspace.landedOid.slice(0, 10)}` : ""} \u2014 this checkout is a leftover; \`deepspace workspace drop ${id}\` cleans it up.`
12086
+ );
12087
+ } else if (workspace.status === "dropped") {
12088
+ p29.log.info(
12089
+ `Already dropped \u2014 this checkout is a leftover; \`deepspace workspace drop ${id}\` cleans it up.`
12090
+ );
12091
+ } else if (relation === "behind") {
12092
+ p29.log.warn(
12093
+ `Local HEAD ${headOid?.slice(0, 10)} is BEHIND the published tip ${view.tipOid?.slice(0, 10)} (another checkout synced ahead) \u2014 fast-forward first (\`git pull --no-rebase ${SPACE_REMOTE} ${workspace.ref}\`).`
12094
+ );
12095
+ } else if (relation === "diverged") {
12096
+ p29.log.warn(
12097
+ `Local HEAD ${headOid?.slice(0, 10)} has DIVERGED from the published tip ${view.tipOid?.slice(0, 10)} \u2014 integrate it first (\`git pull --no-rebase ${SPACE_REMOTE} ${workspace.ref}\`), then \`deepspace workspace sync\`.`
12098
+ );
12099
+ } else {
12100
+ p29.log.warn(
12101
+ `Local HEAD ${headOid?.slice(0, 10)} is not synced (server has ${view.tipOid?.slice(0, 10) ?? "nothing"}) \u2014 run \`deepspace workspace sync\`.`
12102
+ );
12103
+ }
12104
+ }
12080
12105
  if (dirty.length > 0) p29.log.warn(`${dirty.length} uncommitted file(s) in the worktree.`);
12081
12106
  if (trunkOverlap.length > 0) {
12082
12107
  p29.log.warn(
@@ -12094,13 +12119,13 @@ import * as p30 from "@clack/prompts";
12094
12119
 
12095
12120
  // src/cli/lib/validation.ts
12096
12121
  import { sync as spawnSync7 } from "cross-spawn";
12097
- import { readFileSync as readFileSync27 } from "fs";
12122
+ import { readFileSync as readFileSync26 } from "fs";
12098
12123
  import { join as join23 } from "path";
12099
12124
  var MAX_SUMMARY_CHARS = 4e3;
12100
12125
  var MAX_OUTPUT_BUFFER = 64 * 1024 * 1024;
12101
12126
  function configuredCommand(appDir) {
12102
12127
  try {
12103
- const pkg2 = JSON.parse(readFileSync27(join23(appDir, "package.json"), "utf-8"));
12128
+ const pkg2 = JSON.parse(readFileSync26(join23(appDir, "package.json"), "utf-8"));
12104
12129
  if (pkg2.scripts?.validate) return `${detectPackageManager(appDir)} run --silent validate`;
12105
12130
  } catch {
12106
12131
  }
@@ -12130,6 +12155,14 @@ ${res.stderr?.toString("utf-8") ?? ""}`.trim();
12130
12155
  function hasLeftoverConflictMarkers(diffCheckOutput) {
12131
12156
  return /leftover conflict marker/.test(diffCheckOutput);
12132
12157
  }
12158
+ function conflictMarkerFiles(diffCheckOutput) {
12159
+ const files = /* @__PURE__ */ new Set();
12160
+ for (const line of diffCheckOutput.split("\n")) {
12161
+ const match = /^(.+?):\d+: leftover conflict marker/.exec(line);
12162
+ if (match) files.add(match[1]);
12163
+ }
12164
+ return [...files];
12165
+ }
12133
12166
  function landResumeArgv(args) {
12134
12167
  const argv = ["deepspace", "workspace", "land"];
12135
12168
  const workspace = args.workspace?.trim();
@@ -12195,7 +12228,20 @@ var landWorkspaceCommand = defineDeepspaceCommand({
12195
12228
  });
12196
12229
  const recoveryAction = { cwd: appDir, argv: resumeArgv };
12197
12230
  const resumeNext = humanCommand(resumeArgv);
12198
- assertSelectedWorkspaceCheckout(appDir, id, resumeArgv);
12231
+ const dropAction = {
12232
+ cwd: appDir,
12233
+ argv: ["deepspace", "workspace", "drop", id, "--app", appId]
12234
+ };
12235
+ try {
12236
+ assertSelectedWorkspaceCheckout(appDir, id, resumeArgv);
12237
+ } catch (mismatch) {
12238
+ const current = await api4.getWorkspace(id).then((r) => r.view).catch(() => null);
12239
+ if (current && current.workspace.status !== "active") {
12240
+ spinner2?.stop("Workspace finished.");
12241
+ throw workspaceNotActiveRefusal(id, current.workspace.status, dropAction);
12242
+ }
12243
+ throw mismatch;
12244
+ }
12199
12245
  const headBefore = resolveCommit(appDir, "HEAD");
12200
12246
  if (!headBefore) {
12201
12247
  throw new Refusal("The workspace has no commits \u2014 nothing to land.", "no_commits");
@@ -12211,13 +12257,10 @@ var landWorkspaceCommand = defineDeepspaceCommand({
12211
12257
  const [{ view }, refs] = await Promise.all([api4.getWorkspace(id), api4.getRefs()]);
12212
12258
  if (view.workspace.status !== "active") {
12213
12259
  spinner2?.stop("Workspace finished.");
12214
- throw new Refusal(
12215
- `Workspace ${id} is already ${view.workspace.status}.`,
12216
- "workspace_not_active",
12217
- { extra: { status: view.workspace.status } }
12218
- );
12260
+ throw workspaceNotActiveRefusal(id, view.workspace.status, dropAction);
12219
12261
  }
12220
12262
  ensureSpaceRemote(appDir, appId);
12263
+ ensureGitIdentity(appDir, token);
12221
12264
  const isResumedMerge = resolveCommit(appDir, "HEAD^2") !== null && view.tipOid !== null && resolveCommit(appDir, "HEAD^") === view.tipOid;
12222
12265
  if (!isResumedMerge) {
12223
12266
  pushWorkspaceRef(appDir, token, view.workspace.ref, headBefore);
@@ -12264,6 +12307,7 @@ var landWorkspaceCommand = defineDeepspaceCommand({
12264
12307
  "merge_conflict",
12265
12308
  {
12266
12309
  actionRequired: true,
12310
+ action: recoveryAction,
12267
12311
  extra: { workspaceId: id, conflict: true }
12268
12312
  }
12269
12313
  );
@@ -12273,14 +12317,17 @@ var landWorkspaceCommand = defineDeepspaceCommand({
12273
12317
  const markerCheck = runGit(appDir, ["diff", "--check", "HEAD^", "HEAD"], {
12274
12318
  allowFail: true
12275
12319
  });
12276
- if (hasLeftoverConflictMarkers(markerCheck.stdout.toString("utf-8"))) {
12320
+ const markerOutput = markerCheck.stdout.toString("utf-8");
12321
+ if (hasLeftoverConflictMarkers(markerOutput)) {
12322
+ const files = conflictMarkerFiles(markerOutput);
12277
12323
  spinner2?.stop("Conflict markers in merge.");
12278
12324
  throw new Refusal(
12279
- `The merge commit still contains unresolved conflict markers (<<<<<<< / ======= / >>>>>>>). Fix the marked files, commit the correction, then re-run \`${resumeNext}\`. Nothing was pushed to ${intoBranch}.`,
12325
+ `The merge commit still contains unresolved conflict markers (<<<<<<< / ======= / >>>>>>>)${files.length ? ` in ${files.join(", ")}` : ""}. Fix the marked files, commit the correction, then re-run \`${resumeNext}\`. Nothing was pushed to ${intoBranch}.`,
12280
12326
  "conflict_markers",
12281
12327
  {
12282
12328
  actionRequired: true,
12283
- extra: { workspaceId: id, conflict: true }
12329
+ action: recoveryAction,
12330
+ extra: { workspaceId: id, conflict: true, ...files.length ? { files } : {} }
12284
12331
  }
12285
12332
  );
12286
12333
  }
@@ -12314,6 +12361,7 @@ var landWorkspaceCommand = defineDeepspaceCommand({
12314
12361
  "validation_failed",
12315
12362
  {
12316
12363
  actionRequired: true,
12364
+ action: recoveryAction,
12317
12365
  extra: { workspaceId: id, passed: false, outputTail }
12318
12366
  }
12319
12367
  );
@@ -12325,6 +12373,7 @@ var landWorkspaceCommand = defineDeepspaceCommand({
12325
12373
  "validation_mutated_tree",
12326
12374
  {
12327
12375
  actionRequired: true,
12376
+ action: recoveryAction,
12328
12377
  extra: { workspaceId: id }
12329
12378
  }
12330
12379
  );
@@ -12360,7 +12409,37 @@ var landWorkspaceCommand = defineDeepspaceCommand({
12360
12409
  "push_rejected"
12361
12410
  );
12362
12411
  }
12363
- await api4.landWorkspace(id, { landedOid, intoRef });
12412
+ try {
12413
+ await api4.landWorkspace(id, { landedOid, intoRef });
12414
+ } catch (err) {
12415
+ const code = errorCode(err);
12416
+ if (code === "workspace_not_active") {
12417
+ const status3 = await api4.getWorkspace(id).then((r) => r.view.workspace.status).catch(() => null);
12418
+ if (status3 && status3 !== "active") {
12419
+ spinner2?.stop("Workspace finished.");
12420
+ throw new Refusal(
12421
+ `${intoBranch} took the land push (${landedOid.slice(0, 10)}), and the workspace is already ${status3} \u2014 a concurrent land or drop finished first, so there is nothing left to record. This checkout is a leftover; \`deepspace workspace drop ${id}\` cleans it up.`,
12422
+ "workspace_not_active",
12423
+ {
12424
+ actionRequired: true,
12425
+ action: dropAction,
12426
+ extra: { workspaceId: id, into: intoBranch, landedOid, pushed: true, status: status3 }
12427
+ }
12428
+ );
12429
+ }
12430
+ }
12431
+ spinner2?.stop("Landed; not recorded.");
12432
+ const detail = err instanceof Error ? err.message : String(err);
12433
+ throw new Refusal(
12434
+ `The land push succeeded (${intoBranch} was at ${landedOid.slice(0, 10)}), but recording the land failed (${detail}). Resolve that, then re-run \`${resumeNext}\` \u2014 it re-checks the merge (a no-op when already pushed) and records it.`,
12435
+ "land_unrecorded",
12436
+ {
12437
+ actionRequired: true,
12438
+ action: recoveryAction,
12439
+ extra: { workspaceId: id, into: intoBranch, landedOid, pushed: true }
12440
+ }
12441
+ );
12442
+ }
12364
12443
  spinner2?.stop(`Landed ${id} into ${intoBranch} at ${landedOid.slice(0, 10)}.`);
12365
12444
  const cleanup = keepWorktree ? null : cleanupWorkspaceLocal(appDir, id, intoBranch, { expectedBranchOid: landedOid });
12366
12445
  const inOwnWorktree = !cleanup && inOwnLinkedWorktree(appDir, id);
@@ -12405,6 +12484,22 @@ function isWorkspaceTipPublished(appDir, localTip, publishedOids) {
12405
12484
  );
12406
12485
  }
12407
12486
  function workspaceUnsyncedRefusal(args) {
12487
+ const dropKeepArgv = [
12488
+ "deepspace",
12489
+ "workspace",
12490
+ "drop",
12491
+ args.id,
12492
+ "--app",
12493
+ args.appId,
12494
+ "--keep-worktree"
12495
+ ];
12496
+ if (args.status !== "active") {
12497
+ return new Refusal(
12498
+ `${args.id} is already ${args.status} on the server, and this checkout has committed work that was never published \u2014 nothing can publish it now. Keep it with \`${humanCommand(dropKeepArgv)}\` (retains the branch/worktree for manual disposal), or delete the branch yourself. Nothing was dropped.`,
12499
+ "workspace_unsynced",
12500
+ { extra: { workspaceId: args.id, status: args.status } }
12501
+ );
12502
+ }
12408
12503
  const syncArgv = ["deepspace", "workspace", "sync", "--app", args.appId, "--workspace", args.id];
12409
12504
  const dropArgv = ["deepspace", "workspace", "drop", args.id, "--app", args.appId];
12410
12505
  const publish = 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)}\`.`;
@@ -12412,11 +12507,25 @@ function workspaceUnsyncedRefusal(args) {
12412
12507
  `${args.id} has committed work that isn't published yet. ${publish} Alternatively, pass \`--keep-worktree\` to drop the cloud workspace while retaining the local branch/worktree for manual disposal. Nothing was dropped.`,
12413
12508
  "workspace_unsynced",
12414
12509
  {
12510
+ // Exit 1 with an unblock action, deliberately: drop is destructive and
12511
+ // refusing it is a hard stop, not the "your turn" hand-off exit 2 means.
12415
12512
  ...args.workspaceDir ? { action: { cwd: args.workspaceDir, argv: syncArgv } } : {},
12416
- extra: { workspaceId: args.id }
12513
+ extra: { workspaceId: args.id, status: args.status }
12417
12514
  }
12418
12515
  );
12419
12516
  }
12517
+ async function dropRemoteTolerant(api4, id, statusBefore) {
12518
+ try {
12519
+ const view = (await api4.dropWorkspace(id)).view;
12520
+ return { view, remoteDropped: statusBefore === "active" && view.workspace.status === "dropped" };
12521
+ } catch (err) {
12522
+ const code = errorCode(err);
12523
+ if (code !== "workspace_not_active") throw err;
12524
+ const current = await api4.getWorkspace(id).catch(() => null);
12525
+ if (!current || current.view.workspace.status === "active") throw err;
12526
+ return { view: current.view, remoteDropped: false };
12527
+ }
12528
+ }
12420
12529
  var dropWorkspaceCommand = defineDeepspaceCommand({
12421
12530
  meta: { name: "drop", description: "Abandon a workspace (ref retained briefly, then GC)" },
12422
12531
  args: {
@@ -12441,32 +12550,47 @@ var dropWorkspaceCommand = defineDeepspaceCommand({
12441
12550
  spinner2?.start(`Preparing to drop ${explicitId ?? "workspace"}\u2026`);
12442
12551
  if (!findAppDir() && explicitId) {
12443
12552
  const { api: api5 } = await resolveApiOnly(appArg);
12444
- const { view: view2 } = await api5.dropWorkspace(explicitId);
12553
+ const statusBefore = (await api5.getWorkspace(explicitId)).view.workspace.status;
12554
+ const { view: view2, remoteDropped: remoteDropped2 } = await dropRemoteTolerant(api5, explicitId, statusBefore);
12445
12555
  spinner2?.stop();
12446
12556
  if (!args.json) {
12447
12557
  p31.log.success(
12448
- `Dropped ${explicitId}. Its ref sticks around briefly for undo, then is reaped.`
12558
+ remoteDropped2 ? `Dropped ${explicitId}. Its ref sticks around briefly for undo, then is reaped.` : `Workspace ${explicitId} was already ${view2.workspace.status} on the server \u2014 nothing to drop remotely.`
12449
12559
  );
12450
12560
  }
12451
- return { data: { workspaceId: explicitId, status: view2.workspace.status } };
12561
+ return { data: { workspaceId: explicitId, status: view2.workspace.status, remoteDropped: remoteDropped2 } };
12452
12562
  }
12453
- const { appDir, appId, api: api4 } = await resolveTarget2(appArg);
12563
+ const { appDir, appId, token, api: api4 } = await resolveTarget2(appArg);
12454
12564
  const id = inferWorkspaceId(appDir, explicitId);
12455
12565
  const inspection = inspectWorkspaceCleanup(appDir, id);
12456
12566
  let approvedBranchOid;
12567
+ const { view: before } = await api4.getWorkspace(id);
12457
12568
  if (!keepWorktree && inspection.willDeleteBranch && inspection.branchOid) {
12458
- const { view: before } = await api4.getWorkspace(id);
12459
12569
  const publishedOids = [
12460
12570
  before.tipOid,
12461
12571
  before.workspace.landedOid,
12462
12572
  before.workspace.baseOid
12463
12573
  ].filter((oid) => oid !== null);
12464
- if (!isWorkspaceTipPublished(appDir, inspection.branchOid, publishedOids)) {
12574
+ let published = isWorkspaceTipPublished(appDir, inspection.branchOid, publishedOids);
12575
+ if (!published && before.workspace.status !== "active") {
12576
+ ensureSpaceRemote(appDir, appId);
12577
+ const refs = await api4.getRefs();
12578
+ const trunkRef = refs?.head?.startsWith("refs/heads/") ? refs.head : null;
12579
+ if (trunkRef) {
12580
+ runGitRemote(appDir, token, ["fetch", "--quiet", "--refmap=", SPACE_REMOTE, trunkRef], {
12581
+ allowFail: true
12582
+ });
12583
+ const trunkTip = resolveCommit(appDir, "FETCH_HEAD");
12584
+ published = trunkTip !== null && isWorkspaceTipPublished(appDir, inspection.branchOid, [trunkTip]);
12585
+ }
12586
+ }
12587
+ if (!published) {
12465
12588
  throw workspaceUnsyncedRefusal({
12466
12589
  appId,
12467
12590
  id,
12468
12591
  branch: inspection.branch,
12469
- workspaceDir: inspection.checkout.dir
12592
+ workspaceDir: inspection.checkout.dir,
12593
+ status: before.workspace.status
12470
12594
  });
12471
12595
  }
12472
12596
  approvedBranchOid = inspection.branchOid;
@@ -12481,7 +12605,7 @@ var dropWorkspaceCommand = defineDeepspaceCommand({
12481
12605
  { extra: { workspaceId: id } }
12482
12606
  );
12483
12607
  }
12484
- const { view } = await api4.dropWorkspace(id);
12608
+ const { view, remoteDropped } = await dropRemoteTolerant(api4, id, before.workspace.status);
12485
12609
  spinner2?.message(`Cleaning up ${id} locally\u2026`);
12486
12610
  const cleanup = keepWorktree ? null : cleanupWorkspaceLocal(appDir, id, null, {
12487
12611
  expectedBranchOid: approvedBranchOid,
@@ -12491,12 +12615,15 @@ var dropWorkspaceCommand = defineDeepspaceCommand({
12491
12615
  const data = {
12492
12616
  workspaceId: id,
12493
12617
  status: view.workspace.status,
12618
+ remoteDropped,
12494
12619
  ...retainedWorktree ? { worktree: retainedWorktree } : {},
12495
12620
  ...cleanup ? { cleanup: cleanupJson(cleanup) } : {}
12496
12621
  };
12497
12622
  spinner2?.stop();
12498
12623
  if (!args.json) {
12499
- p31.log.success(`Dropped ${id}. Its ref sticks around briefly for undo, then is reaped.`);
12624
+ p31.log.success(
12625
+ remoteDropped ? `Dropped ${id}. Its ref sticks around briefly for undo, then is reaped.` : `Workspace ${id} was already ${view.workspace.status} on the server \u2014 nothing to drop remotely.`
12626
+ );
12500
12627
  if (cleanup && !cleanup.error) reportCleanupHuman(cleanup);
12501
12628
  else if (retainedWorktree) p31.log.info(`Retained checkout: ${retainedWorktree}`);
12502
12629
  }
@@ -12565,7 +12692,7 @@ function formatEvent2(e, lands, actors) {
12565
12692
  detail = `"${String(s.task ?? "")}" from ${short(s.baseOid)}`;
12566
12693
  break;
12567
12694
  case "workspace.synced":
12568
- detail = `\u2192 ${short(s.headOid)} (${Number(s.files ?? 0)} files vs base)`;
12695
+ detail = `\u2192 ${short(s.headOid)}`;
12569
12696
  break;
12570
12697
  case "workspace.landed":
12571
12698
  detail = `"${String(s.task ?? "")}" \u2192 ${short(s.landedOid)} on ${String(s.into ?? "trunk")}`;
@@ -13063,8 +13190,8 @@ function sameRefs(left, right) {
13063
13190
 
13064
13191
  // src/cli/commands/update.ts
13065
13192
  import * as p34 from "@clack/prompts";
13066
- import { existsSync as existsSync26, readFileSync as readFileSync28, writeFileSync as writeFileSync10 } from "fs";
13067
- import { join as join24, resolve as resolve19 } from "path";
13193
+ import { existsSync as existsSync24, readFileSync as readFileSync27, writeFileSync as writeFileSync10 } from "fs";
13194
+ import { join as join24, resolve as resolve20 } from "path";
13068
13195
  var REGISTRY = "https://registry.npmjs.org/deepspace/latest";
13069
13196
  var DOCS_URL = "https://documentation.deep.space/cli-reference/commands";
13070
13197
  var OLDEST_SUPPORTED_MINOR = 12;
@@ -13079,18 +13206,18 @@ function gapIsTooWide(from, to) {
13079
13206
  }
13080
13207
  function readAppSdkVersion(appDir) {
13081
13208
  const installed = join24(appDir, "node_modules", "deepspace", "package.json");
13082
- if (existsSync26(installed)) {
13083
- const parsed2 = JSON.parse(readFileSync28(installed, "utf8"));
13209
+ if (existsSync24(installed)) {
13210
+ const parsed2 = JSON.parse(readFileSync27(installed, "utf8"));
13084
13211
  if (typeof parsed2.version === "string") return parsed2.version;
13085
13212
  }
13086
13213
  const manifest = join24(appDir, "package.json");
13087
- if (!existsSync26(manifest)) return null;
13088
- const parsed = JSON.parse(readFileSync28(manifest, "utf8"));
13214
+ if (!existsSync24(manifest)) return null;
13215
+ const parsed = JSON.parse(readFileSync27(manifest, "utf8"));
13089
13216
  return parsed.dependencies?.deepspace ?? null;
13090
13217
  }
13091
13218
  function pinSdkVersion(appDir, version) {
13092
13219
  const path = join24(appDir, "package.json");
13093
- const source = readFileSync28(path, "utf8");
13220
+ const source = readFileSync27(path, "utf8");
13094
13221
  const updated = source.replace(
13095
13222
  /("deepspace"\s*:\s*")[^"]+(")/,
13096
13223
  (_match, head, tail) => `${head}^${version}${tail}`
@@ -13155,7 +13282,7 @@ var update_default = defineDeepspaceCommand({
13155
13282
  }
13156
13283
  },
13157
13284
  async run({ args }) {
13158
- const appDir = resolve19(".");
13285
+ const appDir = resolve20(".");
13159
13286
  if (!hasWranglerConfig(appDir)) {
13160
13287
  throw new Refusal(
13161
13288
  "No wrangler.toml found \u2014 run this from inside a DeepSpace app directory.",
@@ -13235,9 +13362,9 @@ var update_default = defineDeepspaceCommand({
13235
13362
  });
13236
13363
 
13237
13364
  // src/cli/cli.ts
13238
- var __dirname = dirname10(fileURLToPath3(import.meta.url));
13239
- var pkg = JSON.parse(readFileSync29(join25(__dirname, "..", "package.json"), "utf-8"));
13240
- if (process.platform === "linux" && !process.env.SSL_CERT_FILE && existsSync27("/etc/ssl/certs/ca-certificates.crt")) {
13365
+ var __dirname = dirname11(fileURLToPath3(import.meta.url));
13366
+ var pkg = JSON.parse(readFileSync28(join25(__dirname, "..", "package.json"), "utf-8"));
13367
+ if (process.platform === "linux" && !process.env.SSL_CERT_FILE && existsSync25("/etc/ssl/certs/ca-certificates.crt")) {
13241
13368
  process.env.SSL_CERT_FILE = "/etc/ssl/certs/ca-certificates.crt";
13242
13369
  }
13243
13370
  var create2 = defineCommand14({
@@ -13363,7 +13490,49 @@ function assertKnownCommandPath(argv) {
13363
13490
  `${unknownCommandMessage(unknown)}
13364
13491
  Run \`${unknown.helpPath.join(" ")} --help\` for the command list.`
13365
13492
  );
13366
- if (unknown.executable) printAction({ cwd: process.cwd(), argv: unknown.suggestion });
13493
+ if (unknown.executable) {
13494
+ printAction(executableAction({ cwd: process.cwd(), argv: unknown.suggestion }));
13495
+ }
13496
+ process.exit(1);
13497
+ }
13498
+ function selectedLeaf(argv) {
13499
+ let command = main;
13500
+ let remaining = argv;
13501
+ const path = [];
13502
+ const commandPrefixArgs = [];
13503
+ for (; ; ) {
13504
+ const subCommands = command.subCommands;
13505
+ if (!subCommands || Object.keys(subCommands).length === 0) {
13506
+ return { command, rawArgs: [...commandPrefixArgs, ...remaining], path };
13507
+ }
13508
+ const index = remaining.findIndex((argument) => !argument.startsWith("-"));
13509
+ if (index < 0) return { command, rawArgs: [...commandPrefixArgs, ...remaining], path };
13510
+ const name = remaining[index];
13511
+ const entry = Object.entries(subCommands).find(([key, child]) => {
13512
+ if (key === name) return true;
13513
+ const alias = child.meta?.alias;
13514
+ return (Array.isArray(alias) ? alias : alias ? [alias] : []).includes(name);
13515
+ });
13516
+ if (!entry) return null;
13517
+ commandPrefixArgs.push(...remaining.slice(0, index));
13518
+ path.push(entry[0]);
13519
+ command = entry[1];
13520
+ remaining = remaining.slice(index + 1);
13521
+ }
13522
+ }
13523
+ function assertKnownOptions(argv) {
13524
+ const selected = selectedLeaf(argv);
13525
+ if (!selected || selected.path.join(" ") === "app create") return;
13526
+ const args = selected.command.args ?? {};
13527
+ const unknown = unknownOptionNamesFromRaw(selected.rawArgs, args);
13528
+ if (unknown.length === 0) return;
13529
+ const commandName = selected.path.join(" ") || "deepspace";
13530
+ const message = unknownOptionMessage(commandName, unknown, args);
13531
+ if (argv.includes("--json")) {
13532
+ console.log(JSON.stringify({ ok: false, code: "unknown_option", error: message }));
13533
+ } else {
13534
+ console.error(withSlug(message, "unknown_option"));
13535
+ }
13367
13536
  process.exit(1);
13368
13537
  }
13369
13538
  var rawArgs = process.argv.slice(2);
@@ -13377,6 +13546,9 @@ if (DEEPSPACE_ENV === "invalid") {
13377
13546
  }
13378
13547
  process.exit(1);
13379
13548
  }
13549
+ if (!wantsHelp && !(rawArgs.length === 1 && ["--version", "-v"].includes(rawArgs[0]))) {
13550
+ assertKnownOptions(rawArgs);
13551
+ }
13380
13552
  if (rawArgs.includes("--json") && !wantsHelp) {
13381
13553
  const unknown = findUnknownCommand(rawArgs, commandTree());
13382
13554
  if (unknown) {
@@ -13385,7 +13557,7 @@ if (rawArgs.includes("--json") && !wantsHelp) {
13385
13557
  ok: false,
13386
13558
  code: "unknown_command",
13387
13559
  error: unknownCommandMessage(unknown),
13388
- ...unknown.executable ? { action: { cwd: process.cwd(), argv: unknown.suggestion } } : {}
13560
+ ...unknown.executable ? { action: executableAction({ cwd: process.cwd(), argv: unknown.suggestion }) } : {}
13389
13561
  })
13390
13562
  );
13391
13563
  process.exit(1);