deepspace 0.3.8 → 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 defineCommand16, 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" };
52
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
+ };
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((resolve7) => setTimeout(resolve7, 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");
@@ -449,13 +550,50 @@ async function ensureToken() {
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,10 +619,10 @@ 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
  }
@@ -497,7 +635,7 @@ var dev_default = defineCommand3({
497
635
  }
498
636
  let payload;
499
637
  try {
500
- payload = JSON.parse(atob(token2.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);
@@ -506,6 +644,7 @@ var dev_default = defineCommand3({
506
644
  console.log(`Environment: ${env}`);
507
645
  console.log(`Port: ${port}`);
508
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,12 +834,13 @@ 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 { spawnSync as spawnSync3 } 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";
702
840
 
703
841
  // src/cli/lib/playwright.ts
704
- import { execSync, spawnSync as spawnSync2 } from "child_process";
842
+ import { execSync } from "child_process";
843
+ import { sync as spawnSync2 } from "cross-spawn";
705
844
  function ensurePlaywright(appDir) {
706
845
  try {
707
846
  execSync("npx playwright --version", { cwd: appDir, stdio: "pipe" });
@@ -714,8 +853,7 @@ function ensurePlaywright(appDir) {
714
853
  const args = isRoot ? ["install", "--with-deps", "chromium"] : ["install", "chromium"];
715
854
  const result = spawnSync2("npx", ["playwright", ...args], {
716
855
  cwd: appDir,
717
- stdio: "inherit",
718
- shell: process.platform === "win32"
856
+ stdio: "inherit"
719
857
  });
720
858
  if (result.status !== 0) {
721
859
  console.error("\nPlaywright install failed. On Linux, system libs may be missing. Try:");
@@ -744,10 +882,10 @@ var test_default = defineCommand5({
744
882
  }
745
883
  },
746
884
  async run({ args }) {
747
- const appDir = resolve2(".");
885
+ const appDir = resolve3(".");
748
886
  const suite = args.suite ?? "default";
749
887
  const port = resolvePort3(args.port);
750
- if (!existsSync5(join6(appDir, "wrangler.toml"))) {
888
+ if (!existsSync6(join6(appDir, "wrangler.toml"))) {
751
889
  console.error("No wrangler.toml found. Are you in a DeepSpace app directory?");
752
890
  process.exit(1);
753
891
  }
@@ -755,7 +893,7 @@ var test_default = defineCommand5({
755
893
  let ownerId;
756
894
  try {
757
895
  token2 = await ensureToken();
758
- const payload = JSON.parse(atob(token2.split(".")[1]));
896
+ const payload = decodeJwtPayload(token2);
759
897
  ownerId = payload.sub;
760
898
  } catch (err) {
761
899
  console.error("`deepspace test` requires you to be logged in. Run `deepspace login` first.");
@@ -763,6 +901,7 @@ var test_default = defineCommand5({
763
901
  }
764
902
  await writeDevVars(appDir, "dev", ownerId, token2);
765
903
  if (suite !== "unit") {
904
+ preflightWindowsWorkerd(appDir);
766
905
  ensurePlaywright(appDir);
767
906
  }
768
907
  let exitCode = 0;
@@ -808,7 +947,6 @@ function runPlaywright(appDir, testFiles, port) {
808
947
  ], {
809
948
  cwd: appDir,
810
949
  stdio: "inherit",
811
- shell: process.platform === "win32",
812
950
  env: { ...process.env, DEEPSPACE_PORT: String(port) }
813
951
  });
814
952
  return result.status ?? 1;
@@ -816,8 +954,7 @@ function runPlaywright(appDir, testFiles, port) {
816
954
  function runVitest(appDir) {
817
955
  const result = spawnSync3("npx", ["vitest", "run", "--passWithNoTests"], {
818
956
  cwd: appDir,
819
- stdio: "inherit",
820
- shell: process.platform === "win32"
957
+ stdio: "inherit"
821
958
  });
822
959
  return result.status ?? 1;
823
960
  }
@@ -834,8 +971,8 @@ function resolvePort3(arg) {
834
971
 
835
972
  // src/cli/commands/screenshot.ts
836
973
  import { defineCommand as defineCommand6 } from "citty";
837
- import { spawnSync as spawnSync4 } from "child_process";
838
- import { resolve as resolve3 } from "path";
974
+ import { sync as spawnSync4 } from "cross-spawn";
975
+ import { resolve as resolve4 } from "path";
839
976
  var screenshot_default = defineCommand6({
840
977
  meta: {
841
978
  name: "screenshot",
@@ -864,7 +1001,7 @@ var screenshot_default = defineCommand6({
864
1001
  }
865
1002
  },
866
1003
  run({ args }) {
867
- const appDir = resolve3(".");
1004
+ const appDir = resolve4(".");
868
1005
  ensurePlaywright(appDir);
869
1006
  const playwrightArgs = ["playwright", "screenshot", args.url, args.output];
870
1007
  if (args["full-page"]) playwrightArgs.push("--full-page");
@@ -873,8 +1010,7 @@ var screenshot_default = defineCommand6({
873
1010
  }
874
1011
  const result = spawnSync4("npx", playwrightArgs, {
875
1012
  cwd: appDir,
876
- stdio: "inherit",
877
- shell: process.platform === "win32"
1013
+ stdio: "inherit"
878
1014
  });
879
1015
  process.exit(result.status ?? 1);
880
1016
  }
@@ -883,7 +1019,7 @@ var screenshot_default = defineCommand6({
883
1019
  // src/cli/commands/test-accounts.ts
884
1020
  import { defineCommand as defineCommand7 } from "citty";
885
1021
  import * as p3 from "@clack/prompts";
886
- 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";
887
1023
  import { join as join7 } from "path";
888
1024
  import { homedir as homedir4 } from "os";
889
1025
  var SESSION_COOKIE2 = "__Secure-better-auth.session_token";
@@ -891,7 +1027,7 @@ var AUTH_URL4 = process.env.DEEPSPACE_AUTH_URL ?? ENVS.prod.auth;
891
1027
  var DIR3 = join7(homedir4(), ".deepspace");
892
1028
  var ACCOUNTS_PATH = join7(DIR3, "test-accounts.json");
893
1029
  function loadAccounts() {
894
- if (!existsSync6(ACCOUNTS_PATH)) return [];
1030
+ if (!existsSync7(ACCOUNTS_PATH)) return [];
895
1031
  try {
896
1032
  return JSON.parse(readFileSync4(ACCOUNTS_PATH, "utf-8"));
897
1033
  } catch {
@@ -1170,9 +1306,10 @@ var test_accounts_default = defineCommand7({
1170
1306
  // src/cli/commands/deploy.ts
1171
1307
  import { defineCommand as defineCommand8 } from "citty";
1172
1308
  import { execSync as execSync2 } from "child_process";
1173
- import { readFileSync as readFileSync5, readdirSync, existsSync as existsSync7 } from "fs";
1309
+ import { readFileSync as readFileSync5, readdirSync, existsSync as existsSync8 } from "fs";
1174
1310
  import { parse as parseToml2 } from "smol-toml";
1175
- import { join as join8, resolve as resolve4, dirname } from "path";
1311
+ import { join as join8, resolve as resolve5, dirname } from "path";
1312
+ import { pathToFileURL } from "url";
1176
1313
  import * as p4 from "@clack/prompts";
1177
1314
 
1178
1315
  // src/server/rooms/binding-manifest.ts
@@ -1378,61 +1515,32 @@ function asObjectArray(v) {
1378
1515
  return v.filter(isObject);
1379
1516
  }
1380
1517
 
1381
- // src/server/rooms/app-name.ts
1382
- var APP_NAME_RULES = {
1383
- /** ^[a-z0-9](-?[a-z0-9])+$ — RFC 1035 host label, no consecutive dashes. */
1384
- pattern: /^[a-z0-9](?:-?[a-z0-9])+$/,
1385
- minLength: 2,
1386
- maxLength: 63
1387
- };
1388
- function validateAppName(raw) {
1389
- if (typeof raw !== "string") {
1390
- return { valid: false, reason: "App name must be a string" };
1391
- }
1392
- const name = raw.trim();
1393
- if (!name) {
1394
- return { valid: false, reason: "App name is required" };
1395
- }
1396
- if (name.length < APP_NAME_RULES.minLength) {
1397
- return { valid: false, reason: `App name "${name}" is too short (min ${APP_NAME_RULES.minLength} chars)` };
1398
- }
1399
- if (name.length > APP_NAME_RULES.maxLength) {
1400
- return { valid: false, reason: `App name "${name}" is too long (max ${APP_NAME_RULES.maxLength} chars)` };
1401
- }
1402
- if (!APP_NAME_RULES.pattern.test(name)) {
1403
- return {
1404
- valid: false,
1405
- 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.`
1406
- };
1407
- }
1408
- return { valid: true, name };
1409
- }
1410
- function resolveAppName(raw) {
1411
- const strict = validateAppName(raw);
1412
- if (strict.valid) return { ok: true, name: strict.name };
1413
- if (typeof raw !== "string") return { ok: false, reason: strict.reason };
1414
- const trimmed = raw.trim();
1415
- if (!trimmed) return { ok: false, reason: "App name is required" };
1416
- const sanitized = sanitizeAppName(trimmed);
1417
- const sanitizedStrict = validateAppName(sanitized);
1418
- if (!sanitizedStrict.valid) {
1419
- return {
1420
- ok: false,
1421
- 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.`
1422
- };
1423
- }
1424
- return {
1425
- ok: true,
1426
- name: sanitized,
1427
- warning: `App name "${trimmed}" doesn't conform to deploy rules \u2014 using "${sanitized}" instead. Update wrangler.toml's \`name\` to silence this warning.`
1428
- };
1429
- }
1430
- function sanitizeAppName(raw) {
1431
- return raw.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "");
1432
- }
1433
-
1434
1518
  // src/cli/commands/deploy.ts
1435
1519
  var DEPLOY_URL = process.env.DEEPSPACE_DEPLOY_URL ?? ENVS.prod.deploy;
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
+ }
1436
1544
  var deploy_default = defineCommand8({
1437
1545
  meta: {
1438
1546
  name: "deploy",
@@ -1446,10 +1554,10 @@ var deploy_default = defineCommand8({
1446
1554
  }
1447
1555
  },
1448
1556
  async run({ args }) {
1449
- const appDir = resolve4(args.dir ?? ".");
1557
+ const appDir = resolve5(args.dir ?? ".");
1450
1558
  p4.intro("Deploying DeepSpace app");
1451
1559
  const wranglerPath = join8(appDir, "wrangler.toml");
1452
- if (!existsSync7(wranglerPath)) {
1560
+ if (!existsSync8(wranglerPath)) {
1453
1561
  p4.cancel("No wrangler.toml found. Are you in a DeepSpace app directory?");
1454
1562
  process.exit(1);
1455
1563
  }
@@ -1459,7 +1567,12 @@ var deploy_default = defineCommand8({
1459
1567
  p4.cancel(`wrangler.toml: ${nameRes.reason}`);
1460
1568
  process.exit(1);
1461
1569
  }
1462
- 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
+ }
1463
1576
  const appName = nameRes.name;
1464
1577
  p4.log.info(`App: ${appName}`);
1465
1578
  let token2;
@@ -1480,25 +1593,25 @@ var deploy_default = defineCommand8({
1480
1593
  }
1481
1594
  s.stop("Built");
1482
1595
  const deployConfigPath = join8(appDir, ".wrangler", "deploy", "config.json");
1483
- if (!existsSync7(deployConfigPath)) {
1596
+ if (!existsSync8(deployConfigPath)) {
1484
1597
  p4.cancel("Build output config not found at .wrangler/deploy/config.json");
1485
1598
  process.exit(1);
1486
1599
  }
1487
1600
  const deployConfig = JSON.parse(readFileSync5(deployConfigPath, "utf-8"));
1488
- const outputWranglerPath = resolve4(dirname(deployConfigPath), deployConfig.configPath);
1489
- if (!existsSync7(outputWranglerPath)) {
1601
+ const outputWranglerPath = resolve5(dirname(deployConfigPath), deployConfig.configPath);
1602
+ if (!existsSync8(outputWranglerPath)) {
1490
1603
  p4.cancel(`Output wrangler.json not found at ${outputWranglerPath}`);
1491
1604
  process.exit(1);
1492
1605
  }
1493
1606
  const outputConfig = JSON.parse(readFileSync5(outputWranglerPath, "utf-8"));
1494
1607
  const workerDir = dirname(outputWranglerPath);
1495
1608
  const workerBundlePath = join8(workerDir, outputConfig.main);
1496
- const clientDir = outputConfig.assets?.directory ? resolve4(workerDir, outputConfig.assets.directory) : null;
1497
- if (!existsSync7(workerBundlePath)) {
1609
+ const clientDir = outputConfig.assets?.directory ? resolve5(workerDir, outputConfig.assets.directory) : null;
1610
+ if (!existsSync8(workerBundlePath)) {
1498
1611
  p4.cancel(`Worker bundle not found at ${workerBundlePath}`);
1499
1612
  process.exit(1);
1500
1613
  }
1501
- if (!clientDir || !existsSync7(clientDir)) {
1614
+ if (!clientDir || !existsSync8(clientDir)) {
1502
1615
  p4.cancel(`Client assets not found at ${clientDir}`);
1503
1616
  process.exit(1);
1504
1617
  }
@@ -1534,7 +1647,7 @@ var deploy_default = defineCommand8({
1534
1647
  }
1535
1648
  const devVarsPath = join8(appDir, ".dev.vars");
1536
1649
  let userSecrets = {};
1537
- if (existsSync7(devVarsPath)) {
1650
+ if (existsSync8(devVarsPath)) {
1538
1651
  const customSection = extractCustomDevVars(readFileSync5(devVarsPath, "utf-8"));
1539
1652
  if (customSection) {
1540
1653
  try {
@@ -1580,6 +1693,11 @@ var deploy_default = defineCommand8({
1580
1693
  if (userSecretNames.length) {
1581
1694
  form.append("userSecrets", JSON.stringify(userSecrets));
1582
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
+ }
1583
1701
  const res = await fetch(`${DEPLOY_URL}/api/deploy/${appName}`, {
1584
1702
  method: "POST",
1585
1703
  headers: { Authorization: `Bearer ${token2}` },
@@ -1598,15 +1716,228 @@ var deploy_default = defineCommand8({
1598
1716
  s.stop("Deployed (edge propagation still in progress after 90s)");
1599
1717
  p4.log.warn("First requests may briefly hit the assets transitional page; retry in a minute.");
1600
1718
  p4.log.success(`Live at: ${body.url}`);
1719
+ await syncSubscriptionPlans(appDir, appName, token2);
1720
+ await syncOneTimeProducts(appDir, appName, token2);
1601
1721
  p4.outro("Done");
1602
1722
  return;
1603
1723
  }
1604
1724
  }
1605
1725
  s.stop("Deployed!");
1606
1726
  p4.log.success(`Live at: ${body.url}`);
1727
+ await syncSubscriptionPlans(appDir, appName, token2);
1728
+ await syncOneTimeProducts(appDir, appName, token2);
1607
1729
  p4.outro("Done");
1608
1730
  }
1609
1731
  });
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
+ );
1788
+ }
1789
+ const changes = (body.planChanges ?? []).filter((c) => c.affectedSubscribers > 0);
1790
+ if (changes.length > 0) {
1791
+ await promptAndCancelChangedPlans(appName, token2, changes);
1792
+ }
1793
+ return;
1794
+ }
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
+ );
1799
+ }
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
+ }
1610
1941
  async function waitForAssetsReady(url, timeoutMs) {
1611
1942
  const deadline = Date.now() + timeoutMs;
1612
1943
  let attempt = 0;
@@ -1648,7 +1979,7 @@ function collectAssets(dir) {
1648
1979
 
1649
1980
  // src/cli/commands/undeploy.ts
1650
1981
  import { defineCommand as defineCommand9 } from "citty";
1651
- import { readFileSync as readFileSync6, existsSync as existsSync8 } from "fs";
1982
+ import { readFileSync as readFileSync6, existsSync as existsSync9 } from "fs";
1652
1983
  import { join as join9 } from "path";
1653
1984
  import { parse as parseToml3 } from "smol-toml";
1654
1985
  import * as p5 from "@clack/prompts";
@@ -1669,7 +2000,7 @@ var undeploy_default = defineCommand9({
1669
2000
  let appName = args.name;
1670
2001
  if (!appName) {
1671
2002
  const wranglerPath = join9(process.cwd(), "wrangler.toml");
1672
- if (existsSync8(wranglerPath)) {
2003
+ if (existsSync9(wranglerPath)) {
1673
2004
  const config = parseToml3(readFileSync6(wranglerPath, "utf-8"));
1674
2005
  if (config.name) appName = config.name;
1675
2006
  }
@@ -1727,7 +2058,7 @@ var whoami_default = defineCommand10({
1727
2058
  }
1728
2059
  let payload;
1729
2060
  try {
1730
- payload = JSON.parse(atob(jwt.split(".")[1]));
2061
+ payload = decodeJwtPayload(jwt);
1731
2062
  } catch {
1732
2063
  console.error("Malformed session token. Run `deepspace login` again.");
1733
2064
  process.exit(1);
@@ -1750,6 +2081,7 @@ var whoami_default = defineCommand10({
1750
2081
  console.log(`UserID: ${payload.sub ?? "(none)"}`);
1751
2082
  console.log(`Type: ${accountType}`);
1752
2083
  if (payload.iss) console.log(`Issuer: ${payload.iss}`);
2084
+ console.log(`Dashboard: ${DASHBOARD_URL}`);
1753
2085
  if (isTestAccount) {
1754
2086
  console.log("");
1755
2087
  console.log("Note: this is a test account, not a real OAuth developer.");
@@ -1760,12 +2092,12 @@ var whoami_default = defineCommand10({
1760
2092
 
1761
2093
  // src/cli/commands/add.ts
1762
2094
  import { defineCommand as defineCommand11 } from "citty";
1763
- import { existsSync as existsSync9 } from "fs";
1764
- import { resolve as resolve5, join as join10 } from "path";
2095
+ import { existsSync as existsSync10 } from "fs";
2096
+ import { resolve as resolve6, join as join10 } from "path";
1765
2097
  import { execSync as execSync3 } from "child_process";
1766
2098
  function findScript(from) {
1767
2099
  const candidate = join10(from, ".deepspace", "scripts", "add-feature.cjs");
1768
- if (existsSync9(candidate)) return candidate;
2100
+ if (existsSync10(candidate)) return candidate;
1769
2101
  return null;
1770
2102
  }
1771
2103
  var add_default = defineCommand11({
@@ -1797,7 +2129,7 @@ var add_default = defineCommand11({
1797
2129
  }
1798
2130
  },
1799
2131
  run({ args }) {
1800
- const appDir = resolve5(args.dir ?? ".");
2132
+ const appDir = resolve6(args.dir ?? ".");
1801
2133
  const script = findScript(appDir);
1802
2134
  if (!script) {
1803
2135
  console.error("No .deepspace/scripts/add-feature.cjs found.");
@@ -1826,12 +2158,12 @@ import { defineCommand as defineCommand12 } from "citty";
1826
2158
  import { execSync as execSync4 } from "child_process";
1827
2159
 
1828
2160
  // src/cli/lib/app-context.ts
1829
- import { existsSync as existsSync10, readFileSync as readFileSync7 } from "fs";
1830
- import { join as join11, resolve as resolve6 } from "path";
2161
+ import { existsSync as existsSync11, readFileSync as readFileSync7 } from "fs";
2162
+ import { join as join11, resolve as resolve7 } from "path";
1831
2163
  import { parse as parseToml4 } from "smol-toml";
1832
2164
  function detectAppName(cwd = process.cwd()) {
1833
- const wranglerPath = join11(resolve6(cwd), "wrangler.toml");
1834
- if (!existsSync10(wranglerPath)) return null;
2165
+ const wranglerPath = join11(resolve7(cwd), "wrangler.toml");
2166
+ if (!existsSync11(wranglerPath)) return null;
1835
2167
  try {
1836
2168
  const cfg = parseToml4(readFileSync7(wranglerPath, "utf-8"));
1837
2169
  return typeof cfg.name === "string" && cfg.name.length > 0 ? cfg.name : null;
@@ -1849,9 +2181,9 @@ function requireAppName(explicit, cwd) {
1849
2181
  }
1850
2182
 
1851
2183
  // src/cli/commands/domain.ts
1852
- var API_URL = process.env.DEEPSPACE_API_URL ?? ENVS.prod.api;
2184
+ var API_URL2 = process.env.DEEPSPACE_API_URL ?? ENVS.prod.api;
1853
2185
  async function api(token2, path, init) {
1854
- const res = await fetch(`${API_URL}${path}`, {
2186
+ const res = await fetch(`${API_URL2}${path}`, {
1855
2187
  ...init,
1856
2188
  headers: {
1857
2189
  Authorization: `Bearer ${token2}`,
@@ -1890,16 +2222,16 @@ function fmtDate(iso) {
1890
2222
  if (!iso) return "\u2014";
1891
2223
  return iso.slice(0, 10);
1892
2224
  }
1893
- async function confirm2(message) {
2225
+ async function confirm3(message) {
1894
2226
  if (!process.stdin.isTTY) {
1895
2227
  throw new Error(`${message} \u2014 pass --yes to confirm non-interactively`);
1896
2228
  }
1897
2229
  process.stdout.write(`${message} [y/N] `);
1898
- return new Promise((resolve7) => {
2230
+ return new Promise((resolve9) => {
1899
2231
  process.stdin.setEncoding("utf-8");
1900
2232
  process.stdin.once("data", (data) => {
1901
2233
  const answer = data.toString().trim().toLowerCase();
1902
- resolve7(answer === "y" || answer === "yes");
2234
+ resolve9(answer === "y" || answer === "yes");
1903
2235
  });
1904
2236
  });
1905
2237
  }
@@ -1958,7 +2290,7 @@ var buy = defineCommand12({
1958
2290
  const price = fmtCents(result.chargedCents);
1959
2291
  const expectedTime = result.registrar === "cloudflare" ? "~60-90 seconds" : "15-60 minutes (registry NS propagation)";
1960
2292
  if (!args.yes) {
1961
- const ok = await confirm2(
2293
+ const ok = await confirm3(
1962
2294
  `Buy ${args.domain} for ${price}/yr (auto-renews; non-refundable), attach to ${appName}? Provisioning takes ${expectedTime}.`
1963
2295
  );
1964
2296
  if (!ok) {
@@ -2123,7 +2455,7 @@ var detach = defineCommand12({
2123
2455
  process.exit(1);
2124
2456
  }
2125
2457
  if (!args.yes) {
2126
- const ok = await confirm2(
2458
+ const ok = await confirm3(
2127
2459
  `Detach ${args.domain}? Routing will stop; the domain stays registered (auto-renew is unchanged).`
2128
2460
  );
2129
2461
  if (!ok) {
@@ -2179,7 +2511,7 @@ import { defineCommand as defineCommand13 } from "citty";
2179
2511
 
2180
2512
  // src/cli/commands/_invoke-impl.ts
2181
2513
  import { readFileSync as readFileSync8 } from "fs";
2182
- var API_URL2 = process.env.DEEPSPACE_API_URL ?? ENVS.prod.api;
2514
+ var API_URL3 = process.env.DEEPSPACE_API_URL ?? ENVS.prod.api;
2183
2515
  var DEFAULT_TIMEOUT_MS = 12e4;
2184
2516
  function parseTarget(target) {
2185
2517
  if (!target || typeof target !== "string") {
@@ -2217,7 +2549,7 @@ function flushAndExit(code) {
2217
2549
  });
2218
2550
  }
2219
2551
  async function fetchCatalog() {
2220
- const res = await fetch(`${API_URL2}/api/integrations`);
2552
+ const res = await fetch(`${API_URL3}/api/integrations`);
2221
2553
  if (!res.ok) {
2222
2554
  throw new Error(`Failed to fetch integration catalog (${res.status})`);
2223
2555
  }
@@ -2298,7 +2630,7 @@ async function runInvoke(args) {
2298
2630
  const startedAt = Date.now();
2299
2631
  let res;
2300
2632
  try {
2301
- res = await fetch(`${API_URL2}/api/integrations/${integration}/${endpoint}`, {
2633
+ res = await fetch(`${API_URL3}/api/integrations/${integration}/${endpoint}`, {
2302
2634
  method: "POST",
2303
2635
  headers: {
2304
2636
  "Content-Type": "application/json",
@@ -2537,11 +2869,11 @@ var invoke_default = defineCommand14({
2537
2869
 
2538
2870
  // src/cli/commands/managed-repos.ts
2539
2871
  import { defineCommand as defineCommand15 } from "citty";
2540
- var API_URL3 = process.env.DEEPSPACE_API_URL ?? ENVS.prod.api;
2872
+ var API_URL4 = process.env.DEEPSPACE_API_URL ?? ENVS.prod.api;
2541
2873
  var SIMPLE_APP_ID = /^[a-z0-9][a-z0-9_.:-]{0,119}$/i;
2542
2874
  var SIMPLE_APP_NAME = /^[a-z0-9][a-z0-9_-]{0,119}$/i;
2543
2875
  async function api2(token2, path, init) {
2544
- const res = await fetch(`${API_URL3}${path}`, {
2876
+ const res = await fetch(`${API_URL4}${path}`, {
2545
2877
  ...init,
2546
2878
  headers: {
2547
2879
  Authorization: `Bearer ${token2}`,
@@ -2582,16 +2914,16 @@ function printRepo(repo) {
2582
2914
  console.log(` URL: ${repo.htmlUrl}`);
2583
2915
  console.log(` Clone: ${repo.cloneUrl}`);
2584
2916
  }
2585
- async function confirm3(message) {
2917
+ async function confirm4(message) {
2586
2918
  if (!process.stdin.isTTY) {
2587
2919
  throw new Error(`${message} \u2014 pass --yes to confirm non-interactively`);
2588
2920
  }
2589
2921
  process.stdout.write(`${message} [y/N] `);
2590
- return new Promise((resolve7) => {
2922
+ return new Promise((resolve9) => {
2591
2923
  process.stdin.setEncoding("utf-8");
2592
2924
  process.stdin.once("data", (data) => {
2593
2925
  const answer = data.toString().trim().toLowerCase();
2594
- resolve7(answer === "y" || answer === "yes");
2926
+ resolve9(answer === "y" || answer === "yes");
2595
2927
  });
2596
2928
  });
2597
2929
  }
@@ -2737,7 +3069,7 @@ var remove = defineCommand15({
2737
3069
  async run({ args }) {
2738
3070
  if (!args.yes) {
2739
3071
  try {
2740
- const ok = await confirm3(
3072
+ const ok = await confirm4(
2741
3073
  "Delete this DeepSpace-managed GitHub repo? This removes the GitHub repository and counts against your daily delete quota."
2742
3074
  );
2743
3075
  if (!ok) {
@@ -2778,13 +3110,229 @@ var managed_repos_default = defineCommand15({
2778
3110
  }
2779
3111
  });
2780
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
+
2781
3329
  // src/cli/cli.ts
2782
3330
  var __dirname = dirname2(fileURLToPath(import.meta.url));
2783
- var pkg = JSON.parse(readFileSync9(join12(__dirname, "..", "package.json"), "utf-8"));
2784
- 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")) {
2785
3333
  process.env.SSL_CERT_FILE = "/etc/ssl/certs/ca-certificates.crt";
2786
3334
  }
2787
- var create3 = defineCommand16({
3335
+ var create3 = defineCommand17({
2788
3336
  meta: {
2789
3337
  name: "create",
2790
3338
  description: "Create a new DeepSpace app"
@@ -2803,7 +3351,7 @@ var create3 = defineCommand16({
2803
3351
  execSync5(`npm create deepspace@latest${name}`, { stdio: "inherit" });
2804
3352
  }
2805
3353
  });
2806
- var main = defineCommand16({
3354
+ var main = defineCommand17({
2807
3355
  meta: {
2808
3356
  name: "deepspace",
2809
3357
  version: pkg.version,
@@ -2825,7 +3373,8 @@ var main = defineCommand16({
2825
3373
  invoke: invoke_default,
2826
3374
  deploy: deploy_default,
2827
3375
  undeploy: undeploy_default,
2828
- domain: domain_default
3376
+ domain: domain_default,
3377
+ library: library_default
2829
3378
  }
2830
3379
  // No `run()` here — citty cascades parent run() AFTER subcommand finishes,
2831
3380
  // which corrupts agent-friendly output (`--json`) and prints noise on every