deepspace 0.3.7 → 0.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli/cli.ts
4
- import { defineCommand as defineCommand14, runMain } from "citty";
4
+ import { defineCommand as defineCommand17, runMain } from "citty";
5
5
  import { execSync as execSync5 } from "child_process";
6
- import { readFileSync as readFileSync9, existsSync as existsSync11 } from "fs";
7
- import { dirname as dirname2, join as join12 } from "path";
6
+ import { readFileSync as readFileSync10, existsSync as existsSync13 } from "fs";
7
+ import { dirname as dirname2, join as join13 } from "path";
8
8
  import { fileURLToPath } from "url";
9
9
 
10
10
  // src/cli/commands/login.ts
@@ -37,20 +37,72 @@ function createSpinner() {
37
37
  import { existsSync, readFileSync, writeFileSync } from "fs";
38
38
  import { join } from "path";
39
39
  import { parse as parseToml } from "smol-toml";
40
- var ENVS = {
41
- dev: {
42
- auth: "https://deepspace-auth.eudaimonicincorporated.workers.dev",
43
- api: "https://deepspace-api.eudaimonicincorporated.workers.dev",
44
- platform: "https://deepspace-platform-worker.eudaimonicincorporated.workers.dev",
45
- deploy: "https://deepspace-deploy.eudaimonicincorporated.workers.dev"
46
- },
47
- prod: {
48
- auth: "https://deepspace-auth.eudaimonicincorporated.workers.dev",
49
- api: "https://deepspace-api.eudaimonicincorporated.workers.dev",
50
- platform: "https://deepspace-platform-worker.eudaimonicincorporated.workers.dev",
51
- deploy: "https://deepspace-deploy.eudaimonicincorporated.workers.dev"
40
+
41
+ // src/server/rooms/app-name.ts
42
+ var APP_NAME_RULES = {
43
+ /** ^[a-z0-9](-?[a-z0-9])+$ — RFC 1035 host label, no consecutive dashes. */
44
+ pattern: /^[a-z0-9](?:-?[a-z0-9])+$/,
45
+ minLength: 2,
46
+ maxLength: 63
47
+ };
48
+ function validateAppName(raw) {
49
+ if (typeof raw !== "string") {
50
+ return { valid: false, reason: "App name must be a string" };
51
+ }
52
+ const name = raw.trim();
53
+ if (!name) {
54
+ return { valid: false, reason: "App name is required" };
55
+ }
56
+ if (name.length < APP_NAME_RULES.minLength) {
57
+ return { valid: false, reason: `App name "${name}" is too short (min ${APP_NAME_RULES.minLength} chars)` };
58
+ }
59
+ if (name.length > APP_NAME_RULES.maxLength) {
60
+ return { valid: false, reason: `App name "${name}" is too long (max ${APP_NAME_RULES.maxLength} chars)` };
61
+ }
62
+ if (!APP_NAME_RULES.pattern.test(name)) {
63
+ return {
64
+ valid: false,
65
+ reason: `App name "${name}" is invalid. Use lowercase letters, digits, and single hyphens only (must start + end with alphanumeric). Examples: my-app, search-v2, unison-search.`
66
+ };
67
+ }
68
+ return { valid: true, name };
69
+ }
70
+ function resolveAppName(raw) {
71
+ const strict = validateAppName(raw);
72
+ if (strict.valid) return { ok: true, name: strict.name };
73
+ if (typeof raw !== "string") return { ok: false, reason: strict.reason };
74
+ const trimmed = raw.trim();
75
+ if (!trimmed) return { ok: false, reason: "App name is required" };
76
+ const sanitized = sanitizeAppName(trimmed);
77
+ const sanitizedStrict = validateAppName(sanitized);
78
+ if (!sanitizedStrict.valid) {
79
+ return {
80
+ ok: false,
81
+ reason: `App name "${trimmed}" can't be sanitized into a valid name (${sanitizedStrict.reason}). Pick something matching: lowercase letters, digits, single hyphens, ${APP_NAME_RULES.minLength}-${APP_NAME_RULES.maxLength} chars.`
82
+ };
52
83
  }
84
+ return {
85
+ ok: true,
86
+ name: sanitized,
87
+ warning: `App name "${trimmed}" doesn't conform to deploy rules \u2014 using "${sanitized}" instead. Update wrangler.toml's \`name\` to silence this warning.`
88
+ };
89
+ }
90
+ function sanitizeAppName(raw) {
91
+ return raw.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "");
92
+ }
93
+
94
+ // src/cli/env.ts
95
+ var ENV_URLS = {
96
+ auth: "https://deepspace-auth.eudaimonicincorporated.workers.dev",
97
+ api: "https://deepspace-api.eudaimonicincorporated.workers.dev",
98
+ platform: "https://deepspace-platform-worker.eudaimonicincorporated.workers.dev",
99
+ deploy: "https://deepspace-deploy.eudaimonicincorporated.workers.dev"
53
100
  };
101
+ var ENVS = {
102
+ dev: ENV_URLS,
103
+ prod: ENV_URLS
104
+ };
105
+ var DASHBOARD_URL = "https://dashboard.deep.space";
54
106
  async function fetchPublicKey(authUrl) {
55
107
  const res = await fetch(`${authUrl}/api/auth/jwks`);
56
108
  if (!res.ok) {
@@ -68,10 +120,16 @@ function readAppName(appDir) {
68
120
  throw new Error("No wrangler.toml found. Are you in a DeepSpace app directory?");
69
121
  }
70
122
  const parsed = parseToml(readFileSync(wranglerPath, "utf-8"));
71
- if (!parsed.name) {
72
- throw new Error("wrangler.toml has no `name` field");
123
+ const resolved = resolveAppName(parsed.name);
124
+ if (!resolved.ok) {
125
+ throw new Error(`wrangler.toml: ${resolved.reason}`);
126
+ }
127
+ if (parsed.name !== resolved.name) {
128
+ throw new Error(
129
+ `wrangler.toml: name "${parsed.name}" is not in canonical form. Update \`name\` to "${resolved.name}" and re-run.`
130
+ );
73
131
  }
74
- return parsed.name;
132
+ return resolved.name;
75
133
  }
76
134
  async function mintAppOwnerJwt(authUrl, callerJwt, appName) {
77
135
  const res = await fetch(`${authUrl}/api/auth/mint-app-token`, {
@@ -92,6 +150,27 @@ async function mintAppOwnerJwt(authUrl, callerJwt, appName) {
92
150
  }
93
151
  return body.token;
94
152
  }
153
+ async function fetchAppIdentityToken(deployUrl, callerJwt, appName) {
154
+ const res = await fetch(
155
+ `${deployUrl}/api/apps/${encodeURIComponent(appName)}/identity-token`,
156
+ {
157
+ method: "POST",
158
+ headers: { Authorization: `Bearer ${callerJwt}` }
159
+ }
160
+ );
161
+ if (res.status === 404) {
162
+ return null;
163
+ }
164
+ if (!res.ok) {
165
+ const err = await res.text().catch(() => "");
166
+ throw new Error(`Failed to fetch APP_IDENTITY_TOKEN (${res.status}): ${err}`);
167
+ }
168
+ const body = await res.json();
169
+ if (!body.token) {
170
+ throw new Error(`Deploy worker returned no token: ${body.error ?? "unknown error"}`);
171
+ }
172
+ return body.token;
173
+ }
95
174
  var SDK_MANAGED_KEYS = /* @__PURE__ */ new Set([
96
175
  "AUTH_JWT_PUBLIC_KEY",
97
176
  "AUTH_JWT_ISSUER",
@@ -100,6 +179,7 @@ var SDK_MANAGED_KEYS = /* @__PURE__ */ new Set([
100
179
  "PLATFORM_WORKER_URL",
101
180
  "OWNER_USER_ID",
102
181
  "APP_OWNER_JWT",
182
+ "APP_IDENTITY_TOKEN",
103
183
  "INTERNAL_STORAGE_HMAC_SECRET",
104
184
  "ALLOW_DEBUG_ROUTES"
105
185
  ]);
@@ -212,6 +292,7 @@ async function writeDevVars(appDir, env, ownerId, callerJwt) {
212
292
  const publicKey = await fetchPublicKey(urls.auth);
213
293
  const appName = readAppName(appDir);
214
294
  const appOwnerJwt = await mintAppOwnerJwt(urls.auth, callerJwt, appName);
295
+ const appIdentityToken = await fetchAppIdentityToken(urls.deploy, callerJwt, appName);
215
296
  const sdkVars = [
216
297
  `AUTH_JWT_PUBLIC_KEY="${publicKey}"`,
217
298
  `AUTH_JWT_ISSUER=${urls.auth}/api/auth`,
@@ -220,6 +301,7 @@ async function writeDevVars(appDir, env, ownerId, callerJwt) {
220
301
  `PLATFORM_WORKER_URL=${urls.platform}`,
221
302
  `OWNER_USER_ID=${ownerId}`,
222
303
  `APP_OWNER_JWT=${appOwnerJwt}`,
304
+ ...appIdentityToken ? [`APP_IDENTITY_TOKEN=${appIdentityToken}`] : [],
223
305
  `INTERNAL_STORAGE_HMAC_SECRET=dev-${Date.now()}`,
224
306
  // Open /api/debug/* (proxied to RecordRoom) for dev + tests only.
225
307
  // Production secrets never set this, so deployed apps 404 the route.
@@ -307,7 +389,15 @@ var login_default = defineCommand({
307
389
  }
308
390
  storeCredentials(result.sessionToken, result.jwt);
309
391
  s.stop("Authenticated");
310
- p2.outro(`Logged in as ${result.name ?? result.email}`);
392
+ p2.log.success(`Logged in as ${result.name ?? result.email}`);
393
+ p2.note(
394
+ `Manage your account, deployed apps, billing, and subscription
395
+ plans on the web dashboard:
396
+
397
+ ${DASHBOARD_URL}`,
398
+ "Dashboard"
399
+ );
400
+ p2.outro("Done");
311
401
  }
312
402
  });
313
403
  async function pollForCompletion(sessionId) {
@@ -345,7 +435,7 @@ function openBrowser(url) {
345
435
  });
346
436
  }
347
437
  function sleep(ms) {
348
- return new Promise((resolve6) => setTimeout(resolve6, ms));
438
+ return new Promise((resolve9) => setTimeout(resolve9, ms));
349
439
  }
350
440
  async function doEmailLogin(email, password) {
351
441
  const res = await fetch(`${AUTH_URL}/api/auth/sign-in/email`, {
@@ -416,14 +506,25 @@ var logout_default = defineCommand2({
416
506
 
417
507
  // src/cli/commands/dev.ts
418
508
  import { defineCommand as defineCommand3 } from "citty";
419
- import { existsSync as existsSync4 } from "fs";
420
- import { join as join5, resolve } from "path";
421
- import { spawn } from "child_process";
509
+ import { existsSync as existsSync5 } from "fs";
510
+ import { join as join5, resolve as resolve2 } from "path";
511
+ import spawn from "cross-spawn";
422
512
 
423
513
  // src/cli/auth.ts
424
514
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
425
515
  import { join as join4 } from "path";
426
516
  import { homedir as homedir3 } from "os";
517
+
518
+ // src/cli/jwt.ts
519
+ function decodeJwtPayload(token2) {
520
+ const payloadSegment = token2.split(".")[1];
521
+ if (!payloadSegment) {
522
+ throw new Error("JWT payload is missing");
523
+ }
524
+ return JSON.parse(Buffer.from(payloadSegment, "base64url").toString("utf8"));
525
+ }
526
+
527
+ // src/cli/auth.ts
427
528
  var AUTH_URL3 = process.env.DEEPSPACE_AUTH_URL ?? ENVS.prod.auth;
428
529
  var DIR2 = join4(homedir3(), ".deepspace");
429
530
  var SESSION_PATH2 = join4(DIR2, "session");
@@ -439,23 +540,60 @@ async function ensureToken() {
439
540
  return existing;
440
541
  }
441
542
  }
442
- const token = await exchangeSession(AUTH_URL3, sessionToken);
443
- if (!token) {
543
+ const token2 = await exchangeSession(AUTH_URL3, sessionToken);
544
+ if (!token2) {
444
545
  throw new Error("Session expired. Run `deepspace login` to re-authenticate.");
445
546
  }
446
547
  mkdirSync2(DIR2, { recursive: true });
447
- writeFileSync3(TOKEN_PATH2, token, { mode: 384 });
448
- return token;
548
+ writeFileSync3(TOKEN_PATH2, token2, { mode: 384 });
549
+ return token2;
449
550
  }
450
551
  function isTokenValid(jwt) {
451
552
  try {
452
- const payload = JSON.parse(atob(jwt.split(".")[1]));
453
- return payload.exp * 1e3 > Date.now() + 3e4;
553
+ const payload = decodeJwtPayload(jwt);
554
+ return typeof payload.exp === "number" && payload.exp * 1e3 > Date.now() + 3e4;
454
555
  } catch {
455
556
  return false;
456
557
  }
457
558
  }
458
559
 
560
+ // src/cli/lib/preflight.ts
561
+ import { execFileSync } from "child_process";
562
+ import { existsSync as existsSync4 } from "fs";
563
+ import { resolve } from "path";
564
+ var DLL_NOT_FOUND = -1073741515;
565
+ function preflightWindowsWorkerd(appDir) {
566
+ if (process.platform !== "win32") return;
567
+ const workerdExe = resolve(
568
+ appDir,
569
+ "node_modules",
570
+ "@cloudflare",
571
+ "workerd-windows-64",
572
+ "bin",
573
+ "workerd.exe"
574
+ );
575
+ if (!existsSync4(workerdExe)) return;
576
+ try {
577
+ execFileSync(workerdExe, ["--help"], { stdio: "pipe", timeout: 1e4 });
578
+ return;
579
+ } catch (err) {
580
+ const status2 = err.status;
581
+ if (status2 !== DLL_NOT_FOUND) return;
582
+ console.error("");
583
+ console.error(" workerd (Cloudflare Workers runtime) cannot start on this system.");
584
+ console.error(" It needs the Microsoft Visual C++ Redistributable, which is not installed.");
585
+ console.error("");
586
+ console.error(" Install it with any of:");
587
+ console.error(" winget install Microsoft.VCRedist.2015+.x64");
588
+ console.error(" choco install -y vcredist140");
589
+ console.error(" https://aka.ms/vs/17/release/vc_redist.x64.exe");
590
+ console.error("");
591
+ console.error(" Then restart this terminal and re-run the command.");
592
+ console.error("");
593
+ process.exit(1);
594
+ }
595
+ }
596
+
459
597
  // src/cli/commands/dev.ts
460
598
  var DEFAULT_PORT = 5173;
461
599
  var dev_default = defineCommand3({
@@ -481,23 +619,23 @@ var dev_default = defineCommand3({
481
619
  }
482
620
  },
483
621
  async run({ args }) {
484
- const appDir = resolve(args.dir ?? ".");
622
+ const appDir = resolve2(args.dir ?? ".");
485
623
  const env = args.prod ? "prod" : "dev";
486
624
  const port = resolvePort(args.port);
487
- if (!existsSync4(join5(appDir, "wrangler.toml"))) {
625
+ if (!existsSync5(join5(appDir, "wrangler.toml"))) {
488
626
  console.error("No wrangler.toml found. Are you in a DeepSpace app directory?");
489
627
  process.exit(1);
490
628
  }
491
- let token;
629
+ let token2;
492
630
  try {
493
- token = await ensureToken();
631
+ token2 = await ensureToken();
494
632
  } catch (err) {
495
633
  console.error(err.message);
496
634
  process.exit(1);
497
635
  }
498
636
  let payload;
499
637
  try {
500
- payload = JSON.parse(atob(token.split(".")[1]));
638
+ payload = decodeJwtPayload(token2);
501
639
  } catch {
502
640
  console.error("Malformed session token. Run `npx deepspace login`.");
503
641
  process.exit(1);
@@ -505,7 +643,8 @@ var dev_default = defineCommand3({
505
643
  console.log(`Logged in as ${payload.name ?? payload.email}`);
506
644
  console.log(`Environment: ${env}`);
507
645
  console.log(`Port: ${port}`);
508
- await writeDevVars(appDir, env, payload.sub, token);
646
+ await writeDevVars(appDir, env, payload.sub, token2);
647
+ preflightWindowsWorkerd(appDir);
509
648
  console.log("Starting dev server...\n");
510
649
  const vite = spawn(
511
650
  "npx",
@@ -513,7 +652,6 @@ var dev_default = defineCommand3({
513
652
  {
514
653
  cwd: appDir,
515
654
  stdio: "inherit",
516
- shell: process.platform === "win32",
517
655
  env: { ...process.env, DEEPSPACE_PORT: String(port) }
518
656
  }
519
657
  );
@@ -696,9 +834,35 @@ function isAlive(pid) {
696
834
 
697
835
  // src/cli/commands/test.ts
698
836
  import { defineCommand as defineCommand5 } from "citty";
699
- import { existsSync as existsSync5 } from "fs";
700
- import { join as join6, resolve as resolve2 } from "path";
701
- import { execSync, spawnSync as spawnSync2 } from "child_process";
837
+ import { existsSync as existsSync6 } from "fs";
838
+ import { join as join6, resolve as resolve3 } from "path";
839
+ import { sync as spawnSync3 } from "cross-spawn";
840
+
841
+ // src/cli/lib/playwright.ts
842
+ import { execSync } from "child_process";
843
+ import { sync as spawnSync2 } from "cross-spawn";
844
+ function ensurePlaywright(appDir) {
845
+ try {
846
+ execSync("npx playwright --version", { cwd: appDir, stdio: "pipe" });
847
+ } catch {
848
+ console.log("Installing Playwright...");
849
+ execSync("npm install -D @playwright/test", { cwd: appDir, stdio: "pipe" });
850
+ }
851
+ const isLinux = process.platform === "linux";
852
+ const isRoot = isLinux && typeof process.getuid === "function" && process.getuid() === 0;
853
+ const args = isRoot ? ["install", "--with-deps", "chromium"] : ["install", "chromium"];
854
+ const result = spawnSync2("npx", ["playwright", ...args], {
855
+ cwd: appDir,
856
+ stdio: "inherit"
857
+ });
858
+ if (result.status !== 0) {
859
+ console.error("\nPlaywright install failed. On Linux, system libs may be missing. Try:");
860
+ console.error(" sudo npx playwright install --with-deps chromium");
861
+ process.exit(1);
862
+ }
863
+ }
864
+
865
+ // src/cli/commands/test.ts
702
866
  var DEFAULT_PORT3 = 5173;
703
867
  var test_default = defineCommand5({
704
868
  meta: {
@@ -718,25 +882,26 @@ var test_default = defineCommand5({
718
882
  }
719
883
  },
720
884
  async run({ args }) {
721
- const appDir = resolve2(".");
885
+ const appDir = resolve3(".");
722
886
  const suite = args.suite ?? "default";
723
887
  const port = resolvePort3(args.port);
724
- if (!existsSync5(join6(appDir, "wrangler.toml"))) {
888
+ if (!existsSync6(join6(appDir, "wrangler.toml"))) {
725
889
  console.error("No wrangler.toml found. Are you in a DeepSpace app directory?");
726
890
  process.exit(1);
727
891
  }
728
- let token;
892
+ let token2;
729
893
  let ownerId;
730
894
  try {
731
- token = await ensureToken();
732
- const payload = JSON.parse(atob(token.split(".")[1]));
895
+ token2 = await ensureToken();
896
+ const payload = decodeJwtPayload(token2);
733
897
  ownerId = payload.sub;
734
898
  } catch (err) {
735
899
  console.error("`deepspace test` requires you to be logged in. Run `deepspace login` first.");
736
900
  process.exit(1);
737
901
  }
738
- await writeDevVars(appDir, "dev", ownerId, token);
902
+ await writeDevVars(appDir, "dev", ownerId, token2);
739
903
  if (suite !== "unit") {
904
+ preflightWindowsWorkerd(appDir);
740
905
  ensurePlaywright(appDir);
741
906
  }
742
907
  let exitCode = 0;
@@ -773,7 +938,7 @@ var test_default = defineCommand5({
773
938
  }
774
939
  });
775
940
  function runPlaywright(appDir, testFiles, port) {
776
- const result = spawnSync2("npx", [
941
+ const result = spawnSync3("npx", [
777
942
  "playwright",
778
943
  "test",
779
944
  "--config",
@@ -782,36 +947,17 @@ function runPlaywright(appDir, testFiles, port) {
782
947
  ], {
783
948
  cwd: appDir,
784
949
  stdio: "inherit",
785
- shell: process.platform === "win32",
786
950
  env: { ...process.env, DEEPSPACE_PORT: String(port) }
787
951
  });
788
952
  return result.status ?? 1;
789
953
  }
790
954
  function runVitest(appDir) {
791
- const result = spawnSync2("npx", ["vitest", "run", "--passWithNoTests"], {
955
+ const result = spawnSync3("npx", ["vitest", "run", "--passWithNoTests"], {
792
956
  cwd: appDir,
793
- stdio: "inherit",
794
- shell: process.platform === "win32"
957
+ stdio: "inherit"
795
958
  });
796
959
  return result.status ?? 1;
797
960
  }
798
- function ensurePlaywright(appDir) {
799
- try {
800
- execSync("npx playwright --version", { cwd: appDir, stdio: "pipe" });
801
- } catch {
802
- console.log("Installing Playwright...");
803
- execSync("npm install -D @playwright/test", { cwd: appDir, stdio: "pipe" });
804
- }
805
- const isLinux = process.platform === "linux";
806
- const isRoot = isLinux && typeof process.getuid === "function" && process.getuid() === 0;
807
- const args = isRoot ? ["install", "--with-deps", "chromium"] : ["install", "chromium"];
808
- const result = spawnSync2("npx", ["playwright", ...args], { cwd: appDir, stdio: "inherit" });
809
- if (result.status !== 0) {
810
- console.error("\nPlaywright install failed. On Linux, system libs may be missing \u2014 try:");
811
- console.error(" sudo npx playwright install --with-deps chromium");
812
- process.exit(1);
813
- }
814
- }
815
961
  function resolvePort3(arg) {
816
962
  const raw = arg ?? process.env.DEEPSPACE_PORT;
817
963
  if (!raw) return DEFAULT_PORT3;
@@ -823,10 +969,57 @@ function resolvePort3(arg) {
823
969
  return n;
824
970
  }
825
971
 
826
- // src/cli/commands/test-accounts.ts
972
+ // src/cli/commands/screenshot.ts
827
973
  import { defineCommand as defineCommand6 } from "citty";
974
+ import { sync as spawnSync4 } from "cross-spawn";
975
+ import { resolve as resolve4 } from "path";
976
+ var screenshot_default = defineCommand6({
977
+ meta: {
978
+ name: "screenshot",
979
+ description: "Take a Playwright screenshot, installing Chromium on demand"
980
+ },
981
+ args: {
982
+ url: {
983
+ type: "positional",
984
+ description: "URL to capture, for example http://localhost:5173/",
985
+ required: true
986
+ },
987
+ output: {
988
+ type: "positional",
989
+ description: "Output image path, for example screenshot.png",
990
+ required: true
991
+ },
992
+ "full-page": {
993
+ type: "boolean",
994
+ description: "Capture the full scrollable page",
995
+ required: false
996
+ },
997
+ "wait-for-timeout": {
998
+ type: "string",
999
+ description: "Milliseconds to wait before capture",
1000
+ required: false
1001
+ }
1002
+ },
1003
+ run({ args }) {
1004
+ const appDir = resolve4(".");
1005
+ ensurePlaywright(appDir);
1006
+ const playwrightArgs = ["playwright", "screenshot", args.url, args.output];
1007
+ if (args["full-page"]) playwrightArgs.push("--full-page");
1008
+ if (args["wait-for-timeout"]) {
1009
+ playwrightArgs.push("--wait-for-timeout", String(args["wait-for-timeout"]));
1010
+ }
1011
+ const result = spawnSync4("npx", playwrightArgs, {
1012
+ cwd: appDir,
1013
+ stdio: "inherit"
1014
+ });
1015
+ process.exit(result.status ?? 1);
1016
+ }
1017
+ });
1018
+
1019
+ // src/cli/commands/test-accounts.ts
1020
+ import { defineCommand as defineCommand7 } from "citty";
828
1021
  import * as p3 from "@clack/prompts";
829
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync6, mkdirSync as mkdirSync3 } from "fs";
1022
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync7, mkdirSync as mkdirSync3 } from "fs";
830
1023
  import { join as join7 } from "path";
831
1024
  import { homedir as homedir4 } from "os";
832
1025
  var SESSION_COOKIE2 = "__Secure-better-auth.session_token";
@@ -834,7 +1027,7 @@ var AUTH_URL4 = process.env.DEEPSPACE_AUTH_URL ?? ENVS.prod.auth;
834
1027
  var DIR3 = join7(homedir4(), ".deepspace");
835
1028
  var ACCOUNTS_PATH = join7(DIR3, "test-accounts.json");
836
1029
  function loadAccounts() {
837
- if (!existsSync6(ACCOUNTS_PATH)) return [];
1030
+ if (!existsSync7(ACCOUNTS_PATH)) return [];
838
1031
  try {
839
1032
  return JSON.parse(readFileSync4(ACCOUNTS_PATH, "utf-8"));
840
1033
  } catch {
@@ -846,8 +1039,8 @@ function saveAccounts(accounts) {
846
1039
  writeFileSync4(ACCOUNTS_PATH, JSON.stringify(accounts, null, 2), { mode: 384 });
847
1040
  }
848
1041
  function sessionCookie() {
849
- const token = readFileSync4(SESSION_PATH2, "utf-8").trim();
850
- return `${SESSION_COOKIE2}=${encodeURIComponent(token)}`;
1042
+ const token2 = readFileSync4(SESSION_PATH2, "utf-8").trim();
1043
+ return `${SESSION_COOKIE2}=${encodeURIComponent(token2)}`;
851
1044
  }
852
1045
  async function fetchRemoteAccounts() {
853
1046
  const res = await fetch(`${AUTH_URL4}/api/auth/test-accounts`, {
@@ -869,7 +1062,7 @@ async function deleteRemote(id) {
869
1062
  throw new Error(data.error ?? `DELETE returned ${res.status}`);
870
1063
  }
871
1064
  }
872
- var create = defineCommand6({
1065
+ var create = defineCommand7({
873
1066
  meta: {
874
1067
  name: "create",
875
1068
  description: "Create a test account"
@@ -939,7 +1132,7 @@ var create = defineCommand6({
939
1132
  Saved to ${ACCOUNTS_PATH}`);
940
1133
  }
941
1134
  });
942
- var list = defineCommand6({
1135
+ var list = defineCommand7({
943
1136
  meta: {
944
1137
  name: "list",
945
1138
  description: "List your test accounts"
@@ -974,7 +1167,7 @@ var list = defineCommand6({
974
1167
  }
975
1168
  }
976
1169
  });
977
- var del = defineCommand6({
1170
+ var del = defineCommand7({
978
1171
  meta: {
979
1172
  name: "delete",
980
1173
  description: "Delete a test account by --email or --id"
@@ -1031,7 +1224,7 @@ var del = defineCommand6({
1031
1224
  console.log(`Test account deleted${targetEmail ? `: ${targetEmail}` : `: ${targetId}`}`);
1032
1225
  }
1033
1226
  });
1034
- var clear = defineCommand6({
1227
+ var clear = defineCommand7({
1035
1228
  meta: {
1036
1229
  name: "clear",
1037
1230
  description: "Delete all your test accounts (or those matching --label)"
@@ -1097,7 +1290,7 @@ Failed:`);
1097
1290
  }
1098
1291
  }
1099
1292
  });
1100
- var test_accounts_default = defineCommand6({
1293
+ var test_accounts_default = defineCommand7({
1101
1294
  meta: {
1102
1295
  name: "test-accounts",
1103
1296
  description: "Manage test accounts for development"
@@ -1111,11 +1304,12 @@ var test_accounts_default = defineCommand6({
1111
1304
  });
1112
1305
 
1113
1306
  // src/cli/commands/deploy.ts
1114
- import { defineCommand as defineCommand7 } from "citty";
1307
+ import { defineCommand as defineCommand8 } from "citty";
1115
1308
  import { execSync as execSync2 } from "child_process";
1116
- import { readFileSync as readFileSync5, readdirSync, existsSync as existsSync7 } from "fs";
1309
+ import { readFileSync as readFileSync5, readdirSync, existsSync as existsSync8 } from "fs";
1117
1310
  import { parse as parseToml2 } from "smol-toml";
1118
- import { join as join8, resolve as resolve3, dirname } from "path";
1311
+ import { join as join8, resolve as resolve5, dirname } from "path";
1312
+ import { pathToFileURL } from "url";
1119
1313
  import * as p4 from "@clack/prompts";
1120
1314
 
1121
1315
  // src/server/rooms/binding-manifest.ts
@@ -1321,62 +1515,33 @@ function asObjectArray(v) {
1321
1515
  return v.filter(isObject);
1322
1516
  }
1323
1517
 
1324
- // src/server/rooms/app-name.ts
1325
- var APP_NAME_RULES = {
1326
- /** ^[a-z0-9](-?[a-z0-9])+$ — RFC 1035 host label, no consecutive dashes. */
1327
- pattern: /^[a-z0-9](?:-?[a-z0-9])+$/,
1328
- minLength: 2,
1329
- maxLength: 63
1330
- };
1331
- function validateAppName(raw) {
1332
- if (typeof raw !== "string") {
1333
- return { valid: false, reason: "App name must be a string" };
1334
- }
1335
- const name = raw.trim();
1336
- if (!name) {
1337
- return { valid: false, reason: "App name is required" };
1338
- }
1339
- if (name.length < APP_NAME_RULES.minLength) {
1340
- return { valid: false, reason: `App name "${name}" is too short (min ${APP_NAME_RULES.minLength} chars)` };
1341
- }
1342
- if (name.length > APP_NAME_RULES.maxLength) {
1343
- return { valid: false, reason: `App name "${name}" is too long (max ${APP_NAME_RULES.maxLength} chars)` };
1344
- }
1345
- if (!APP_NAME_RULES.pattern.test(name)) {
1346
- return {
1347
- valid: false,
1348
- reason: `App name "${name}" is invalid. Use lowercase letters, digits, and single hyphens only (must start + end with alphanumeric). Examples: my-app, search-v2, unison-search.`
1349
- };
1350
- }
1351
- return { valid: true, name };
1352
- }
1353
- function resolveAppName(raw) {
1354
- const strict = validateAppName(raw);
1355
- if (strict.valid) return { ok: true, name: strict.name };
1356
- if (typeof raw !== "string") return { ok: false, reason: strict.reason };
1357
- const trimmed = raw.trim();
1358
- if (!trimmed) return { ok: false, reason: "App name is required" };
1359
- const sanitized = sanitizeAppName(trimmed);
1360
- const sanitizedStrict = validateAppName(sanitized);
1361
- if (!sanitizedStrict.valid) {
1362
- return {
1363
- ok: false,
1364
- reason: `App name "${trimmed}" can't be sanitized into a valid name (${sanitizedStrict.reason}). Pick something matching: lowercase letters, digits, single hyphens, ${APP_NAME_RULES.minLength}-${APP_NAME_RULES.maxLength} chars.`
1365
- };
1366
- }
1367
- return {
1368
- ok: true,
1369
- name: sanitized,
1370
- warning: `App name "${trimmed}" doesn't conform to deploy rules \u2014 using "${sanitized}" instead. Update wrangler.toml's \`name\` to silence this warning.`
1371
- };
1372
- }
1373
- function sanitizeAppName(raw) {
1374
- return raw.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "");
1375
- }
1376
-
1377
1518
  // src/cli/commands/deploy.ts
1378
1519
  var DEPLOY_URL = process.env.DEEPSPACE_DEPLOY_URL ?? ENVS.prod.deploy;
1379
- var deploy_default = defineCommand7({
1520
+ var API_URL = process.env.DEEPSPACE_API_URL ?? ENVS.prod.api;
1521
+ var RESERVED_RUN_WORKER_FIRST = /* @__PURE__ */ new Set([
1522
+ "/api/*",
1523
+ "/ws/*",
1524
+ "/internal/*",
1525
+ "/v1/*",
1526
+ "/_deepspace/*"
1527
+ ]);
1528
+ function extractRunWorkerFirst(cfg) {
1529
+ const raw = cfg.assets?.run_worker_first;
1530
+ if (!Array.isArray(raw)) return [];
1531
+ const out = [];
1532
+ const seen = /* @__PURE__ */ new Set();
1533
+ for (const entry of raw) {
1534
+ if (typeof entry !== "string") continue;
1535
+ const trimmed = entry.trim();
1536
+ if (!trimmed.startsWith("/")) continue;
1537
+ if (RESERVED_RUN_WORKER_FIRST.has(trimmed)) continue;
1538
+ if (seen.has(trimmed)) continue;
1539
+ seen.add(trimmed);
1540
+ out.push(trimmed);
1541
+ }
1542
+ return out;
1543
+ }
1544
+ var deploy_default = defineCommand8({
1380
1545
  meta: {
1381
1546
  name: "deploy",
1382
1547
  description: "Build and deploy your DeepSpace app"
@@ -1389,10 +1554,10 @@ var deploy_default = defineCommand7({
1389
1554
  }
1390
1555
  },
1391
1556
  async run({ args }) {
1392
- const appDir = resolve3(args.dir ?? ".");
1557
+ const appDir = resolve5(args.dir ?? ".");
1393
1558
  p4.intro("Deploying DeepSpace app");
1394
1559
  const wranglerPath = join8(appDir, "wrangler.toml");
1395
- if (!existsSync7(wranglerPath)) {
1560
+ if (!existsSync8(wranglerPath)) {
1396
1561
  p4.cancel("No wrangler.toml found. Are you in a DeepSpace app directory?");
1397
1562
  process.exit(1);
1398
1563
  }
@@ -1402,12 +1567,17 @@ var deploy_default = defineCommand7({
1402
1567
  p4.cancel(`wrangler.toml: ${nameRes.reason}`);
1403
1568
  process.exit(1);
1404
1569
  }
1405
- if (nameRes.warning) p4.log.warn(nameRes.warning);
1570
+ if (wranglerConfig.name !== nameRes.name) {
1571
+ p4.cancel(
1572
+ `wrangler.toml: name "${wranglerConfig.name}" is not in canonical form. Update \`name\` to "${nameRes.name}" and re-run.`
1573
+ );
1574
+ process.exit(1);
1575
+ }
1406
1576
  const appName = nameRes.name;
1407
1577
  p4.log.info(`App: ${appName}`);
1408
- let token;
1578
+ let token2;
1409
1579
  try {
1410
- token = await ensureToken();
1580
+ token2 = await ensureToken();
1411
1581
  } catch (err) {
1412
1582
  p4.cancel(err.message);
1413
1583
  process.exit(1);
@@ -1423,25 +1593,25 @@ var deploy_default = defineCommand7({
1423
1593
  }
1424
1594
  s.stop("Built");
1425
1595
  const deployConfigPath = join8(appDir, ".wrangler", "deploy", "config.json");
1426
- if (!existsSync7(deployConfigPath)) {
1596
+ if (!existsSync8(deployConfigPath)) {
1427
1597
  p4.cancel("Build output config not found at .wrangler/deploy/config.json");
1428
1598
  process.exit(1);
1429
1599
  }
1430
1600
  const deployConfig = JSON.parse(readFileSync5(deployConfigPath, "utf-8"));
1431
- const outputWranglerPath = resolve3(dirname(deployConfigPath), deployConfig.configPath);
1432
- if (!existsSync7(outputWranglerPath)) {
1601
+ const outputWranglerPath = resolve5(dirname(deployConfigPath), deployConfig.configPath);
1602
+ if (!existsSync8(outputWranglerPath)) {
1433
1603
  p4.cancel(`Output wrangler.json not found at ${outputWranglerPath}`);
1434
1604
  process.exit(1);
1435
1605
  }
1436
1606
  const outputConfig = JSON.parse(readFileSync5(outputWranglerPath, "utf-8"));
1437
1607
  const workerDir = dirname(outputWranglerPath);
1438
1608
  const workerBundlePath = join8(workerDir, outputConfig.main);
1439
- const clientDir = outputConfig.assets?.directory ? resolve3(workerDir, outputConfig.assets.directory) : null;
1440
- if (!existsSync7(workerBundlePath)) {
1609
+ const clientDir = outputConfig.assets?.directory ? resolve5(workerDir, outputConfig.assets.directory) : null;
1610
+ if (!existsSync8(workerBundlePath)) {
1441
1611
  p4.cancel(`Worker bundle not found at ${workerBundlePath}`);
1442
1612
  process.exit(1);
1443
1613
  }
1444
- if (!clientDir || !existsSync7(clientDir)) {
1614
+ if (!clientDir || !existsSync8(clientDir)) {
1445
1615
  p4.cancel(`Client assets not found at ${clientDir}`);
1446
1616
  process.exit(1);
1447
1617
  }
@@ -1477,7 +1647,7 @@ var deploy_default = defineCommand7({
1477
1647
  }
1478
1648
  const devVarsPath = join8(appDir, ".dev.vars");
1479
1649
  let userSecrets = {};
1480
- if (existsSync7(devVarsPath)) {
1650
+ if (existsSync8(devVarsPath)) {
1481
1651
  const customSection = extractCustomDevVars(readFileSync5(devVarsPath, "utf-8"));
1482
1652
  if (customSection) {
1483
1653
  try {
@@ -1523,9 +1693,14 @@ var deploy_default = defineCommand7({
1523
1693
  if (userSecretNames.length) {
1524
1694
  form.append("userSecrets", JSON.stringify(userSecrets));
1525
1695
  }
1696
+ const extraRoutes = extractRunWorkerFirst(wranglerConfig);
1697
+ if (extraRoutes.length) {
1698
+ form.append("extraRunWorkerFirst", JSON.stringify(extraRoutes));
1699
+ p4.log.info(`Custom worker-first routes: ${extraRoutes.join(", ")}`);
1700
+ }
1526
1701
  const res = await fetch(`${DEPLOY_URL}/api/deploy/${appName}`, {
1527
1702
  method: "POST",
1528
- headers: { Authorization: `Bearer ${token}` },
1703
+ headers: { Authorization: `Bearer ${token2}` },
1529
1704
  body: form
1530
1705
  });
1531
1706
  const body = await res.json().catch(() => ({}));
@@ -1541,34 +1716,247 @@ var deploy_default = defineCommand7({
1541
1716
  s.stop("Deployed (edge propagation still in progress after 90s)");
1542
1717
  p4.log.warn("First requests may briefly hit the assets transitional page; retry in a minute.");
1543
1718
  p4.log.success(`Live at: ${body.url}`);
1719
+ await syncSubscriptionPlans(appDir, appName, token2);
1720
+ await syncOneTimeProducts(appDir, appName, token2);
1544
1721
  p4.outro("Done");
1545
1722
  return;
1546
1723
  }
1547
1724
  }
1548
1725
  s.stop("Deployed!");
1549
1726
  p4.log.success(`Live at: ${body.url}`);
1727
+ await syncSubscriptionPlans(appDir, appName, token2);
1728
+ await syncOneTimeProducts(appDir, appName, token2);
1550
1729
  p4.outro("Done");
1551
1730
  }
1552
1731
  });
1553
- async function waitForAssetsReady(url, timeoutMs) {
1554
- const deadline = Date.now() + timeoutMs;
1555
- let attempt = 0;
1556
- while (Date.now() < deadline) {
1557
- attempt++;
1558
- try {
1559
- const res = await fetch(url, { redirect: "manual" });
1560
- const body = await res.text();
1561
- if (!body.includes("Assets have not yet deployed")) {
1562
- return true;
1563
- }
1564
- } catch {
1732
+ async function syncSubscriptionPlans(appDir, appName, token2) {
1733
+ const subsPath = join8(appDir, "src", "subscriptions.ts");
1734
+ if (!existsSync8(subsPath)) return;
1735
+ const s = createSpinner();
1736
+ s.start("Syncing subscription plans...");
1737
+ let plans;
1738
+ try {
1739
+ const esbuild = await import("esbuild");
1740
+ const outDir = join8(appDir, ".wrangler", "deploy");
1741
+ const outFile = join8(outDir, "subscriptions.bundle.mjs");
1742
+ await esbuild.build({
1743
+ entryPoints: [subsPath],
1744
+ bundle: true,
1745
+ format: "esm",
1746
+ platform: "node",
1747
+ target: "node18",
1748
+ outfile: outFile,
1749
+ logLevel: "silent",
1750
+ write: true
1751
+ });
1752
+ const mod = await import(`${pathToFileURL(outFile).href}?t=${Date.now()}`);
1753
+ plans = mod.subscriptionPlans;
1754
+ } catch (err) {
1755
+ s.stop("Plan sync skipped");
1756
+ p4.log.warn(`Could not load src/subscriptions.ts: ${err.message}`);
1757
+ return;
1758
+ }
1759
+ if (!Array.isArray(plans)) {
1760
+ s.stop("Plan sync skipped");
1761
+ p4.log.warn(
1762
+ "src/subscriptions.ts must `export const subscriptionPlans = [...] as const`"
1763
+ );
1764
+ return;
1765
+ }
1766
+ let res;
1767
+ try {
1768
+ res = await fetch(`${API_URL}/api/subscriptions/sync`, {
1769
+ method: "POST",
1770
+ headers: {
1771
+ Authorization: `Bearer ${token2}`,
1772
+ "Content-Type": "application/json"
1773
+ },
1774
+ body: JSON.stringify({ appName, plans })
1775
+ });
1776
+ } catch (err) {
1777
+ s.stop("Plan sync skipped");
1778
+ p4.log.warn(`Plan sync request failed (network): ${err.message ?? err}`);
1779
+ return;
1780
+ }
1781
+ const body = await res.json().catch(() => ({}));
1782
+ if (res.ok && body.ok) {
1783
+ s.stop(`Synced ${plans.length} plan${plans.length === 1 ? "" : "s"} to Stripe`);
1784
+ if (body.connectMissing) {
1785
+ p4.log.warn(
1786
+ "Stripe Connect not yet set up. Customers can subscribe and earnings will queue in your platform balance until you finish onboarding at /earnings on the dashboard. The first payout after you connect will sweep everything that accumulated in the meantime."
1787
+ );
1565
1788
  }
1566
- const wait2 = Math.min(8e3, 1e3 * 2 ** (attempt - 1));
1567
- await new Promise((r) => setTimeout(r, wait2));
1789
+ const changes = (body.planChanges ?? []).filter((c) => c.affectedSubscribers > 0);
1790
+ if (changes.length > 0) {
1791
+ await promptAndCancelChangedPlans(appName, token2, changes);
1792
+ }
1793
+ return;
1568
1794
  }
1569
- return false;
1795
+ s.stop("Plan sync skipped");
1796
+ p4.log.warn(
1797
+ `Plan sync failed: ${body.error ?? `HTTP ${res.status}`}${body.hint ? ` \u2014 ${body.hint}` : ""}`
1798
+ );
1570
1799
  }
1571
- function collectAssets(dir) {
1800
+ function formatCents(c) {
1801
+ return c === 0 ? "free" : `$${(c / 100).toFixed(2)}`;
1802
+ }
1803
+ function summaryLine(c) {
1804
+ const n = c.affectedSubscribers;
1805
+ const suffix = `${n} subscriber${n === 1 ? "" : "s"}`;
1806
+ if (c.reason === "removed") {
1807
+ return ` \u2022 ${c.slug} \u2014 removed (${suffix} still on it)`;
1808
+ }
1809
+ const diffs = (c.intervals ?? []).map((i) => `${i.interval}: ${formatCents(i.oldCents)} \u2192 ${formatCents(i.newCents)}`).join("; ");
1810
+ return ` \u2022 ${c.slug} \u2014 repriced (${diffs}; ${suffix} grandfathered on old price)`;
1811
+ }
1812
+ async function promptAndCancelChangedPlans(appName, token2, changes) {
1813
+ const summary = changes.map(summaryLine).join("\n");
1814
+ p4.log.warn(
1815
+ `Plan change${changes.length === 1 ? "" : "s"} from this deploy left subscribers stranded:
1816
+ ${summary}
1817
+
1818
+ Stripe will keep charging them until you cancel. Cancel at the end of their
1819
+ current billing period? (Skip to leave them grandfathered.)`
1820
+ );
1821
+ const choice = await p4.confirm({
1822
+ message: `Cancel ${changes.length === 1 ? "these subscribers" : "all of them"} at period end?`,
1823
+ initialValue: false
1824
+ });
1825
+ if (p4.isCancel(choice) || !choice) {
1826
+ p4.log.info("Skipped \u2014 existing subscribers stay on their current plan/price.");
1827
+ return;
1828
+ }
1829
+ const MAX_ITERATIONS = 100;
1830
+ for (const c of changes) {
1831
+ const s = createSpinner();
1832
+ s.start(`Canceling ${c.affectedSubscribers} subscriber${c.affectedSubscribers === 1 ? "" : "s"} on ${c.slug}...`);
1833
+ let totalCanceled = 0;
1834
+ let totalFailed = 0;
1835
+ let iteration = 0;
1836
+ let lastError = null;
1837
+ let stopped = false;
1838
+ try {
1839
+ while (iteration < MAX_ITERATIONS) {
1840
+ const res = await fetch(`${API_URL}/api/subscriptions/admin-cancel`, {
1841
+ method: "POST",
1842
+ headers: {
1843
+ Authorization: `Bearer ${token2}`,
1844
+ "Content-Type": "application/json"
1845
+ },
1846
+ body: JSON.stringify({
1847
+ appName,
1848
+ planSlug: c.slug,
1849
+ atPeriodEnd: true,
1850
+ reason: `plan ${c.reason} via deploy`
1851
+ })
1852
+ });
1853
+ const body = await res.json().catch(() => ({}));
1854
+ if (!res.ok || !body.success) {
1855
+ lastError = body.error ?? `HTTP ${res.status}`;
1856
+ s.stop(`Cancel failed for ${c.slug}: ${lastError}`);
1857
+ stopped = true;
1858
+ break;
1859
+ }
1860
+ const batchCanceled = body.canceled ?? 0;
1861
+ totalCanceled += batchCanceled;
1862
+ totalFailed += body.failures?.length ?? 0;
1863
+ if (!body.hasMore) break;
1864
+ if (batchCanceled === 0) {
1865
+ lastError = "no progress (all rows failed this batch)";
1866
+ break;
1867
+ }
1868
+ iteration++;
1869
+ }
1870
+ if (!stopped) {
1871
+ const tail = totalFailed > 0 ? `; ${totalFailed} failed (see Stripe)` : "";
1872
+ const cap = iteration >= MAX_ITERATIONS ? ` (batch cap reached \u2014 re-run deploy to finish remaining)` : "";
1873
+ const errSuffix = lastError ? ` \u2014 ${lastError}` : "";
1874
+ s.stop(`Canceled ${totalCanceled} on ${c.slug}${tail}${cap}${errSuffix}`);
1875
+ }
1876
+ } catch (err) {
1877
+ s.stop(`Cancel failed for ${c.slug}: ${err.message}`);
1878
+ }
1879
+ }
1880
+ }
1881
+ async function syncOneTimeProducts(appDir, appName, token2) {
1882
+ const productsPath = join8(appDir, "src", "products.ts");
1883
+ if (!existsSync8(productsPath)) return;
1884
+ const s = createSpinner();
1885
+ s.start("Syncing one-time products...");
1886
+ let products;
1887
+ try {
1888
+ const esbuild = await import("esbuild");
1889
+ const outDir = join8(appDir, ".wrangler", "deploy");
1890
+ const outFile = join8(outDir, "products.bundle.mjs");
1891
+ await esbuild.build({
1892
+ entryPoints: [productsPath],
1893
+ bundle: true,
1894
+ format: "esm",
1895
+ platform: "node",
1896
+ target: "node18",
1897
+ outfile: outFile,
1898
+ logLevel: "silent",
1899
+ write: true
1900
+ });
1901
+ const mod = await import(`${pathToFileURL(outFile).href}?t=${Date.now()}`);
1902
+ products = mod.oneTimeProducts;
1903
+ } catch (err) {
1904
+ s.stop("Product sync skipped");
1905
+ p4.log.warn(`Could not load src/products.ts: ${err.message}`);
1906
+ return;
1907
+ }
1908
+ if (!Array.isArray(products)) {
1909
+ s.stop("Product sync skipped");
1910
+ p4.log.warn(
1911
+ "src/products.ts must `export const oneTimeProducts = [...] as const`"
1912
+ );
1913
+ return;
1914
+ }
1915
+ let res;
1916
+ try {
1917
+ res = await fetch(`${API_URL}/api/charges/products/sync`, {
1918
+ method: "POST",
1919
+ headers: {
1920
+ Authorization: `Bearer ${token2}`,
1921
+ "Content-Type": "application/json"
1922
+ },
1923
+ body: JSON.stringify({ appName, products })
1924
+ });
1925
+ } catch (err) {
1926
+ s.stop("Product sync skipped");
1927
+ p4.log.warn(`Product sync request failed (network): ${err.message ?? err}`);
1928
+ return;
1929
+ }
1930
+ const body = await res.json().catch(() => ({}));
1931
+ if (res.ok && body.ok) {
1932
+ const n = body.count ?? products.length;
1933
+ s.stop(`Synced ${n} one-time product${n === 1 ? "" : "s"}`);
1934
+ return;
1935
+ }
1936
+ s.stop("Product sync skipped");
1937
+ p4.log.warn(
1938
+ `Product sync failed: ${body.error ?? `HTTP ${res.status}`}${body.details ? ` \u2014 ${JSON.stringify(body.details)}` : ""}`
1939
+ );
1940
+ }
1941
+ async function waitForAssetsReady(url, timeoutMs) {
1942
+ const deadline = Date.now() + timeoutMs;
1943
+ let attempt = 0;
1944
+ while (Date.now() < deadline) {
1945
+ attempt++;
1946
+ try {
1947
+ const res = await fetch(url, { redirect: "manual" });
1948
+ const body = await res.text();
1949
+ if (!body.includes("Assets have not yet deployed")) {
1950
+ return true;
1951
+ }
1952
+ } catch {
1953
+ }
1954
+ const wait2 = Math.min(8e3, 1e3 * 2 ** (attempt - 1));
1955
+ await new Promise((r) => setTimeout(r, wait2));
1956
+ }
1957
+ return false;
1958
+ }
1959
+ function collectAssets(dir) {
1572
1960
  const assets = [];
1573
1961
  function walk(d, prefix) {
1574
1962
  for (const entry of readdirSync(d, { withFileTypes: true })) {
@@ -1590,13 +1978,13 @@ function collectAssets(dir) {
1590
1978
  }
1591
1979
 
1592
1980
  // src/cli/commands/undeploy.ts
1593
- import { defineCommand as defineCommand8 } from "citty";
1594
- import { readFileSync as readFileSync6, existsSync as existsSync8 } from "fs";
1981
+ import { defineCommand as defineCommand9 } from "citty";
1982
+ import { readFileSync as readFileSync6, existsSync as existsSync9 } from "fs";
1595
1983
  import { join as join9 } from "path";
1596
1984
  import { parse as parseToml3 } from "smol-toml";
1597
1985
  import * as p5 from "@clack/prompts";
1598
1986
  var DEPLOY_URL2 = process.env.DEEPSPACE_DEPLOY_URL ?? ENVS.prod.deploy;
1599
- var undeploy_default = defineCommand8({
1987
+ var undeploy_default = defineCommand9({
1600
1988
  meta: {
1601
1989
  name: "undeploy",
1602
1990
  description: "Remove a deployed DeepSpace app"
@@ -1612,7 +2000,7 @@ var undeploy_default = defineCommand8({
1612
2000
  let appName = args.name;
1613
2001
  if (!appName) {
1614
2002
  const wranglerPath = join9(process.cwd(), "wrangler.toml");
1615
- if (existsSync8(wranglerPath)) {
2003
+ if (existsSync9(wranglerPath)) {
1616
2004
  const config = parseToml3(readFileSync6(wranglerPath, "utf-8"));
1617
2005
  if (config.name) appName = config.name;
1618
2006
  }
@@ -1621,9 +2009,9 @@ var undeploy_default = defineCommand8({
1621
2009
  p5.cancel("Provide an app name or run from a DeepSpace app directory.");
1622
2010
  process.exit(1);
1623
2011
  }
1624
- let token;
2012
+ let token2;
1625
2013
  try {
1626
- token = await ensureToken();
2014
+ token2 = await ensureToken();
1627
2015
  } catch (err) {
1628
2016
  p5.cancel(err.message);
1629
2017
  process.exit(1);
@@ -1633,7 +2021,7 @@ var undeploy_default = defineCommand8({
1633
2021
  s.start("Removing...");
1634
2022
  const res = await fetch(`${DEPLOY_URL2}/api/deploy/${appName}`, {
1635
2023
  method: "DELETE",
1636
- headers: { Authorization: `Bearer ${token}` }
2024
+ headers: { Authorization: `Bearer ${token2}` }
1637
2025
  });
1638
2026
  const body = await res.json().catch(() => ({}));
1639
2027
  if (!res.ok || !body.success) {
@@ -1647,8 +2035,8 @@ var undeploy_default = defineCommand8({
1647
2035
  });
1648
2036
 
1649
2037
  // src/cli/commands/whoami.ts
1650
- import { defineCommand as defineCommand9 } from "citty";
1651
- var whoami_default = defineCommand9({
2038
+ import { defineCommand as defineCommand10 } from "citty";
2039
+ var whoami_default = defineCommand10({
1652
2040
  meta: {
1653
2041
  name: "whoami",
1654
2042
  description: "Show the currently logged-in user"
@@ -1670,7 +2058,7 @@ var whoami_default = defineCommand9({
1670
2058
  }
1671
2059
  let payload;
1672
2060
  try {
1673
- payload = JSON.parse(atob(jwt.split(".")[1]));
2061
+ payload = decodeJwtPayload(jwt);
1674
2062
  } catch {
1675
2063
  console.error("Malformed session token. Run `deepspace login` again.");
1676
2064
  process.exit(1);
@@ -1693,6 +2081,7 @@ var whoami_default = defineCommand9({
1693
2081
  console.log(`UserID: ${payload.sub ?? "(none)"}`);
1694
2082
  console.log(`Type: ${accountType}`);
1695
2083
  if (payload.iss) console.log(`Issuer: ${payload.iss}`);
2084
+ console.log(`Dashboard: ${DASHBOARD_URL}`);
1696
2085
  if (isTestAccount) {
1697
2086
  console.log("");
1698
2087
  console.log("Note: this is a test account, not a real OAuth developer.");
@@ -1702,16 +2091,16 @@ var whoami_default = defineCommand9({
1702
2091
  });
1703
2092
 
1704
2093
  // src/cli/commands/add.ts
1705
- import { defineCommand as defineCommand10 } from "citty";
1706
- import { existsSync as existsSync9 } from "fs";
1707
- import { resolve as resolve4, join as join10 } from "path";
2094
+ import { defineCommand as defineCommand11 } from "citty";
2095
+ import { existsSync as existsSync10 } from "fs";
2096
+ import { resolve as resolve6, join as join10 } from "path";
1708
2097
  import { execSync as execSync3 } from "child_process";
1709
2098
  function findScript(from) {
1710
2099
  const candidate = join10(from, ".deepspace", "scripts", "add-feature.cjs");
1711
- if (existsSync9(candidate)) return candidate;
2100
+ if (existsSync10(candidate)) return candidate;
1712
2101
  return null;
1713
2102
  }
1714
- var add_default = defineCommand10({
2103
+ var add_default = defineCommand11({
1715
2104
  meta: {
1716
2105
  name: "add",
1717
2106
  description: "Add a feature to your DeepSpace app"
@@ -1740,7 +2129,7 @@ var add_default = defineCommand10({
1740
2129
  }
1741
2130
  },
1742
2131
  run({ args }) {
1743
- const appDir = resolve4(args.dir ?? ".");
2132
+ const appDir = resolve6(args.dir ?? ".");
1744
2133
  const script = findScript(appDir);
1745
2134
  if (!script) {
1746
2135
  console.error("No .deepspace/scripts/add-feature.cjs found.");
@@ -1765,16 +2154,16 @@ var add_default = defineCommand10({
1765
2154
  });
1766
2155
 
1767
2156
  // src/cli/commands/domain.ts
1768
- import { defineCommand as defineCommand11 } from "citty";
2157
+ import { defineCommand as defineCommand12 } from "citty";
1769
2158
  import { execSync as execSync4 } from "child_process";
1770
2159
 
1771
2160
  // src/cli/lib/app-context.ts
1772
- import { existsSync as existsSync10, readFileSync as readFileSync7 } from "fs";
1773
- import { join as join11, resolve as resolve5 } from "path";
2161
+ import { existsSync as existsSync11, readFileSync as readFileSync7 } from "fs";
2162
+ import { join as join11, resolve as resolve7 } from "path";
1774
2163
  import { parse as parseToml4 } from "smol-toml";
1775
2164
  function detectAppName(cwd = process.cwd()) {
1776
- const wranglerPath = join11(resolve5(cwd), "wrangler.toml");
1777
- if (!existsSync10(wranglerPath)) return null;
2165
+ const wranglerPath = join11(resolve7(cwd), "wrangler.toml");
2166
+ if (!existsSync11(wranglerPath)) return null;
1778
2167
  try {
1779
2168
  const cfg = parseToml4(readFileSync7(wranglerPath, "utf-8"));
1780
2169
  return typeof cfg.name === "string" && cfg.name.length > 0 ? cfg.name : null;
@@ -1792,12 +2181,12 @@ function requireAppName(explicit, cwd) {
1792
2181
  }
1793
2182
 
1794
2183
  // src/cli/commands/domain.ts
1795
- var API_URL = process.env.DEEPSPACE_API_URL ?? ENVS.prod.api;
1796
- async function api(token, path, init) {
1797
- const res = await fetch(`${API_URL}${path}`, {
2184
+ var API_URL2 = process.env.DEEPSPACE_API_URL ?? ENVS.prod.api;
2185
+ async function api(token2, path, init) {
2186
+ const res = await fetch(`${API_URL2}${path}`, {
1798
2187
  ...init,
1799
2188
  headers: {
1800
- Authorization: `Bearer ${token}`,
2189
+ Authorization: `Bearer ${token2}`,
1801
2190
  "Content-Type": "application/json",
1802
2191
  ...init?.headers ?? {}
1803
2192
  }
@@ -1833,20 +2222,20 @@ function fmtDate(iso) {
1833
2222
  if (!iso) return "\u2014";
1834
2223
  return iso.slice(0, 10);
1835
2224
  }
1836
- async function confirm2(message) {
2225
+ async function confirm3(message) {
1837
2226
  if (!process.stdin.isTTY) {
1838
2227
  throw new Error(`${message} \u2014 pass --yes to confirm non-interactively`);
1839
2228
  }
1840
2229
  process.stdout.write(`${message} [y/N] `);
1841
- return new Promise((resolve6) => {
2230
+ return new Promise((resolve9) => {
1842
2231
  process.stdin.setEncoding("utf-8");
1843
2232
  process.stdin.once("data", (data) => {
1844
2233
  const answer = data.toString().trim().toLowerCase();
1845
- resolve6(answer === "y" || answer === "yes");
2234
+ resolve9(answer === "y" || answer === "yes");
1846
2235
  });
1847
2236
  });
1848
2237
  }
1849
- var search = defineCommand11({
2238
+ var search = defineCommand12({
1850
2239
  meta: { name: "search", description: "Search for available domains" },
1851
2240
  args: {
1852
2241
  query: { type: "positional", description: "Domain or keyword", required: true },
@@ -1854,10 +2243,10 @@ var search = defineCommand11({
1854
2243
  json: { type: "boolean", description: "Emit JSON instead of a table" }
1855
2244
  },
1856
2245
  async run({ args }) {
1857
- const token = await ensureToken();
2246
+ const token2 = await ensureToken();
1858
2247
  const limit = parseInt(args.limit ?? "10", 10) || 10;
1859
2248
  const result = await api(
1860
- token,
2249
+ token2,
1861
2250
  `/api/domains/search?q=${encodeURIComponent(args.query)}&limit=${limit}`
1862
2251
  );
1863
2252
  if (args.json) {
@@ -1876,7 +2265,7 @@ var search = defineCommand11({
1876
2265
  }
1877
2266
  }
1878
2267
  });
1879
- var buy = defineCommand11({
2268
+ var buy = defineCommand12({
1880
2269
  meta: { name: "buy", description: "Buy a domain and attach it to an app" },
1881
2270
  args: {
1882
2271
  domain: { type: "positional", description: "Domain to buy", required: true },
@@ -1887,9 +2276,9 @@ var buy = defineCommand11({
1887
2276
  json: { type: "boolean", description: "Emit JSON to stdout", default: false }
1888
2277
  },
1889
2278
  async run({ args }) {
1890
- const token = await ensureToken();
2279
+ const token2 = await ensureToken();
1891
2280
  const appName = requireAppName(args.app);
1892
- const priceCheck = await api(token, "/api/domains/check", {
2281
+ const priceCheck = await api(token2, "/api/domains/check", {
1893
2282
  method: "POST",
1894
2283
  body: JSON.stringify({ domains: [args.domain] })
1895
2284
  });
@@ -1901,7 +2290,7 @@ var buy = defineCommand11({
1901
2290
  const price = fmtCents(result.chargedCents);
1902
2291
  const expectedTime = result.registrar === "cloudflare" ? "~60-90 seconds" : "15-60 minutes (registry NS propagation)";
1903
2292
  if (!args.yes) {
1904
- const ok = await confirm2(
2293
+ const ok = await confirm3(
1905
2294
  `Buy ${args.domain} for ${price}/yr (auto-renews; non-refundable), attach to ${appName}? Provisioning takes ${expectedTime}.`
1906
2295
  );
1907
2296
  if (!ok) {
@@ -1910,7 +2299,7 @@ var buy = defineCommand11({
1910
2299
  }
1911
2300
  }
1912
2301
  const checkout = await api(
1913
- token,
2302
+ token2,
1914
2303
  "/api/domains/checkout",
1915
2304
  {
1916
2305
  method: "POST",
@@ -1937,7 +2326,7 @@ var buy = defineCommand11({
1937
2326
  await new Promise((r) => setTimeout(r, 5e3));
1938
2327
  try {
1939
2328
  const detail = await api(
1940
- token,
2329
+ token2,
1941
2330
  `/api/domains/${checkout.purchaseId}`
1942
2331
  );
1943
2332
  if (detail.domain.status !== lastStatus) {
@@ -1965,14 +2354,14 @@ var buy = defineCommand11({
1965
2354
  process.exit(1);
1966
2355
  }
1967
2356
  });
1968
- var list2 = defineCommand11({
2357
+ var list2 = defineCommand12({
1969
2358
  meta: { name: "list", description: "List your domains" },
1970
2359
  args: {
1971
2360
  json: { type: "boolean", description: "Emit JSON" }
1972
2361
  },
1973
2362
  async run({ args }) {
1974
- const token = await ensureToken();
1975
- const result = await api(token, "/api/domains");
2363
+ const token2 = await ensureToken();
2364
+ const result = await api(token2, "/api/domains");
1976
2365
  if (args.json) {
1977
2366
  process.stdout.write(JSON.stringify(scrubInternal(result), null, 2) + "\n");
1978
2367
  return;
@@ -1989,15 +2378,15 @@ var list2 = defineCommand11({
1989
2378
  }
1990
2379
  }
1991
2380
  });
1992
- var status = defineCommand11({
2381
+ var status = defineCommand12({
1993
2382
  meta: { name: "status", description: "Show details for one domain" },
1994
2383
  args: {
1995
2384
  domain: { type: "positional", description: "Domain", required: true },
1996
2385
  json: { type: "boolean" }
1997
2386
  },
1998
2387
  async run({ args }) {
1999
- const token = await ensureToken();
2000
- const all = await api(token, "/api/domains");
2388
+ const token2 = await ensureToken();
2389
+ const all = await api(token2, "/api/domains");
2001
2390
  const found = all.domains.find((d) => d.domain === args.domain);
2002
2391
  if (!found) {
2003
2392
  console.error(`No domain ${args.domain} in your account`);
@@ -2017,7 +2406,7 @@ var status = defineCommand11({
2017
2406
  console.log(`Charged: ${fmtCents(found.chargedCents)}/yr`);
2018
2407
  }
2019
2408
  });
2020
- var attach = defineCommand11({
2409
+ var attach = defineCommand12({
2021
2410
  meta: { name: "attach", description: "Re-point a domain at a different app" },
2022
2411
  args: {
2023
2412
  domain: { type: "positional", description: "Domain to re-point", required: true },
@@ -2025,16 +2414,16 @@ var attach = defineCommand11({
2025
2414
  json: { type: "boolean" }
2026
2415
  },
2027
2416
  async run({ args }) {
2028
- const token = await ensureToken();
2417
+ const token2 = await ensureToken();
2029
2418
  const targetApp = requireAppName(args.app);
2030
- const all = await api(token, "/api/domains");
2419
+ const all = await api(token2, "/api/domains");
2031
2420
  const found = all.domains.find((d) => d.domain === args.domain);
2032
2421
  if (!found) {
2033
2422
  console.error(`No domain ${args.domain} in your account`);
2034
2423
  process.exit(1);
2035
2424
  }
2036
2425
  const result = await api(
2037
- token,
2426
+ token2,
2038
2427
  `/api/domains/${found.id}/reattach`,
2039
2428
  {
2040
2429
  method: "POST",
@@ -2048,7 +2437,7 @@ var attach = defineCommand11({
2048
2437
  console.log(`\u2713 ${args.domain} now points at ${targetApp}`);
2049
2438
  }
2050
2439
  });
2051
- var detach = defineCommand11({
2440
+ var detach = defineCommand12({
2052
2441
  meta: {
2053
2442
  name: "detach",
2054
2443
  description: "Stop routing the domain (keeps the registration; auto-renew unchanged)"
@@ -2058,15 +2447,15 @@ var detach = defineCommand11({
2058
2447
  yes: { type: "boolean", description: "Skip the confirmation prompt" }
2059
2448
  },
2060
2449
  async run({ args }) {
2061
- const token = await ensureToken();
2062
- const all = await api(token, "/api/domains");
2450
+ const token2 = await ensureToken();
2451
+ const all = await api(token2, "/api/domains");
2063
2452
  const found = all.domains.find((d) => d.domain === args.domain);
2064
2453
  if (!found) {
2065
2454
  console.error(`No domain ${args.domain} in your account`);
2066
2455
  process.exit(1);
2067
2456
  }
2068
2457
  if (!args.yes) {
2069
- const ok = await confirm2(
2458
+ const ok = await confirm3(
2070
2459
  `Detach ${args.domain}? Routing will stop; the domain stays registered (auto-renew is unchanged).`
2071
2460
  );
2072
2461
  if (!ok) {
@@ -2074,11 +2463,11 @@ var detach = defineCommand11({
2074
2463
  return;
2075
2464
  }
2076
2465
  }
2077
- await api(token, `/api/domains/${found.id}`, { method: "DELETE" });
2466
+ await api(token2, `/api/domains/${found.id}`, { method: "DELETE" });
2078
2467
  console.log(`\u2713 ${args.domain} detached. Use \`deepspace domain attach\` to re-route, or \`renew --auto off\` to stop auto-renewal.`);
2079
2468
  }
2080
2469
  });
2081
- var renew = defineCommand11({
2470
+ var renew = defineCommand12({
2082
2471
  meta: { name: "renew", description: "Toggle auto-renewal at the registrar" },
2083
2472
  args: {
2084
2473
  domain: { type: "positional", description: "Domain", required: true },
@@ -2089,15 +2478,15 @@ var renew = defineCommand11({
2089
2478
  console.error('--auto must be "on" or "off"');
2090
2479
  process.exit(1);
2091
2480
  }
2092
- const token = await ensureToken();
2093
- const all = await api(token, "/api/domains");
2481
+ const token2 = await ensureToken();
2482
+ const all = await api(token2, "/api/domains");
2094
2483
  const found = all.domains.find((d) => d.domain === args.domain);
2095
2484
  if (!found) {
2096
2485
  console.error(`No domain ${args.domain} in your account`);
2097
2486
  process.exit(1);
2098
2487
  }
2099
2488
  const enabled = args.auto === "on";
2100
- await api(token, `/api/domains/${found.id}/auto-renew`, {
2489
+ await api(token2, `/api/domains/${found.id}/auto-renew`, {
2101
2490
  method: "POST",
2102
2491
  body: JSON.stringify({ autoRenew: enabled })
2103
2492
  });
@@ -2112,17 +2501,17 @@ function openBrowser2(url) {
2112
2501
  } catch {
2113
2502
  }
2114
2503
  }
2115
- var domain_default = defineCommand11({
2504
+ var domain_default = defineCommand12({
2116
2505
  meta: { name: "domain", description: "Buy and manage custom domains" },
2117
2506
  subCommands: { search, buy, list: list2, status, attach, detach, renew }
2118
2507
  });
2119
2508
 
2120
2509
  // src/cli/commands/integrations.ts
2121
- import { defineCommand as defineCommand12 } from "citty";
2510
+ import { defineCommand as defineCommand13 } from "citty";
2122
2511
 
2123
2512
  // src/cli/commands/_invoke-impl.ts
2124
2513
  import { readFileSync as readFileSync8 } from "fs";
2125
- var API_URL2 = process.env.DEEPSPACE_API_URL ?? ENVS.prod.api;
2514
+ var API_URL3 = process.env.DEEPSPACE_API_URL ?? ENVS.prod.api;
2126
2515
  var DEFAULT_TIMEOUT_MS = 12e4;
2127
2516
  function parseTarget(target) {
2128
2517
  if (!target || typeof target !== "string") {
@@ -2160,7 +2549,7 @@ function flushAndExit(code) {
2160
2549
  });
2161
2550
  }
2162
2551
  async function fetchCatalog() {
2163
- const res = await fetch(`${API_URL2}/api/integrations`);
2552
+ const res = await fetch(`${API_URL3}/api/integrations`);
2164
2553
  if (!res.ok) {
2165
2554
  throw new Error(`Failed to fetch integration catalog (${res.status})`);
2166
2555
  }
@@ -2241,7 +2630,7 @@ async function runInvoke(args) {
2241
2630
  const startedAt = Date.now();
2242
2631
  let res;
2243
2632
  try {
2244
- res = await fetch(`${API_URL2}/api/integrations/${integration}/${endpoint}`, {
2633
+ res = await fetch(`${API_URL3}/api/integrations/${integration}/${endpoint}`, {
2245
2634
  method: "POST",
2246
2635
  headers: {
2247
2636
  "Content-Type": "application/json",
@@ -2292,7 +2681,7 @@ async function runInvoke(args) {
2292
2681
  }
2293
2682
 
2294
2683
  // src/cli/commands/integrations.ts
2295
- var list3 = defineCommand12({
2684
+ var list3 = defineCommand13({
2296
2685
  meta: {
2297
2686
  name: "list",
2298
2687
  description: "List all available integrations"
@@ -2313,7 +2702,7 @@ var list3 = defineCommand12({
2313
2702
  }
2314
2703
  }
2315
2704
  });
2316
- var info = defineCommand12({
2705
+ var info = defineCommand13({
2317
2706
  meta: {
2318
2707
  name: "info",
2319
2708
  description: "Print schema + example body for an integration endpoint"
@@ -2339,7 +2728,7 @@ var info = defineCommand12({
2339
2728
  }
2340
2729
  }
2341
2730
  });
2342
- var invoke = defineCommand12({
2731
+ var invoke = defineCommand13({
2343
2732
  meta: {
2344
2733
  name: "invoke",
2345
2734
  description: "Invoke a platform integration as the logged-in user"
@@ -2392,7 +2781,7 @@ var invoke = defineCommand12({
2392
2781
  }
2393
2782
  }
2394
2783
  });
2395
- var integrations_default = defineCommand12({
2784
+ var integrations_default = defineCommand13({
2396
2785
  meta: {
2397
2786
  name: "integrations",
2398
2787
  description: "List, inspect, and invoke platform integrations"
@@ -2405,8 +2794,8 @@ var integrations_default = defineCommand12({
2405
2794
  });
2406
2795
 
2407
2796
  // src/cli/commands/invoke.ts
2408
- import { defineCommand as defineCommand13 } from "citty";
2409
- var invoke_default = defineCommand13({
2797
+ import { defineCommand as defineCommand14 } from "citty";
2798
+ var invoke_default = defineCommand14({
2410
2799
  meta: {
2411
2800
  name: "invoke",
2412
2801
  description: "Invoke a platform integration as the logged-in user"
@@ -2478,13 +2867,472 @@ var invoke_default = defineCommand13({
2478
2867
  }
2479
2868
  });
2480
2869
 
2870
+ // src/cli/commands/managed-repos.ts
2871
+ import { defineCommand as defineCommand15 } from "citty";
2872
+ var API_URL4 = process.env.DEEPSPACE_API_URL ?? ENVS.prod.api;
2873
+ var SIMPLE_APP_ID = /^[a-z0-9][a-z0-9_.:-]{0,119}$/i;
2874
+ var SIMPLE_APP_NAME = /^[a-z0-9][a-z0-9_-]{0,119}$/i;
2875
+ async function api2(token2, path, init) {
2876
+ const res = await fetch(`${API_URL4}${path}`, {
2877
+ ...init,
2878
+ headers: {
2879
+ Authorization: `Bearer ${token2}`,
2880
+ "Content-Type": "application/json",
2881
+ ...init?.headers ?? {}
2882
+ }
2883
+ });
2884
+ const text = await res.text();
2885
+ if (!res.ok) {
2886
+ let msg = text;
2887
+ try {
2888
+ msg = JSON.parse(text).error ?? text;
2889
+ } catch {
2890
+ }
2891
+ throw new Error(`API ${path} (${res.status}): ${msg}`);
2892
+ }
2893
+ return JSON.parse(text);
2894
+ }
2895
+ function defaultAppId(appName) {
2896
+ return `cli-${slugId(appName)}`;
2897
+ }
2898
+ function slugId(raw) {
2899
+ return raw.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "") || "app";
2900
+ }
2901
+ function assertSimple(value, pattern, label) {
2902
+ if (!pattern.test(value)) {
2903
+ throw new Error(`${label} must start with a letter or number and contain only simple identifier characters.`);
2904
+ }
2905
+ }
2906
+ function printQuota(quota) {
2907
+ console.log(`Repo quota: ${quota.activeCount} / ${quota.limit} active (${quota.remaining} remaining)`);
2908
+ }
2909
+ function printRepo(repo) {
2910
+ console.log(`${repo.fullName}`);
2911
+ console.log(` ID: ${repo.id}`);
2912
+ console.log(` App: ${repo.appName}`);
2913
+ console.log(` App ID: ${repo.appId}`);
2914
+ console.log(` URL: ${repo.htmlUrl}`);
2915
+ console.log(` Clone: ${repo.cloneUrl}`);
2916
+ }
2917
+ async function confirm4(message) {
2918
+ if (!process.stdin.isTTY) {
2919
+ throw new Error(`${message} \u2014 pass --yes to confirm non-interactively`);
2920
+ }
2921
+ process.stdout.write(`${message} [y/N] `);
2922
+ return new Promise((resolve9) => {
2923
+ process.stdin.setEncoding("utf-8");
2924
+ process.stdin.once("data", (data) => {
2925
+ const answer = data.toString().trim().toLowerCase();
2926
+ resolve9(answer === "y" || answer === "yes");
2927
+ });
2928
+ });
2929
+ }
2930
+ var list4 = defineCommand15({
2931
+ meta: {
2932
+ name: "list",
2933
+ description: "List your DeepSpace-managed repos and quota"
2934
+ },
2935
+ args: {
2936
+ json: {
2937
+ type: "boolean",
2938
+ description: "Emit JSON instead of human output",
2939
+ default: false
2940
+ }
2941
+ },
2942
+ async run({ args }) {
2943
+ const token2 = await ensureToken();
2944
+ const result = await api2(token2, "/api/github/repos");
2945
+ if (args.json) {
2946
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
2947
+ return;
2948
+ }
2949
+ printQuota(result.quota);
2950
+ if (result.repos.length === 0) {
2951
+ console.log("No DeepSpace-managed repos. Create one with `deepspace managed-repos create <app-name>`.");
2952
+ return;
2953
+ }
2954
+ console.log("");
2955
+ for (const repo of result.repos) {
2956
+ printRepo(repo);
2957
+ }
2958
+ }
2959
+ });
2960
+ var create2 = defineCommand15({
2961
+ meta: {
2962
+ name: "create",
2963
+ description: "Create a private DeepSpace-managed GitHub repo"
2964
+ },
2965
+ args: {
2966
+ appName: {
2967
+ type: "positional",
2968
+ description: "Working app name for this repo",
2969
+ required: true
2970
+ },
2971
+ appId: {
2972
+ type: "string",
2973
+ description: "Stable DeepSpace repo app id (defaults to cli-<app-name>)",
2974
+ required: false
2975
+ },
2976
+ nameHint: {
2977
+ type: "string",
2978
+ description: "Repo-name hint (defaults to app name)",
2979
+ required: false
2980
+ },
2981
+ json: {
2982
+ type: "boolean",
2983
+ description: "Emit JSON instead of human output",
2984
+ default: false
2985
+ }
2986
+ },
2987
+ async run({ args }) {
2988
+ const appName = String(args.appName);
2989
+ const appId = args.appId ? String(args.appId) : defaultAppId(appName);
2990
+ const nameHint = args.nameHint ? String(args.nameHint) : void 0;
2991
+ try {
2992
+ assertSimple(appName, SIMPLE_APP_NAME, "appName");
2993
+ assertSimple(appId, SIMPLE_APP_ID, "appId");
2994
+ if (nameHint) assertSimple(nameHint, SIMPLE_APP_NAME, "nameHint");
2995
+ } catch (err) {
2996
+ console.error(err.message);
2997
+ process.exit(1);
2998
+ }
2999
+ const token2 = await ensureToken();
3000
+ const result = await api2(token2, "/api/github/repos", {
3001
+ method: "POST",
3002
+ body: JSON.stringify({ appName, appId, ...nameHint ? { nameHint } : {} })
3003
+ });
3004
+ if (args.json) {
3005
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
3006
+ return;
3007
+ }
3008
+ console.log(result.alreadyExists ? "Existing DeepSpace-managed repo:" : "Created DeepSpace-managed repo:");
3009
+ printRepo(result.repo);
3010
+ }
3011
+ });
3012
+ var token = defineCommand15({
3013
+ meta: {
3014
+ name: "token",
3015
+ description: "Mint a short-lived GitHub token for a DeepSpace-managed repo"
3016
+ },
3017
+ args: {
3018
+ repoId: {
3019
+ type: "positional",
3020
+ description: "Repo id from `deepspace managed-repos list`",
3021
+ required: true
3022
+ },
3023
+ json: {
3024
+ type: "boolean",
3025
+ description: "Emit JSON instead of human output",
3026
+ default: false
3027
+ }
3028
+ },
3029
+ async run({ args }) {
3030
+ const jwt = await ensureToken();
3031
+ const result = await api2(
3032
+ jwt,
3033
+ `/api/github/repos/${encodeURIComponent(String(args.repoId))}/agent-token`,
3034
+ { method: "POST" }
3035
+ );
3036
+ if (args.json) {
3037
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
3038
+ return;
3039
+ }
3040
+ console.log(`Repo: ${result.repo.fullName}`);
3041
+ console.log(`Expires: ${result.token.expiresAt}`);
3042
+ console.log(`Token: ${result.token.token}`);
3043
+ console.log("");
3044
+ console.log(`Clone: git clone https://x-access-token:${encodeURIComponent(result.token.token)}@github.com/${result.repo.fullName}.git`);
3045
+ }
3046
+ });
3047
+ var remove = defineCommand15({
3048
+ meta: {
3049
+ name: "delete",
3050
+ description: "Delete a DeepSpace-managed GitHub repo"
3051
+ },
3052
+ args: {
3053
+ repoId: {
3054
+ type: "positional",
3055
+ description: "Repo id from `deepspace managed-repos list`",
3056
+ required: true
3057
+ },
3058
+ yes: {
3059
+ type: "boolean",
3060
+ description: "Skip the confirmation prompt",
3061
+ default: false
3062
+ },
3063
+ json: {
3064
+ type: "boolean",
3065
+ description: "Emit JSON instead of human output",
3066
+ default: false
3067
+ }
3068
+ },
3069
+ async run({ args }) {
3070
+ if (!args.yes) {
3071
+ try {
3072
+ const ok = await confirm4(
3073
+ "Delete this DeepSpace-managed GitHub repo? This removes the GitHub repository and counts against your daily delete quota."
3074
+ );
3075
+ if (!ok) {
3076
+ console.log("Cancelled.");
3077
+ return;
3078
+ }
3079
+ } catch (err) {
3080
+ console.error(err.message);
3081
+ process.exit(1);
3082
+ }
3083
+ }
3084
+ const jwt = await ensureToken();
3085
+ const result = await api2(
3086
+ jwt,
3087
+ `/api/github/repos/${encodeURIComponent(String(args.repoId))}`,
3088
+ { method: "DELETE" }
3089
+ );
3090
+ if (args.json) {
3091
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
3092
+ return;
3093
+ }
3094
+ console.log(`Deleted ${result.repo.fullName}`);
3095
+ console.log(
3096
+ `Delete quota: ${result.deletionQuota.deletedCount} / ${result.deletionQuota.limit} used today (${result.deletionQuota.remaining} remaining)`
3097
+ );
3098
+ }
3099
+ });
3100
+ var managed_repos_default = defineCommand15({
3101
+ meta: {
3102
+ name: "managed-repos",
3103
+ description: "Create and manage DeepSpace-owned GitHub repos"
3104
+ },
3105
+ subCommands: {
3106
+ list: list4,
3107
+ create: create2,
3108
+ token,
3109
+ delete: remove
3110
+ }
3111
+ });
3112
+
3113
+ // src/cli/commands/library.ts
3114
+ import { defineCommand as defineCommand16 } from "citty";
3115
+ import { existsSync as existsSync12, readFileSync as readFileSync9 } from "fs";
3116
+ import { join as join12, resolve as resolve8 } from "path";
3117
+ import { parse as parseToml5 } from "smol-toml";
3118
+ import * as p6 from "@clack/prompts";
3119
+ var DEFAULT_LIBRARY_APP = "deepdotspace-site";
3120
+ var publish = defineCommand16({
3121
+ meta: {
3122
+ name: "publish",
3123
+ description: "Publish the current app to the DeepSpace library"
3124
+ },
3125
+ args: {
3126
+ dir: {
3127
+ type: "string",
3128
+ description: "App directory (default: current directory)",
3129
+ required: false
3130
+ },
3131
+ app: {
3132
+ type: "string",
3133
+ description: "App name (default: read from wrangler.toml)",
3134
+ required: false
3135
+ },
3136
+ name: {
3137
+ type: "string",
3138
+ description: "Display name (default: app name)",
3139
+ required: false
3140
+ },
3141
+ description: {
3142
+ type: "string",
3143
+ description: "Short description",
3144
+ required: false
3145
+ },
3146
+ category: {
3147
+ type: "string",
3148
+ description: "Category (e.g. Productivity, Finance, AI). Default: General",
3149
+ required: false
3150
+ },
3151
+ tags: {
3152
+ type: "string",
3153
+ description: "Comma-separated tags",
3154
+ required: false
3155
+ },
3156
+ visibility: {
3157
+ type: "string",
3158
+ description: "'public' (default), 'unlisted', or 'private'",
3159
+ required: false
3160
+ },
3161
+ screenshot: {
3162
+ type: "string",
3163
+ description: "Override the auto-captured preview with a public PNG URL",
3164
+ required: false
3165
+ },
3166
+ "library-app": {
3167
+ type: "string",
3168
+ description: `Target library app subdomain (default: ${DEFAULT_LIBRARY_APP})`,
3169
+ required: false
3170
+ },
3171
+ json: {
3172
+ type: "boolean",
3173
+ description: "Print only the JSON response (machine-readable)",
3174
+ default: false
3175
+ }
3176
+ },
3177
+ async run({ args }) {
3178
+ const json = !!args.json;
3179
+ const intro5 = (s) => {
3180
+ if (!json) p6.intro(s);
3181
+ };
3182
+ const info2 = (s) => {
3183
+ if (!json) p6.log.info(s);
3184
+ };
3185
+ const die = (msg) => {
3186
+ if (json) console.error(JSON.stringify({ ok: false, error: msg }));
3187
+ else p6.cancel(msg);
3188
+ process.exit(1);
3189
+ };
3190
+ intro5("Publishing to the DeepSpace library");
3191
+ let appName = typeof args.app === "string" && args.app.trim() ? args.app.trim() : void 0;
3192
+ if (!appName) {
3193
+ const appDir = resolve8(typeof args.dir === "string" && args.dir ? args.dir : ".");
3194
+ const wranglerPath = join12(appDir, "wrangler.toml");
3195
+ if (!existsSync12(wranglerPath)) {
3196
+ return die("No --app provided and no wrangler.toml found. Either pass --app <name> or run from your app's directory.");
3197
+ }
3198
+ const cfg = parseToml5(readFileSync9(wranglerPath, "utf-8"));
3199
+ const nameRes = resolveAppName(cfg.name);
3200
+ if (!nameRes.ok) return die(`wrangler.toml: ${nameRes.reason}`);
3201
+ appName = nameRes.name;
3202
+ }
3203
+ info2(`App: ${appName}`);
3204
+ const visibility = args.visibility ?? "public";
3205
+ if (!["public", "private", "unlisted"].includes(visibility)) {
3206
+ return die("--visibility must be 'public', 'unlisted', or 'private'.");
3207
+ }
3208
+ const tags = typeof args.tags === "string" && args.tags ? args.tags.split(",").map((t) => t.trim()).filter(Boolean) : void 0;
3209
+ const libraryApp = typeof args["library-app"] === "string" && args["library-app"] ? args["library-app"] : DEFAULT_LIBRARY_APP;
3210
+ const libraryHost = process.env.DEEPSPACE_LIBRARY_HOST ?? `https://${libraryApp}.app.space`;
3211
+ let token2;
3212
+ try {
3213
+ token2 = await ensureToken();
3214
+ } catch (err) {
3215
+ return die(err instanceof Error ? err.message : String(err));
3216
+ }
3217
+ const payload = {
3218
+ appName,
3219
+ ...args.name ? { name: args.name } : {},
3220
+ ...args.description ? { description: args.description } : {},
3221
+ ...args.category ? { category: args.category } : {},
3222
+ ...tags ? { tags } : {},
3223
+ visibility,
3224
+ ...args.screenshot ? { screenshotUrl: args.screenshot } : {}
3225
+ };
3226
+ const url = `${libraryHost}/api/actions/library.publish`;
3227
+ let res;
3228
+ try {
3229
+ res = await fetch(url, {
3230
+ method: "POST",
3231
+ headers: {
3232
+ "Content-Type": "application/json",
3233
+ Authorization: `Bearer ${token2}`
3234
+ },
3235
+ body: JSON.stringify(payload)
3236
+ });
3237
+ } catch (err) {
3238
+ return die(`Network error reaching ${url}: ${err instanceof Error ? err.message : String(err)}`);
3239
+ }
3240
+ const body = await res.text();
3241
+ let parsed;
3242
+ try {
3243
+ parsed = JSON.parse(body);
3244
+ } catch {
3245
+ return die(`Server returned non-JSON (${res.status}): ${body.slice(0, 400)}`);
3246
+ }
3247
+ if (json) {
3248
+ console.log(JSON.stringify(parsed));
3249
+ process.exit(parsed.success ? 0 : 1);
3250
+ }
3251
+ if (!parsed.success) {
3252
+ return die(parsed.error ?? `Publish failed (HTTP ${res.status}).`);
3253
+ }
3254
+ p6.log.success(`Published ${appName} as ${parsed.data?.templateHandle} (v${parsed.data?.version})`);
3255
+ if (parsed.data?.url) p6.log.message(parsed.data.url);
3256
+ p6.outro("Done");
3257
+ }
3258
+ });
3259
+ var unpublish = defineCommand16({
3260
+ meta: {
3261
+ name: "unpublish",
3262
+ description: "Remove a library entry (owner or admin only)"
3263
+ },
3264
+ args: {
3265
+ handle: {
3266
+ type: "positional",
3267
+ description: "templateHandle to remove (usually your app name)",
3268
+ required: true
3269
+ },
3270
+ "library-app": {
3271
+ type: "string",
3272
+ description: `Target library app subdomain (default: ${DEFAULT_LIBRARY_APP})`,
3273
+ required: false
3274
+ },
3275
+ json: {
3276
+ type: "boolean",
3277
+ description: "Print only the JSON response",
3278
+ default: false
3279
+ }
3280
+ },
3281
+ async run({ args }) {
3282
+ const json = !!args.json;
3283
+ const die = (msg) => {
3284
+ if (json) console.error(JSON.stringify({ ok: false, error: msg }));
3285
+ else p6.cancel(msg);
3286
+ process.exit(1);
3287
+ };
3288
+ if (!json) p6.intro("Unpublishing library entry");
3289
+ let token2;
3290
+ try {
3291
+ token2 = await ensureToken();
3292
+ } catch (err) {
3293
+ return die(err instanceof Error ? err.message : String(err));
3294
+ }
3295
+ const libraryApp = typeof args["library-app"] === "string" && args["library-app"] ? args["library-app"] : DEFAULT_LIBRARY_APP;
3296
+ const libraryHost = process.env.DEEPSPACE_LIBRARY_HOST ?? `https://${libraryApp}.app.space`;
3297
+ const res = await fetch(`${libraryHost}/api/actions/library.unpublish`, {
3298
+ method: "POST",
3299
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${token2}` },
3300
+ body: JSON.stringify({ templateHandle: String(args.handle) })
3301
+ });
3302
+ const body = await res.text();
3303
+ let parsed;
3304
+ try {
3305
+ parsed = JSON.parse(body);
3306
+ } catch {
3307
+ return die(`Server returned non-JSON (${res.status}): ${body.slice(0, 400)}`);
3308
+ }
3309
+ if (json) {
3310
+ console.log(JSON.stringify(parsed));
3311
+ process.exit(parsed.success ? 0 : 1);
3312
+ }
3313
+ if (!parsed.success) return die(parsed.error ?? `HTTP ${res.status}`);
3314
+ p6.log.success(`Unpublished ${parsed.data?.templateHandle}`);
3315
+ p6.outro("Done");
3316
+ }
3317
+ });
3318
+ var library_default = defineCommand16({
3319
+ meta: {
3320
+ name: "library",
3321
+ description: "Manage your DeepSpace library entries"
3322
+ },
3323
+ subCommands: {
3324
+ publish,
3325
+ unpublish
3326
+ }
3327
+ });
3328
+
2481
3329
  // src/cli/cli.ts
2482
3330
  var __dirname = dirname2(fileURLToPath(import.meta.url));
2483
- var pkg = JSON.parse(readFileSync9(join12(__dirname, "..", "package.json"), "utf-8"));
2484
- if (process.platform === "linux" && !process.env.SSL_CERT_FILE && existsSync11("/etc/ssl/certs/ca-certificates.crt")) {
3331
+ var pkg = JSON.parse(readFileSync10(join13(__dirname, "..", "package.json"), "utf-8"));
3332
+ if (process.platform === "linux" && !process.env.SSL_CERT_FILE && existsSync13("/etc/ssl/certs/ca-certificates.crt")) {
2485
3333
  process.env.SSL_CERT_FILE = "/etc/ssl/certs/ca-certificates.crt";
2486
3334
  }
2487
- var create2 = defineCommand14({
3335
+ var create3 = defineCommand17({
2488
3336
  meta: {
2489
3337
  name: "create",
2490
3338
  description: "Create a new DeepSpace app"
@@ -2503,27 +3351,30 @@ var create2 = defineCommand14({
2503
3351
  execSync5(`npm create deepspace@latest${name}`, { stdio: "inherit" });
2504
3352
  }
2505
3353
  });
2506
- var main = defineCommand14({
3354
+ var main = defineCommand17({
2507
3355
  meta: {
2508
3356
  name: "deepspace",
2509
3357
  version: pkg.version,
2510
3358
  description: "DeepSpace SDK CLI"
2511
3359
  },
2512
3360
  subCommands: {
2513
- create: create2,
3361
+ create: create3,
2514
3362
  login: login_default,
2515
3363
  logout: logout_default,
2516
3364
  whoami: whoami_default,
2517
3365
  dev: dev_default,
2518
3366
  kill: kill_default,
2519
3367
  test: test_default,
3368
+ screenshot: screenshot_default,
2520
3369
  "test-accounts": test_accounts_default,
2521
3370
  add: add_default,
2522
3371
  integrations: integrations_default,
3372
+ "managed-repos": managed_repos_default,
2523
3373
  invoke: invoke_default,
2524
3374
  deploy: deploy_default,
2525
3375
  undeploy: undeploy_default,
2526
- domain: domain_default
3376
+ domain: domain_default,
3377
+ library: library_default
2527
3378
  }
2528
3379
  // No `run()` here — citty cascades parent run() AFTER subcommand finishes,
2529
3380
  // which corrupts agent-friendly output (`--json`) and prints noise on every