deepspace 0.4.2 → 0.4.4

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,16 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli/cli.ts
4
- import { defineCommand as defineCommand19, runMain } from "citty";
4
+ import { defineCommand as defineCommand21, runMain } from "citty";
5
5
  import { execSync as execSync5 } from "child_process";
6
- import { readFileSync as readFileSync11, existsSync as existsSync14 } from "fs";
7
- import { dirname as dirname2, join as join16 } from "path";
8
- import { fileURLToPath } from "url";
6
+ import { readFileSync as readFileSync13, existsSync as existsSync14 } from "fs";
7
+ import { dirname as dirname4, join as join16 } from "path";
8
+ import { fileURLToPath as fileURLToPath2 } from "url";
9
9
 
10
10
  // src/cli/commands/login.ts
11
11
  import { defineCommand } from "citty";
12
12
  import { writeFileSync as writeFileSync2, mkdirSync } from "fs";
13
- import { join as join2 } from "path";
13
+ import { join as join3 } from "path";
14
14
  import { homedir } from "os";
15
15
  import { exec } from "child_process";
16
16
  import { platform } from "os";
@@ -35,7 +35,11 @@ function createSpinner() {
35
35
  }
36
36
 
37
37
  // src/cli/env.ts
38
- import { existsSync, readFileSync, writeFileSync } from "fs";
38
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
39
+ import { join as join2 } from "path";
40
+
41
+ // src/cli/lib/wrangler-env.ts
42
+ import { existsSync, readFileSync } from "fs";
39
43
  import { join } from "path";
40
44
  import { parse as parseToml } from "smol-toml";
41
45
 
@@ -92,6 +96,54 @@ function sanitizeAppName(raw) {
92
96
  return raw.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "");
93
97
  }
94
98
 
99
+ // src/cli/lib/wrangler-env.ts
100
+ function resolveAppNameForEnv(config, envName) {
101
+ if (!envName) {
102
+ return resolveAppName(config.name);
103
+ }
104
+ const envBlock = config.env?.[envName];
105
+ if (!envBlock) {
106
+ return {
107
+ ok: false,
108
+ reason: `wrangler.toml: no [env.${envName}] block found. Add one with a distinct \`name\` to deploy this environment.`
109
+ };
110
+ }
111
+ if (typeof envBlock.name !== "string" || envBlock.name.trim() === "") {
112
+ return {
113
+ ok: false,
114
+ reason: `wrangler.toml: [env.${envName}].name is missing. Set a distinct app name (e.g. "${typeof config.name === "string" ? config.name : "myapp"}-${envName}") so this environment doesn't overwrite the default.`
115
+ };
116
+ }
117
+ if (envBlock.name === config.name) {
118
+ return {
119
+ ok: false,
120
+ reason: `wrangler.toml: [env.${envName}].name "${envBlock.name}" is the same as the top-level name. Pick a distinct name (e.g. "${config.name}-${envName}") so deploys don't collide.`
121
+ };
122
+ }
123
+ return resolveAppName(envBlock.name);
124
+ }
125
+ var WranglerConfigError = class extends Error {
126
+ constructor(path, cause) {
127
+ const detail = cause instanceof Error ? cause.message : String(cause);
128
+ super(`wrangler.toml: malformed TOML at ${path}: ${detail}`);
129
+ this.path = path;
130
+ this.cause = cause;
131
+ this.name = "WranglerConfigError";
132
+ }
133
+ };
134
+ function readWranglerConfig(appDir) {
135
+ const path = join(appDir, "wrangler.toml");
136
+ const raw = readFileSync(path, "utf-8");
137
+ try {
138
+ return parseToml(raw);
139
+ } catch (err) {
140
+ throw new WranglerConfigError(path, err);
141
+ }
142
+ }
143
+ function hasWranglerConfig(appDir) {
144
+ return existsSync(join(appDir, "wrangler.toml"));
145
+ }
146
+
95
147
  // src/cli/env.ts
96
148
  var ENV_URLS = {
97
149
  auth: "https://auth.deep.space",
@@ -115,19 +167,18 @@ async function fetchPublicKey(authUrl) {
115
167
  }
116
168
  return data.publicKey;
117
169
  }
118
- function readAppName(appDir) {
119
- const wranglerPath = join(appDir, "wrangler.toml");
120
- if (!existsSync(wranglerPath)) {
170
+ function readAppName(appDir, wranglerEnv) {
171
+ if (!hasWranglerConfig(appDir)) {
121
172
  throw new Error("No wrangler.toml found. Are you in a DeepSpace app directory?");
122
173
  }
123
- const parsed = parseToml(readFileSync(wranglerPath, "utf-8"));
124
- const resolved = resolveAppName(parsed.name);
125
- if (!resolved.ok) {
126
- throw new Error(`wrangler.toml: ${resolved.reason}`);
127
- }
128
- if (parsed.name !== resolved.name) {
174
+ const config = readWranglerConfig(appDir);
175
+ const resolved = resolveAppNameForEnv(config, wranglerEnv);
176
+ if (!resolved.ok) throw new Error(resolved.reason);
177
+ const rawName = wranglerEnv ? config.env?.[wranglerEnv]?.name : config.name;
178
+ if (rawName !== resolved.name) {
179
+ const where = wranglerEnv ? `[env.${wranglerEnv}].name` : "`name`";
129
180
  throw new Error(
130
- `wrangler.toml: name "${parsed.name}" is not in canonical form. Update \`name\` to "${resolved.name}" and re-run.`
181
+ `wrangler.toml: ${where} "${rawName}" is not in canonical form. Update it to "${resolved.name}" and re-run.`
131
182
  );
132
183
  }
133
184
  return resolved.name;
@@ -181,8 +232,22 @@ var SDK_MANAGED_KEYS = /* @__PURE__ */ new Set([
181
232
  "OWNER_USER_ID",
182
233
  "APP_OWNER_JWT",
183
234
  "APP_IDENTITY_TOKEN",
235
+ "APP_SECRETS_KEY",
236
+ "APP_SECRETS_LOCAL_FALLBACK",
184
237
  "ALLOW_DEBUG_ROUTES"
185
238
  ]);
239
+ var MANAGED_ENV_HEADER = "# Managed by `deepspace secrets` and regenerated by `deepspace dev`/`deepspace test`.";
240
+ var MANAGED_ENV_SYNCED_HEADER = "# DeepSpace synced secrets - mirrored with the deployed app secrets store.";
241
+ var MANAGED_ENV_DEV_ONLY_HEADER = "# DeepSpace dev-only secrets - used by `deepspace dev`; never synced unless pushed.";
242
+ function localEnvPath(appDir) {
243
+ return join2(appDir, ".env");
244
+ }
245
+ function legacyDevVarsPath(appDir, wranglerEnv) {
246
+ return join2(appDir, wranglerEnv ? `.dev.vars.${wranglerEnv}` : ".dev.vars");
247
+ }
248
+ function hasLegacyDevVars(appDir, wranglerEnv) {
249
+ return existsSync2(legacyDevVarsPath(appDir, wranglerEnv));
250
+ }
186
251
  var DEV_VARS_DIVIDER = "# --- not managed by the SDK; preserved across dev/test runs ---";
187
252
  function extractCustomDevVars(content) {
188
253
  const lines = content.split("\n");
@@ -289,10 +354,193 @@ function parseDevVars(content) {
289
354
  }
290
355
  return result;
291
356
  }
292
- async function writeDevVars(appDir, env, ownerId, callerJwt) {
357
+ function parseDevVarsStrict(content, file = ".env") {
358
+ const result = {};
359
+ const lines = content.split("\n");
360
+ let i = 0;
361
+ while (i < lines.length) {
362
+ const line = lines[i];
363
+ const trimmed = line.trim();
364
+ if (!trimmed || trimmed.startsWith("#")) {
365
+ i++;
366
+ continue;
367
+ }
368
+ const eq = line.indexOf("=");
369
+ if (eq < 0) {
370
+ throw new Error(`${file}: invalid dotenv syntax on line ${i + 1}. Expected KEY=value.`);
371
+ }
372
+ const keyMatch = line.slice(0, eq).match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*$/);
373
+ if (!keyMatch) {
374
+ throw new Error(`${file}: invalid variable name on line ${i + 1}.`);
375
+ }
376
+ const key = keyMatch[1];
377
+ let value = line.slice(eq + 1);
378
+ let closed = !value.startsWith('"') || hasUnescapedQuote(value.slice(1));
379
+ if (!closed) {
380
+ i++;
381
+ while (i < lines.length) {
382
+ value += "\n" + lines[i];
383
+ if (hasUnescapedQuote(lines[i])) {
384
+ closed = true;
385
+ i++;
386
+ break;
387
+ }
388
+ i++;
389
+ }
390
+ } else {
391
+ i++;
392
+ }
393
+ if (value.startsWith('"') && !closed) {
394
+ throw new Error(`${file}: unterminated quoted value for key "${key}".`);
395
+ }
396
+ if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
397
+ value = value.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
398
+ }
399
+ result[key] = value;
400
+ }
401
+ return result;
402
+ }
403
+ function quoteEnvValue(value) {
404
+ return /^[A-Za-z0-9_./:@-]*$/.test(value) && !value.includes("\n") ? value : `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
405
+ }
406
+ function stripSdkManaged(vars) {
407
+ const out = {};
408
+ for (const [name, value] of Object.entries(vars)) {
409
+ if (!SDK_MANAGED_KEYS.has(name)) out[name] = value;
410
+ }
411
+ return out;
412
+ }
413
+ function sortRecord(input) {
414
+ const out = {};
415
+ for (const key of Object.keys(input).sort()) out[key] = input[key];
416
+ return out;
417
+ }
418
+ function readManagedEnvSections(appDir) {
419
+ const path = localEnvPath(appDir);
420
+ if (!existsSync2(path)) return { sdk: {}, synced: {}, devOnly: {} };
421
+ return parseManagedEnvSections(readFileSync2(path, "utf-8"));
422
+ }
423
+ function parseManagedEnvSections(content) {
424
+ const sections = { sdk: {}, synced: {}, devOnly: {} };
425
+ const legacyAppLines = [];
426
+ let target = "sdk";
427
+ const lines = content.split("\n");
428
+ for (let i = 0; i < lines.length; i++) {
429
+ const line = lines[i];
430
+ if (line.trim() === MANAGED_ENV_SYNCED_HEADER.trim()) {
431
+ target = "synced";
432
+ continue;
433
+ }
434
+ if (line.trim() === MANAGED_ENV_DEV_ONLY_HEADER.trim()) {
435
+ target = "devOnly";
436
+ continue;
437
+ }
438
+ if (line.trim().startsWith("#")) continue;
439
+ const name = assignmentName(line);
440
+ if (!name) {
441
+ if (line.trim()) legacyAppLines.push(line);
442
+ continue;
443
+ }
444
+ const block = [line];
445
+ const eq = line.indexOf("=");
446
+ const valStart = eq >= 0 ? line.slice(eq + 1) : "";
447
+ if (valStart.startsWith('"') && !hasUnescapedQuote(valStart.slice(1))) {
448
+ i++;
449
+ while (i < lines.length) {
450
+ block.push(lines[i]);
451
+ const closed = hasUnescapedQuote(lines[i]);
452
+ if (closed) break;
453
+ i++;
454
+ }
455
+ }
456
+ if (target === "sdk" && !SDK_MANAGED_KEYS.has(name)) {
457
+ legacyAppLines.push(...block);
458
+ continue;
459
+ }
460
+ const parsed = parseDevVarsStrict(block.join("\n"), ".env");
461
+ Object.assign(sections[target], parsed);
462
+ }
463
+ if (legacyAppLines.length) {
464
+ Object.assign(sections.synced, stripSdkManaged(parseDevVarsStrict(legacyAppLines.join("\n"), ".env")));
465
+ }
466
+ return sections;
467
+ }
468
+ function assignmentName(line) {
469
+ const eq = line.indexOf("=");
470
+ if (eq < 0) return null;
471
+ const match = line.slice(0, eq).match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*$/);
472
+ return match?.[1] ?? null;
473
+ }
474
+ function readManagedAppEnv(appDir) {
475
+ const sections = readManagedEnvSections(appDir);
476
+ return { ...sections.synced, ...sections.devOnly };
477
+ }
478
+ function readManagedSyncedEnv(appDir) {
479
+ return { ...readManagedEnvSections(appDir).synced };
480
+ }
481
+ function readManagedDevOnlyEnv(appDir) {
482
+ return { ...readManagedEnvSections(appDir).devOnly };
483
+ }
484
+ function writeManagedEnvSections(appDir, sections) {
485
+ ensureLocalEnvIgnored(appDir);
486
+ const lines = [
487
+ MANAGED_ENV_HEADER,
488
+ "# Do not commit this file. Edit secrets with `deepspace secrets`, not by hand."
489
+ ];
490
+ for (const [name, value] of Object.entries(sortRecord(sections.sdk))) {
491
+ lines.push(`${name}=${quoteEnvValue(value)}`);
492
+ }
493
+ lines.push("", MANAGED_ENV_SYNCED_HEADER);
494
+ for (const [name, value] of Object.entries(sortRecord(sections.synced))) {
495
+ lines.push(`${name}=${quoteEnvValue(value)}`);
496
+ }
497
+ lines.push("", MANAGED_ENV_DEV_ONLY_HEADER);
498
+ for (const [name, value] of Object.entries(sortRecord(sections.devOnly))) {
499
+ lines.push(`${name}=${quoteEnvValue(value)}`);
500
+ }
501
+ writeFileSync(localEnvPath(appDir), lines.join("\n") + "\n", { mode: 384 });
502
+ }
503
+ function ensureLocalEnvIgnored(appDir) {
504
+ const path = join2(appDir, ".gitignore");
505
+ const existing = existsSync2(path) ? readFileSync2(path, "utf-8") : "";
506
+ const lines = new Set(existing.split("\n").map((line) => line.trim()));
507
+ const additions = [".env", ".dev.vars"].filter((entry) => !lines.has(entry));
508
+ if (!additions.length) return;
509
+ const prefix = existing && !existing.endsWith("\n") ? existing + "\n" : existing;
510
+ writeFileSync(path, `${prefix}${additions.join("\n")}
511
+ `);
512
+ }
513
+ function patchManagedSyncedEnv(appDir, set2 = {}, removeNames = []) {
514
+ const sections = readManagedEnvSections(appDir);
515
+ for (const [name, value] of Object.entries(set2)) sections.synced[name] = value;
516
+ for (const name of removeNames) delete sections.synced[name];
517
+ writeManagedEnvSections(appDir, sections);
518
+ }
519
+ function patchManagedDevOnlyEnv(appDir, set2 = {}, removeNames = []) {
520
+ const sections = readManagedEnvSections(appDir);
521
+ for (const [name, value] of Object.entries(set2)) sections.devOnly[name] = value;
522
+ for (const name of removeNames) delete sections.devOnly[name];
523
+ writeManagedEnvSections(appDir, sections);
524
+ }
525
+ function replaceManagedSyncedEnv(appDir, syncedVars) {
526
+ const sections = readManagedEnvSections(appDir);
527
+ sections.synced = stripSdkManaged(syncedVars);
528
+ writeManagedEnvSections(appDir, sections);
529
+ }
530
+ function moveManagedDevOnlyToSynced(appDir, names) {
531
+ const sections = readManagedEnvSections(appDir);
532
+ for (const name of names) {
533
+ if (name in sections.devOnly) {
534
+ sections.synced[name] = sections.devOnly[name];
535
+ delete sections.devOnly[name];
536
+ }
537
+ }
538
+ writeManagedEnvSections(appDir, sections);
539
+ }
540
+ async function writeDevVars(appDir, env, ownerId, callerJwt, wranglerEnv) {
293
541
  const urls = ENVS[env];
294
542
  const publicKey = await fetchPublicKey(urls.auth);
295
- const appName = readAppName(appDir);
543
+ const appName = readAppName(appDir, wranglerEnv);
296
544
  const appOwnerJwt = await mintAppOwnerJwt(urls.auth, callerJwt, appName);
297
545
  const appIdentityToken = await fetchAppIdentityToken(urls.deploy, callerJwt, appName);
298
546
  const sdkVars = [
@@ -304,12 +552,19 @@ async function writeDevVars(appDir, env, ownerId, callerJwt) {
304
552
  `OWNER_USER_ID=${ownerId}`,
305
553
  `APP_OWNER_JWT=${appOwnerJwt}`,
306
554
  ...appIdentityToken ? [`APP_IDENTITY_TOKEN=${appIdentityToken}`] : [],
555
+ `APP_SECRETS_LOCAL_FALLBACK=true`,
307
556
  // Open /api/debug/* (proxied to RecordRoom) for dev + tests only.
308
557
  // Production secrets never set this, so deployed apps 404 the route.
309
558
  `ALLOW_DEBUG_ROUTES=true`
310
559
  ].join("\n");
311
- const devVarsPath = join(appDir, ".dev.vars");
312
- const existing = existsSync(devVarsPath) ? readFileSync(devVarsPath, "utf-8") : "";
560
+ const sdkEnv = parseDevVarsStrict(sdkVars, "SDK env");
561
+ if (!hasLegacyDevVars(appDir, wranglerEnv)) {
562
+ const sections = readManagedEnvSections(appDir);
563
+ writeManagedEnvSections(appDir, { ...sections, sdk: sdkEnv });
564
+ return;
565
+ }
566
+ const devVarsPath = legacyDevVarsPath(appDir, wranglerEnv);
567
+ const existing = existsSync2(devVarsPath) ? readFileSync2(devVarsPath, "utf-8") : "";
313
568
  const custom = extractCustomDevVars(existing);
314
569
  const body = custom ? `${sdkVars}
315
570
 
@@ -433,10 +688,10 @@ async function pollForCompletion(sessionId, codeVerifier) {
433
688
  return null;
434
689
  }
435
690
  function storeCredentials(sessionToken, jwt) {
436
- const dir = join2(homedir(), ".deepspace");
691
+ const dir = join3(homedir(), ".deepspace");
437
692
  mkdirSync(dir, { recursive: true, mode: 448 });
438
- writeFileSync2(join2(dir, "session"), sessionToken, { mode: 384 });
439
- writeFileSync2(join2(dir, "token"), jwt, { mode: 384 });
693
+ writeFileSync2(join3(dir, "session"), sessionToken, { mode: 384 });
694
+ writeFileSync2(join3(dir, "token"), jwt, { mode: 384 });
440
695
  }
441
696
  function openBrowser(url) {
442
697
  const cmd = platform() === "darwin" ? `open "${url}"` : platform() === "win32" ? `start "" "${url}"` : `xdg-open "${url}"`;
@@ -475,20 +730,20 @@ async function doEmailLogin(email, password) {
475
730
 
476
731
  // src/cli/commands/logout.ts
477
732
  import { defineCommand as defineCommand2 } from "citty";
478
- import { existsSync as existsSync2, readFileSync as readFileSync2, rmSync } from "fs";
479
- import { join as join3 } from "path";
733
+ import { existsSync as existsSync3, readFileSync as readFileSync3, rmSync } from "fs";
734
+ import { join as join4 } from "path";
480
735
  import { homedir as homedir2 } from "os";
481
736
  var AUTH_URL2 = process.env.DEEPSPACE_AUTH_URL ?? ENVS.prod.auth;
482
- var DIR = join3(homedir2(), ".deepspace");
483
- var SESSION_PATH = join3(DIR, "session");
484
- var TOKEN_PATH = join3(DIR, "token");
737
+ var DIR = join4(homedir2(), ".deepspace");
738
+ var SESSION_PATH = join4(DIR, "session");
739
+ var TOKEN_PATH = join4(DIR, "token");
485
740
  var logout_default = defineCommand2({
486
741
  meta: {
487
742
  name: "logout",
488
743
  description: "Sign out and remove cached credentials"
489
744
  },
490
745
  async run() {
491
- const sessionToken = existsSync2(SESSION_PATH) ? readFileSync2(SESSION_PATH, "utf-8").trim() : null;
746
+ const sessionToken = existsSync3(SESSION_PATH) ? readFileSync3(SESSION_PATH, "utf-8").trim() : null;
492
747
  if (sessionToken) {
493
748
  try {
494
749
  await fetch(`${AUTH_URL2}/api/auth/sign-out`, {
@@ -503,7 +758,7 @@ var logout_default = defineCommand2({
503
758
  }
504
759
  let removed = 0;
505
760
  for (const path of [SESSION_PATH, TOKEN_PATH]) {
506
- if (existsSync2(path)) {
761
+ if (existsSync3(path)) {
507
762
  rmSync(path);
508
763
  removed++;
509
764
  }
@@ -517,14 +772,13 @@ var logout_default = defineCommand2({
517
772
  });
518
773
 
519
774
  // src/cli/commands/dev.ts
520
- import { defineCommand as defineCommand3 } from "citty";
521
- import { existsSync as existsSync6 } from "fs";
522
- import { join as join6, resolve as resolve2 } from "path";
775
+ import { defineCommand as defineCommand4 } from "citty";
776
+ import { resolve as resolve3, basename } from "path";
523
777
  import spawn from "cross-spawn";
524
778
 
525
779
  // src/cli/auth.ts
526
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
527
- import { join as join4 } from "path";
780
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync4, mkdirSync as mkdirSync2 } from "fs";
781
+ import { join as join5 } from "path";
528
782
  import { homedir as homedir3 } from "os";
529
783
 
530
784
  // src/cli/jwt.ts
@@ -538,20 +792,20 @@ function decodeJwtPayload(token2) {
538
792
 
539
793
  // src/cli/auth.ts
540
794
  var AUTH_URL3 = process.env.DEEPSPACE_AUTH_URL ?? ENVS.prod.auth;
541
- var DIR2 = join4(homedir3(), ".deepspace");
542
- var SESSION_PATH2 = join4(DIR2, "session");
543
- var TOKEN_PATH2 = join4(DIR2, "token");
795
+ var DIR2 = join5(homedir3(), ".deepspace");
796
+ var SESSION_PATH2 = join5(DIR2, "session");
797
+ var TOKEN_PATH2 = join5(DIR2, "token");
544
798
  async function ensureToken() {
545
- if (existsSync3(TOKEN_PATH2)) {
546
- const existing = readFileSync3(TOKEN_PATH2, "utf-8").trim();
799
+ if (existsSync4(TOKEN_PATH2)) {
800
+ const existing = readFileSync4(TOKEN_PATH2, "utf-8").trim();
547
801
  if (isTokenValid(existing)) {
548
802
  return existing;
549
803
  }
550
804
  }
551
- if (!existsSync3(SESSION_PATH2)) {
805
+ if (!existsSync4(SESSION_PATH2)) {
552
806
  throw new Error("Not logged in. Run `deepspace login` first.");
553
807
  }
554
- const sessionToken = readFileSync3(SESSION_PATH2, "utf-8").trim();
808
+ const sessionToken = readFileSync4(SESSION_PATH2, "utf-8").trim();
555
809
  const token2 = await exchangeSession(AUTH_URL3, sessionToken);
556
810
  if (!token2) {
557
811
  throw new Error("Session expired. Run `deepspace login` to re-authenticate.");
@@ -569,27 +823,86 @@ function isTokenValid(jwt) {
569
823
  }
570
824
  }
571
825
 
826
+ // src/cli/lib/app-context.ts
827
+ import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync, writeFileSync as writeFileSync4, mkdirSync as mkdirSync3 } from "fs";
828
+ import { join as join6, resolve, dirname } from "path";
829
+ import { parse as parseToml2 } from "smol-toml";
830
+ function detectAppName(cwd = process.cwd()) {
831
+ const wranglerPath = join6(resolve(cwd), "wrangler.toml");
832
+ if (!existsSync5(wranglerPath)) return null;
833
+ try {
834
+ const cfg = parseToml2(readFileSync5(wranglerPath, "utf-8"));
835
+ return typeof cfg.name === "string" && cfg.name.length > 0 ? cfg.name : null;
836
+ } catch {
837
+ return null;
838
+ }
839
+ }
840
+ function requireAppName(explicit, cwd) {
841
+ if (explicit) return explicit;
842
+ const detected = detectAppName(cwd);
843
+ if (detected) return detected;
844
+ throw new Error(
845
+ "No app specified. Pass --app <name>, or run from an app directory with a wrangler.toml."
846
+ );
847
+ }
848
+ function findAppDir(start = process.cwd()) {
849
+ let dir = resolve(start);
850
+ for (; ; ) {
851
+ if (existsSync5(join6(dir, "wrangler.toml"))) return dir;
852
+ const parent = dirname(dir);
853
+ if (parent === dir) return null;
854
+ dir = parent;
855
+ }
856
+ }
857
+ function findChildApps(dir) {
858
+ try {
859
+ return readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && existsSync5(join6(dir, e.name, "wrangler.toml"))).map((e) => e.name).sort();
860
+ } catch {
861
+ return [];
862
+ }
863
+ }
864
+ function writeLaunchConfigIfMissing(appDir, appName, port) {
865
+ const launchPath = join6(appDir, ".claude", "launch.json");
866
+ if (existsSync5(launchPath)) return;
867
+ const config = {
868
+ version: "0.0.1",
869
+ configurations: [
870
+ {
871
+ name: appName,
872
+ runtimeExecutable: "npx",
873
+ runtimeArgs: ["deepspace", "dev", "--port", String(port)],
874
+ port
875
+ }
876
+ ]
877
+ };
878
+ try {
879
+ mkdirSync3(join6(appDir, ".claude"), { recursive: true });
880
+ writeFileSync4(launchPath, JSON.stringify(config, null, 2) + "\n");
881
+ } catch {
882
+ }
883
+ }
884
+
572
885
  // src/cli/lib/install-status.ts
573
- import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
574
- import { join as join5 } from "path";
886
+ import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
887
+ import { join as join7 } from "path";
575
888
  function ensureInstallReady(appDir) {
576
- if (existsSync4(join5(appDir, "node_modules", "deepspace", "package.json"))) return;
577
- const sentinelDir = join5(appDir, ".deepspace");
578
- const errPath = join5(sentinelDir, "install.err");
579
- const startedPath = join5(sentinelDir, "install.started");
580
- const donePath = join5(sentinelDir, "install.done");
581
- const logPath = join5(sentinelDir, "install.log");
582
- if (existsSync4(errPath)) {
889
+ if (existsSync6(join7(appDir, "node_modules", "deepspace", "package.json"))) return;
890
+ const sentinelDir = join7(appDir, ".deepspace");
891
+ const errPath = join7(sentinelDir, "install.err");
892
+ const startedPath = join7(sentinelDir, "install.started");
893
+ const donePath = join7(sentinelDir, "install.done");
894
+ const logPath = join7(sentinelDir, "install.log");
895
+ if (existsSync6(errPath)) {
583
896
  console.error("Background install failed:");
584
- console.error(readFileSync4(errPath, "utf-8").trimEnd());
585
- if (existsSync4(logPath)) console.error(`
897
+ console.error(readFileSync6(errPath, "utf-8").trimEnd());
898
+ if (existsSync6(logPath)) console.error(`
586
899
  Full log: ${logPath}`);
587
900
  console.error("\nRun `npm install` (or `bun install`) manually, then retry.");
588
901
  process.exit(1);
589
902
  }
590
- if (existsSync4(startedPath) && !existsSync4(donePath)) {
903
+ if (existsSync6(startedPath) && !existsSync6(donePath)) {
591
904
  console.error("Dependencies are still installing in the background.");
592
- if (existsSync4(logPath)) console.error(`Tail progress: tail -f ${logPath}`);
905
+ if (existsSync6(logPath)) console.error(`Tail progress: tail -f ${logPath}`);
593
906
  console.error("Retry once it finishes.");
594
907
  process.exit(1);
595
908
  }
@@ -599,12 +912,12 @@ Full log: ${logPath}`);
599
912
 
600
913
  // src/cli/lib/preflight.ts
601
914
  import { execFileSync } from "child_process";
602
- import { existsSync as existsSync5 } from "fs";
603
- import { resolve } from "path";
915
+ import { existsSync as existsSync7 } from "fs";
916
+ import { resolve as resolve2 } from "path";
604
917
  var DLL_NOT_FOUND = -1073741515;
605
918
  function preflightWindowsWorkerd(appDir) {
606
919
  if (process.platform !== "win32") return;
607
- const workerdExe = resolve(
920
+ const workerdExe = resolve2(
608
921
  appDir,
609
922
  "node_modules",
610
923
  "@cloudflare",
@@ -612,7 +925,7 @@ function preflightWindowsWorkerd(appDir) {
612
925
  "bin",
613
926
  "workerd.exe"
614
927
  );
615
- if (!existsSync5(workerdExe)) return;
928
+ if (!existsSync7(workerdExe)) return;
616
929
  try {
617
930
  execFileSync(workerdExe, ["--help"], { stdio: "pipe", timeout: 1e4 });
618
931
  return;
@@ -632,11 +945,527 @@ function preflightWindowsWorkerd(appDir) {
632
945
  console.error("");
633
946
  process.exit(1);
634
947
  }
635
- }
948
+ }
949
+
950
+ // src/cli/commands/secrets.ts
951
+ import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
952
+ import { join as join8 } from "path";
953
+ import { defineCommand as defineCommand3 } from "citty";
954
+ var SECRET_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
955
+ var ERROR_SNIPPET_CHARS = 500;
956
+ var COLLABORATOR_ROUTES_404 = "Platform API does not expose app-collaborator routes; deploy/update api-worker before using collaborator commands.";
957
+ var COLLABORATOR_SCHEMA_NOT_MIGRATED = "Platform API is missing app-collaborator database tables. Run api-worker /_migrate with x-migration-secret, then retry collaborator commands.";
958
+ function parseAssignment(value) {
959
+ const eq = value.indexOf("=");
960
+ if (eq <= 0) throw new Error("Use KEY=value.");
961
+ const name = value.slice(0, eq);
962
+ if (!SECRET_NAME_RE.test(name)) throw new Error(`Invalid secret name "${name}".`);
963
+ if (SDK_MANAGED_KEYS.has(name)) throw new Error(`${name} is managed by DeepSpace and cannot be set as an app secret.`);
964
+ return [name, value.slice(eq + 1)];
965
+ }
966
+ function devTargetPath() {
967
+ return join8(process.cwd(), ".dev.vars");
968
+ }
969
+ function readDevTarget() {
970
+ if (!hasLegacyDevVars(process.cwd())) {
971
+ return readManagedAppEnv(process.cwd());
972
+ }
973
+ const path = devTargetPath();
974
+ if (!existsSync8(path)) return {};
975
+ return stripSdkManaged(parseDevVars(splitDevTarget(readFileSync7(path, "utf-8")).custom));
976
+ }
977
+ function readSyncedTarget() {
978
+ if (hasLegacyDevVars(process.cwd())) return readDevTarget();
979
+ return readManagedSyncedEnv(process.cwd());
980
+ }
981
+ function readDevOnlyTarget() {
982
+ if (hasLegacyDevVars(process.cwd())) return readDevTarget();
983
+ return readManagedDevOnlyEnv(process.cwd());
984
+ }
985
+ function writeLocalPatch(set2 = {}, removeNames = []) {
986
+ for (const name of [...Object.keys(set2), ...removeNames]) {
987
+ if (SDK_MANAGED_KEYS.has(name)) {
988
+ throw new Error(`${name} is managed by DeepSpace and cannot be set as an app secret.`);
989
+ }
990
+ }
991
+ if (hasLegacyDevVars(process.cwd())) writeDevTargetPatch(set2, removeNames);
992
+ else {
993
+ patchManagedDevOnlyEnv(process.cwd(), set2, removeNames);
994
+ }
995
+ }
996
+ function quote(value) {
997
+ return quoteEnvValue(value);
998
+ }
999
+ function splitLocal(raw) {
1000
+ const idx = raw.indexOf(DEV_VARS_DIVIDER);
1001
+ if (idx < 0) return { top: "", custom: raw };
1002
+ const after = raw.slice(idx + DEV_VARS_DIVIDER.length);
1003
+ return {
1004
+ top: raw.slice(0, idx).trimEnd(),
1005
+ custom: after.startsWith("\n") ? after.slice(1) : after
1006
+ };
1007
+ }
1008
+ function splitDevTarget(raw) {
1009
+ const idx = raw.indexOf(DEV_VARS_DIVIDER);
1010
+ if (idx >= 0) return splitLocal(raw);
1011
+ const lines = raw.split("\n");
1012
+ const top = [];
1013
+ const custom = [];
1014
+ let target = top;
1015
+ let i = 0;
1016
+ while (i < lines.length) {
1017
+ const line = lines[i];
1018
+ const name = assignmentName2(line);
1019
+ const eq = line.indexOf("=");
1020
+ const valStart = eq >= 0 ? line.slice(eq + 1) : "";
1021
+ const block = [line];
1022
+ i++;
1023
+ if (name && valStart.startsWith('"') && !hasUnescapedQuote2(valStart.slice(1))) {
1024
+ while (i < lines.length) {
1025
+ block.push(lines[i]);
1026
+ const closed = hasUnescapedQuote2(lines[i]);
1027
+ i++;
1028
+ if (closed) break;
1029
+ }
1030
+ }
1031
+ if (name && !SDK_MANAGED_KEYS.has(name)) target = custom;
1032
+ target.push(...block);
1033
+ }
1034
+ while (top.length && top[top.length - 1] === "") top.pop();
1035
+ while (custom.length && custom[custom.length - 1] === "") custom.pop();
1036
+ return { top: top.join("\n"), custom: custom.join("\n") };
1037
+ }
1038
+ function assignmentName2(line) {
1039
+ const eq = line.indexOf("=");
1040
+ if (eq < 0) return null;
1041
+ const match = line.slice(0, eq).match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*$/);
1042
+ return match?.[1] ?? null;
1043
+ }
1044
+ function hasUnescapedQuote2(s) {
1045
+ let escaped = false;
1046
+ for (const ch of s) {
1047
+ if (escaped) {
1048
+ escaped = false;
1049
+ continue;
1050
+ }
1051
+ if (ch === "\\") {
1052
+ escaped = true;
1053
+ continue;
1054
+ }
1055
+ if (ch === '"') return true;
1056
+ }
1057
+ return false;
1058
+ }
1059
+ function rewriteCustomSection(custom, set2, remove2) {
1060
+ const lines = custom.split("\n");
1061
+ const out = [];
1062
+ const pending = new Set(Object.keys(set2));
1063
+ let i = 0;
1064
+ while (i < lines.length) {
1065
+ const line = lines[i];
1066
+ const name = assignmentName2(line);
1067
+ const eq = line.indexOf("=");
1068
+ const valStart = eq >= 0 ? line.slice(eq + 1) : "";
1069
+ const block = [line];
1070
+ i++;
1071
+ if (name && valStart.startsWith('"') && !hasUnescapedQuote2(valStart.slice(1))) {
1072
+ while (i < lines.length) {
1073
+ block.push(lines[i]);
1074
+ const closed = hasUnescapedQuote2(lines[i]);
1075
+ i++;
1076
+ if (closed) break;
1077
+ }
1078
+ }
1079
+ if (name && (remove2.has(name) || pending.has(name))) {
1080
+ if (pending.has(name)) {
1081
+ out.push(`${name}=${quote(set2[name])}`);
1082
+ pending.delete(name);
1083
+ }
1084
+ continue;
1085
+ }
1086
+ out.push(...block);
1087
+ }
1088
+ while (out.length && out[out.length - 1] === "") out.pop();
1089
+ for (const name of Array.from(pending).sort()) {
1090
+ out.push(`${name}=${quote(set2[name])}`);
1091
+ }
1092
+ return out.join("\n");
1093
+ }
1094
+ function writeDevTargetPatch(set2 = {}, removeNames = []) {
1095
+ const path = devTargetPath();
1096
+ const existing = existsSync8(path) ? readFileSync7(path, "utf-8") : "";
1097
+ const { top, custom: existingCustom } = splitDevTarget(existing);
1098
+ const custom = rewriteCustomSection(existingCustom, set2, new Set(removeNames));
1099
+ const body = top ? `${top}
1100
+
1101
+ ${DEV_VARS_DIVIDER}
1102
+ ${custom}${custom ? "\n" : ""}` : `${DEV_VARS_DIVIDER}
1103
+ ${custom}${custom ? "\n" : ""}`;
1104
+ writeFileSync5(path, body);
1105
+ }
1106
+ async function remoteDeployState(appName, token2) {
1107
+ const res = await fetch(`${process.env.DEEPSPACE_DEPLOY_URL ?? "https://deploy-worker.deep.space"}/api/deploy/${encodeURIComponent(appName)}`, {
1108
+ headers: { Authorization: `Bearer ${token2}` }
1109
+ });
1110
+ const text2 = await res.text().catch(() => "");
1111
+ let body = {};
1112
+ if (text2) {
1113
+ try {
1114
+ body = JSON.parse(text2);
1115
+ } catch (err) {
1116
+ const message = err instanceof Error ? err.message : String(err);
1117
+ throw new Error(`Could not determine deploy status for ${appName}: invalid JSON response (${message}).`);
1118
+ }
1119
+ }
1120
+ if (!res.ok) {
1121
+ const detail = body.error ?? (text2 || "unknown error");
1122
+ throw new Error(`Could not determine deploy status for ${appName} (${res.status}): ${detail}`);
1123
+ }
1124
+ if (body.deployed === true) return "deployed";
1125
+ if (body.deployed === false) return "not-deployed";
1126
+ throw new Error(`Could not determine deploy status for ${appName}: response missing "deployed".`);
1127
+ }
1128
+ async function remoteAvailable(appName, token2) {
1129
+ return await remoteDeployState(appName, token2) === "deployed";
1130
+ }
1131
+ async function mintScopedSecretsToken(appName, token2) {
1132
+ const body = await collaboratorFetch(appName, token2, "/secrets-token", { method: "POST" });
1133
+ if (!body.token) throw new Error("Platform API did not return an app-secret access token.");
1134
+ return body.token;
1135
+ }
1136
+ async function remoteFetch(appName, token2, path = "", init = {}) {
1137
+ const scopedToken = await mintScopedSecretsToken(appName, token2);
1138
+ const res = await fetch(`https://${appName}.app.space/_deepspace/secrets${path}`, {
1139
+ ...init,
1140
+ headers: {
1141
+ "Content-Type": "application/json",
1142
+ Authorization: `Bearer ${scopedToken}`,
1143
+ ...init.headers ?? {}
1144
+ }
1145
+ });
1146
+ const contentType = res.headers.get("content-type") ?? "unknown";
1147
+ const text2 = await res.text().catch(() => "");
1148
+ let body = null;
1149
+ let parseError = null;
1150
+ if (text2 && contentType.toLowerCase().includes("json")) {
1151
+ try {
1152
+ body = JSON.parse(text2);
1153
+ } catch (err) {
1154
+ parseError = err instanceof Error ? err.message : String(err);
1155
+ }
1156
+ }
1157
+ if (!res.ok) {
1158
+ if (body?.error) throw new Error(body.error);
1159
+ const snippet = text2 ? ` Body: ${text2.slice(0, ERROR_SNIPPET_CHARS)}` : " Body: <empty>";
1160
+ const parseDetail2 = parseError ? ` JSON parse failed: ${parseError}.` : "";
1161
+ throw new Error(`Remote secrets request failed (${res.status}; content-type: ${contentType}).${parseDetail2}${snippet}`);
1162
+ }
1163
+ if (body) return body;
1164
+ if (!text2) return {};
1165
+ const parseDetail = parseError ? ` JSON parse failed: ${parseError}.` : "";
1166
+ throw new Error(`Remote secrets response was not JSON (${res.status}; content-type: ${contentType}).${parseDetail} Body: ${text2.slice(0, ERROR_SNIPPET_CHARS)}`);
1167
+ }
1168
+ function apiUrl() {
1169
+ return process.env.DEEPSPACE_API_URL ?? ENVS.prod.api;
1170
+ }
1171
+ async function collaboratorFetch(appName, token2, path, init = {}) {
1172
+ const url = `${apiUrl()}/api/app-collaborators/${encodeURIComponent(appName)}${path}`;
1173
+ const res = await fetch(url, {
1174
+ ...init,
1175
+ headers: {
1176
+ "Content-Type": "application/json",
1177
+ Authorization: `Bearer ${token2}`,
1178
+ ...init.headers ?? {}
1179
+ }
1180
+ });
1181
+ const text2 = await res.text().catch(() => "");
1182
+ let body = {};
1183
+ if (text2) {
1184
+ try {
1185
+ body = JSON.parse(text2);
1186
+ } catch {
1187
+ if (res.status === 404) throw new Error(COLLABORATOR_ROUTES_404);
1188
+ if (!res.ok) throw new Error(`Collaborator request failed (${res.status}): ${text2.slice(0, ERROR_SNIPPET_CHARS)}`);
1189
+ throw new Error(`Collaborator response was not JSON (${res.status}): ${text2.slice(0, ERROR_SNIPPET_CHARS)}`);
1190
+ }
1191
+ }
1192
+ if (!res.ok) {
1193
+ if (res.status === 404 && (!body.error || body.error === "Not found" || body.error === "not_found")) {
1194
+ throw new Error(COLLABORATOR_ROUTES_404);
1195
+ }
1196
+ if (body.error === "platform_schema_not_migrated") {
1197
+ const diagnostic = body;
1198
+ const action = typeof diagnostic.action === "string" ? ` ${diagnostic.action}` : "";
1199
+ throw new Error(`${COLLABORATOR_SCHEMA_NOT_MIGRATED}${action}`);
1200
+ }
1201
+ throw new Error(body.error ?? `Collaborator request failed (${res.status})`);
1202
+ }
1203
+ return body;
1204
+ }
1205
+ async function requireRemoteSecretsAvailable(appName, token2) {
1206
+ const state = await remoteDeployState(appName, token2);
1207
+ if (state === "deployed") return;
1208
+ throw new Error(
1209
+ "Remote secrets are available after the first deploy. Deploy this app first, then run `deepspace secrets push`; `deepspace secrets set --dev` writes dev-only secrets for `deepspace dev` without touching remote."
1210
+ );
1211
+ }
1212
+ function diffMaps(local, remote) {
1213
+ const localOnly = [];
1214
+ const remoteOnly = [];
1215
+ const changed = [];
1216
+ const matching = [];
1217
+ for (const [name, value] of Object.entries(local)) {
1218
+ if (!(name in remote)) localOnly.push(name);
1219
+ else if (remote[name] !== value) changed.push(name);
1220
+ else matching.push(name);
1221
+ }
1222
+ for (const name of Object.keys(remote)) {
1223
+ if (!(name in local)) remoteOnly.push(name);
1224
+ }
1225
+ return { localOnly, remoteOnly, changed, matching };
1226
+ }
1227
+ function diffNames(local, remote) {
1228
+ const localSet = new Set(local);
1229
+ const remoteSet = new Set(remote);
1230
+ return {
1231
+ localOnly: local.filter((name) => !remoteSet.has(name)).sort(),
1232
+ remoteOnly: remote.filter((name) => !localSet.has(name)).sort()
1233
+ };
1234
+ }
1235
+ function statusLine(d, remote) {
1236
+ if (!remote) return "Secrets: remote unavailable until first deploy; dev-only .env section updated.";
1237
+ if (!d || !d.localOnly.length && !d.remoteOnly.length && !d.changed.length) {
1238
+ return "Secrets: synced .env section matches remote.";
1239
+ }
1240
+ return `Secrets: ${d.localOnly.length} local-only, ${d.remoteOnly.length} remote-only, ${d.changed.length} changed. Run \`deepspace secrets diff\`.`;
1241
+ }
1242
+ function nameStatusLine(d) {
1243
+ if (!d.localOnly.length && !d.remoteOnly.length) {
1244
+ return "Secrets: synced .env section has the same names as remote.";
1245
+ }
1246
+ return `Secrets: ${d.localOnly.length} local-only, ${d.remoteOnly.length} remote-only. Run \`deepspace secrets diff\` to compare values.`;
1247
+ }
1248
+ async function compare(appName, token2) {
1249
+ if (!await remoteAvailable(appName, token2)) return null;
1250
+ const local = readSyncedTarget();
1251
+ const remote = await remoteFetch(appName, token2, "/pull");
1252
+ return diffMaps(local, remote.secrets ?? {});
1253
+ }
1254
+ var set = defineCommand3({
1255
+ meta: { name: "set", description: "Set an app secret" },
1256
+ args: {
1257
+ assignment: { type: "positional", required: true, description: "KEY=value" },
1258
+ dev: {
1259
+ type: "boolean",
1260
+ default: false,
1261
+ description: "Write only to the dev-only .env section used by `deepspace dev`; never sync to the deployed app"
1262
+ }
1263
+ },
1264
+ async run({ args }) {
1265
+ const [name, value] = parseAssignment(String(args.assignment));
1266
+ const token2 = await ensureToken();
1267
+ const appName = requireAppName(void 0);
1268
+ if (args.dev) {
1269
+ writeLocalPatch({ [name]: value });
1270
+ console.log("Secrets: updated dev-only .env section for `deepspace dev`; remote was not changed.");
1271
+ return;
1272
+ }
1273
+ const state = await remoteDeployState(appName, token2);
1274
+ if (state === "not-deployed") {
1275
+ writeLocalPatch({ [name]: value });
1276
+ console.log("Secrets: app is not deployed; wrote to dev-only .env section. Run `deepspace secrets push` after deploy.");
1277
+ return;
1278
+ }
1279
+ await remoteFetch(appName, token2, "", { method: "PATCH", body: JSON.stringify({ set: { [name]: value } }) });
1280
+ if (hasLegacyDevVars(process.cwd())) writeDevTargetPatch({ [name]: value });
1281
+ else patchManagedSyncedEnv(process.cwd(), { [name]: value });
1282
+ console.log("Secrets: updated remote and synced .env section.");
1283
+ }
1284
+ });
1285
+ var unset = defineCommand3({
1286
+ meta: { name: "unset", description: "Remove an app secret" },
1287
+ args: {
1288
+ name: { type: "positional", required: true, description: "Secret name" },
1289
+ dev: {
1290
+ type: "boolean",
1291
+ default: false,
1292
+ description: "Remove only from the dev-only .env section used by `deepspace dev`; never sync to the deployed app"
1293
+ }
1294
+ },
1295
+ async run({ args }) {
1296
+ const name = String(args.name);
1297
+ if (!SECRET_NAME_RE.test(name)) throw new Error(`Invalid secret name "${name}".`);
1298
+ const token2 = await ensureToken();
1299
+ const appName = requireAppName(void 0);
1300
+ if (args.dev) {
1301
+ writeLocalPatch({}, [name]);
1302
+ console.log("Secrets: removed from dev-only .env section; remote was not changed.");
1303
+ return;
1304
+ }
1305
+ const state = await remoteDeployState(appName, token2);
1306
+ if (state === "not-deployed") {
1307
+ writeLocalPatch({}, [name]);
1308
+ console.log("Secrets: app is not deployed; removed from dev-only .env section if present.");
1309
+ return;
1310
+ }
1311
+ await remoteFetch(appName, token2, "", { method: "PATCH", body: JSON.stringify({ delete: [name] }) });
1312
+ if (hasLegacyDevVars(process.cwd())) writeDevTargetPatch({}, [name]);
1313
+ else patchManagedSyncedEnv(process.cwd(), {}, [name]);
1314
+ console.log("Secrets: removed from remote and synced .env section.");
1315
+ }
1316
+ });
1317
+ var list = defineCommand3({
1318
+ meta: { name: "list", description: "List app secret names and metadata" },
1319
+ async run() {
1320
+ const token2 = await ensureToken();
1321
+ const appName = requireAppName(void 0);
1322
+ await requireRemoteSecretsAvailable(appName, token2);
1323
+ const body = await remoteFetch(appName, token2);
1324
+ for (const item of body.secrets ?? []) console.log(`${item.name} version=${item.version} updated=${item.updatedAt}`);
1325
+ const d = diffNames(Object.keys(readSyncedTarget()), (body.secrets ?? []).map((item) => item.name));
1326
+ console.log(nameStatusLine(d));
1327
+ }
1328
+ });
1329
+ var pull = defineCommand3({
1330
+ meta: { name: "pull", description: "Pull remote app secrets into the synced .env section" },
1331
+ async run() {
1332
+ const token2 = await ensureToken();
1333
+ const appName = requireAppName(void 0);
1334
+ await requireRemoteSecretsAvailable(appName, token2);
1335
+ const body = await remoteFetch(appName, token2, "/pull");
1336
+ const remoteSecrets = body.secrets ?? {};
1337
+ if (hasLegacyDevVars(process.cwd())) {
1338
+ writeDevTargetPatch(remoteSecrets);
1339
+ } else {
1340
+ replaceManagedSyncedEnv(process.cwd(), remoteSecrets);
1341
+ }
1342
+ const count = Object.keys(remoteSecrets).length;
1343
+ console.log(`Secrets: pulled ${count} remote secret${count === 1 ? "" : "s"} into synced .env section.`);
1344
+ }
1345
+ });
1346
+ var push = defineCommand3({
1347
+ meta: { name: "push", description: "Push dev-only .env secrets to remote and move them to synced" },
1348
+ async run() {
1349
+ const token2 = await ensureToken();
1350
+ const appName = requireAppName(void 0);
1351
+ const local = readDevOnlyTarget();
1352
+ await requireRemoteSecretsAvailable(appName, token2);
1353
+ const names = Object.keys(local);
1354
+ if (!names.length) {
1355
+ console.log("Secrets: no dev-only .env secrets to push.");
1356
+ return;
1357
+ }
1358
+ await remoteFetch(appName, token2, "", { method: "PATCH", body: JSON.stringify({ set: local }) });
1359
+ if (hasLegacyDevVars(process.cwd())) {
1360
+ console.log(`Secrets: pushed ${names.length} dev-only secret${names.length === 1 ? "" : "s"} to remote.`);
1361
+ } else {
1362
+ moveManagedDevOnlyToSynced(process.cwd(), names);
1363
+ console.log(`Secrets: pushed ${names.length} dev-only secret${names.length === 1 ? "" : "s"} to remote and moved ${names.length === 1 ? "it" : "them"} to synced .env section.`);
1364
+ }
1365
+ }
1366
+ });
1367
+ var diff = defineCommand3({
1368
+ meta: { name: "diff", description: "Compare synced .env section with remote app secrets" },
1369
+ async run() {
1370
+ const token2 = await ensureToken();
1371
+ const appName = requireAppName(void 0);
1372
+ await requireRemoteSecretsAvailable(appName, token2);
1373
+ const d = await compare(appName, token2);
1374
+ for (const name of d?.localOnly ?? []) console.log(`local-only ${name}`);
1375
+ for (const name of d?.remoteOnly ?? []) console.log(`remote-only ${name}`);
1376
+ for (const name of d?.changed ?? []) console.log(`changed ${name}`);
1377
+ console.log(statusLine(d, true));
1378
+ }
1379
+ });
1380
+ var collaboratorsList = defineCommand3({
1381
+ meta: { name: "list", description: "List app secret collaborators" },
1382
+ async run() {
1383
+ const token2 = await ensureToken();
1384
+ const appName = requireAppName(void 0);
1385
+ const body = await collaboratorFetch(appName, token2, "/collaborators");
1386
+ for (const item of body.collaborators ?? []) {
1387
+ console.log(`${item.emailDisplay} ${item.userId} created=${new Date(item.createdAt).toISOString()}`);
1388
+ }
1389
+ if (!body.collaborators?.length) console.log("No secrets collaborators.");
1390
+ }
1391
+ });
1392
+ var collaboratorsAdd = defineCommand3({
1393
+ meta: { name: "add", description: "Add an existing DeepSpace user as a secrets collaborator" },
1394
+ args: {
1395
+ email: { type: "positional", required: true, description: "Collaborator email" }
1396
+ },
1397
+ async run({ args }) {
1398
+ const token2 = await ensureToken();
1399
+ const appName = requireAppName(void 0);
1400
+ const body = await collaboratorFetch(appName, token2, "/collaborators", {
1401
+ method: "POST",
1402
+ body: JSON.stringify({ email: String(args.email) })
1403
+ });
1404
+ console.log(`Added ${body.collaborator.emailDisplay} (${body.collaborator.userId}) as a secrets collaborator.`);
1405
+ }
1406
+ });
1407
+ var collaboratorsRemove = defineCommand3({
1408
+ meta: { name: "remove", description: "Remove a secrets collaborator" },
1409
+ args: {
1410
+ target: { type: "positional", required: true, description: "Collaborator email or user id" }
1411
+ },
1412
+ async run({ args }) {
1413
+ const token2 = await ensureToken();
1414
+ const appName = requireAppName(void 0);
1415
+ const target = String(args.target);
1416
+ let userId = target;
1417
+ if (target.includes("@")) {
1418
+ const body = await collaboratorFetch(appName, token2, "/collaborators");
1419
+ const normalized = target.trim().toLowerCase();
1420
+ const match = body.collaborators.find((item) => item.emailNormalized === normalized);
1421
+ if (!match) throw new Error(`No collaborator found for ${target}.`);
1422
+ userId = match.userId;
1423
+ }
1424
+ await collaboratorFetch(appName, token2, `/collaborators/${encodeURIComponent(userId)}`, { method: "DELETE" });
1425
+ console.log(`Removed secrets collaborator ${target}.`);
1426
+ }
1427
+ });
1428
+ var invitesList = defineCommand3({
1429
+ meta: { name: "list", description: "List app secret collaborator invites" },
1430
+ async run() {
1431
+ const token2 = await ensureToken();
1432
+ const appName = requireAppName(void 0);
1433
+ const body = await collaboratorFetch(appName, token2, "/invites");
1434
+ for (const invite of body.invites ?? []) {
1435
+ const status2 = invite.revokedAt ? "revoked" : invite.acceptedAt ? "accepted" : new Date(invite.expiresAt).getTime() <= Date.now() ? "expired" : "pending";
1436
+ console.log(`${invite.id} ${invite.emailDisplay} status=${status2} expires=${new Date(invite.expiresAt).toISOString()}`);
1437
+ }
1438
+ if (!body.invites?.length) console.log("No secrets collaborator invites.");
1439
+ }
1440
+ });
1441
+ var invitesRevoke = defineCommand3({
1442
+ meta: { name: "revoke", description: "Revoke a pending app secret collaborator invite" },
1443
+ args: {
1444
+ inviteId: { type: "positional", required: true, description: "Invite id" }
1445
+ },
1446
+ async run({ args }) {
1447
+ const token2 = await ensureToken();
1448
+ const appName = requireAppName(void 0);
1449
+ await collaboratorFetch(appName, token2, `/invites/${encodeURIComponent(String(args.inviteId))}`, { method: "DELETE" });
1450
+ console.log(`Revoked secrets collaborator invite ${String(args.inviteId)}.`);
1451
+ }
1452
+ });
1453
+ var invites = defineCommand3({
1454
+ meta: { name: "invites", description: "Manage app secret collaborator invites" },
1455
+ subCommands: { list: invitesList, revoke: invitesRevoke }
1456
+ });
1457
+ var collaborators = defineCommand3({
1458
+ meta: { name: "collaborators", description: "Manage app secret collaborators" },
1459
+ subCommands: { list: collaboratorsList, add: collaboratorsAdd, remove: collaboratorsRemove, invites }
1460
+ });
1461
+ var secrets_default = defineCommand3({
1462
+ meta: { name: "secrets", description: "Manage app-owned DeepSpace secrets" },
1463
+ subCommands: { set, unset, list, pull, push, diff, collaborators }
1464
+ });
636
1465
 
637
1466
  // src/cli/commands/dev.ts
638
1467
  var DEFAULT_PORT = 5173;
639
- var dev_default = defineCommand3({
1468
+ var dev_default = defineCommand4({
640
1469
  meta: {
641
1470
  name: "dev",
642
1471
  description: "Start local development server"
@@ -652,20 +1481,40 @@ var dev_default = defineCommand3({
652
1481
  description: "Use production workers (real auth, real API calls)",
653
1482
  default: false
654
1483
  },
1484
+ env: {
1485
+ type: "string",
1486
+ description: "wrangler.toml [env.<name>] block to run (e.g. --env staging). Legacy apps read .dev.vars.<name>; new apps use managed .env.",
1487
+ required: false
1488
+ },
655
1489
  port: {
656
1490
  type: "string",
657
1491
  description: `Port to bind (default ${DEFAULT_PORT}, or $DEEPSPACE_PORT)`,
658
1492
  required: false
1493
+ },
1494
+ "no-pull-env": {
1495
+ type: "boolean",
1496
+ description: "Skip pulling remote app secrets into the synced .env section on startup",
1497
+ default: false
659
1498
  }
660
1499
  },
661
1500
  async run({ args }) {
662
- const appDir = resolve2(args.dir ?? ".");
663
1501
  const env = args.prod ? "prod" : "dev";
1502
+ const wranglerEnv = typeof args.env === "string" && args.env.trim() ? args.env.trim() : void 0;
664
1503
  const port = resolvePort(args.port);
665
- if (!existsSync6(join6(appDir, "wrangler.toml"))) {
666
- console.error("No wrangler.toml found. Are you in a DeepSpace app directory?");
1504
+ const start = resolve3(args.dir ?? ".");
1505
+ const appDir = findAppDir(start);
1506
+ if (!appDir) {
1507
+ console.error(`No wrangler.toml found at or above ${start}.`);
1508
+ const children = findChildApps(start);
1509
+ if (children.length > 0) {
1510
+ console.error("Did you mean to run inside one of these app directories?");
1511
+ for (const c of children) console.error(` cd ${c}`);
1512
+ } else {
1513
+ console.error("Run from a DeepSpace app directory (one containing wrangler.toml).");
1514
+ }
667
1515
  process.exit(1);
668
1516
  }
1517
+ writeLaunchConfigIfMissing(appDir, detectAppName(appDir) ?? basename(appDir), port);
669
1518
  ensureInstallReady(appDir);
670
1519
  let token2;
671
1520
  try {
@@ -682,9 +1531,25 @@ var dev_default = defineCommand3({
682
1531
  process.exit(1);
683
1532
  }
684
1533
  console.log(`Logged in as ${payload.name ?? payload.email}`);
685
- console.log(`Environment: ${env}`);
1534
+ console.log(`Platform: ${env}`);
1535
+ if (wranglerEnv) console.log(`Wrangler env: ${wranglerEnv}`);
686
1536
  console.log(`Port: ${port}`);
687
- await writeDevVars(appDir, env, payload.sub, token2);
1537
+ await writeDevVars(appDir, env, payload.sub, token2, wranglerEnv);
1538
+ if (env === "dev" && !args["no-pull-env"] && !hasLegacyDevVars(appDir, wranglerEnv)) {
1539
+ const appName = resolveCurrentAppName(appDir, wranglerEnv);
1540
+ if (appName) {
1541
+ try {
1542
+ if (await remoteAvailable(appName, token2)) {
1543
+ const body = await remoteFetch(appName, token2, "/pull");
1544
+ replaceManagedSyncedEnv(appDir, body.secrets ?? {});
1545
+ console.log("Secrets: pulled remote app secrets into synced .env section.");
1546
+ }
1547
+ } catch (err) {
1548
+ const message = err instanceof Error ? err.message : String(err);
1549
+ console.warn(`Secrets: remote app secrets pull skipped (${message}).`);
1550
+ }
1551
+ }
1552
+ }
688
1553
  preflightWindowsWorkerd(appDir);
689
1554
  console.log("Starting dev server...\n");
690
1555
  const vite = spawn(
@@ -693,7 +1558,16 @@ var dev_default = defineCommand3({
693
1558
  {
694
1559
  cwd: appDir,
695
1560
  stdio: "inherit",
696
- env: { ...process.env, DEEPSPACE_PORT: String(port) }
1561
+ // CLOUDFLARE_ENV is what the Cloudflare Vite plugin keys off — it
1562
+ // applies `[env.<name>]` overrides. Legacy .dev.vars apps use
1563
+ // `.dev.vars.<name>` instead of `.dev.vars`; new apps use managed .env.
1564
+ // Set only when --env was passed so the default invocation behaves
1565
+ // exactly as before.
1566
+ env: {
1567
+ ...process.env,
1568
+ DEEPSPACE_PORT: String(port),
1569
+ ...wranglerEnv ? { CLOUDFLARE_ENV: wranglerEnv } : {}
1570
+ }
697
1571
  }
698
1572
  );
699
1573
  process.on("SIGINT", () => vite.kill());
@@ -711,15 +1585,23 @@ function resolvePort(arg) {
711
1585
  }
712
1586
  return n;
713
1587
  }
1588
+ function resolveCurrentAppName(appDir, wranglerEnv) {
1589
+ try {
1590
+ const resolved = resolveAppNameForEnv(readWranglerConfig(appDir), wranglerEnv);
1591
+ return resolved.ok ? resolved.name : null;
1592
+ } catch {
1593
+ return null;
1594
+ }
1595
+ }
714
1596
 
715
1597
  // src/cli/commands/kill.ts
716
- import { defineCommand as defineCommand4 } from "citty";
1598
+ import { defineCommand as defineCommand5 } from "citty";
717
1599
  import { spawnSync } from "child_process";
718
1600
  import { setTimeout as wait } from "timers/promises";
719
1601
  var DEFAULT_PORT2 = 5173;
720
1602
  var SIGTERM_GRACE_MS = 1500;
721
1603
  var IS_WIN = process.platform === "win32";
722
- var kill_default = defineCommand4({
1604
+ var kill_default = defineCommand5({
723
1605
  meta: {
724
1606
  name: "kill",
725
1607
  description: "Kill the local dev server and any orphaned workerd processes"
@@ -874,9 +1756,8 @@ function isAlive(pid) {
874
1756
  }
875
1757
 
876
1758
  // src/cli/commands/test.ts
877
- import { defineCommand as defineCommand5 } from "citty";
878
- import { existsSync as existsSync7 } from "fs";
879
- import { join as join7, resolve as resolve3 } from "path";
1759
+ import { defineCommand as defineCommand6 } from "citty";
1760
+ import { resolve as resolve4 } from "path";
880
1761
  import { sync as spawnSync3 } from "cross-spawn";
881
1762
 
882
1763
  // src/cli/lib/playwright.ts
@@ -905,7 +1786,7 @@ function ensurePlaywright(appDir) {
905
1786
 
906
1787
  // src/cli/commands/test.ts
907
1788
  var DEFAULT_PORT3 = 5173;
908
- var test_default = defineCommand5({
1789
+ var test_default = defineCommand6({
909
1790
  meta: {
910
1791
  name: "test",
911
1792
  description: "Run tests for your DeepSpace app"
@@ -923,11 +1804,19 @@ var test_default = defineCommand5({
923
1804
  }
924
1805
  },
925
1806
  async run({ args }) {
926
- const appDir = resolve3(".");
927
1807
  const suite = args.suite ?? "default";
928
1808
  const port = resolvePort3(args.port);
929
- if (!existsSync7(join7(appDir, "wrangler.toml"))) {
930
- console.error("No wrangler.toml found. Are you in a DeepSpace app directory?");
1809
+ const start = resolve4(".");
1810
+ const appDir = findAppDir(start);
1811
+ if (!appDir) {
1812
+ console.error(`No wrangler.toml found at or above ${start}.`);
1813
+ const children = findChildApps(start);
1814
+ if (children.length > 0) {
1815
+ console.error("Did you mean to run inside one of these app directories?");
1816
+ for (const c of children) console.error(` cd ${c}`);
1817
+ } else {
1818
+ console.error("Run from a DeepSpace app directory (one containing wrangler.toml).");
1819
+ }
931
1820
  process.exit(1);
932
1821
  }
933
1822
  ensureInstallReady(appDir);
@@ -1012,10 +1901,10 @@ function resolvePort3(arg) {
1012
1901
  }
1013
1902
 
1014
1903
  // src/cli/commands/screenshot.ts
1015
- import { defineCommand as defineCommand6 } from "citty";
1904
+ import { defineCommand as defineCommand7 } from "citty";
1016
1905
  import { sync as spawnSync4 } from "cross-spawn";
1017
- import { resolve as resolve4 } from "path";
1018
- var screenshot_default = defineCommand6({
1906
+ import { resolve as resolve5 } from "path";
1907
+ var screenshot_default = defineCommand7({
1019
1908
  meta: {
1020
1909
  name: "screenshot",
1021
1910
  description: "Take a Playwright screenshot, installing Chromium on demand"
@@ -1043,7 +1932,7 @@ var screenshot_default = defineCommand6({
1043
1932
  }
1044
1933
  },
1045
1934
  run({ args }) {
1046
- const appDir = resolve4(".");
1935
+ const appDir = resolve5(".");
1047
1936
  ensurePlaywright(appDir);
1048
1937
  const playwrightArgs = ["playwright", "screenshot", args.url, args.output];
1049
1938
  if (args["full-page"]) playwrightArgs.push("--full-page");
@@ -1059,29 +1948,29 @@ var screenshot_default = defineCommand6({
1059
1948
  });
1060
1949
 
1061
1950
  // src/cli/commands/test-accounts.ts
1062
- import { defineCommand as defineCommand7 } from "citty";
1951
+ import { defineCommand as defineCommand8 } from "citty";
1063
1952
  import * as p3 from "@clack/prompts";
1064
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, existsSync as existsSync8, mkdirSync as mkdirSync3 } from "fs";
1065
- import { join as join8 } from "path";
1953
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, existsSync as existsSync9, mkdirSync as mkdirSync4 } from "fs";
1954
+ import { join as join9 } from "path";
1066
1955
  import { homedir as homedir4 } from "os";
1067
1956
  var SESSION_COOKIE2 = "__Secure-better-auth.session_token";
1068
1957
  var AUTH_URL4 = process.env.DEEPSPACE_AUTH_URL ?? ENVS.prod.auth;
1069
- var DIR3 = join8(homedir4(), ".deepspace");
1070
- var ACCOUNTS_PATH = join8(DIR3, "test-accounts.json");
1958
+ var DIR3 = join9(homedir4(), ".deepspace");
1959
+ var ACCOUNTS_PATH = join9(DIR3, "test-accounts.json");
1071
1960
  function loadAccounts() {
1072
- if (!existsSync8(ACCOUNTS_PATH)) return [];
1961
+ if (!existsSync9(ACCOUNTS_PATH)) return [];
1073
1962
  try {
1074
- return JSON.parse(readFileSync5(ACCOUNTS_PATH, "utf-8"));
1963
+ return JSON.parse(readFileSync8(ACCOUNTS_PATH, "utf-8"));
1075
1964
  } catch {
1076
1965
  return [];
1077
1966
  }
1078
1967
  }
1079
1968
  function saveAccounts(accounts) {
1080
- mkdirSync3(DIR3, { recursive: true });
1081
- writeFileSync4(ACCOUNTS_PATH, JSON.stringify(accounts, null, 2), { mode: 384 });
1969
+ mkdirSync4(DIR3, { recursive: true });
1970
+ writeFileSync6(ACCOUNTS_PATH, JSON.stringify(accounts, null, 2), { mode: 384 });
1082
1971
  }
1083
1972
  function sessionCookie() {
1084
- const token2 = readFileSync5(SESSION_PATH2, "utf-8").trim();
1973
+ const token2 = readFileSync8(SESSION_PATH2, "utf-8").trim();
1085
1974
  return `${SESSION_COOKIE2}=${encodeURIComponent(token2)}`;
1086
1975
  }
1087
1976
  async function fetchRemoteAccounts() {
@@ -1104,7 +1993,7 @@ async function deleteRemote(id) {
1104
1993
  throw new Error(data.error ?? `DELETE returned ${res.status}`);
1105
1994
  }
1106
1995
  }
1107
- var create = defineCommand7({
1996
+ var create = defineCommand8({
1108
1997
  meta: {
1109
1998
  name: "create",
1110
1999
  description: "Create a test account"
@@ -1174,7 +2063,7 @@ var create = defineCommand7({
1174
2063
  Saved to ${ACCOUNTS_PATH}`);
1175
2064
  }
1176
2065
  });
1177
- var list = defineCommand7({
2066
+ var list2 = defineCommand8({
1178
2067
  meta: {
1179
2068
  name: "list",
1180
2069
  description: "List your test accounts"
@@ -1209,7 +2098,7 @@ var list = defineCommand7({
1209
2098
  }
1210
2099
  }
1211
2100
  });
1212
- var del = defineCommand7({
2101
+ var del = defineCommand8({
1213
2102
  meta: {
1214
2103
  name: "delete",
1215
2104
  description: "Delete a test account by --email or --id"
@@ -1266,7 +2155,7 @@ var del = defineCommand7({
1266
2155
  console.log(`Test account deleted${targetEmail ? `: ${targetEmail}` : `: ${targetId}`}`);
1267
2156
  }
1268
2157
  });
1269
- var clear = defineCommand7({
2158
+ var clear = defineCommand8({
1270
2159
  meta: {
1271
2160
  name: "clear",
1272
2161
  description: "Delete all your test accounts (or those matching --label)"
@@ -1332,26 +2221,25 @@ Failed:`);
1332
2221
  }
1333
2222
  }
1334
2223
  });
1335
- var test_accounts_default = defineCommand7({
2224
+ var test_accounts_default = defineCommand8({
1336
2225
  meta: {
1337
2226
  name: "test-accounts",
1338
2227
  description: "Manage test accounts for development"
1339
2228
  },
1340
2229
  subCommands: {
1341
2230
  create,
1342
- list,
2231
+ list: list2,
1343
2232
  delete: del,
1344
2233
  clear
1345
2234
  }
1346
2235
  });
1347
2236
 
1348
2237
  // src/cli/commands/deploy.ts
1349
- import { defineCommand as defineCommand8 } from "citty";
2238
+ import { defineCommand as defineCommand9 } from "citty";
1350
2239
  import { execSync as execSync2 } from "child_process";
1351
2240
  import { randomUUID } from "crypto";
1352
- import { readFileSync as readFileSync6, readdirSync, existsSync as existsSync9 } from "fs";
1353
- import { parse as parseToml2 } from "smol-toml";
1354
- import { join as join9, resolve as resolve5, dirname } from "path";
2241
+ import { readFileSync as readFileSync9, readdirSync as readdirSync2, existsSync as existsSync10 } from "fs";
2242
+ import { join as join10, resolve as resolve6, dirname as dirname2 } from "path";
1355
2243
  import { pathToFileURL } from "url";
1356
2244
  import * as p4 from "@clack/prompts";
1357
2245
 
@@ -1380,6 +2268,7 @@ var RESERVED_BINDING_NAMES = /* @__PURE__ */ new Set([
1380
2268
  "AUTH_WORKER_URL",
1381
2269
  "APP_IDENTITY_TOKEN",
1382
2270
  "APP_OWNER_JWT",
2271
+ "APP_SECRETS_KEY",
1383
2272
  "USAGE_EVENTS"
1384
2273
  ]);
1385
2274
  function validateBindingManifest(manifest) {
@@ -1560,6 +2449,7 @@ function asObjectArray(v) {
1560
2449
  // src/cli/commands/deploy.ts
1561
2450
  var DEPLOY_URL = process.env.DEEPSPACE_DEPLOY_URL ?? ENVS.prod.deploy;
1562
2451
  var API_URL = process.env.DEEPSPACE_API_URL ?? ENVS.prod.api;
2452
+ var SECRET_NAME_RE2 = /^[A-Za-z_][A-Za-z0-9_]*$/;
1563
2453
  var CLOUDFLARE_DEPLOY_ERROR_HINT = "Deployment reached the DeepSpace deploy worker, but Cloudflare's deployment control-plane API failed. This is often caused by a Cloudflare Dashboard/API incident or Workers for Platforms entitlement outage. Already deployed apps should keep serving; wait for Cloudflare to recover and retry.";
1564
2454
  var RESERVED_RUN_WORKER_FIRST = /* @__PURE__ */ new Set([
1565
2455
  "/api/*",
@@ -1602,6 +2492,14 @@ function formatDeployWorkerError(status2, error) {
1602
2492
 
1603
2493
  Underlying error: ${detail}`;
1604
2494
  }
2495
+ function legacyDevVarsAppSecretsRoomWarning(userSecrets, doManifest) {
2496
+ if (!Object.keys(userSecrets).length) return null;
2497
+ const hasAppSecretsRoom = (doManifest ?? []).some(
2498
+ (binding) => binding.binding === "SECRETS_ROOMS" || binding.className === "AppSecretsRoom"
2499
+ );
2500
+ if (!hasAppSecretsRoom) return null;
2501
+ return "Legacy .dev.vars secrets are still being deployed as direct Worker bindings even though AppSecretsRoom is configured. Move these values with `deepspace secrets set` to manage them through the app secrets store.";
2502
+ }
1605
2503
  var MAX_GROUP_BYTES = 40 * 1024 * 1024;
1606
2504
  var GROUP_BYTES = (() => {
1607
2505
  const raw = Number(process.env.DEEPSPACE_DEPLOY_GROUP_BYTES);
@@ -1655,7 +2553,7 @@ async function abortStagedUpload(appName, uploadId, totalGroups, token2) {
1655
2553
  } catch {
1656
2554
  }
1657
2555
  }
1658
- var deploy_default = defineCommand8({
2556
+ var deploy_default = defineCommand9({
1659
2557
  meta: {
1660
2558
  name: "deploy",
1661
2559
  description: "Build and deploy your DeepSpace app"
@@ -1665,31 +2563,38 @@ var deploy_default = defineCommand8({
1665
2563
  type: "positional",
1666
2564
  description: "App directory (default: current directory)",
1667
2565
  required: false
2566
+ },
2567
+ env: {
2568
+ type: "string",
2569
+ description: "wrangler.toml [env.<name>] block to deploy (e.g. --env staging). Omit to deploy the top-level config.",
2570
+ required: false
1668
2571
  }
1669
2572
  },
1670
2573
  async run({ args }) {
1671
- const appDir = resolve5(args.dir ?? ".");
1672
- const wranglerPath = join9(appDir, "wrangler.toml");
1673
- if (!existsSync9(wranglerPath)) {
2574
+ const appDir = resolve6(args.dir ?? ".");
2575
+ const envName = typeof args.env === "string" && args.env.trim() ? args.env.trim() : void 0;
2576
+ if (!hasWranglerConfig(appDir)) {
1674
2577
  console.error("No wrangler.toml found. Are you in a DeepSpace app directory?");
1675
2578
  process.exit(1);
1676
2579
  }
1677
2580
  ensureInstallReady(appDir);
1678
2581
  p4.intro("Deploying DeepSpace app");
1679
- const wranglerConfig = parseToml2(readFileSync6(wranglerPath, "utf-8"));
1680
- const nameRes = resolveAppName(wranglerConfig.name);
2582
+ const wranglerConfig = readWranglerConfig(appDir);
2583
+ const nameRes = resolveAppNameForEnv(wranglerConfig, envName);
1681
2584
  if (!nameRes.ok) {
1682
- p4.cancel(`wrangler.toml: ${nameRes.reason}`);
2585
+ p4.cancel(nameRes.reason);
1683
2586
  process.exit(1);
1684
2587
  }
1685
- if (wranglerConfig.name !== nameRes.name) {
2588
+ const declaredName = envName ? wranglerConfig.env?.[envName]?.name : wranglerConfig.name;
2589
+ if (declaredName !== nameRes.name) {
2590
+ const where = envName ? `[env.${envName}].name` : "`name`";
1686
2591
  p4.cancel(
1687
- `wrangler.toml: name "${wranglerConfig.name}" is not in canonical form. Update \`name\` to "${nameRes.name}" and re-run.`
2592
+ `wrangler.toml: ${where} "${declaredName}" is not in canonical form. Update it to "${nameRes.name}" and re-run.`
1688
2593
  );
1689
2594
  process.exit(1);
1690
2595
  }
1691
2596
  const appName = nameRes.name;
1692
- p4.log.info(`App: ${appName}`);
2597
+ p4.log.info(envName ? `App: ${appName} (env: ${envName})` : `App: ${appName}`);
1693
2598
  let token2;
1694
2599
  try {
1695
2600
  token2 = await ensureToken();
@@ -1700,7 +2605,11 @@ var deploy_default = defineCommand8({
1700
2605
  const s = createSpinner();
1701
2606
  s.start("Building...");
1702
2607
  try {
1703
- execSync2("npx vite build", { cwd: appDir, stdio: "pipe" });
2608
+ execSync2("npx vite build", {
2609
+ cwd: appDir,
2610
+ stdio: "pipe",
2611
+ env: envName ? { ...process.env, CLOUDFLARE_ENV: envName } : process.env
2612
+ });
1704
2613
  } catch (err) {
1705
2614
  s.stop("Build failed");
1706
2615
  const detail = [err.stdout?.toString(), err.stderr?.toString()].filter(Boolean).join("\n").trim();
@@ -1708,33 +2617,40 @@ var deploy_default = defineCommand8({
1708
2617
  process.exit(1);
1709
2618
  }
1710
2619
  s.stop("Built");
1711
- const deployConfigPath = join9(appDir, ".wrangler", "deploy", "config.json");
1712
- if (!existsSync9(deployConfigPath)) {
2620
+ const deployConfigPath = join10(appDir, ".wrangler", "deploy", "config.json");
2621
+ if (!existsSync10(deployConfigPath)) {
1713
2622
  p4.cancel("Build output config not found at .wrangler/deploy/config.json");
1714
2623
  process.exit(1);
1715
2624
  }
1716
- const deployConfig = JSON.parse(readFileSync6(deployConfigPath, "utf-8"));
1717
- const outputWranglerPath = resolve5(dirname(deployConfigPath), deployConfig.configPath);
1718
- if (!existsSync9(outputWranglerPath)) {
2625
+ const deployConfig = JSON.parse(readFileSync9(deployConfigPath, "utf-8"));
2626
+ const outputWranglerPath = resolve6(dirname2(deployConfigPath), deployConfig.configPath);
2627
+ if (!existsSync10(outputWranglerPath)) {
1719
2628
  p4.cancel(`Output wrangler.json not found at ${outputWranglerPath}`);
1720
2629
  process.exit(1);
1721
2630
  }
1722
- const outputConfig = JSON.parse(readFileSync6(outputWranglerPath, "utf-8"));
1723
- const workerDir = dirname(outputWranglerPath);
1724
- const workerBundlePath = join9(workerDir, outputConfig.main);
1725
- const clientDir = outputConfig.assets?.directory ? resolve5(workerDir, outputConfig.assets.directory) : null;
1726
- if (!existsSync9(workerBundlePath)) {
2631
+ const outputConfig = JSON.parse(readFileSync9(outputWranglerPath, "utf-8"));
2632
+ if (outputConfig.name && outputConfig.name !== appName) {
2633
+ const envHint = envName ? ` (--env ${envName})` : "";
2634
+ p4.cancel(
2635
+ `Build output mismatch: expected name "${appName}"${envHint}, but the built wrangler.json declares "${outputConfig.name}". This usually means the Cloudflare Vite plugin didn't apply the requested env block. Check the plugin version and re-run.`
2636
+ );
2637
+ process.exit(1);
2638
+ }
2639
+ const workerDir = dirname2(outputWranglerPath);
2640
+ const workerBundlePath = join10(workerDir, outputConfig.main);
2641
+ const clientDir = outputConfig.assets?.directory ? resolve6(workerDir, outputConfig.assets.directory) : null;
2642
+ if (!existsSync10(workerBundlePath)) {
1727
2643
  p4.cancel(`Worker bundle not found at ${workerBundlePath}`);
1728
2644
  process.exit(1);
1729
2645
  }
1730
- if (!clientDir || !existsSync9(clientDir)) {
2646
+ if (!clientDir || !existsSync10(clientDir)) {
1731
2647
  p4.cancel(`Client assets not found at ${clientDir}`);
1732
2648
  process.exit(1);
1733
2649
  }
1734
2650
  s.start("Collecting assets...");
1735
2651
  const assets = collectAssets(clientDir);
1736
2652
  s.stop(`Collected ${assets.length} assets`);
1737
- const workerJs = readFileSync6(workerBundlePath, "utf-8");
2653
+ const workerJs = readFileSync9(workerBundlePath, "utf-8");
1738
2654
  const doBindings = outputConfig.durable_objects?.bindings;
1739
2655
  const sqliteClasses = new Set(
1740
2656
  outputConfig.migrations?.flatMap((m) => m.new_sqlite_classes ?? []) ?? []
@@ -1761,40 +2677,20 @@ var deploy_default = defineCommand8({
1761
2677
  `Custom bindings: ${customBindings.map((b) => `${b.name} (${b.type})`).join(", ")}`
1762
2678
  );
1763
2679
  }
1764
- const devVarsPath = join9(appDir, ".dev.vars");
1765
2680
  let userSecrets = {};
1766
- if (existsSync9(devVarsPath)) {
1767
- const customSection = extractCustomDevVars(readFileSync6(devVarsPath, "utf-8"));
1768
- if (customSection) {
1769
- try {
1770
- userSecrets = parseDevVars(customSection);
1771
- } catch (err) {
1772
- p4.cancel(`.dev.vars: ${err.message}`);
1773
- process.exit(1);
1774
- }
1775
- }
1776
- }
1777
- const userSecretNames = Object.keys(userSecrets);
1778
- const declaredBindingNames = /* @__PURE__ */ new Set([
1779
- ...customBindings.map((b) => b.name),
1780
- ...(doManifest ?? []).map((d) => d.binding)
1781
- ]);
1782
- for (const name of userSecretNames) {
1783
- if (RESERVED_BINDING_NAMES.has(name)) {
1784
- p4.cancel(
1785
- `.dev.vars: "${name}" is a reserved binding name and cannot be used as an app secret.`
1786
- );
1787
- process.exit(1);
1788
- }
1789
- if (declaredBindingNames.has(name)) {
1790
- p4.cancel(
1791
- `.dev.vars: "${name}" collides with a binding declared in wrangler.toml \u2014 rename one or the other.`
1792
- );
2681
+ if (hasLegacyDevVars(appDir, envName)) {
2682
+ try {
2683
+ userSecrets = readLegacyUserSecrets(appDir, envName, customBindings, doManifest);
2684
+ } catch (err) {
2685
+ const message = err instanceof Error ? err.message : String(err);
2686
+ p4.cancel(message);
1793
2687
  process.exit(1);
1794
2688
  }
1795
2689
  }
1796
- if (userSecretNames.length) {
1797
- p4.log.info(`App secrets (from .dev.vars): ${userSecretNames.join(", ")}`);
2690
+ if (Object.keys(userSecrets).length) {
2691
+ p4.log.info(`Legacy .dev.vars secrets: ${Object.keys(userSecrets).sort().join(", ")}`);
2692
+ const warning = legacyDevVarsAppSecretsRoomWarning(userSecrets, doManifest);
2693
+ if (warning) p4.log.warn(warning);
1798
2694
  }
1799
2695
  s.start(`Deploying to ${appName}.app.space...`);
1800
2696
  const extraRoutes = extractRunWorkerFirst(wranglerConfig);
@@ -1858,12 +2754,12 @@ var deploy_default = defineCommand8({
1858
2754
  if (customBindings.length) {
1859
2755
  form.append("bindingManifest", JSON.stringify(customBindings));
1860
2756
  }
1861
- if (userSecretNames.length) {
1862
- form.append("userSecrets", JSON.stringify(userSecrets));
1863
- }
1864
2757
  if (extraRoutes.length) {
1865
2758
  form.append("extraRunWorkerFirst", JSON.stringify(extraRoutes));
1866
2759
  }
2760
+ if (Object.keys(userSecrets).length) {
2761
+ form.append("userSecrets", JSON.stringify(userSecrets));
2762
+ }
1867
2763
  return form;
1868
2764
  };
1869
2765
  let res;
@@ -1900,6 +2796,7 @@ var deploy_default = defineCommand8({
1900
2796
  s.stop("Deployed (edge propagation still in progress after 90s)");
1901
2797
  p4.log.warn("First requests may briefly hit the assets transitional page; retry in a minute.");
1902
2798
  p4.log.success(`Live at: ${body.url}`);
2799
+ p4.log.info("Secrets: local dev-only values are not pushed automatically. Run `deepspace secrets push` when you want to sync them.");
1903
2800
  await syncSubscriptionPlans(appDir, appName, token2);
1904
2801
  await syncOneTimeProducts(appDir, appName, token2);
1905
2802
  p4.outro("Done");
@@ -1908,21 +2805,43 @@ var deploy_default = defineCommand8({
1908
2805
  }
1909
2806
  s.stop("Deployed!");
1910
2807
  p4.log.success(`Live at: ${body.url}`);
2808
+ p4.log.info("Secrets: local dev-only values are not pushed automatically. Run `deepspace secrets push` when you want to sync them.");
1911
2809
  await syncSubscriptionPlans(appDir, appName, token2);
1912
2810
  await syncOneTimeProducts(appDir, appName, token2);
1913
2811
  p4.outro("Done");
1914
2812
  }
1915
2813
  });
2814
+ function readLegacyUserSecrets(appDir, wranglerEnv, customBindings, doManifest) {
2815
+ const path = join10(appDir, wranglerEnv ? `.dev.vars.${wranglerEnv}` : ".dev.vars");
2816
+ if (!existsSync10(path)) return {};
2817
+ const vars = parseDevVars(extractCustomDevVars(readFileSync9(path, "utf-8")));
2818
+ const reserved = /* @__PURE__ */ new Set([
2819
+ ...SDK_MANAGED_KEYS,
2820
+ ...customBindings.map((b) => b.name),
2821
+ ...(doManifest ?? []).flatMap((b) => [b.binding, b.className])
2822
+ ]);
2823
+ const out = {};
2824
+ for (const [name, value] of Object.entries(vars)) {
2825
+ if (!SECRET_NAME_RE2.test(name)) {
2826
+ throw new Error(`.dev.vars: invalid secret name "${name}".`);
2827
+ }
2828
+ if (reserved.has(name)) {
2829
+ throw new Error(`.dev.vars: "${name}" is reserved and cannot be deployed as a user secret.`);
2830
+ }
2831
+ out[name] = value;
2832
+ }
2833
+ return out;
2834
+ }
1916
2835
  async function syncSubscriptionPlans(appDir, appName, token2) {
1917
- const subsPath = join9(appDir, "src", "subscriptions.ts");
1918
- if (!existsSync9(subsPath)) return;
2836
+ const subsPath = join10(appDir, "src", "subscriptions.ts");
2837
+ if (!existsSync10(subsPath)) return;
1919
2838
  const s = createSpinner();
1920
2839
  s.start("Syncing subscription plans...");
1921
2840
  let plans;
1922
2841
  try {
1923
2842
  const esbuild = await import("esbuild");
1924
- const outDir = join9(appDir, ".wrangler", "deploy");
1925
- const outFile = join9(outDir, "subscriptions.bundle.mjs");
2843
+ const outDir = join10(appDir, ".wrangler", "deploy");
2844
+ const outFile = join10(outDir, "subscriptions.bundle.mjs");
1926
2845
  await esbuild.build({
1927
2846
  entryPoints: [subsPath],
1928
2847
  bundle: true,
@@ -2063,15 +2982,15 @@ current billing period? (Skip to leave them grandfathered.)`
2063
2982
  }
2064
2983
  }
2065
2984
  async function syncOneTimeProducts(appDir, appName, token2) {
2066
- const productsPath = join9(appDir, "src", "products.ts");
2067
- if (!existsSync9(productsPath)) return;
2985
+ const productsPath = join10(appDir, "src", "products.ts");
2986
+ if (!existsSync10(productsPath)) return;
2068
2987
  const s = createSpinner();
2069
2988
  s.start("Syncing one-time products...");
2070
2989
  let products;
2071
2990
  try {
2072
2991
  const esbuild = await import("esbuild");
2073
- const outDir = join9(appDir, ".wrangler", "deploy");
2074
- const outFile = join9(outDir, "products.bundle.mjs");
2992
+ const outDir = join10(appDir, ".wrangler", "deploy");
2993
+ const outFile = join10(outDir, "products.bundle.mjs");
2075
2994
  await esbuild.build({
2076
2995
  entryPoints: [productsPath],
2077
2996
  bundle: true,
@@ -2143,16 +3062,16 @@ async function waitForAssetsReady(url, timeoutMs) {
2143
3062
  function collectAssets(dir) {
2144
3063
  const assets = [];
2145
3064
  function walk(d, prefix) {
2146
- for (const entry of readdirSync(d, { withFileTypes: true })) {
3065
+ for (const entry of readdirSync2(d, { withFileTypes: true })) {
2147
3066
  if (entry.name === ".assetsignore") continue;
2148
- const full = join9(d, entry.name);
3067
+ const full = join10(d, entry.name);
2149
3068
  const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
2150
3069
  if (entry.isDirectory()) {
2151
3070
  walk(full, rel);
2152
3071
  } else {
2153
3072
  assets.push({
2154
3073
  path: "/" + rel,
2155
- contentBase64: readFileSync6(full).toString("base64")
3074
+ contentBase64: readFileSync9(full).toString("base64")
2156
3075
  });
2157
3076
  }
2158
3077
  }
@@ -2162,13 +3081,10 @@ function collectAssets(dir) {
2162
3081
  }
2163
3082
 
2164
3083
  // src/cli/commands/undeploy.ts
2165
- import { defineCommand as defineCommand9 } from "citty";
2166
- import { readFileSync as readFileSync7, existsSync as existsSync10 } from "fs";
2167
- import { join as join10 } from "path";
2168
- import { parse as parseToml3 } from "smol-toml";
3084
+ import { defineCommand as defineCommand10 } from "citty";
2169
3085
  import * as p5 from "@clack/prompts";
2170
3086
  var DEPLOY_URL2 = process.env.DEEPSPACE_DEPLOY_URL ?? ENVS.prod.deploy;
2171
- var undeploy_default = defineCommand9({
3087
+ var undeploy_default = defineCommand10({
2172
3088
  meta: {
2173
3089
  name: "undeploy",
2174
3090
  description: "Remove a deployed DeepSpace app"
@@ -2178,15 +3094,25 @@ var undeploy_default = defineCommand9({
2178
3094
  type: "positional",
2179
3095
  description: "App name to undeploy (reads from wrangler.toml if omitted)",
2180
3096
  required: false
3097
+ },
3098
+ env: {
3099
+ type: "string",
3100
+ description: "wrangler.toml [env.<name>] block whose deployed app to remove (e.g. --env staging). Ignored if a positional name is given.",
3101
+ required: false
2181
3102
  }
2182
3103
  },
2183
3104
  async run({ args }) {
2184
3105
  let appName = args.name;
3106
+ const envName = typeof args.env === "string" && args.env.trim() ? args.env.trim() : void 0;
2185
3107
  if (!appName) {
2186
- const wranglerPath = join10(process.cwd(), "wrangler.toml");
2187
- if (existsSync10(wranglerPath)) {
2188
- const config = parseToml3(readFileSync7(wranglerPath, "utf-8"));
2189
- if (config.name) appName = config.name;
3108
+ if (hasWranglerConfig(process.cwd())) {
3109
+ const config = readWranglerConfig(process.cwd());
3110
+ const nameRes = resolveAppNameForEnv(config, envName);
3111
+ if (!nameRes.ok) {
3112
+ p5.cancel(nameRes.reason);
3113
+ process.exit(1);
3114
+ }
3115
+ appName = nameRes.name;
2190
3116
  }
2191
3117
  }
2192
3118
  if (!appName) {
@@ -2219,8 +3145,8 @@ var undeploy_default = defineCommand9({
2219
3145
  });
2220
3146
 
2221
3147
  // src/cli/commands/whoami.ts
2222
- import { defineCommand as defineCommand10 } from "citty";
2223
- var whoami_default = defineCommand10({
3148
+ import { defineCommand as defineCommand11 } from "citty";
3149
+ var whoami_default = defineCommand11({
2224
3150
  meta: {
2225
3151
  name: "whoami",
2226
3152
  description: "Show the currently logged-in user"
@@ -2275,9 +3201,9 @@ var whoami_default = defineCommand10({
2275
3201
  });
2276
3202
 
2277
3203
  // src/cli/commands/add.ts
2278
- import { defineCommand as defineCommand11 } from "citty";
3204
+ import { defineCommand as defineCommand12 } from "citty";
2279
3205
  import { existsSync as existsSync11 } from "fs";
2280
- import { resolve as resolve6, join as join11 } from "path";
3206
+ import { resolve as resolve7, join as join11 } from "path";
2281
3207
  import { execSync as execSync3 } from "child_process";
2282
3208
  function findScript(from) {
2283
3209
  const inNodeModules = join11(from, "node_modules", "deepspace", "scripts", "add-feature.cjs");
@@ -2286,7 +3212,7 @@ function findScript(from) {
2286
3212
  if (existsSync11(inDotDeepspace)) return inDotDeepspace;
2287
3213
  return null;
2288
3214
  }
2289
- var add_default = defineCommand11({
3215
+ var add_default = defineCommand12({
2290
3216
  meta: {
2291
3217
  name: "add",
2292
3218
  description: "Add a feature to your DeepSpace app"
@@ -2315,7 +3241,7 @@ var add_default = defineCommand11({
2315
3241
  }
2316
3242
  },
2317
3243
  run({ args }) {
2318
- const appDir = resolve6(args.dir ?? ".");
3244
+ const appDir = resolve7(args.dir ?? ".");
2319
3245
  ensureInstallReady(appDir);
2320
3246
  const script = findScript(appDir);
2321
3247
  if (!script) {
@@ -2344,9 +3270,9 @@ var add_default = defineCommand11({
2344
3270
  });
2345
3271
 
2346
3272
  // src/cli/commands/skills.ts
2347
- import { defineCommand as defineCommand12 } from "citty";
3273
+ import { defineCommand as defineCommand13 } from "citty";
2348
3274
  import spawn2 from "cross-spawn";
2349
- import { mkdirSync as mkdirSync4 } from "fs";
3275
+ import { mkdirSync as mkdirSync5 } from "fs";
2350
3276
  import { join as join12 } from "path";
2351
3277
  import { homedir as homedir5 } from "os";
2352
3278
  var SKILL_REPO = "deepdotspace/deepspace-skill";
@@ -2366,9 +3292,9 @@ function runSkillsAdd(extraArgs) {
2366
3292
  });
2367
3293
  }
2368
3294
  function precreateClaudeDir(baseDir) {
2369
- mkdirSync4(join12(baseDir, ".claude"), { recursive: true });
3295
+ mkdirSync5(join12(baseDir, ".claude"), { recursive: true });
2370
3296
  }
2371
- var add = defineCommand12({
3297
+ var add = defineCommand13({
2372
3298
  meta: {
2373
3299
  name: "add",
2374
3300
  description: "Install the DeepSpace skill for your coding agent"
@@ -2410,7 +3336,7 @@ var add = defineCommand12({
2410
3336
  if (code !== 0) process.exit(code);
2411
3337
  }
2412
3338
  });
2413
- var skills_default = defineCommand12({
3339
+ var skills_default = defineCommand13({
2414
3340
  meta: {
2415
3341
  name: "skills",
2416
3342
  description: "Manage the DeepSpace agent skill (Claude Code, Codex, OpenCode, Cursor, ...)"
@@ -2421,33 +3347,8 @@ var skills_default = defineCommand12({
2421
3347
  });
2422
3348
 
2423
3349
  // src/cli/commands/domain.ts
2424
- import { defineCommand as defineCommand13 } from "citty";
3350
+ import { defineCommand as defineCommand14 } from "citty";
2425
3351
  import { execSync as execSync4 } from "child_process";
2426
-
2427
- // src/cli/lib/app-context.ts
2428
- import { existsSync as existsSync12, readFileSync as readFileSync8 } from "fs";
2429
- import { join as join13, resolve as resolve7 } from "path";
2430
- import { parse as parseToml4 } from "smol-toml";
2431
- function detectAppName(cwd = process.cwd()) {
2432
- const wranglerPath = join13(resolve7(cwd), "wrangler.toml");
2433
- if (!existsSync12(wranglerPath)) return null;
2434
- try {
2435
- const cfg = parseToml4(readFileSync8(wranglerPath, "utf-8"));
2436
- return typeof cfg.name === "string" && cfg.name.length > 0 ? cfg.name : null;
2437
- } catch {
2438
- return null;
2439
- }
2440
- }
2441
- function requireAppName(explicit, cwd) {
2442
- if (explicit) return explicit;
2443
- const detected = detectAppName(cwd);
2444
- if (detected) return detected;
2445
- throw new Error(
2446
- "No app specified. Pass --app <name>, or run from an app directory with a wrangler.toml."
2447
- );
2448
- }
2449
-
2450
- // src/cli/commands/domain.ts
2451
3352
  var API_URL2 = process.env.DEEPSPACE_API_URL ?? ENVS.prod.api;
2452
3353
  async function api(token2, path, init) {
2453
3354
  const res = await fetch(`${API_URL2}${path}`, {
@@ -2458,16 +3359,16 @@ async function api(token2, path, init) {
2458
3359
  ...init?.headers ?? {}
2459
3360
  }
2460
3361
  });
2461
- const text = await res.text();
3362
+ const text2 = await res.text();
2462
3363
  if (!res.ok) {
2463
- let msg = text;
3364
+ let msg = text2;
2464
3365
  try {
2465
- msg = JSON.parse(text).error ?? text;
3366
+ msg = JSON.parse(text2).error ?? text2;
2466
3367
  } catch {
2467
3368
  }
2468
3369
  throw new Error(`API ${path} (${res.status}): ${msg}`);
2469
3370
  }
2470
- return JSON.parse(text);
3371
+ return JSON.parse(text2);
2471
3372
  }
2472
3373
  function scrubInternal(value) {
2473
3374
  if (Array.isArray(value)) return value.map(scrubInternal);
@@ -2502,7 +3403,7 @@ async function confirm3(message) {
2502
3403
  });
2503
3404
  });
2504
3405
  }
2505
- var search = defineCommand13({
3406
+ var search = defineCommand14({
2506
3407
  meta: { name: "search", description: "Search for available domains" },
2507
3408
  args: {
2508
3409
  query: { type: "positional", description: "Domain or keyword", required: true },
@@ -2532,7 +3433,7 @@ var search = defineCommand13({
2532
3433
  }
2533
3434
  }
2534
3435
  });
2535
- var buy = defineCommand13({
3436
+ var buy = defineCommand14({
2536
3437
  meta: { name: "buy", description: "Buy a domain and attach it to an app" },
2537
3438
  args: {
2538
3439
  domain: { type: "positional", description: "Domain to buy", required: true },
@@ -2621,7 +3522,7 @@ var buy = defineCommand13({
2621
3522
  process.exit(1);
2622
3523
  }
2623
3524
  });
2624
- var list2 = defineCommand13({
3525
+ var list3 = defineCommand14({
2625
3526
  meta: { name: "list", description: "List your domains" },
2626
3527
  args: {
2627
3528
  json: { type: "boolean", description: "Emit JSON" }
@@ -2645,7 +3546,7 @@ var list2 = defineCommand13({
2645
3546
  }
2646
3547
  }
2647
3548
  });
2648
- var status = defineCommand13({
3549
+ var status = defineCommand14({
2649
3550
  meta: { name: "status", description: "Show details for one domain" },
2650
3551
  args: {
2651
3552
  domain: { type: "positional", description: "Domain", required: true },
@@ -2673,7 +3574,7 @@ var status = defineCommand13({
2673
3574
  console.log(`Charged: ${fmtCents(found.chargedCents)}/yr`);
2674
3575
  }
2675
3576
  });
2676
- var attach = defineCommand13({
3577
+ var attach = defineCommand14({
2677
3578
  meta: { name: "attach", description: "Re-point a domain at a different app" },
2678
3579
  args: {
2679
3580
  domain: { type: "positional", description: "Domain to re-point", required: true },
@@ -2704,7 +3605,7 @@ var attach = defineCommand13({
2704
3605
  console.log(`\u2713 ${args.domain} now points at ${targetApp}`);
2705
3606
  }
2706
3607
  });
2707
- var detach = defineCommand13({
3608
+ var detach = defineCommand14({
2708
3609
  meta: {
2709
3610
  name: "detach",
2710
3611
  description: "Stop routing the domain (keeps the registration; auto-renew unchanged)"
@@ -2734,7 +3635,7 @@ var detach = defineCommand13({
2734
3635
  console.log(`\u2713 ${args.domain} detached. Use \`deepspace domain attach\` to re-route, or \`renew --auto off\` to stop auto-renewal.`);
2735
3636
  }
2736
3637
  });
2737
- var renew = defineCommand13({
3638
+ var renew = defineCommand14({
2738
3639
  meta: { name: "renew", description: "Toggle auto-renewal at the registrar" },
2739
3640
  args: {
2740
3641
  domain: { type: "positional", description: "Domain", required: true },
@@ -2768,16 +3669,16 @@ function openBrowser2(url) {
2768
3669
  } catch {
2769
3670
  }
2770
3671
  }
2771
- var domain_default = defineCommand13({
3672
+ var domain_default = defineCommand14({
2772
3673
  meta: { name: "domain", description: "Buy and manage custom domains" },
2773
- subCommands: { search, buy, list: list2, status, attach, detach, renew }
3674
+ subCommands: { search, buy, list: list3, status, attach, detach, renew }
2774
3675
  });
2775
3676
 
2776
3677
  // src/cli/commands/integrations.ts
2777
- import { defineCommand as defineCommand14 } from "citty";
3678
+ import { defineCommand as defineCommand15 } from "citty";
2778
3679
 
2779
3680
  // src/cli/commands/_invoke-impl.ts
2780
- import { readFileSync as readFileSync9 } from "fs";
3681
+ import { readFileSync as readFileSync10 } from "fs";
2781
3682
  var API_URL3 = process.env.DEEPSPACE_API_URL ?? ENVS.prod.api;
2782
3683
  var DEFAULT_TIMEOUT_MS = 12e4;
2783
3684
  function parseTarget(target) {
@@ -2798,7 +3699,7 @@ function resolveBody(opts) {
2798
3699
  if (opts.body != null) {
2799
3700
  raw = opts.body;
2800
3701
  } else if (opts.bodyFile != null) {
2801
- raw = opts.bodyFile === "-" ? readFileSync9(0, "utf-8") : readFileSync9(opts.bodyFile, "utf-8");
3702
+ raw = opts.bodyFile === "-" ? readFileSync10(0, "utf-8") : readFileSync10(opts.bodyFile, "utf-8");
2802
3703
  } else {
2803
3704
  return "{}";
2804
3705
  }
@@ -2815,8 +3716,9 @@ function flushAndExit(code) {
2815
3716
  process.stdout.write("", () => process.exit(code));
2816
3717
  });
2817
3718
  }
2818
- async function fetchCatalog() {
2819
- const res = await fetch(`${API_URL3}/api/integrations`);
3719
+ async function fetchCatalog(opts = {}) {
3720
+ const url = opts.summary ? `${API_URL3}/api/integrations?summary=1` : `${API_URL3}/api/integrations`;
3721
+ const res = await fetch(url);
2820
3722
  if (!res.ok) {
2821
3723
  throw new Error(`Failed to fetch integration catalog (${res.status})`);
2822
3724
  }
@@ -2832,7 +3734,7 @@ function formatCurrency(value, currency) {
2832
3734
  return `${value} ${currency}`;
2833
3735
  }
2834
3736
  async function runList(args) {
2835
- const catalog = await fetchCatalog();
3737
+ const catalog = await fetchCatalog({ summary: true });
2836
3738
  if (args.json) {
2837
3739
  process.stdout.write(JSON.stringify(catalog) + "\n");
2838
3740
  await flushAndExit(0);
@@ -2948,7 +3850,7 @@ async function runInvoke(args) {
2948
3850
  }
2949
3851
 
2950
3852
  // src/cli/commands/integrations.ts
2951
- var list3 = defineCommand14({
3853
+ var list4 = defineCommand15({
2952
3854
  meta: {
2953
3855
  name: "list",
2954
3856
  description: "List all available integrations"
@@ -2969,7 +3871,7 @@ var list3 = defineCommand14({
2969
3871
  }
2970
3872
  }
2971
3873
  });
2972
- var info = defineCommand14({
3874
+ var info = defineCommand15({
2973
3875
  meta: {
2974
3876
  name: "info",
2975
3877
  description: "Print schema + example body for an integration endpoint"
@@ -2995,7 +3897,7 @@ var info = defineCommand14({
2995
3897
  }
2996
3898
  }
2997
3899
  });
2998
- var invoke = defineCommand14({
3900
+ var invoke = defineCommand15({
2999
3901
  meta: {
3000
3902
  name: "invoke",
3001
3903
  description: "Invoke a platform integration as the logged-in user"
@@ -3048,21 +3950,21 @@ var invoke = defineCommand14({
3048
3950
  }
3049
3951
  }
3050
3952
  });
3051
- var integrations_default = defineCommand14({
3953
+ var integrations_default = defineCommand15({
3052
3954
  meta: {
3053
3955
  name: "integrations",
3054
3956
  description: "List, inspect, and invoke platform integrations"
3055
3957
  },
3056
3958
  subCommands: {
3057
- list: list3,
3959
+ list: list4,
3058
3960
  info,
3059
3961
  invoke
3060
3962
  }
3061
3963
  });
3062
3964
 
3063
3965
  // src/cli/commands/invoke.ts
3064
- import { defineCommand as defineCommand15 } from "citty";
3065
- var invoke_default = defineCommand15({
3966
+ import { defineCommand as defineCommand16 } from "citty";
3967
+ var invoke_default = defineCommand16({
3066
3968
  meta: {
3067
3969
  name: "invoke",
3068
3970
  description: "Invoke a platform integration as the logged-in user"
@@ -3135,7 +4037,7 @@ var invoke_default = defineCommand15({
3135
4037
  });
3136
4038
 
3137
4039
  // src/cli/commands/managed-repos.ts
3138
- import { defineCommand as defineCommand16 } from "citty";
4040
+ import { defineCommand as defineCommand17 } from "citty";
3139
4041
  var API_URL4 = process.env.DEEPSPACE_API_URL ?? ENVS.prod.api;
3140
4042
  var SIMPLE_APP_ID = /^[a-z0-9][a-z0-9_.:-]{0,119}$/i;
3141
4043
  var SIMPLE_APP_NAME = /^[a-z0-9][a-z0-9_-]{0,119}$/i;
@@ -3148,16 +4050,16 @@ async function api2(token2, path, init) {
3148
4050
  ...init?.headers ?? {}
3149
4051
  }
3150
4052
  });
3151
- const text = await res.text();
4053
+ const text2 = await res.text();
3152
4054
  if (!res.ok) {
3153
- let msg = text;
4055
+ let msg = text2;
3154
4056
  try {
3155
- msg = JSON.parse(text).error ?? text;
4057
+ msg = JSON.parse(text2).error ?? text2;
3156
4058
  } catch {
3157
4059
  }
3158
4060
  throw new Error(`API ${path} (${res.status}): ${msg}`);
3159
4061
  }
3160
- return JSON.parse(text);
4062
+ return JSON.parse(text2);
3161
4063
  }
3162
4064
  function defaultAppId(appName) {
3163
4065
  return `cli-${slugId(appName)}`;
@@ -3194,7 +4096,7 @@ async function confirm4(message) {
3194
4096
  });
3195
4097
  });
3196
4098
  }
3197
- var list4 = defineCommand16({
4099
+ var list5 = defineCommand17({
3198
4100
  meta: {
3199
4101
  name: "list",
3200
4102
  description: "List your DeepSpace-managed repos and quota"
@@ -3224,7 +4126,7 @@ var list4 = defineCommand16({
3224
4126
  }
3225
4127
  }
3226
4128
  });
3227
- var create2 = defineCommand16({
4129
+ var create2 = defineCommand17({
3228
4130
  meta: {
3229
4131
  name: "create",
3230
4132
  description: "Create a private DeepSpace-managed GitHub repo"
@@ -3276,7 +4178,7 @@ var create2 = defineCommand16({
3276
4178
  printRepo(result.repo);
3277
4179
  }
3278
4180
  });
3279
- var token = defineCommand16({
4181
+ var token = defineCommand17({
3280
4182
  meta: {
3281
4183
  name: "token",
3282
4184
  description: "Mint a short-lived GitHub token for a DeepSpace-managed repo"
@@ -3311,7 +4213,7 @@ var token = defineCommand16({
3311
4213
  console.log(`Clone: git clone https://x-access-token:${encodeURIComponent(result.token.token)}@github.com/${result.repo.fullName}.git`);
3312
4214
  }
3313
4215
  });
3314
- var remove = defineCommand16({
4216
+ var remove = defineCommand17({
3315
4217
  meta: {
3316
4218
  name: "delete",
3317
4219
  description: "Delete a DeepSpace-managed GitHub repo"
@@ -3364,13 +4266,13 @@ var remove = defineCommand16({
3364
4266
  );
3365
4267
  }
3366
4268
  });
3367
- var managed_repos_default = defineCommand16({
4269
+ var managed_repos_default = defineCommand17({
3368
4270
  meta: {
3369
4271
  name: "managed-repos",
3370
4272
  description: "Create and manage DeepSpace-owned GitHub repos"
3371
4273
  },
3372
4274
  subCommands: {
3373
- list: list4,
4275
+ list: list5,
3374
4276
  create: create2,
3375
4277
  token,
3376
4278
  delete: remove
@@ -3378,13 +4280,13 @@ var managed_repos_default = defineCommand16({
3378
4280
  });
3379
4281
 
3380
4282
  // src/cli/commands/library.ts
3381
- import { defineCommand as defineCommand17 } from "citty";
3382
- import { existsSync as existsSync13, readFileSync as readFileSync10 } from "fs";
3383
- import { join as join14, resolve as resolve8 } from "path";
3384
- import { parse as parseToml5 } from "smol-toml";
4283
+ import { defineCommand as defineCommand18 } from "citty";
4284
+ import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
4285
+ import { join as join13, resolve as resolve8 } from "path";
4286
+ import { parse as parseToml3 } from "smol-toml";
3385
4287
  import * as p6 from "@clack/prompts";
3386
4288
  var DEFAULT_LIBRARY_APP = "deepdotspace-site";
3387
- var publish = defineCommand17({
4289
+ var publish = defineCommand18({
3388
4290
  meta: {
3389
4291
  name: "publish",
3390
4292
  description: "Publish the current app to the DeepSpace library"
@@ -3443,7 +4345,7 @@ var publish = defineCommand17({
3443
4345
  },
3444
4346
  async run({ args }) {
3445
4347
  const json = !!args.json;
3446
- const intro5 = (s) => {
4348
+ const intro6 = (s) => {
3447
4349
  if (!json) p6.intro(s);
3448
4350
  };
3449
4351
  const info2 = (s) => {
@@ -3454,15 +4356,15 @@ var publish = defineCommand17({
3454
4356
  else p6.cancel(msg);
3455
4357
  process.exit(1);
3456
4358
  };
3457
- intro5("Publishing to the DeepSpace library");
4359
+ intro6("Publishing to the DeepSpace library");
3458
4360
  let appName = typeof args.app === "string" && args.app.trim() ? args.app.trim() : void 0;
3459
4361
  if (!appName) {
3460
4362
  const appDir = resolve8(typeof args.dir === "string" && args.dir ? args.dir : ".");
3461
- const wranglerPath = join14(appDir, "wrangler.toml");
3462
- if (!existsSync13(wranglerPath)) {
4363
+ const wranglerPath = join13(appDir, "wrangler.toml");
4364
+ if (!existsSync12(wranglerPath)) {
3463
4365
  return die("No --app provided and no wrangler.toml found. Either pass --app <name> or run from your app's directory.");
3464
4366
  }
3465
- const cfg = parseToml5(readFileSync10(wranglerPath, "utf-8"));
4367
+ const cfg = parseToml3(readFileSync11(wranglerPath, "utf-8"));
3466
4368
  const nameRes = resolveAppName(cfg.name);
3467
4369
  if (!nameRes.ok) return die(`wrangler.toml: ${nameRes.reason}`);
3468
4370
  appName = nameRes.name;
@@ -3523,7 +4425,7 @@ var publish = defineCommand17({
3523
4425
  p6.outro("Done");
3524
4426
  }
3525
4427
  });
3526
- var unpublish = defineCommand17({
4428
+ var unpublish = defineCommand18({
3527
4429
  meta: {
3528
4430
  name: "unpublish",
3529
4431
  description: "Remove a library entry (owner or admin only)"
@@ -3582,7 +4484,7 @@ var unpublish = defineCommand17({
3582
4484
  p6.outro("Done");
3583
4485
  }
3584
4486
  });
3585
- var library_default = defineCommand17({
4487
+ var library_default = defineCommand18({
3586
4488
  meta: {
3587
4489
  name: "library",
3588
4490
  description: "Manage your DeepSpace library entries"
@@ -3594,12 +4496,12 @@ var library_default = defineCommand17({
3594
4496
  });
3595
4497
 
3596
4498
  // src/cli/commands/admin.ts
3597
- import { defineCommand as defineCommand18 } from "citty";
3598
- import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
4499
+ import { defineCommand as defineCommand19 } from "citty";
4500
+ import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync7 } from "fs";
3599
4501
  import { homedir as homedir6 } from "os";
3600
- import { join as join15 } from "path";
4502
+ import { join as join14 } from "path";
3601
4503
  var AUTH_URL5 = process.env.DEEPSPACE_AUTH_URL ?? ENVS.prod.auth;
3602
- var impersonate = defineCommand18({
4504
+ var impersonate = defineCommand19({
3603
4505
  meta: {
3604
4506
  name: "impersonate",
3605
4507
  description: "Store a short-lived CLI token for another user"
@@ -3647,15 +4549,15 @@ var impersonate = defineCommand18({
3647
4549
  console.error(`Failed: ${data.error ?? `HTTP ${res.status}`}`);
3648
4550
  process.exit(1);
3649
4551
  }
3650
- const dir = join15(homedir6(), ".deepspace");
3651
- mkdirSync5(dir, { recursive: true });
3652
- writeFileSync5(TOKEN_PATH2, data.token, { mode: 384 });
4552
+ const dir = join14(homedir6(), ".deepspace");
4553
+ mkdirSync6(dir, { recursive: true });
4554
+ writeFileSync7(TOKEN_PATH2, data.token, { mode: 384 });
3653
4555
  console.log(`Impersonating ${data.user.name ?? data.user.email ?? data.user.id}`);
3654
4556
  console.log(`Email: ${data.user.email ?? "(none)"}`);
3655
4557
  console.log(`UserID: ${data.user.id}`);
3656
4558
  }
3657
4559
  });
3658
- var admin_default = defineCommand18({
4560
+ var admin_default = defineCommand19({
3659
4561
  meta: {
3660
4562
  name: "admin",
3661
4563
  description: "Maintainer-only admin commands"
@@ -3665,13 +4567,276 @@ var admin_default = defineCommand18({
3665
4567
  }
3666
4568
  });
3667
4569
 
4570
+ // src/cli/commands/feedback.ts
4571
+ import { defineCommand as defineCommand20 } from "citty";
4572
+ import * as p7 from "@clack/prompts";
4573
+ import { existsSync as existsSync13, readFileSync as readFileSync12 } from "fs";
4574
+ import { dirname as dirname3, join as join15 } from "path";
4575
+ import { fileURLToPath } from "url";
4576
+ import { release } from "os";
4577
+ var API_URL5 = process.env.DEEPSPACE_API_URL ?? ENVS.prod.api;
4578
+ var TYPES = ["bug", "feature", "other"];
4579
+ var TYPE_LABELS = {
4580
+ bug: "Bug report",
4581
+ feature: "Feature request",
4582
+ other: "Other feedback"
4583
+ };
4584
+ var MAX_TITLE_LEN = 200;
4585
+ var MAX_BODY_LEN = 1e4;
4586
+ var MAX_CONTEXT_LEN = 200;
4587
+ async function api3(token2, path, init) {
4588
+ const res = await fetch(`${API_URL5}${path}`, {
4589
+ ...init,
4590
+ headers: {
4591
+ Authorization: `Bearer ${token2}`,
4592
+ "Content-Type": "application/json",
4593
+ ...init?.headers ?? {}
4594
+ }
4595
+ });
4596
+ const text2 = await res.text();
4597
+ if (!res.ok) {
4598
+ let msg = text2;
4599
+ try {
4600
+ msg = JSON.parse(text2).error ?? text2;
4601
+ } catch {
4602
+ }
4603
+ throw new Error(`API ${path} (${res.status}): ${msg}`);
4604
+ }
4605
+ return JSON.parse(text2);
4606
+ }
4607
+ function normalizeType(raw) {
4608
+ const value = (raw ?? "bug").toLowerCase();
4609
+ if (TYPES.includes(value)) return value;
4610
+ throw new Error(`Invalid --type "${raw}". Use one of: ${TYPES.join(", ")}.`);
4611
+ }
4612
+ function readCliVersion() {
4613
+ try {
4614
+ const here = dirname3(fileURLToPath(import.meta.url));
4615
+ const candidates = [
4616
+ join15(here, "..", "package.json"),
4617
+ join15(here, "..", "..", "..", "package.json")
4618
+ ];
4619
+ for (const path of candidates) {
4620
+ if (!existsSync13(path)) continue;
4621
+ const pkg2 = JSON.parse(readFileSync12(path, "utf-8"));
4622
+ if (pkg2.name === "deepspace" && pkg2.version) return pkg2.version;
4623
+ }
4624
+ } catch {
4625
+ }
4626
+ return "unknown";
4627
+ }
4628
+ function detectAppName2() {
4629
+ try {
4630
+ const cwd = process.cwd();
4631
+ if (!hasWranglerConfig(cwd)) return void 0;
4632
+ const config = readWranglerConfig(cwd);
4633
+ return typeof config.name === "string" ? config.name : void 0;
4634
+ } catch {
4635
+ return void 0;
4636
+ }
4637
+ }
4638
+ function clampContext(value) {
4639
+ return value.length > MAX_CONTEXT_LEN ? value.slice(0, MAX_CONTEXT_LEN) : value;
4640
+ }
4641
+ function collectContext() {
4642
+ const appName = detectAppName2();
4643
+ return {
4644
+ cliVersion: clampContext(readCliVersion()),
4645
+ nodeVersion: clampContext(process.version),
4646
+ platform: clampContext(`${process.platform} ${release()}`),
4647
+ ...appName ? { appName: clampContext(appName) } : {}
4648
+ };
4649
+ }
4650
+ function buildFeedbackPayload(input) {
4651
+ const title = input.title.trim();
4652
+ const body = input.body.trim();
4653
+ if (!title) throw new Error("A title is required.");
4654
+ if (!body) throw new Error("A message/description is required.");
4655
+ if (title.length > MAX_TITLE_LEN) {
4656
+ throw new Error(
4657
+ `Title must be ${MAX_TITLE_LEN} characters or fewer (got ${title.length}).`
4658
+ );
4659
+ }
4660
+ if (body.length > MAX_BODY_LEN) {
4661
+ throw new Error(
4662
+ `Description must be ${MAX_BODY_LEN} characters or fewer (got ${body.length}).`
4663
+ );
4664
+ }
4665
+ return {
4666
+ type: normalizeType(input.type),
4667
+ title,
4668
+ body,
4669
+ ...input.context
4670
+ };
4671
+ }
4672
+ var feedback_default = defineCommand20({
4673
+ meta: {
4674
+ name: "feedback",
4675
+ description: "Submit a bug report, feature request, or feedback to DeepSpace"
4676
+ },
4677
+ args: {
4678
+ title: {
4679
+ type: "positional",
4680
+ description: "Short summary (prompted if omitted in a terminal)",
4681
+ required: false
4682
+ },
4683
+ type: {
4684
+ type: "string",
4685
+ alias: "t",
4686
+ description: `Report type: ${TYPES.join(" | ")} (default: bug)`,
4687
+ required: false
4688
+ },
4689
+ message: {
4690
+ type: "string",
4691
+ alias: "m",
4692
+ description: "Details / description (prompted if omitted in a terminal)",
4693
+ required: false
4694
+ },
4695
+ yes: {
4696
+ type: "boolean",
4697
+ description: "Skip the confirmation prompt",
4698
+ default: false
4699
+ },
4700
+ json: {
4701
+ type: "boolean",
4702
+ description: "Emit JSON instead of human output",
4703
+ default: false
4704
+ }
4705
+ },
4706
+ async run({ args }) {
4707
+ const interactive = process.stdin.isTTY && !args.json;
4708
+ const fail = (message) => {
4709
+ if (args.json) {
4710
+ process.stdout.write(JSON.stringify({ ok: false, error: message }) + "\n");
4711
+ } else {
4712
+ console.error(message);
4713
+ }
4714
+ process.exit(1);
4715
+ };
4716
+ let type;
4717
+ try {
4718
+ type = normalizeType(args.type ? String(args.type) : void 0);
4719
+ } catch (err) {
4720
+ return fail(err.message);
4721
+ }
4722
+ let title = args.title ? String(args.title) : "";
4723
+ let body = args.message ? String(args.message) : "";
4724
+ if ((!title || !body) && !interactive) {
4725
+ return fail(
4726
+ 'Provide a title and --message to submit non-interactively, e.g. `deepspace feedback "Title" --message "Details"`.'
4727
+ );
4728
+ }
4729
+ if (interactive && (!args.type || !title || !body)) {
4730
+ p7.intro("DeepSpace Feedback");
4731
+ if (!args.type) {
4732
+ const picked = await p7.select({
4733
+ message: "What kind of feedback is this?",
4734
+ options: TYPES.map((t) => ({ value: t, label: TYPE_LABELS[t] })),
4735
+ initialValue: type
4736
+ });
4737
+ if (p7.isCancel(picked)) {
4738
+ p7.cancel("Cancelled.");
4739
+ process.exit(0);
4740
+ }
4741
+ type = picked;
4742
+ }
4743
+ if (!title) {
4744
+ const t = await p7.text({
4745
+ message: "Title",
4746
+ placeholder: "Short summary of the issue",
4747
+ validate: (v) => {
4748
+ if (!v?.trim()) return "Title is required";
4749
+ if (v.length > MAX_TITLE_LEN)
4750
+ return `Title must be ${MAX_TITLE_LEN} characters or fewer`;
4751
+ return void 0;
4752
+ }
4753
+ });
4754
+ if (p7.isCancel(t)) {
4755
+ p7.cancel("Cancelled.");
4756
+ process.exit(0);
4757
+ }
4758
+ title = String(t);
4759
+ }
4760
+ if (!body) {
4761
+ const m = await p7.text({
4762
+ message: "Details",
4763
+ placeholder: "What happened? Steps to reproduce, what you expected\u2026",
4764
+ validate: (v) => {
4765
+ if (!v?.trim()) return "A description is required";
4766
+ if (v.length > MAX_BODY_LEN)
4767
+ return `Description must be ${MAX_BODY_LEN} characters or fewer`;
4768
+ return void 0;
4769
+ }
4770
+ });
4771
+ if (p7.isCancel(m)) {
4772
+ p7.cancel("Cancelled.");
4773
+ process.exit(0);
4774
+ }
4775
+ body = String(m);
4776
+ }
4777
+ }
4778
+ let payload;
4779
+ try {
4780
+ payload = buildFeedbackPayload({
4781
+ type,
4782
+ title,
4783
+ body,
4784
+ context: collectContext()
4785
+ });
4786
+ } catch (err) {
4787
+ return fail(err.message);
4788
+ }
4789
+ if (interactive && !args.yes) {
4790
+ p7.note(
4791
+ `${TYPE_LABELS[payload.type]}
4792
+ ${payload.title}
4793
+
4794
+ ${payload.body}
4795
+
4796
+ Context: deepspace ${payload.cliVersion} \xB7 ${payload.nodeVersion} \xB7 ${payload.platform}` + (payload.appName ? ` \xB7 app ${payload.appName}` : ""),
4797
+ "Review"
4798
+ );
4799
+ const ok = await p7.confirm({ message: "Send this to the DeepSpace team?" });
4800
+ if (p7.isCancel(ok) || !ok) {
4801
+ p7.cancel("Not sent.");
4802
+ process.exit(0);
4803
+ }
4804
+ }
4805
+ let token2;
4806
+ try {
4807
+ token2 = await ensureToken();
4808
+ } catch (err) {
4809
+ return fail(err.message ?? "Not signed in. Run `deepspace login`.");
4810
+ }
4811
+ let result;
4812
+ try {
4813
+ result = await api3(token2, "/api/feedback", {
4814
+ method: "POST",
4815
+ body: JSON.stringify(payload)
4816
+ });
4817
+ } catch (err) {
4818
+ return fail(err.message);
4819
+ }
4820
+ if (args.json) {
4821
+ process.stdout.write(JSON.stringify({ ok: true, report: result.report }) + "\n");
4822
+ return;
4823
+ }
4824
+ if (interactive) {
4825
+ p7.outro(`Thanks! Submitted as ${result.report.id}.`);
4826
+ } else {
4827
+ console.log(`Submitted feedback ${result.report.id} (${result.report.type}).`);
4828
+ }
4829
+ console.log(`Track triage status at ${DASHBOARD_URL}.`);
4830
+ }
4831
+ });
4832
+
3668
4833
  // src/cli/cli.ts
3669
- var __dirname = dirname2(fileURLToPath(import.meta.url));
3670
- var pkg = JSON.parse(readFileSync11(join16(__dirname, "..", "package.json"), "utf-8"));
4834
+ var __dirname = dirname4(fileURLToPath2(import.meta.url));
4835
+ var pkg = JSON.parse(readFileSync13(join16(__dirname, "..", "package.json"), "utf-8"));
3671
4836
  if (process.platform === "linux" && !process.env.SSL_CERT_FILE && existsSync14("/etc/ssl/certs/ca-certificates.crt")) {
3672
4837
  process.env.SSL_CERT_FILE = "/etc/ssl/certs/ca-certificates.crt";
3673
4838
  }
3674
- var create3 = defineCommand19({
4839
+ var create3 = defineCommand21({
3675
4840
  meta: {
3676
4841
  name: "create",
3677
4842
  description: "Create a new DeepSpace app"
@@ -3690,7 +4855,7 @@ var create3 = defineCommand19({
3690
4855
  execSync5(`npm create deepspace@latest${name}`, { stdio: "inherit" });
3691
4856
  }
3692
4857
  });
3693
- var main = defineCommand19({
4858
+ var main = defineCommand21({
3694
4859
  meta: {
3695
4860
  name: "deepspace",
3696
4861
  version: pkg.version,
@@ -3715,7 +4880,9 @@ var main = defineCommand19({
3715
4880
  undeploy: undeploy_default,
3716
4881
  domain: domain_default,
3717
4882
  library: library_default,
3718
- admin: admin_default
4883
+ admin: admin_default,
4884
+ feedback: feedback_default,
4885
+ secrets: secrets_default
3719
4886
  }
3720
4887
  // No `run()` here — citty cascades parent run() AFTER subcommand finishes,
3721
4888
  // which corrupts agent-friendly output (`--json`) and prints noise on every