deepspace 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,9 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli/cli.ts
4
- import { defineCommand as defineCommand14, runMain, runCommand, renderUsage } from "citty";
4
+ import {
5
+ defineCommand as defineCommand14,
6
+ runMain,
7
+ runCommand,
8
+ renderUsage
9
+ } from "citty";
5
10
  import { sync as spawnSync8 } from "cross-spawn";
6
- import { readFileSync as readFileSync30, existsSync as existsSync27 } from "fs";
11
+ import { readFileSync as readFileSync28, existsSync as existsSync25 } from "fs";
7
12
  import { dirname as dirname11, join as join25 } from "path";
8
13
  import { fileURLToPath as fileURLToPath3 } from "url";
9
14
 
@@ -260,6 +265,146 @@ function wrapCommandErrors(cmd) {
260
265
  return cmd;
261
266
  }
262
267
 
268
+ // src/cli/lib/command.ts
269
+ import { defineCommand, parseArgs } from "citty";
270
+ import * as p3 from "@clack/prompts";
271
+ var Refusal = class extends Error {
272
+ code;
273
+ action;
274
+ actionRequired;
275
+ extra;
276
+ constructor(message, code, opts = {}) {
277
+ super(message);
278
+ this.name = "Refusal";
279
+ this.code = code;
280
+ this.action = opts.action ? executableAction(opts.action) : void 0;
281
+ this.actionRequired = opts.actionRequired ?? false;
282
+ this.extra = opts.extra ?? {};
283
+ }
284
+ };
285
+ function apiErrorDetails(err) {
286
+ return err instanceof ApiError ? err.details : void 0;
287
+ }
288
+ function withoutReservedKeys(payload) {
289
+ return Object.fromEntries(
290
+ Object.entries(payload).filter(
291
+ ([key]) => ![
292
+ "ok",
293
+ "code",
294
+ "error",
295
+ "action",
296
+ "actionRequired",
297
+ "next",
298
+ "nextAction",
299
+ "resume"
300
+ ].includes(key)
301
+ )
302
+ );
303
+ }
304
+ function exitWhenFlushed(code) {
305
+ if (process.stdout.writableLength === 0) process.exit(code);
306
+ process.stdout.write("", () => process.exit(code));
307
+ return void 0;
308
+ }
309
+ function nameVariants(name) {
310
+ return [
311
+ name,
312
+ name.replace(/-([a-z])/g, (_, c) => c.toUpperCase()),
313
+ name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)
314
+ ];
315
+ }
316
+ function unknownOptionNames(parsed, args) {
317
+ const known = /* @__PURE__ */ new Set(["_"]);
318
+ for (const [name, def] of Object.entries(args)) {
319
+ for (const variant of nameVariants(name)) known.add(variant);
320
+ const alias = def.alias;
321
+ for (const a of Array.isArray(alias) ? alias : alias ? [alias] : []) {
322
+ for (const variant of nameVariants(a)) known.add(variant);
323
+ }
324
+ }
325
+ return Object.keys(parsed).filter((key) => !known.has(key));
326
+ }
327
+ function unknownOptionNamesFromRaw(rawArgs2, args) {
328
+ const relaxedArgs = Object.fromEntries(
329
+ Object.entries(args).map(([name, definition]) => [
330
+ name,
331
+ definition.type === "enum" ? { ...definition, type: "string", required: false, options: void 0 } : { ...definition, required: false }
332
+ ])
333
+ );
334
+ return unknownOptionNames(parseArgs(rawArgs2, relaxedArgs), args);
335
+ }
336
+ function unknownOptionMessage(commandName, unknown, args) {
337
+ const valid = Object.entries(args).filter(([, definition]) => definition.type !== "positional").map(([name]) => name).sort().join(", ");
338
+ return `Unknown ${unknown.length === 1 ? "option" : "options"}: ${unknown.map((name) => `--${name}`).join(", ")}. \`${commandName}\` accepts: ${valid || "(none)"}.`;
339
+ }
340
+ function defineDeepspaceCommand(def) {
341
+ const declaredArgs = {
342
+ ...def.args ?? {},
343
+ json: {
344
+ type: "boolean",
345
+ description: "Emit a single-line JSON result for scripts/agents",
346
+ default: false
347
+ }
348
+ };
349
+ return defineCommand({
350
+ meta: def.meta,
351
+ args: declaredArgs,
352
+ async run({ args }) {
353
+ const json = Boolean(args.json);
354
+ let succeeded = false;
355
+ try {
356
+ const unknown = unknownOptionNames(args, declaredArgs);
357
+ if (unknown.length > 0) {
358
+ throw new Refusal(
359
+ unknownOptionMessage(def.meta.name, unknown, declaredArgs),
360
+ "unknown_option"
361
+ );
362
+ }
363
+ const out = await def.run({
364
+ args
365
+ }) ?? {};
366
+ if (out.action) out.action = executableAction(out.action);
367
+ if (json) {
368
+ console.log(
369
+ JSON.stringify({
370
+ ok: true,
371
+ ...withoutReservedKeys(out.data ?? {}),
372
+ ...out.action ? { action: out.action } : {}
373
+ })
374
+ );
375
+ } else if (out.action) {
376
+ printAction(out.action);
377
+ }
378
+ succeeded = true;
379
+ } catch (err) {
380
+ stopActiveSpinner();
381
+ const refusal = err instanceof Refusal ? err : null;
382
+ const code = refusal?.code ?? errorCode(err);
383
+ const message = err instanceof Error ? err.message : String(err);
384
+ if (json) {
385
+ console.log(
386
+ JSON.stringify({
387
+ ok: false,
388
+ ...code ? { code } : {},
389
+ ...refusal?.actionRequired ? { actionRequired: true } : {},
390
+ error: message,
391
+ ...refusal?.action ? { action: refusal.action } : {},
392
+ ...withoutReservedKeys(refusal?.extra ?? apiErrorDetails(err) ?? {})
393
+ })
394
+ );
395
+ } else {
396
+ const line = code ? withSlug(message, code) : message;
397
+ if (refusal?.actionRequired) p3.log.warn(line);
398
+ else p3.log.error(line);
399
+ if (refusal?.action) printAction(refusal.action);
400
+ }
401
+ exitWhenFlushed(refusal?.actionRequired ? 2 : 1);
402
+ }
403
+ if (succeeded) exitWhenFlushed(0);
404
+ }
405
+ });
406
+ }
407
+
263
408
  // src/cli/lib/command-suggestions.ts
264
409
  function splitTokenPath(token, table) {
265
410
  const parts = token.trim().split(/\s+/);
@@ -398,105 +543,6 @@ async function exchangeSession(authUrl, sessionToken) {
398
543
  return data.token ?? null;
399
544
  }
400
545
 
401
- // src/cli/lib/command.ts
402
- import { defineCommand } from "citty";
403
- import * as p3 from "@clack/prompts";
404
- var Refusal = class extends Error {
405
- code;
406
- action;
407
- actionRequired;
408
- extra;
409
- constructor(message, code, opts = {}) {
410
- super(message);
411
- this.name = "Refusal";
412
- this.code = code;
413
- this.action = opts.action ? executableAction(opts.action) : void 0;
414
- this.actionRequired = opts.actionRequired ?? false;
415
- this.extra = opts.extra ?? {};
416
- }
417
- };
418
- function apiErrorDetails(err) {
419
- return err instanceof ApiError ? err.details : void 0;
420
- }
421
- function withoutReservedKeys(payload) {
422
- return Object.fromEntries(
423
- Object.entries(payload).filter(
424
- ([key]) => ![
425
- "ok",
426
- "code",
427
- "error",
428
- "action",
429
- "actionRequired",
430
- "next",
431
- "nextAction",
432
- "resume"
433
- ].includes(key)
434
- )
435
- );
436
- }
437
- function exitWhenFlushed(code) {
438
- if (process.stdout.writableLength === 0) process.exit(code);
439
- process.stdout.write("", () => process.exit(code));
440
- return void 0;
441
- }
442
- function defineDeepspaceCommand(def) {
443
- return defineCommand({
444
- meta: def.meta,
445
- args: {
446
- ...def.args ?? {},
447
- json: {
448
- type: "boolean",
449
- description: "Emit a single-line JSON result for scripts/agents",
450
- default: false
451
- }
452
- },
453
- async run({ args }) {
454
- const json = Boolean(args.json);
455
- let succeeded = false;
456
- try {
457
- const out = await def.run({ args }) ?? {};
458
- if (out.action) out.action = executableAction(out.action);
459
- if (json) {
460
- console.log(
461
- JSON.stringify({
462
- ok: true,
463
- ...withoutReservedKeys(out.data ?? {}),
464
- ...out.action ? { action: out.action } : {}
465
- })
466
- );
467
- } else if (out.action) {
468
- printAction(out.action);
469
- }
470
- succeeded = true;
471
- } catch (err) {
472
- stopActiveSpinner();
473
- const refusal = err instanceof Refusal ? err : null;
474
- const code = refusal?.code ?? errorCode(err);
475
- const message = err instanceof Error ? err.message : String(err);
476
- if (json) {
477
- console.log(
478
- JSON.stringify({
479
- ok: false,
480
- ...code ? { code } : {},
481
- ...refusal?.actionRequired ? { actionRequired: true } : {},
482
- error: message,
483
- ...refusal?.action ? { action: refusal.action } : {},
484
- ...withoutReservedKeys(refusal?.extra ?? apiErrorDetails(err) ?? {})
485
- })
486
- );
487
- } else {
488
- const line = code ? withSlug(message, code) : message;
489
- if (refusal?.actionRequired) p3.log.warn(line);
490
- else p3.log.error(line);
491
- if (refusal?.action) printAction(refusal.action);
492
- }
493
- exitWhenFlushed(refusal?.actionRequired ? 2 : 1);
494
- }
495
- if (succeeded) exitWhenFlushed(0);
496
- }
497
- });
498
- }
499
-
500
546
  // src/cli/auth.ts
501
547
  var AUTH_URL = process.env.DEEPSPACE_AUTH_URL ?? PLATFORM_URLS.auth;
502
548
  var PROD_AUTH_URL = "https://auth.deep.space";
@@ -1377,9 +1423,6 @@ function resolveWorktreePort(appDir) {
1377
1423
  return deriveWorktreePort(linked.worktreeRoot);
1378
1424
  }
1379
1425
 
1380
- // src/cli/lib/dev-vars.ts
1381
- import { existsSync as existsSync7, readFileSync as readFileSync10 } from "fs";
1382
-
1383
1426
  // src/cli/lib/app-tokens.ts
1384
1427
  async function fetchPublicKey(authUrl) {
1385
1428
  const response = await fetch(`${authUrl}/api/auth/jwks`);
@@ -1429,1069 +1472,907 @@ async function fetchAppIdentityToken(deployUrl, callerJwt, appId) {
1429
1472
  return body.token;
1430
1473
  }
1431
1474
 
1432
- // src/server/rooms/binding-manifest.ts
1433
- var AUTO_PROVISION_SENTINEL = "auto";
1434
- var VECTORIZE_METRICS = /* @__PURE__ */ new Set(["cosine", "euclidean", "dot-product"]);
1435
- var ALLOWED_BINDING_TYPES = /* @__PURE__ */ new Set([
1436
- "vectorize",
1437
- "ai",
1438
- "r2_bucket",
1439
- "kv_namespace",
1440
- "d1",
1441
- "queue",
1442
- "browser_rendering",
1443
- "analytics_engine",
1444
- "hyperdrive",
1445
- "ratelimit"
1446
- ]);
1447
- var RESERVED_BINDING_NAMES = /* @__PURE__ */ new Set([
1448
- "ASSETS",
1449
- "PLATFORM_WORKER",
1450
- "API_WORKER",
1451
- "DEEPSPACE_APP_ID",
1452
- "DEEPSPACE_RESOURCE_ID",
1453
- "DEEPSPACE_RELEASE_OPERATION_ID",
1454
- "APP_NAME",
1455
- "OWNER_USER_ID",
1456
- "AUTH_JWT_PUBLIC_KEY",
1457
- "AUTH_JWT_ISSUER",
1458
- "AUTH_WORKER_URL",
1459
- "APP_IDENTITY_TOKEN",
1460
- "APP_OWNER_JWT",
1461
- "USAGE_EVENTS"
1462
- ]);
1463
- function validateBindingManifest(manifest) {
1464
- if (!Array.isArray(manifest)) {
1465
- return { valid: false, errors: [{ reason: "Manifest must be an array" }] };
1475
+ // src/cli/lib/secure-file.ts
1476
+ import { chmodSync, writeFileSync as writeFileSync5 } from "fs";
1477
+ function writeSecretFileSync(path, content) {
1478
+ writeFileSync5(path, content, { mode: 384 });
1479
+ try {
1480
+ chmodSync(path, 384);
1481
+ } catch {
1466
1482
  }
1467
- const errors = [];
1468
- const seenNames = /* @__PURE__ */ new Set();
1469
- for (const entry of manifest) {
1470
- if (!entry || typeof entry !== "object") {
1471
- errors.push({
1472
- reason: `Entry must be an object (got ${entry === null ? "null" : typeof entry})`
1473
- });
1474
- continue;
1475
- }
1476
- const e = entry;
1477
- const type = e.type;
1478
- const name = e.name;
1479
- if (typeof type !== "string" || !ALLOWED_BINDING_TYPES.has(type)) {
1480
- errors.push({
1481
- reason: `Disallowed binding type: ${typeof type === "string" ? type : `(${typeof type})`}. Allowed: ${[...ALLOWED_BINDING_TYPES].sort().join(", ")}`
1482
- });
1483
- continue;
1484
- }
1485
- if (typeof name !== "string" || !name) {
1486
- errors.push({ reason: `Binding of type "${type}" missing 'name' (string)` });
1487
- continue;
1488
- }
1489
- if (RESERVED_BINDING_NAMES.has(name)) {
1490
- errors.push({ reason: `Binding name "${name}" is reserved by the SDK` });
1491
- continue;
1492
- }
1493
- if (seenNames.has(name)) {
1494
- errors.push({ reason: `Duplicate binding name "${name}"` });
1495
- continue;
1496
- }
1497
- seenNames.add(name);
1498
- const binding = entry;
1499
- const fieldErr = requiredFieldError(binding);
1500
- if (fieldErr) errors.push({ binding, reason: fieldErr });
1501
- }
1502
- if (errors.length) return { valid: false, errors };
1503
- return { valid: true, bindings: manifest };
1504
1483
  }
1505
- function requiredFieldError(b) {
1506
- switch (b.type) {
1507
- case "vectorize": {
1508
- if (!b.index_name) return `vectorize binding "${b.name}" missing index_name (or "auto")`;
1509
- if (b.index_name === AUTO_PROVISION_SENTINEL) {
1510
- if (typeof b.dimensions !== "number" || b.dimensions <= 0) {
1511
- return `vectorize binding "${b.name}" with index_name="auto" requires "dimensions" (positive number)`;
1512
- }
1513
- if (!b.metric || !VECTORIZE_METRICS.has(b.metric)) {
1514
- return `vectorize binding "${b.name}" with index_name="auto" requires "metric" (one of: ${[...VECTORIZE_METRICS].join(", ")})`;
1515
- }
1516
- }
1517
- return null;
1518
- }
1519
- case "r2_bucket":
1520
- return b.bucket_name ? null : `r2_bucket binding "${b.name}" missing bucket_name (or "auto")`;
1521
- case "kv_namespace": {
1522
- if (!b.namespace_id)
1523
- return `kv_namespace binding "${b.name}" missing namespace_id (or "auto")`;
1524
- if (b.namespace_id === AUTO_PROVISION_SENTINEL && !b.title) {
1525
- return `kv_namespace binding "${b.name}" with namespace_id="auto" requires "title"`;
1526
- }
1527
- return null;
1528
- }
1529
- case "d1": {
1530
- if (!b.id) return `d1 binding "${b.name}" missing id (or "auto")`;
1531
- if (b.id === AUTO_PROVISION_SENTINEL && !b.database_name) {
1532
- return `d1 binding "${b.name}" with id="auto" requires "database_name"`;
1533
- }
1534
- return null;
1535
- }
1536
- case "queue":
1537
- return b.queue_name ? null : `queue binding "${b.name}" missing queue_name (or "auto")`;
1538
- case "hyperdrive":
1539
- if (!b.id) return `hyperdrive binding "${b.name}" missing id`;
1540
- if (b.id === AUTO_PROVISION_SENTINEL) {
1541
- return `hyperdrive binding "${b.name}": auto-provisioning not yet supported (provide a real id)`;
1542
- }
1543
- return null;
1544
- case "ratelimit":
1545
- if (!/^\d+$/u.test(b.namespace_id) || Number(b.namespace_id) <= 0) {
1546
- return `ratelimit binding "${b.name}" requires a positive integer namespace_id string`;
1547
- }
1548
- if (!Number.isInteger(b.simple?.limit) || b.simple.limit <= 0) {
1549
- return `ratelimit binding "${b.name}" requires simple.limit (positive integer)`;
1550
- }
1551
- if (b.simple.period !== 10 && b.simple.period !== 60) {
1552
- return `ratelimit binding "${b.name}" requires simple.period (10 or 60)`;
1553
- }
1554
- return null;
1555
- case "ai":
1556
- case "browser_rendering":
1557
- case "analytics_engine":
1558
- return null;
1559
- // no required fields beyond `name`
1560
- default: {
1561
- const _exhaustive = b;
1562
- void _exhaustive;
1563
- return null;
1564
- }
1565
- }
1566
- }
1567
- function bindingManifestFromOutputConfig(outputConfig) {
1568
- const out = [];
1569
- const aiBinding = pluckString(outputConfig.ai, "binding");
1570
- if (aiBinding) out.push({ type: "ai", name: aiBinding });
1571
- for (const v of asObjectArray(outputConfig.vectorize)) {
1572
- const name = pluckString(v, "binding");
1573
- const index_name = pluckString(v, "index_name");
1574
- if (!name || !index_name) continue;
1575
- const dimensions = pluckNumber(v, "dimensions");
1576
- const metric = pluckString(v, "metric");
1577
- out.push({
1578
- type: "vectorize",
1579
- name,
1580
- index_name,
1581
- ...dimensions != null && { dimensions },
1582
- ...metric && (metric === "cosine" || metric === "euclidean" || metric === "dot-product") && { metric }
1583
- });
1584
- }
1585
- for (const r of asObjectArray(outputConfig.r2_buckets)) {
1586
- const name = pluckString(r, "binding");
1587
- const bucket_name = pluckString(r, "bucket_name");
1588
- if (name && bucket_name) out.push({ type: "r2_bucket", name, bucket_name });
1589
- }
1590
- for (const k of asObjectArray(outputConfig.kv_namespaces)) {
1591
- const name = pluckString(k, "binding");
1592
- const namespace_id = pluckString(k, "id");
1593
- if (!name || !namespace_id) continue;
1594
- const title = pluckString(k, "title");
1595
- out.push({
1596
- type: "kv_namespace",
1597
- name,
1598
- namespace_id,
1599
- ...title && { title }
1600
- });
1601
- }
1602
- for (const d of asObjectArray(outputConfig.d1_databases)) {
1603
- const name = pluckString(d, "binding");
1604
- const id = pluckString(d, "database_id");
1605
- if (!name || !id) continue;
1606
- const database_name = pluckString(d, "database_name");
1607
- out.push({
1608
- type: "d1",
1609
- name,
1610
- id,
1611
- ...database_name && { database_name }
1612
- });
1484
+
1485
+ // src/cli/lib/wrangler-env.ts
1486
+ import { existsSync as existsSync6, readFileSync as readFileSync9, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
1487
+ import { join as join7 } from "path";
1488
+ import { parse as parseToml3, stringify as stringifyToml } from "smol-toml";
1489
+
1490
+ // src/server/rooms/app-name.ts
1491
+ var APP_NAME_RULES = {
1492
+ /** ^[a-z0-9](-?[a-z0-9])+$ — RFC 1035 host label, no consecutive dashes. */
1493
+ pattern: /^[a-z0-9](?:-?[a-z0-9])+$/,
1494
+ minLength: 2,
1495
+ maxLength: 63
1496
+ };
1497
+ function validateAppName(raw) {
1498
+ if (typeof raw !== "string") {
1499
+ return { valid: false, reason: "App name must be a string" };
1613
1500
  }
1614
- const queues = isObject(outputConfig.queues) ? outputConfig.queues : null;
1615
- for (const q of asObjectArray(queues?.producers)) {
1616
- const name = pluckString(q, "binding");
1617
- const queue_name = pluckString(q, "queue");
1618
- if (name && queue_name) out.push({ type: "queue", name, queue_name });
1501
+ const name = raw.trim();
1502
+ if (!name) {
1503
+ return { valid: false, reason: "App name is required" };
1619
1504
  }
1620
- const browserBinding = pluckString(outputConfig.browser, "binding");
1621
- if (browserBinding) out.push({ type: "browser_rendering", name: browserBinding });
1622
- for (const a of asObjectArray(outputConfig.analytics_engine_datasets)) {
1623
- const name = pluckString(a, "binding");
1624
- const dataset = pluckString(a, "dataset");
1625
- if (name) out.push({ type: "analytics_engine", name, dataset: dataset ?? void 0 });
1505
+ if (name.length < APP_NAME_RULES.minLength) {
1506
+ return { valid: false, reason: `App name "${name}" is too short (min ${APP_NAME_RULES.minLength} chars)` };
1626
1507
  }
1627
- for (const h of asObjectArray(outputConfig.hyperdrive)) {
1628
- const name = pluckString(h, "binding");
1629
- const id = pluckString(h, "id");
1630
- if (name && id) out.push({ type: "hyperdrive", name, id });
1508
+ if (name.length > APP_NAME_RULES.maxLength) {
1509
+ return { valid: false, reason: `App name "${name}" is too long (max ${APP_NAME_RULES.maxLength} chars)` };
1631
1510
  }
1632
- for (const r of asObjectArray(outputConfig.ratelimits)) {
1633
- const name = pluckString(r, "name");
1634
- const namespace_id = pluckString(r, "namespace_id");
1635
- const simple = isObject(r.simple) ? r.simple : null;
1636
- const limit = pluckNumber(simple, "limit");
1637
- const period = pluckNumber(simple, "period");
1638
- if (!name || !namespace_id || limit === null || period !== 10 && period !== 60) continue;
1639
- out.push({ type: "ratelimit", name, namespace_id, simple: { limit, period } });
1511
+ if (!APP_NAME_RULES.pattern.test(name)) {
1512
+ return {
1513
+ valid: false,
1514
+ 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.`
1515
+ };
1640
1516
  }
1641
- return out;
1642
- }
1643
- function isObject(v) {
1644
- return v !== null && typeof v === "object" && !Array.isArray(v);
1645
- }
1646
- function pluckString(v, key) {
1647
- if (!isObject(v)) return null;
1648
- const val = v[key];
1649
- return typeof val === "string" && val.length > 0 ? val : null;
1517
+ return { valid: true, name };
1650
1518
  }
1651
- function pluckNumber(v, key) {
1652
- if (!isObject(v)) return null;
1653
- const val = v[key];
1654
- return typeof val === "number" && Number.isFinite(val) ? val : null;
1519
+ function resolveAppName(raw) {
1520
+ const strict = validateAppName(raw);
1521
+ if (strict.valid) return { ok: true, name: strict.name };
1522
+ if (typeof raw !== "string") return { ok: false, reason: strict.reason };
1523
+ const trimmed = raw.trim();
1524
+ if (!trimmed) return { ok: false, reason: "App name is required" };
1525
+ const sanitized = sanitizeAppName(trimmed);
1526
+ const sanitizedStrict = validateAppName(sanitized);
1527
+ if (!sanitizedStrict.valid) {
1528
+ return {
1529
+ ok: false,
1530
+ 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.`
1531
+ };
1532
+ }
1533
+ return {
1534
+ ok: true,
1535
+ name: sanitized,
1536
+ warning: `App name "${trimmed}" doesn't conform to deploy rules \u2014 using "${sanitized}" instead. Update wrangler.toml's \`name\` to silence this warning.`
1537
+ };
1655
1538
  }
1656
- function asObjectArray(v) {
1657
- if (!Array.isArray(v)) return [];
1658
- return v.filter(isObject);
1539
+ function sanitizeAppName(raw) {
1540
+ return raw.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "");
1659
1541
  }
1660
1542
 
1661
- // src/cli/lib/secrets.ts
1662
- var GENERATED_SECRETS_DIVIDER = "# --- DeepSpace secrets cache: generated by `npx deepspace secrets`; do not edit manually ---";
1663
- var RESERVED_SECRET_NAMES = /* @__PURE__ */ new Set([
1664
- ...RESERVED_BINDING_NAMES,
1665
- "API_WORKER_URL",
1666
- "PLATFORM_WORKER_URL"
1543
+ // src/cli/lib/wrangler-env.ts
1544
+ var NON_INHERITABLE_ENV_KEYS = /* @__PURE__ */ new Set([
1545
+ "define",
1546
+ "vars",
1547
+ "durable_objects",
1548
+ "kv_namespaces",
1549
+ "r2_buckets",
1550
+ "ai_search_namespaces",
1551
+ "ai_search",
1552
+ "ratelimits",
1553
+ "vectorize",
1554
+ "services",
1555
+ "queues",
1556
+ "workflows",
1557
+ "tail_consumers",
1558
+ "secrets",
1559
+ "secrets_store_secrets"
1667
1560
  ]);
1668
- var SECRET_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
1669
- var CONFIG_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
1670
- var MAX_SECRET_VALUE_BYTES = 32 * 1024;
1671
- function validateConfigName(configName) {
1672
- if (!CONFIG_NAME_RE.test(configName)) {
1673
- throw new Error(
1674
- `Invalid config "${configName}". Use letters, numbers, underscores, or hyphens; start with a letter or number.`
1675
- );
1676
- }
1677
- return configName;
1678
- }
1679
- function validateSecretName(name) {
1680
- if (!SECRET_NAME_RE.test(name)) {
1681
- throw new Error(
1682
- `Invalid secret name "${name}". Use letters, numbers, and underscores; start with a letter or underscore.`
1683
- );
1561
+ var generatedConfigSeq = 0;
1562
+ function resolveAppNameForEnv(config, envName) {
1563
+ if (!envName) {
1564
+ return resolveAppName(config.name);
1684
1565
  }
1685
- if (RESERVED_SECRET_NAMES.has(name)) {
1686
- throw new Error(
1687
- `"${name}" is managed by the DeepSpace SDK and can't be set as an app secret \u2014 the platform injects it at deploy time.`
1688
- );
1566
+ const envBlock = config.env?.[envName];
1567
+ if (!envBlock) {
1568
+ return {
1569
+ ok: false,
1570
+ reason: `wrangler.toml: no [env.${envName}] block found. Add one with a distinct \`name\` to deploy this environment.`
1571
+ };
1689
1572
  }
1690
- return name;
1691
- }
1692
- function validateSecretValue(key, value) {
1693
- if (new TextEncoder().encode(value).byteLength > MAX_SECRET_VALUE_BYTES) {
1694
- throw new Error(`Secret "${key}" exceeds ${MAX_SECRET_VALUE_BYTES} bytes`);
1573
+ if (typeof envBlock.name !== "string" || envBlock.name.trim() === "") {
1574
+ return {
1575
+ ok: false,
1576
+ reason: `wrangler.toml: [env.${envName}].name is missing. Set a distinct app name (e.g. "${typeof config.name === "string" ? config.name : "myapp"}-${envName}") so this environment doesn't overwrite the default.`
1577
+ };
1695
1578
  }
1696
- }
1697
- function defaultConfigNameForEnv(wranglerEnv) {
1698
- return wranglerEnv ?? "prd";
1699
- }
1700
- async function secretsApi(deployUrl, token, path, init = {}) {
1701
- const res = await fetch(`${deployUrl}/api/secrets${path}`, {
1702
- ...init,
1703
- headers: {
1704
- Authorization: `Bearer ${token}`,
1705
- "Content-Type": "application/json",
1706
- ...init.headers ?? {}
1707
- }
1708
- });
1709
- const body = await res.json().catch(() => ({}));
1710
- if (!res.ok) {
1711
- const err = new Error(body.error ?? `Secrets request failed (${res.status})`);
1712
- err.status = res.status;
1713
- err.apiPath = path;
1714
- throw err;
1579
+ if (envBlock.name === config.name) {
1580
+ return {
1581
+ ok: false,
1582
+ reason: `wrangler.toml: [env.${envName}].name "${envBlock.name}" is the same as the top-level name. Pick a distinct name (e.g. "${config.name}-${envName}") so deploys don't collide.`
1583
+ };
1715
1584
  }
1716
- return body;
1717
- }
1718
- function listSecrets(deployUrl, token, appId, configName) {
1719
- return secretsApi(deployUrl, token, `/${appId}/configs/${encodeURIComponent(configName)}`);
1720
- }
1721
- function fetchSecretsValues(deployUrl, token, appId, configName) {
1722
- return secretsApi(deployUrl, token, `/${appId}/configs/${encodeURIComponent(configName)}/values`);
1585
+ return resolveAppName(envBlock.name);
1723
1586
  }
1724
- function getSecretPlain(deployUrl, token, appId, configName, key) {
1725
- return secretsApi(
1726
- deployUrl,
1727
- token,
1728
- `/${appId}/configs/${encodeURIComponent(configName)}/secrets/${encodeURIComponent(key)}?plain=true`
1729
- );
1587
+ function devVarsPathFor(appDir) {
1588
+ return join7(appDir, ".dev.vars");
1730
1589
  }
1731
- function setSecret(deployUrl, token, appId, configName, key, value) {
1732
- return secretsApi(
1733
- deployUrl,
1734
- token,
1735
- `/${appId}/configs/${encodeURIComponent(configName)}/secrets/${encodeURIComponent(key)}`,
1736
- { method: "PUT", body: JSON.stringify({ value }) }
1590
+ function prepareWranglerEnvConfig(appDir, envName) {
1591
+ if (!envName) return { cleanup() {
1592
+ } };
1593
+ const config = readWranglerConfig(appDir);
1594
+ const resolved = resolveAppNameForEnv(config, envName);
1595
+ if (!resolved.ok) throw new Error(resolved.reason);
1596
+ const flattened = flattenWranglerEnvConfig(config, envName);
1597
+ const configPath = join7(
1598
+ appDir,
1599
+ `.wrangler.deepspace.${safeGeneratedConfigSegment(envName)}.${process.pid}.${++generatedConfigSeq}.toml`
1737
1600
  );
1738
- }
1739
- function deleteSecret(deployUrl, token, appId, configName, key) {
1740
- return secretsApi(
1741
- deployUrl,
1742
- token,
1743
- `/${appId}/configs/${encodeURIComponent(configName)}/secrets/${encodeURIComponent(key)}`,
1744
- { method: "DELETE" }
1601
+ writeFileSync6(
1602
+ configPath,
1603
+ [
1604
+ "# Generated by DeepSpace for this CLI run.",
1605
+ "# Do not edit; wrangler.toml remains the source of truth.",
1606
+ stringifyToml(flattened)
1607
+ ].join("\n").replace(/\s*$/u, "\n")
1745
1608
  );
1609
+ return {
1610
+ configPath,
1611
+ cleanup() {
1612
+ rmSync2(configPath, { force: true });
1613
+ }
1614
+ };
1746
1615
  }
1747
- function uploadSecrets(deployUrl, token, appId, configName, secrets, replace) {
1748
- return secretsApi(
1749
- deployUrl,
1750
- token,
1751
- `/${appId}/configs/${encodeURIComponent(configName)}/secrets`,
1752
- { method: "PUT", body: JSON.stringify({ secrets, replace }) }
1753
- );
1616
+ function wranglerViteEnv(baseEnv, prepared, extraEnv = {}) {
1617
+ const env = { ...baseEnv, ...extraEnv };
1618
+ if (prepared.configPath) {
1619
+ delete env.CLOUDFLARE_ENV;
1620
+ env.CLOUDFLARE_VITE_WRANGLER_CONFIG_PATH = prepared.configPath;
1621
+ }
1622
+ return env;
1754
1623
  }
1755
- function listConfigs(deployUrl, token, appId) {
1756
- return secretsApi(deployUrl, token, `/${appId}/configs`);
1624
+ function flattenWranglerEnvConfig(config, envName) {
1625
+ const envRoot = asTomlRecord(config.env);
1626
+ const envBlock = asTomlRecord(envRoot?.[envName]);
1627
+ if (!envBlock) {
1628
+ throw new Error(`wrangler.toml: no [env.${envName}] block found.`);
1629
+ }
1630
+ const flattened = {};
1631
+ for (const [key, value] of Object.entries(config)) {
1632
+ if (key === "env" || NON_INHERITABLE_ENV_KEYS.has(key)) continue;
1633
+ flattened[key] = value;
1634
+ }
1635
+ for (const [key, value] of Object.entries(envBlock)) {
1636
+ flattened[key] = value;
1637
+ }
1638
+ if (!("durable_objects" in envBlock) && config.durable_objects) {
1639
+ flattened.durable_objects = config.durable_objects;
1640
+ }
1641
+ const envVars = asTomlRecord(envBlock.vars) ?? {};
1642
+ if (!("APP_NAME" in envVars) && typeof envBlock.name === "string") {
1643
+ flattened.vars = { ...envVars, APP_NAME: envBlock.name };
1644
+ }
1645
+ return flattened;
1757
1646
  }
1758
- function createConfig(deployUrl, token, appId, configName, copyFrom) {
1759
- return secretsApi(deployUrl, token, `/${appId}/configs`, {
1760
- method: "POST",
1761
- body: JSON.stringify({ name: configName, ...copyFrom ? { copyFrom } : {} })
1762
- });
1647
+ function asTomlRecord(value) {
1648
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
1763
1649
  }
1764
- function deleteConfig(deployUrl, token, appId, configName) {
1765
- return secretsApi(deployUrl, token, `/${appId}/configs/${encodeURIComponent(configName)}`, {
1766
- method: "DELETE"
1767
- });
1650
+ function safeGeneratedConfigSegment(value) {
1651
+ return value.replace(/[^A-Za-z0-9._-]/gu, "_");
1768
1652
  }
1769
- async function pullAppSecretsCache(deployUrl, token, appId, configName) {
1653
+ var WranglerConfigError = class extends Error {
1654
+ constructor(path, cause) {
1655
+ const detail = cause instanceof Error ? cause.message : String(cause);
1656
+ super(`wrangler.toml: malformed TOML at ${path}: ${detail}`);
1657
+ this.path = path;
1658
+ this.cause = cause;
1659
+ this.name = "WranglerConfigError";
1660
+ }
1661
+ };
1662
+ function readWranglerConfig(appDir) {
1663
+ const path = join7(appDir, "wrangler.toml");
1664
+ const raw = readFileSync9(path, "utf-8");
1770
1665
  try {
1771
- const { secrets } = await fetchSecretsValues(deployUrl, token, appId, configName);
1772
- if (secrets === null || typeof secrets !== "object" || Array.isArray(secrets)) {
1773
- throw new ApiError(
1774
- `The secrets service at ${deployUrl} returned an unexpected response shape \u2014 check DEEPSPACE_DEPLOY_URL (is this the service the app lives on?).`,
1775
- 200,
1776
- "invalid_response",
1777
- `/${appId}/configs/${configName}/values`
1778
- );
1779
- }
1780
- return { appId, configName, values: secrets };
1666
+ return parseToml3(raw);
1781
1667
  } catch (err) {
1782
- if (err?.status === 404) return null;
1783
- throw err;
1784
- }
1785
- }
1786
- function quoteDotenvValue(value) {
1787
- if (/^[A-Za-z0-9_./:@+-]*$/.test(value) && value !== "") return value;
1788
- const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r");
1789
- return `"${escaped}"`;
1790
- }
1791
- function renderSecretsCache(secrets, source) {
1792
- const keys = Object.keys(secrets).sort();
1793
- const lines = [
1794
- GENERATED_SECRETS_DIVIDER,
1795
- `# app ${source.appId} \xB7 config ${source.configName}`,
1796
- "# Use `npx deepspace secrets` to change these values."
1797
- ];
1798
- for (const key of keys) {
1799
- validateSecretValue(key, secrets[key]);
1800
- lines.push(`${key}=${quoteDotenvValue(secrets[key])}`);
1668
+ throw new WranglerConfigError(path, err);
1801
1669
  }
1802
- return lines.join("\n");
1803
1670
  }
1804
- function stripGeneratedSecretsCache(content) {
1805
- const idx = content.indexOf(GENERATED_SECRETS_DIVIDER);
1806
- if (idx === -1) return content;
1807
- return content.slice(0, idx).trimEnd() + "\n";
1671
+ function hasWranglerConfig(appDir) {
1672
+ return existsSync6(join7(appDir, "wrangler.toml"));
1808
1673
  }
1809
- function parseSecretsUpload(content) {
1810
- const trimmed = content.trim();
1811
- if (!trimmed) return {};
1812
- if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
1813
- const parsed = JSON.parse(trimmed);
1814
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1815
- throw new Error("JSON upload must be an object of KEY=value strings.");
1816
- }
1817
- const out2 = {};
1818
- for (const [key, value] of Object.entries(parsed)) {
1819
- validateSecretName(key);
1820
- if (typeof value !== "string") throw new Error(`Secret "${key}" must be a string`);
1821
- validateSecretValue(key, value);
1822
- out2[key] = value;
1823
- }
1824
- return out2;
1825
- }
1826
- const out = {};
1827
- const lines = trimmed.split(/\r?\n/);
1828
- for (let i = 0; i < lines.length; i++) {
1829
- const line = lines[i].trim();
1830
- if (!line || line.startsWith("#")) continue;
1831
- const eq = line.indexOf("=");
1832
- if (eq <= 0) continue;
1833
- const key = line.slice(0, eq).trim();
1834
- if (!SECRET_NAME_RE.test(key)) continue;
1835
- validateSecretName(key);
1836
- const raw = line.slice(eq + 1).trim();
1837
- const quote = raw[0] === '"' || raw[0] === "'" ? raw[0] : null;
1838
- let value;
1839
- if (!quote) {
1840
- value = raw;
1841
- } else if (endsWithUnescapedQuote(raw.slice(1), quote)) {
1842
- const inner = raw.slice(1, -1);
1843
- value = quote === '"' ? unescapeDotenv(inner) : inner;
1844
- } else {
1845
- const startLine = i + 1;
1846
- const parts = [raw.slice(1)];
1847
- let closed = false;
1848
- while (++i < lines.length) {
1849
- const cont = lines[i].trimEnd();
1850
- if (endsWithUnescapedQuote(cont, quote)) {
1851
- parts.push(cont.slice(0, -1));
1852
- closed = true;
1853
- break;
1854
- }
1855
- parts.push(lines[i]);
1856
- }
1857
- if (!closed) {
1858
- throw new Error(
1859
- `Unterminated quoted value for "${key}" (opens on line ${startLine}). Close the ${quote === '"' ? "double" : "single"} quote, or escape newlines as \\n on one line.`
1860
- );
1861
- }
1862
- value = parts.join("\n");
1863
- if (quote === '"') value = unescapeDotenv(value);
1864
- }
1865
- validateSecretValue(key, value);
1866
- out[key] = value;
1867
- }
1868
- return out;
1674
+
1675
+ // src/cli/lib/dev-vars.ts
1676
+ var HEADER = [
1677
+ "# Generated by deepspace \u2014 do not edit; this file is rewritten on every",
1678
+ "# dev, test, deploy, and secrets pull run. App values live in the secret store:",
1679
+ "# npx deepspace secrets set KEY=value"
1680
+ ].join("\n");
1681
+ function requireAppIdFor(appDir, wranglerEnv) {
1682
+ const id = readAppId(appDir, wranglerEnv);
1683
+ if (id) return id;
1684
+ throw new Error(
1685
+ "wrangler.toml has no DEEPSPACE_APP_ID" + (wranglerEnv ? ` for [env.${wranglerEnv}]` : "") + ". Run `deepspace app init` (or `deepspace app init --env <name>`) and retry."
1686
+ );
1869
1687
  }
1870
- function endsWithUnescapedQuote(s, q) {
1871
- if (!s.endsWith(q)) return false;
1872
- if (q === "'") return true;
1873
- let backslashes = 0;
1874
- for (let j = s.length - 2; j >= 0 && s[j] === "\\"; j--) backslashes++;
1875
- return backslashes % 2 === 0;
1688
+ async function writeDevVars(appDir, ownerId, callerJwt, wranglerEnv, opts = {}) {
1689
+ const urls = PLATFORM_URLS;
1690
+ const publicKey = await fetchPublicKey(urls.auth);
1691
+ const appId = opts.appId ?? requireAppIdFor(appDir, wranglerEnv);
1692
+ const appOwnerJwt = await mintAppOwnerJwt(urls.auth, callerJwt, appId);
1693
+ const appIdentityToken = await fetchAppIdentityToken(urls.deploy, callerJwt, appId);
1694
+ const sdkVars = [
1695
+ `AUTH_JWT_PUBLIC_KEY="${publicKey}"`,
1696
+ `AUTH_JWT_ISSUER=${urls.auth}/api/auth`,
1697
+ `AUTH_WORKER_URL=${urls.auth}`,
1698
+ `API_WORKER_URL=${urls.api}`,
1699
+ `PLATFORM_WORKER_URL=${urls.platform}`,
1700
+ `OWNER_USER_ID=${ownerId}`,
1701
+ `APP_OWNER_JWT=${appOwnerJwt}`,
1702
+ ...appIdentityToken ? [`APP_IDENTITY_TOKEN=${appIdentityToken}`] : [],
1703
+ `ALLOW_DEBUG_ROUTES=true`
1704
+ ].join("\n");
1705
+ const devVarsPath = devVarsPathFor(appDir);
1706
+ const cache = opts.generatedSecretsCache;
1707
+ writeSecretFileSync(
1708
+ devVarsPath,
1709
+ [HEADER, "", sdkVars, ...cache ? ["", cache] : [], ""].join("\n")
1710
+ );
1876
1711
  }
1877
- function unescapeDotenv(v) {
1878
- let out = "";
1879
- for (let i = 0; i < v.length; i++) {
1880
- if (v[i] === "\\" && i + 1 < v.length) {
1881
- const c = v[++i];
1882
- out += c === "n" ? "\n" : c === "r" ? "\r" : c;
1883
- } else {
1884
- out += v[i];
1712
+
1713
+ // src/cli/lib/install-status.ts
1714
+ import { existsSync as existsSync8, readFileSync as readFileSync11, realpathSync as realpathSync3 } from "fs";
1715
+ import { dirname as dirname5, join as join9, resolve as resolve6 } from "path";
1716
+
1717
+ // src/cli/lib/package-manager.ts
1718
+ import { existsSync as existsSync7, readFileSync as readFileSync10 } from "fs";
1719
+ import { join as join8 } from "path";
1720
+ function detectPackageManager(appDir) {
1721
+ try {
1722
+ const pkg2 = JSON.parse(readFileSync10(join8(appDir, "package.json"), "utf-8"));
1723
+ const name = typeof pkg2.packageManager === "string" ? pkg2.packageManager.split("@")[0] : "";
1724
+ if (name === "npm" || name === "pnpm" || name === "yarn" || name === "bun") return name;
1725
+ } catch {
1726
+ }
1727
+ if (existsSync7(join8(appDir, "pnpm-lock.yaml"))) return "pnpm";
1728
+ if (existsSync7(join8(appDir, "yarn.lock"))) return "yarn";
1729
+ if (existsSync7(join8(appDir, "bun.lock")) || existsSync7(join8(appDir, "bun.lockb"))) return "bun";
1730
+ return "npm";
1731
+ }
1732
+ function installCommand(appDir) {
1733
+ return `${detectPackageManager(appDir)} install`;
1734
+ }
1735
+
1736
+ // src/cli/lib/install-status.ts
1737
+ function installState(appDir) {
1738
+ if (resolvesDeepspace(appDir)) return "ready";
1739
+ const sentinelDir = join9(appDir, ".deepspace");
1740
+ if (existsSync8(join9(sentinelDir, "install.err"))) return "failed";
1741
+ if (existsSync8(join9(sentinelDir, "install.started"))) {
1742
+ if (!existsSync8(join9(sentinelDir, "install.done")) && installWorkerAlive(sentinelDir)) {
1743
+ return "installing";
1885
1744
  }
1745
+ return "failed";
1886
1746
  }
1887
- return out;
1747
+ return "missing";
1888
1748
  }
1889
- function shellSingleQuote(value) {
1890
- return `'${value.replace(/'/g, "'\\''")}'`;
1749
+ function tailHint(logPath, platform = process.platform) {
1750
+ return platform === "win32" ? `Get-Content -Wait ${logPath}` : `tail -f ${logPath}`;
1891
1751
  }
1892
- function formatSecretsDownload(secrets, format) {
1893
- const entries = Object.entries(secrets).sort(([a], [b]) => a.localeCompare(b));
1894
- if (format === "json") {
1895
- return JSON.stringify(Object.fromEntries(entries), null, 2) + "\n";
1752
+ function ensureInstallReady(appDir) {
1753
+ if (resolvesDeepspace(appDir)) return;
1754
+ const sentinelDir = join9(appDir, ".deepspace");
1755
+ const errPath = join9(sentinelDir, "install.err");
1756
+ const startedPath = join9(sentinelDir, "install.started");
1757
+ const donePath = join9(sentinelDir, "install.done");
1758
+ const logPath = join9(sentinelDir, "install.log");
1759
+ if (existsSync8(errPath)) {
1760
+ const detail = readFileSync11(errPath, "utf-8").trimEnd();
1761
+ const log29 = existsSync8(logPath) ? ` Full log: ${logPath}.` : "";
1762
+ throw new Refusal(
1763
+ `Dependency install failed: ${detail}.${log29} Run \`npm install\` (or \`bun install\`) manually, then retry.`,
1764
+ "install_failed",
1765
+ { action: { cwd: appDir, argv: [detectPackageManager(appDir), "install"] } }
1766
+ );
1896
1767
  }
1897
- if (format === "shell") {
1898
- return entries.map(([k, v]) => `export ${k}=${shellSingleQuote(v)}`).join("\n") + "\n";
1768
+ if (existsSync8(startedPath) && !existsSync8(donePath)) {
1769
+ if (!installWorkerAlive(sentinelDir)) {
1770
+ const log29 = existsSync8(logPath) ? ` See what happened: ${logPath}.` : "";
1771
+ throw new Refusal(
1772
+ `The dependency install is no longer running and never finished.${log29} Run \`npm install\` (or \`bun install\`) manually, then retry.`,
1773
+ "install_failed",
1774
+ { action: { cwd: appDir, argv: [detectPackageManager(appDir), "install"] } }
1775
+ );
1776
+ }
1777
+ const tail = existsSync8(logPath) ? ` Tail progress: ${tailHint(logPath)}.` : "";
1778
+ throw new Refusal(
1779
+ `Dependencies are still installing.${tail} Retry once it finishes.`,
1780
+ "install_in_progress"
1781
+ );
1899
1782
  }
1900
- return entries.map(([k, v]) => `${k}=${quoteDotenvValue(v)}`).join("\n") + "\n";
1901
- }
1902
- async function refreshSecretsCache(deployUrl, token, appId, wranglerEnv) {
1903
- const configName = defaultConfigNameForEnv(wranglerEnv);
1904
- const pulled = await pullAppSecretsCache(deployUrl, token, appId, configName);
1905
- if (!pulled) return null;
1906
- const count = Object.keys(pulled.values).length;
1907
- return {
1908
- pulled,
1909
- rendered: renderSecretsCache(pulled.values, pulled),
1910
- // An empty store is the common case and not worth a status line.
1911
- summary: count ? `Secrets: ${pulled.configName} (${count} secret${count === 1 ? "" : "s"})` : null
1912
- };
1783
+ throw new Refusal(
1784
+ "Dependencies not installed. Run `npm install` (or `bun install`) first.",
1785
+ "deps_missing",
1786
+ {
1787
+ action: { cwd: appDir, argv: [detectPackageManager(appDir), "install"] }
1788
+ }
1789
+ );
1913
1790
  }
1914
-
1915
- // src/cli/lib/secure-file.ts
1916
- import { chmodSync, writeFileSync as writeFileSync5 } from "fs";
1917
- function writeSecretFileSync(path, content) {
1918
- writeFileSync5(path, content, { mode: 384 });
1791
+ function resolvesDeepspace(fromDir) {
1792
+ let dir = canonicalPath2(fromDir);
1793
+ let checkoutRoot = null;
1919
1794
  try {
1920
- chmodSync(path, 384);
1795
+ checkoutRoot = canonicalPath2(repoToplevel(dir));
1921
1796
  } catch {
1922
1797
  }
1923
- }
1924
-
1925
- // src/cli/lib/wrangler-env.ts
1926
- import { existsSync as existsSync6, readFileSync as readFileSync9, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
1927
- import { join as join7 } from "path";
1928
- import { parse as parseToml3, stringify as stringifyToml } from "smol-toml";
1929
-
1930
- // src/server/rooms/app-name.ts
1931
- var APP_NAME_RULES = {
1932
- /** ^[a-z0-9](-?[a-z0-9])+$ — RFC 1035 host label, no consecutive dashes. */
1933
- pattern: /^[a-z0-9](?:-?[a-z0-9])+$/,
1934
- minLength: 2,
1935
- maxLength: 63
1936
- };
1937
- function validateAppName(raw) {
1938
- if (typeof raw !== "string") {
1939
- return { valid: false, reason: "App name must be a string" };
1940
- }
1941
- const name = raw.trim();
1942
- if (!name) {
1943
- return { valid: false, reason: "App name is required" };
1944
- }
1945
- if (name.length < APP_NAME_RULES.minLength) {
1946
- return { valid: false, reason: `App name "${name}" is too short (min ${APP_NAME_RULES.minLength} chars)` };
1798
+ for (; ; ) {
1799
+ if (existsSync8(join9(dir, "node_modules", "deepspace", "package.json"))) return true;
1800
+ if (dir === checkoutRoot) return false;
1801
+ const parent = dirname5(dir);
1802
+ if (parent === dir) return false;
1803
+ dir = parent;
1947
1804
  }
1948
- if (name.length > APP_NAME_RULES.maxLength) {
1949
- return { valid: false, reason: `App name "${name}" is too long (max ${APP_NAME_RULES.maxLength} chars)` };
1805
+ }
1806
+ function canonicalPath2(path) {
1807
+ try {
1808
+ return realpathSync3(path);
1809
+ } catch {
1810
+ return resolve6(path);
1950
1811
  }
1951
- if (!APP_NAME_RULES.pattern.test(name)) {
1952
- return {
1953
- valid: false,
1954
- 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.`
1955
- };
1812
+ }
1813
+ function installWorkerAlive(sentinelDir) {
1814
+ const pidPath = join9(sentinelDir, "install.pid");
1815
+ if (!existsSync8(pidPath)) return false;
1816
+ const pid = Number(readFileSync11(pidPath, "utf-8").trim());
1817
+ if (!Number.isSafeInteger(pid) || pid <= 0) return false;
1818
+ try {
1819
+ process.kill(pid, 0);
1820
+ return true;
1821
+ } catch (err) {
1822
+ return err.code === "EPERM";
1956
1823
  }
1957
- return { valid: true, name };
1958
1824
  }
1959
- function resolveAppName(raw) {
1960
- const strict = validateAppName(raw);
1961
- if (strict.valid) return { ok: true, name: strict.name };
1962
- if (typeof raw !== "string") return { ok: false, reason: strict.reason };
1963
- const trimmed = raw.trim();
1964
- if (!trimmed) return { ok: false, reason: "App name is required" };
1965
- const sanitized = sanitizeAppName(trimmed);
1966
- const sanitizedStrict = validateAppName(sanitized);
1967
- if (!sanitizedStrict.valid) {
1968
- return {
1969
- ok: false,
1970
- 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.`
1971
- };
1825
+
1826
+ // src/cli/lib/preflight.ts
1827
+ import { execFileSync } from "child_process";
1828
+ import { existsSync as existsSync9 } from "fs";
1829
+ import * as nodeModule from "module";
1830
+ import { resolve as resolve7 } from "path";
1831
+ function preflightNodeVersion(command, hasRegisterHooks = typeof nodeModule.registerHooks === "function", version = process.versions.node) {
1832
+ const [major, minor] = version.split(".").map(Number);
1833
+ const supportedLine = major === 22 && minor >= 15 || major === 24 || major === 26;
1834
+ if (hasRegisterHooks && supportedLine) return;
1835
+ throw new Refusal(
1836
+ `deepspace ${command} requires a supported Node release: 22.15+, 24, or 26 (found v${version}). Install the current LTS from https://nodejs.org and re-run.`,
1837
+ supportedLine ? "node_capability_missing" : "node_unsupported"
1838
+ );
1839
+ }
1840
+ var DLL_NOT_FOUND = -1073741515;
1841
+ function preflightWindowsWorkerd(appDir) {
1842
+ if (process.platform !== "win32") return;
1843
+ const workerdExe = resolve7(
1844
+ appDir,
1845
+ "node_modules",
1846
+ "@cloudflare",
1847
+ "workerd-windows-64",
1848
+ "bin",
1849
+ "workerd.exe"
1850
+ );
1851
+ if (!existsSync9(workerdExe)) return;
1852
+ try {
1853
+ execFileSync(workerdExe, ["--help"], { stdio: "pipe", timeout: 1e4 });
1854
+ return;
1855
+ } catch (err) {
1856
+ const status3 = err.status;
1857
+ if (status3 !== DLL_NOT_FOUND) return;
1858
+ throw new Refusal(
1859
+ "workerd (Cloudflare Workers runtime) cannot start on this system: it needs the Microsoft Visual C++ Redistributable, which is not installed. Install it with `winget install Microsoft.VCRedist.2015+.x64` (or choco install -y vcredist140, or https://aka.ms/vs/17/release/vc_redist.x64.exe), then restart this terminal and re-run.",
1860
+ "vcredist_missing"
1861
+ );
1972
1862
  }
1973
- return {
1974
- ok: true,
1975
- name: sanitized,
1976
- warning: `App name "${trimmed}" doesn't conform to deploy rules \u2014 using "${sanitized}" instead. Update wrangler.toml's \`name\` to silence this warning.`
1977
- };
1978
1863
  }
1979
- function sanitizeAppName(raw) {
1980
- return raw.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "");
1864
+
1865
+ // src/cli/lib/macos-junk.ts
1866
+ import { readdirSync as readdirSync2, rmSync as rmSync3 } from "fs";
1867
+ import { join as join10 } from "path";
1868
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".wrangler", ".vite"]);
1869
+ function removeMacosJunk(dir) {
1870
+ let removed = 0;
1871
+ let entries;
1872
+ try {
1873
+ entries = readdirSync2(dir, { withFileTypes: true });
1874
+ } catch {
1875
+ return removed;
1876
+ }
1877
+ for (const entry of entries) {
1878
+ const path = join10(dir, entry.name);
1879
+ if (entry.isDirectory()) {
1880
+ if (!SKIP_DIRS.has(entry.name)) removed += removeMacosJunk(path);
1881
+ continue;
1882
+ }
1883
+ if (entry.name === ".DS_Store" || entry.name.startsWith("._")) {
1884
+ try {
1885
+ rmSync3(path, { force: true });
1886
+ removed++;
1887
+ } catch {
1888
+ }
1889
+ }
1890
+ }
1891
+ return removed;
1981
1892
  }
1982
1893
 
1983
- // src/cli/lib/wrangler-env.ts
1984
- var NON_INHERITABLE_ENV_KEYS = /* @__PURE__ */ new Set([
1985
- "define",
1986
- "vars",
1987
- "durable_objects",
1988
- "kv_namespaces",
1989
- "r2_buckets",
1990
- "ai_search_namespaces",
1991
- "ai_search",
1992
- "ratelimits",
1894
+ // src/server/rooms/binding-manifest.ts
1895
+ var AUTO_PROVISION_SENTINEL = "auto";
1896
+ var VECTORIZE_METRICS = /* @__PURE__ */ new Set(["cosine", "euclidean", "dot-product"]);
1897
+ var ALLOWED_BINDING_TYPES = /* @__PURE__ */ new Set([
1993
1898
  "vectorize",
1994
- "services",
1995
- "queues",
1996
- "workflows",
1997
- "tail_consumers",
1998
- "secrets",
1999
- "secrets_store_secrets"
1899
+ "ai",
1900
+ "r2_bucket",
1901
+ "kv_namespace",
1902
+ "d1",
1903
+ "queue",
1904
+ "browser_rendering",
1905
+ "analytics_engine",
1906
+ "hyperdrive",
1907
+ "ratelimit"
2000
1908
  ]);
2001
- var generatedConfigSeq = 0;
2002
- function resolveAppNameForEnv(config, envName) {
2003
- if (!envName) {
2004
- return resolveAppName(config.name);
2005
- }
2006
- const envBlock = config.env?.[envName];
2007
- if (!envBlock) {
2008
- return {
2009
- ok: false,
2010
- reason: `wrangler.toml: no [env.${envName}] block found. Add one with a distinct \`name\` to deploy this environment.`
2011
- };
2012
- }
2013
- if (typeof envBlock.name !== "string" || envBlock.name.trim() === "") {
2014
- return {
2015
- ok: false,
2016
- reason: `wrangler.toml: [env.${envName}].name is missing. Set a distinct app name (e.g. "${typeof config.name === "string" ? config.name : "myapp"}-${envName}") so this environment doesn't overwrite the default.`
2017
- };
1909
+ var RESERVED_BINDING_NAMES = /* @__PURE__ */ new Set([
1910
+ "ASSETS",
1911
+ "PLATFORM_WORKER",
1912
+ "API_WORKER",
1913
+ "DEEPSPACE_APP_ID",
1914
+ "DEEPSPACE_RESOURCE_ID",
1915
+ "DEEPSPACE_RELEASE_OPERATION_ID",
1916
+ "APP_NAME",
1917
+ "OWNER_USER_ID",
1918
+ "AUTH_JWT_PUBLIC_KEY",
1919
+ "AUTH_JWT_ISSUER",
1920
+ "AUTH_WORKER_URL",
1921
+ "APP_IDENTITY_TOKEN",
1922
+ "APP_OWNER_JWT",
1923
+ "USAGE_EVENTS"
1924
+ ]);
1925
+ function validateBindingManifest(manifest) {
1926
+ if (!Array.isArray(manifest)) {
1927
+ return { valid: false, errors: [{ reason: "Manifest must be an array" }] };
2018
1928
  }
2019
- if (envBlock.name === config.name) {
2020
- return {
2021
- ok: false,
2022
- reason: `wrangler.toml: [env.${envName}].name "${envBlock.name}" is the same as the top-level name. Pick a distinct name (e.g. "${config.name}-${envName}") so deploys don't collide.`
2023
- };
1929
+ const errors = [];
1930
+ const seenNames = /* @__PURE__ */ new Set();
1931
+ for (const entry of manifest) {
1932
+ if (!entry || typeof entry !== "object") {
1933
+ errors.push({
1934
+ reason: `Entry must be an object (got ${entry === null ? "null" : typeof entry})`
1935
+ });
1936
+ continue;
1937
+ }
1938
+ const e = entry;
1939
+ const type = e.type;
1940
+ const name = e.name;
1941
+ if (typeof type !== "string" || !ALLOWED_BINDING_TYPES.has(type)) {
1942
+ errors.push({
1943
+ reason: `Disallowed binding type: ${typeof type === "string" ? type : `(${typeof type})`}. Allowed: ${[...ALLOWED_BINDING_TYPES].sort().join(", ")}`
1944
+ });
1945
+ continue;
1946
+ }
1947
+ if (typeof name !== "string" || !name) {
1948
+ errors.push({ reason: `Binding of type "${type}" missing 'name' (string)` });
1949
+ continue;
1950
+ }
1951
+ if (RESERVED_BINDING_NAMES.has(name)) {
1952
+ errors.push({ reason: `Binding name "${name}" is reserved by the SDK` });
1953
+ continue;
1954
+ }
1955
+ if (seenNames.has(name)) {
1956
+ errors.push({ reason: `Duplicate binding name "${name}"` });
1957
+ continue;
1958
+ }
1959
+ seenNames.add(name);
1960
+ const binding = entry;
1961
+ const fieldErr = requiredFieldError(binding);
1962
+ if (fieldErr) errors.push({ binding, reason: fieldErr });
2024
1963
  }
2025
- return resolveAppName(envBlock.name);
2026
- }
2027
- function devVarsPathFor(appDir, envName, opts = {}) {
2028
- if (opts.sharedDevVarsCache || !envName) return join7(appDir, ".dev.vars");
2029
- return join7(appDir, `.dev.vars.${envName}`);
2030
- }
2031
- function legacyEnvDevVarsPathFor(appDir, envName) {
2032
- return envName ? join7(appDir, `.dev.vars.${envName}`) : void 0;
2033
- }
2034
- function warnIfLegacyEnvDevVarsExists(appDir, envName, warn = console.warn) {
2035
- const legacyPath = legacyEnvDevVarsPathFor(appDir, envName);
2036
- if (!legacyPath || !existsSync6(legacyPath)) return;
2037
- warn(
2038
- `Warning: ${legacyPath} exists, but DeepSpace now uses ${devVarsPathFor(appDir, envName, { sharedDevVarsCache: true })} for every environment. This run will ignore ${legacyPath}; move any secrets you still need into .dev.vars, then remove the env-specific file when you're ready.`
2039
- );
1964
+ if (errors.length) return { valid: false, errors };
1965
+ return { valid: true, bindings: manifest };
2040
1966
  }
2041
- function prepareWranglerEnvConfig(appDir, envName, opts = {}) {
2042
- if (!envName) return { cleanup() {
2043
- } };
2044
- if (!opts.sharedDevVarsCache) return { wranglerEnv: envName, cleanup() {
2045
- } };
2046
- warnIfLegacyEnvDevVarsExists(appDir, envName, opts.warn);
2047
- const config = readWranglerConfig(appDir);
2048
- const resolved = resolveAppNameForEnv(config, envName);
2049
- if (!resolved.ok) throw new Error(resolved.reason);
2050
- const flattened = flattenWranglerEnvConfig(config, envName);
2051
- const configPath = join7(
2052
- appDir,
2053
- `.wrangler.deepspace.${safeGeneratedConfigSegment(envName)}.${process.pid}.${++generatedConfigSeq}.toml`
2054
- );
2055
- writeFileSync6(
2056
- configPath,
2057
- [
2058
- "# Generated by DeepSpace for this CLI run.",
2059
- "# Do not edit; wrangler.toml remains the source of truth.",
2060
- stringifyToml(flattened)
2061
- ].join("\n").replace(/\s*$/u, "\n")
2062
- );
2063
- return {
2064
- configPath,
2065
- cleanup() {
2066
- rmSync2(configPath, { force: true });
1967
+ function requiredFieldError(b) {
1968
+ switch (b.type) {
1969
+ case "vectorize": {
1970
+ if (!b.index_name) return `vectorize binding "${b.name}" missing index_name (or "auto")`;
1971
+ if (b.index_name === AUTO_PROVISION_SENTINEL) {
1972
+ if (typeof b.dimensions !== "number" || b.dimensions <= 0) {
1973
+ return `vectorize binding "${b.name}" with index_name="auto" requires "dimensions" (positive number)`;
1974
+ }
1975
+ if (!b.metric || !VECTORIZE_METRICS.has(b.metric)) {
1976
+ return `vectorize binding "${b.name}" with index_name="auto" requires "metric" (one of: ${[...VECTORIZE_METRICS].join(", ")})`;
1977
+ }
1978
+ }
1979
+ return null;
1980
+ }
1981
+ case "r2_bucket":
1982
+ return b.bucket_name ? null : `r2_bucket binding "${b.name}" missing bucket_name (or "auto")`;
1983
+ case "kv_namespace": {
1984
+ if (!b.namespace_id)
1985
+ return `kv_namespace binding "${b.name}" missing namespace_id (or "auto")`;
1986
+ if (b.namespace_id === AUTO_PROVISION_SENTINEL && !b.title) {
1987
+ return `kv_namespace binding "${b.name}" with namespace_id="auto" requires "title"`;
1988
+ }
1989
+ return null;
1990
+ }
1991
+ case "d1": {
1992
+ if (!b.id) return `d1 binding "${b.name}" missing id (or "auto")`;
1993
+ if (b.id === AUTO_PROVISION_SENTINEL && !b.database_name) {
1994
+ return `d1 binding "${b.name}" with id="auto" requires "database_name"`;
1995
+ }
1996
+ return null;
1997
+ }
1998
+ case "queue":
1999
+ return b.queue_name ? null : `queue binding "${b.name}" missing queue_name (or "auto")`;
2000
+ case "hyperdrive":
2001
+ if (!b.id) return `hyperdrive binding "${b.name}" missing id`;
2002
+ if (b.id === AUTO_PROVISION_SENTINEL) {
2003
+ return `hyperdrive binding "${b.name}": auto-provisioning not yet supported (provide a real id)`;
2004
+ }
2005
+ return null;
2006
+ case "ratelimit":
2007
+ if (!/^\d+$/u.test(b.namespace_id) || Number(b.namespace_id) <= 0) {
2008
+ return `ratelimit binding "${b.name}" requires a positive integer namespace_id string`;
2009
+ }
2010
+ if (!Number.isInteger(b.simple?.limit) || b.simple.limit <= 0) {
2011
+ return `ratelimit binding "${b.name}" requires simple.limit (positive integer)`;
2012
+ }
2013
+ if (b.simple.period !== 10 && b.simple.period !== 60) {
2014
+ return `ratelimit binding "${b.name}" requires simple.period (10 or 60)`;
2015
+ }
2016
+ return null;
2017
+ case "ai":
2018
+ case "browser_rendering":
2019
+ case "analytics_engine":
2020
+ return null;
2021
+ // no required fields beyond `name`
2022
+ default: {
2023
+ const _exhaustive = b;
2024
+ void _exhaustive;
2025
+ return null;
2067
2026
  }
2068
- };
2069
- }
2070
- function wranglerViteEnv(baseEnv, prepared, extraEnv = {}) {
2071
- const env = { ...baseEnv, ...extraEnv };
2072
- if (prepared.configPath) {
2073
- delete env.CLOUDFLARE_ENV;
2074
- env.CLOUDFLARE_VITE_WRANGLER_CONFIG_PATH = prepared.configPath;
2075
- } else if (prepared.wranglerEnv) {
2076
- env.CLOUDFLARE_ENV = prepared.wranglerEnv;
2077
2027
  }
2078
- return env;
2079
2028
  }
2080
- function flattenWranglerEnvConfig(config, envName) {
2081
- const envRoot = asTomlRecord(config.env);
2082
- const envBlock = asTomlRecord(envRoot?.[envName]);
2083
- if (!envBlock) {
2084
- throw new Error(`wrangler.toml: no [env.${envName}] block found.`);
2029
+ function bindingManifestFromOutputConfig(outputConfig) {
2030
+ const out = [];
2031
+ const aiBinding = pluckString(outputConfig.ai, "binding");
2032
+ if (aiBinding) out.push({ type: "ai", name: aiBinding });
2033
+ for (const v of asObjectArray(outputConfig.vectorize)) {
2034
+ const name = pluckString(v, "binding");
2035
+ const index_name = pluckString(v, "index_name");
2036
+ if (!name || !index_name) continue;
2037
+ const dimensions = pluckNumber(v, "dimensions");
2038
+ const metric = pluckString(v, "metric");
2039
+ out.push({
2040
+ type: "vectorize",
2041
+ name,
2042
+ index_name,
2043
+ ...dimensions != null && { dimensions },
2044
+ ...metric && (metric === "cosine" || metric === "euclidean" || metric === "dot-product") && { metric }
2045
+ });
2085
2046
  }
2086
- const flattened = {};
2087
- for (const [key, value] of Object.entries(config)) {
2088
- if (key === "env" || NON_INHERITABLE_ENV_KEYS.has(key)) continue;
2089
- flattened[key] = value;
2047
+ for (const r of asObjectArray(outputConfig.r2_buckets)) {
2048
+ const name = pluckString(r, "binding");
2049
+ const bucket_name = pluckString(r, "bucket_name");
2050
+ if (name && bucket_name) out.push({ type: "r2_bucket", name, bucket_name });
2090
2051
  }
2091
- for (const [key, value] of Object.entries(envBlock)) {
2092
- flattened[key] = value;
2052
+ for (const k of asObjectArray(outputConfig.kv_namespaces)) {
2053
+ const name = pluckString(k, "binding");
2054
+ const namespace_id = pluckString(k, "id");
2055
+ if (!name || !namespace_id) continue;
2056
+ const title = pluckString(k, "title");
2057
+ out.push({
2058
+ type: "kv_namespace",
2059
+ name,
2060
+ namespace_id,
2061
+ ...title && { title }
2062
+ });
2093
2063
  }
2094
- if (!("durable_objects" in envBlock) && config.durable_objects) {
2095
- flattened.durable_objects = config.durable_objects;
2064
+ for (const d of asObjectArray(outputConfig.d1_databases)) {
2065
+ const name = pluckString(d, "binding");
2066
+ const id = pluckString(d, "database_id");
2067
+ if (!name || !id) continue;
2068
+ const database_name = pluckString(d, "database_name");
2069
+ out.push({
2070
+ type: "d1",
2071
+ name,
2072
+ id,
2073
+ ...database_name && { database_name }
2074
+ });
2096
2075
  }
2097
- const envVars = asTomlRecord(envBlock.vars) ?? {};
2098
- if (!("APP_NAME" in envVars) && typeof envBlock.name === "string") {
2099
- flattened.vars = { ...envVars, APP_NAME: envBlock.name };
2076
+ const queues = isObject(outputConfig.queues) ? outputConfig.queues : null;
2077
+ for (const q of asObjectArray(queues?.producers)) {
2078
+ const name = pluckString(q, "binding");
2079
+ const queue_name = pluckString(q, "queue");
2080
+ if (name && queue_name) out.push({ type: "queue", name, queue_name });
2100
2081
  }
2101
- return flattened;
2102
- }
2103
- function asTomlRecord(value) {
2104
- return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
2105
- }
2106
- function safeGeneratedConfigSegment(value) {
2107
- return value.replace(/[^A-Za-z0-9._-]/gu, "_");
2108
- }
2109
- var WranglerConfigError = class extends Error {
2110
- constructor(path, cause) {
2111
- const detail = cause instanceof Error ? cause.message : String(cause);
2112
- super(`wrangler.toml: malformed TOML at ${path}: ${detail}`);
2113
- this.path = path;
2114
- this.cause = cause;
2115
- this.name = "WranglerConfigError";
2082
+ const browserBinding = pluckString(outputConfig.browser, "binding");
2083
+ if (browserBinding) out.push({ type: "browser_rendering", name: browserBinding });
2084
+ for (const a of asObjectArray(outputConfig.analytics_engine_datasets)) {
2085
+ const name = pluckString(a, "binding");
2086
+ const dataset = pluckString(a, "dataset");
2087
+ if (name) out.push({ type: "analytics_engine", name, dataset: dataset ?? void 0 });
2116
2088
  }
2117
- };
2118
- function readWranglerConfig(appDir) {
2119
- const path = join7(appDir, "wrangler.toml");
2120
- const raw = readFileSync9(path, "utf-8");
2121
- try {
2122
- return parseToml3(raw);
2123
- } catch (err) {
2124
- throw new WranglerConfigError(path, err);
2089
+ for (const h of asObjectArray(outputConfig.hyperdrive)) {
2090
+ const name = pluckString(h, "binding");
2091
+ const id = pluckString(h, "id");
2092
+ if (name && id) out.push({ type: "hyperdrive", name, id });
2093
+ }
2094
+ for (const r of asObjectArray(outputConfig.ratelimits)) {
2095
+ const name = pluckString(r, "name");
2096
+ const namespace_id = pluckString(r, "namespace_id");
2097
+ const simple = isObject(r.simple) ? r.simple : null;
2098
+ const limit = pluckNumber(simple, "limit");
2099
+ const period = pluckNumber(simple, "period");
2100
+ if (!name || !namespace_id || limit === null || period !== 10 && period !== 60) continue;
2101
+ out.push({ type: "ratelimit", name, namespace_id, simple: { limit, period } });
2125
2102
  }
2103
+ return out;
2104
+ }
2105
+ function isObject(v) {
2106
+ return v !== null && typeof v === "object" && !Array.isArray(v);
2126
2107
  }
2127
- function hasWranglerConfig(appDir) {
2128
- return existsSync6(join7(appDir, "wrangler.toml"));
2108
+ function pluckString(v, key) {
2109
+ if (!isObject(v)) return null;
2110
+ const val = v[key];
2111
+ return typeof val === "string" && val.length > 0 ? val : null;
2112
+ }
2113
+ function pluckNumber(v, key) {
2114
+ if (!isObject(v)) return null;
2115
+ const val = v[key];
2116
+ return typeof val === "number" && Number.isFinite(val) ? val : null;
2117
+ }
2118
+ function asObjectArray(v) {
2119
+ if (!Array.isArray(v)) return [];
2120
+ return v.filter(isObject);
2129
2121
  }
2130
2122
 
2131
- // src/cli/lib/dev-vars.ts
2132
- var SDK_MANAGED_KEYS = /* @__PURE__ */ new Set([
2133
- "AUTH_JWT_PUBLIC_KEY",
2134
- "AUTH_JWT_ISSUER",
2135
- "AUTH_WORKER_URL",
2123
+ // src/cli/lib/secrets.ts
2124
+ var RESERVED_SECRET_NAMES = /* @__PURE__ */ new Set([
2125
+ ...RESERVED_BINDING_NAMES,
2136
2126
  "API_WORKER_URL",
2137
- "PLATFORM_WORKER_URL",
2138
- "OWNER_USER_ID",
2139
- "APP_OWNER_JWT",
2140
- "APP_IDENTITY_TOKEN",
2141
- "ALLOW_DEBUG_ROUTES"
2127
+ "PLATFORM_WORKER_URL"
2142
2128
  ]);
2143
- var DEV_VARS_DIVIDER = "# --- not managed by the SDK; preserved across dev/test runs ---";
2144
- function extractCustomDevVars(content) {
2145
- const lines = content.split("\n");
2146
- const out = [];
2147
- let belowDivider = false;
2148
- let i = 0;
2149
- while (i < lines.length) {
2150
- const line = lines[i];
2151
- if (line.trim() === DEV_VARS_DIVIDER.trim()) {
2152
- belowDivider = true;
2153
- i++;
2154
- continue;
2155
- }
2156
- const eq = line.indexOf("=");
2157
- const keyMatch = eq >= 0 ? line.slice(0, eq).match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*$/) : null;
2158
- if (!keyMatch) {
2159
- out.push(line);
2160
- i++;
2161
- continue;
2162
- }
2163
- const key = keyMatch[1];
2164
- const valueStart = line.slice(eq + 1);
2165
- const block = [line];
2166
- if (valueStart.startsWith('"') && !hasUnescapedQuote(valueStart.slice(1))) {
2167
- i++;
2168
- while (i < lines.length) {
2169
- block.push(lines[i]);
2170
- const closed = hasUnescapedQuote(lines[i]);
2171
- i++;
2172
- if (closed) break;
2173
- }
2174
- } else {
2175
- i++;
2176
- }
2177
- if (belowDivider || !SDK_MANAGED_KEYS.has(key)) out.push(...block);
2129
+ var SECRET_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
2130
+ var CONFIG_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
2131
+ var MAX_SECRET_VALUE_BYTES = 32 * 1024;
2132
+ function validateConfigName(configName) {
2133
+ if (!CONFIG_NAME_RE.test(configName)) {
2134
+ throw new Error(
2135
+ `Invalid config "${configName}". Use letters, numbers, underscores, or hyphens; start with a letter or number.`
2136
+ );
2178
2137
  }
2179
- while (out.length && out[out.length - 1].trim() === "") out.pop();
2180
- return out.join("\n");
2138
+ return configName;
2181
2139
  }
2182
- function hasUnescapedQuote(value) {
2183
- let escaped = false;
2184
- for (const char of value) {
2185
- if (escaped) {
2186
- escaped = false;
2187
- continue;
2188
- }
2189
- if (char === "\\") {
2190
- escaped = true;
2191
- continue;
2192
- }
2193
- if (char === '"') return true;
2140
+ function validateSecretName(name) {
2141
+ if (!SECRET_NAME_RE.test(name)) {
2142
+ throw new Error(
2143
+ `Invalid secret name "${name}". Use letters, numbers, and underscores; start with a letter or underscore.`
2144
+ );
2194
2145
  }
2195
- return false;
2196
- }
2197
- function parseDevVars(content) {
2198
- const result = {};
2199
- const lines = content.split("\n");
2200
- let i = 0;
2201
- while (i < lines.length) {
2202
- const line = lines[i];
2203
- const trimmed = line.trim();
2204
- if (!trimmed || trimmed.startsWith("#")) {
2205
- i++;
2206
- continue;
2207
- }
2208
- const eq = line.indexOf("=");
2209
- if (eq < 0) {
2210
- i++;
2211
- continue;
2212
- }
2213
- const keyMatch = line.slice(0, eq).match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*$/);
2214
- if (!keyMatch) {
2215
- i++;
2216
- continue;
2217
- }
2218
- const key = keyMatch[1];
2219
- let value = line.slice(eq + 1);
2220
- let closed = !value.startsWith('"') || hasUnescapedQuote(value.slice(1));
2221
- if (!closed) {
2222
- i++;
2223
- while (i < lines.length) {
2224
- value += "\n" + lines[i];
2225
- if (hasUnescapedQuote(lines[i])) {
2226
- closed = true;
2227
- i++;
2228
- break;
2229
- }
2230
- i++;
2231
- }
2232
- } else {
2233
- i++;
2234
- }
2235
- if (value.startsWith('"') && !closed) {
2236
- throw new Error(
2237
- `parseDevVars: unterminated quoted value for key "${key}" \u2014 check your .dev.vars file for a missing closing quote.`
2238
- );
2239
- }
2240
- if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
2241
- value = unescapeQuotedDevVar(value.slice(1, -1));
2242
- }
2243
- result[key] = value;
2146
+ if (RESERVED_SECRET_NAMES.has(name)) {
2147
+ throw new Error(
2148
+ `"${name}" is managed by the DeepSpace SDK and can't be set as an app secret \u2014 the platform injects it at deploy time.`
2149
+ );
2244
2150
  }
2245
- return result;
2151
+ return name;
2246
2152
  }
2247
- function unescapeQuotedDevVar(value) {
2248
- let out = "";
2249
- for (let i = 0; i < value.length; i++) {
2250
- const char = value[i];
2251
- if (char !== "\\" || i === value.length - 1) {
2252
- out += char;
2253
- continue;
2254
- }
2255
- const next = value[i + 1];
2256
- if (next === "\\" || next === '"') {
2257
- out += next;
2258
- i++;
2259
- continue;
2260
- }
2261
- out += char;
2153
+ function validateSecretValue(key, value) {
2154
+ if (new TextEncoder().encode(value).byteLength > MAX_SECRET_VALUE_BYTES) {
2155
+ throw new Error(`Secret "${key}" exceeds ${MAX_SECRET_VALUE_BYTES} bytes`);
2262
2156
  }
2263
- return out;
2264
2157
  }
2265
- function requireAppIdFor(appDir, wranglerEnv) {
2266
- const id = readAppId(appDir, wranglerEnv);
2267
- if (id) return id;
2268
- throw new Error(
2269
- "wrangler.toml has no DEEPSPACE_APP_ID" + (wranglerEnv ? ` for [env.${wranglerEnv}]` : "") + ". Run `deepspace app init` (or `deepspace app init --env <name>`) and retry."
2270
- );
2158
+ function defaultConfigNameForEnv(wranglerEnv) {
2159
+ return wranglerEnv ?? "prd";
2271
2160
  }
2272
- async function writeDevVars(appDir, ownerId, callerJwt, wranglerEnv, opts = {}) {
2273
- const urls = PLATFORM_URLS;
2274
- const publicKey = await fetchPublicKey(urls.auth);
2275
- const appId = opts.appId ?? requireAppIdFor(appDir, wranglerEnv);
2276
- const appOwnerJwt = await mintAppOwnerJwt(urls.auth, callerJwt, appId);
2277
- const appIdentityToken = await fetchAppIdentityToken(urls.deploy, callerJwt, appId);
2278
- const sdkVars = [
2279
- `AUTH_JWT_PUBLIC_KEY="${publicKey}"`,
2280
- `AUTH_JWT_ISSUER=${urls.auth}/api/auth`,
2281
- `AUTH_WORKER_URL=${urls.auth}`,
2282
- `API_WORKER_URL=${urls.api}`,
2283
- `PLATFORM_WORKER_URL=${urls.platform}`,
2284
- `OWNER_USER_ID=${ownerId}`,
2285
- `APP_OWNER_JWT=${appOwnerJwt}`,
2286
- ...appIdentityToken ? [`APP_IDENTITY_TOKEN=${appIdentityToken}`] : [],
2287
- `ALLOW_DEBUG_ROUTES=true`
2288
- ].join("\n");
2289
- const useSharedDevVarsCache = opts.sharedDevVarsCache ?? opts.generatedSecretsCache !== void 0;
2290
- const devVarsPath = devVarsPathFor(appDir, wranglerEnv, {
2291
- sharedDevVarsCache: useSharedDevVarsCache
2161
+ async function secretsApi(deployUrl, token, path, init = {}) {
2162
+ const res = await fetch(`${deployUrl}/api/secrets${path}`, {
2163
+ ...init,
2164
+ headers: {
2165
+ Authorization: `Bearer ${token}`,
2166
+ "Content-Type": "application/json",
2167
+ ...init.headers ?? {}
2168
+ }
2292
2169
  });
2293
- const existing = existsSync7(devVarsPath) ? readFileSync10(devVarsPath, "utf-8") : "";
2294
- const custom = opts.generatedSecretsCache ?? extractCustomDevVars(stripGeneratedSecretsCache(existing).trimEnd());
2295
- let body;
2296
- if (!custom) {
2297
- body = `${sdkVars}
2298
- `;
2299
- } else if (opts.generatedSecretsCache) {
2300
- body = `${sdkVars}
2301
-
2302
- ${custom}
2303
- `;
2304
- } else {
2305
- body = `${sdkVars}
2306
-
2307
- ${DEV_VARS_DIVIDER}
2308
- ${custom}
2309
- `;
2170
+ const body = await res.json().catch(() => ({}));
2171
+ if (!res.ok) {
2172
+ const err = new Error(body.error ?? `Secrets request failed (${res.status})`);
2173
+ err.status = res.status;
2174
+ err.apiPath = path;
2175
+ throw err;
2310
2176
  }
2311
- writeSecretFileSync(devVarsPath, body);
2177
+ return body;
2312
2178
  }
2313
-
2314
- // src/cli/lib/install-status.ts
2315
- import { existsSync as existsSync9, readFileSync as readFileSync12, realpathSync as realpathSync3 } from "fs";
2316
- import { dirname as dirname5, join as join9, resolve as resolve6 } from "path";
2317
-
2318
- // src/cli/lib/package-manager.ts
2319
- import { existsSync as existsSync8, readFileSync as readFileSync11 } from "fs";
2320
- import { join as join8 } from "path";
2321
- function detectPackageManager(appDir) {
2322
- try {
2323
- const pkg2 = JSON.parse(readFileSync11(join8(appDir, "package.json"), "utf-8"));
2324
- const name = typeof pkg2.packageManager === "string" ? pkg2.packageManager.split("@")[0] : "";
2325
- if (name === "npm" || name === "pnpm" || name === "yarn" || name === "bun") return name;
2326
- } catch {
2327
- }
2328
- if (existsSync8(join8(appDir, "pnpm-lock.yaml"))) return "pnpm";
2329
- if (existsSync8(join8(appDir, "yarn.lock"))) return "yarn";
2330
- if (existsSync8(join8(appDir, "bun.lock")) || existsSync8(join8(appDir, "bun.lockb"))) return "bun";
2331
- return "npm";
2179
+ function listSecrets(deployUrl, token, appId, configName) {
2180
+ return secretsApi(deployUrl, token, `/${appId}/configs/${encodeURIComponent(configName)}`);
2332
2181
  }
2333
- function installCommand(appDir) {
2334
- return `${detectPackageManager(appDir)} install`;
2182
+ function fetchSecretsValues(deployUrl, token, appId, configName) {
2183
+ return secretsApi(deployUrl, token, `/${appId}/configs/${encodeURIComponent(configName)}/values`);
2335
2184
  }
2336
-
2337
- // src/cli/lib/install-status.ts
2338
- function installState(appDir) {
2339
- if (resolvesDeepspace(appDir)) return "ready";
2340
- const sentinelDir = join9(appDir, ".deepspace");
2341
- if (existsSync9(join9(sentinelDir, "install.err"))) return "failed";
2342
- if (existsSync9(join9(sentinelDir, "install.started"))) {
2343
- if (!existsSync9(join9(sentinelDir, "install.done")) && installWorkerAlive(sentinelDir)) {
2344
- return "installing";
2345
- }
2346
- return "failed";
2347
- }
2348
- return "missing";
2185
+ function getSecretPlain(deployUrl, token, appId, configName, key) {
2186
+ return secretsApi(
2187
+ deployUrl,
2188
+ token,
2189
+ `/${appId}/configs/${encodeURIComponent(configName)}/secrets/${encodeURIComponent(key)}?plain=true`
2190
+ );
2349
2191
  }
2350
- function tailHint(logPath, platform = process.platform) {
2351
- return platform === "win32" ? `Get-Content -Wait ${logPath}` : `tail -f ${logPath}`;
2192
+ function setSecret(deployUrl, token, appId, configName, key, value) {
2193
+ return secretsApi(
2194
+ deployUrl,
2195
+ token,
2196
+ `/${appId}/configs/${encodeURIComponent(configName)}/secrets/${encodeURIComponent(key)}`,
2197
+ { method: "PUT", body: JSON.stringify({ value }) }
2198
+ );
2352
2199
  }
2353
- function ensureInstallReady(appDir) {
2354
- if (resolvesDeepspace(appDir)) return;
2355
- const sentinelDir = join9(appDir, ".deepspace");
2356
- const errPath = join9(sentinelDir, "install.err");
2357
- const startedPath = join9(sentinelDir, "install.started");
2358
- const donePath = join9(sentinelDir, "install.done");
2359
- const logPath = join9(sentinelDir, "install.log");
2360
- if (existsSync9(errPath)) {
2361
- const detail = readFileSync12(errPath, "utf-8").trimEnd();
2362
- const log29 = existsSync9(logPath) ? ` Full log: ${logPath}.` : "";
2363
- throw new Refusal(
2364
- `Dependency install failed: ${detail}.${log29} Run \`npm install\` (or \`bun install\`) manually, then retry.`,
2365
- "install_failed",
2366
- { action: { cwd: appDir, argv: [detectPackageManager(appDir), "install"] } }
2367
- );
2368
- }
2369
- if (existsSync9(startedPath) && !existsSync9(donePath)) {
2370
- if (!installWorkerAlive(sentinelDir)) {
2371
- const log29 = existsSync9(logPath) ? ` See what happened: ${logPath}.` : "";
2372
- throw new Refusal(
2373
- `The dependency install is no longer running and never finished.${log29} Run \`npm install\` (or \`bun install\`) manually, then retry.`,
2374
- "install_failed",
2375
- { action: { cwd: appDir, argv: [detectPackageManager(appDir), "install"] } }
2376
- );
2377
- }
2378
- const tail = existsSync9(logPath) ? ` Tail progress: ${tailHint(logPath)}.` : "";
2379
- throw new Refusal(
2380
- `Dependencies are still installing.${tail} Retry once it finishes.`,
2381
- "install_in_progress"
2382
- );
2383
- }
2384
- throw new Refusal(
2385
- "Dependencies not installed. Run `npm install` (or `bun install`) first.",
2386
- "deps_missing",
2387
- {
2388
- action: { cwd: appDir, argv: [detectPackageManager(appDir), "install"] }
2389
- }
2200
+ function deleteSecret(deployUrl, token, appId, configName, key) {
2201
+ return secretsApi(
2202
+ deployUrl,
2203
+ token,
2204
+ `/${appId}/configs/${encodeURIComponent(configName)}/secrets/${encodeURIComponent(key)}`,
2205
+ { method: "DELETE" }
2390
2206
  );
2391
2207
  }
2392
- function resolvesDeepspace(fromDir) {
2393
- let dir = canonicalPath2(fromDir);
2394
- let checkoutRoot = null;
2395
- try {
2396
- checkoutRoot = canonicalPath2(repoToplevel(dir));
2397
- } catch {
2398
- }
2399
- for (; ; ) {
2400
- if (existsSync9(join9(dir, "node_modules", "deepspace", "package.json"))) return true;
2401
- if (dir === checkoutRoot) return false;
2402
- const parent = dirname5(dir);
2403
- if (parent === dir) return false;
2404
- dir = parent;
2405
- }
2208
+ function uploadSecrets(deployUrl, token, appId, configName, secrets, replace) {
2209
+ return secretsApi(
2210
+ deployUrl,
2211
+ token,
2212
+ `/${appId}/configs/${encodeURIComponent(configName)}/secrets`,
2213
+ { method: "PUT", body: JSON.stringify({ secrets, replace }) }
2214
+ );
2406
2215
  }
2407
- function canonicalPath2(path) {
2408
- try {
2409
- return realpathSync3(path);
2410
- } catch {
2411
- return resolve6(path);
2412
- }
2216
+ function listConfigs(deployUrl, token, appId) {
2217
+ return secretsApi(deployUrl, token, `/${appId}/configs`);
2413
2218
  }
2414
- function installWorkerAlive(sentinelDir) {
2415
- const pidPath = join9(sentinelDir, "install.pid");
2416
- if (!existsSync9(pidPath)) return false;
2417
- const pid = Number(readFileSync12(pidPath, "utf-8").trim());
2418
- if (!Number.isSafeInteger(pid) || pid <= 0) return false;
2219
+ function createConfig(deployUrl, token, appId, configName, copyFrom) {
2220
+ return secretsApi(deployUrl, token, `/${appId}/configs`, {
2221
+ method: "POST",
2222
+ body: JSON.stringify({ name: configName, ...copyFrom ? { copyFrom } : {} })
2223
+ });
2224
+ }
2225
+ function deleteConfig(deployUrl, token, appId, configName) {
2226
+ return secretsApi(deployUrl, token, `/${appId}/configs/${encodeURIComponent(configName)}`, {
2227
+ method: "DELETE"
2228
+ });
2229
+ }
2230
+ async function pullAppSecretsCache(deployUrl, token, appId, configName) {
2419
2231
  try {
2420
- process.kill(pid, 0);
2421
- return true;
2232
+ const { secrets } = await fetchSecretsValues(deployUrl, token, appId, configName);
2233
+ if (secrets === null || typeof secrets !== "object" || Array.isArray(secrets)) {
2234
+ throw new ApiError(
2235
+ `The secrets service at ${deployUrl} returned an unexpected response shape \u2014 check DEEPSPACE_DEPLOY_URL (is this the service the app lives on?).`,
2236
+ 200,
2237
+ "invalid_response",
2238
+ `/${appId}/configs/${configName}/values`
2239
+ );
2240
+ }
2241
+ return { appId, configName, values: secrets };
2422
2242
  } catch (err) {
2423
- return err.code === "EPERM";
2243
+ if (err?.status === 404) return null;
2244
+ throw err;
2424
2245
  }
2425
2246
  }
2426
-
2427
- // src/cli/lib/preflight.ts
2428
- import { execFileSync } from "child_process";
2429
- import { existsSync as existsSync10 } from "fs";
2430
- import * as nodeModule from "module";
2431
- import { resolve as resolve7 } from "path";
2432
- function preflightNodeVersion(command, hasRegisterHooks = typeof nodeModule.registerHooks === "function") {
2433
- if (hasRegisterHooks) return;
2434
- throw new Refusal(
2435
- `deepspace ${command} requires Node 22.15 or newer (found v${process.versions.node}). Install the current LTS from https://nodejs.org and re-run.`,
2436
- "node_too_old"
2437
- );
2247
+ function quoteDotenvValue(value) {
2248
+ if (/^[A-Za-z0-9_./:@+-]*$/.test(value) && value !== "") return value;
2249
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r");
2250
+ return `"${escaped}"`;
2438
2251
  }
2439
- var DLL_NOT_FOUND = -1073741515;
2440
- function preflightWindowsWorkerd(appDir) {
2441
- if (process.platform !== "win32") return;
2442
- const workerdExe = resolve7(
2443
- appDir,
2444
- "node_modules",
2445
- "@cloudflare",
2446
- "workerd-windows-64",
2447
- "bin",
2448
- "workerd.exe"
2449
- );
2450
- if (!existsSync10(workerdExe)) return;
2451
- try {
2452
- execFileSync(workerdExe, ["--help"], { stdio: "pipe", timeout: 1e4 });
2453
- return;
2454
- } catch (err) {
2455
- const status3 = err.status;
2456
- if (status3 !== DLL_NOT_FOUND) return;
2457
- throw new Refusal(
2458
- "workerd (Cloudflare Workers runtime) cannot start on this system: it needs the Microsoft Visual C++ Redistributable, which is not installed. Install it with `winget install Microsoft.VCRedist.2015+.x64` (or choco install -y vcredist140, or https://aka.ms/vs/17/release/vc_redist.x64.exe), then restart this terminal and re-run.",
2459
- "vcredist_missing"
2460
- );
2252
+ function renderSecretsCache(secrets, source) {
2253
+ const keys = Object.keys(secrets).sort();
2254
+ const lines = [`# App secrets \xB7 config ${source.configName} \xB7 app ${source.appId}`];
2255
+ for (const key of keys) {
2256
+ validateSecretValue(key, secrets[key]);
2257
+ lines.push(`${key}=${quoteDotenvValue(secrets[key])}`);
2461
2258
  }
2259
+ return lines.join("\n");
2462
2260
  }
2463
-
2464
- // src/cli/lib/macos-junk.ts
2465
- import { readdirSync as readdirSync2, rmSync as rmSync3 } from "fs";
2466
- import { join as join10 } from "path";
2467
- var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".wrangler", ".vite"]);
2468
- function removeMacosJunk(dir) {
2469
- let removed = 0;
2470
- let entries;
2471
- try {
2472
- entries = readdirSync2(dir, { withFileTypes: true });
2473
- } catch {
2474
- return removed;
2261
+ function parseSecretsUpload(content) {
2262
+ const trimmed = content.trim();
2263
+ if (!trimmed) return {};
2264
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
2265
+ const parsed = JSON.parse(trimmed);
2266
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
2267
+ throw new Error("JSON upload must be an object of KEY=value strings.");
2268
+ }
2269
+ const out2 = {};
2270
+ for (const [key, value] of Object.entries(parsed)) {
2271
+ validateSecretName(key);
2272
+ if (typeof value !== "string") throw new Error(`Secret "${key}" must be a string`);
2273
+ validateSecretValue(key, value);
2274
+ out2[key] = value;
2275
+ }
2276
+ return out2;
2475
2277
  }
2476
- for (const entry of entries) {
2477
- const path = join10(dir, entry.name);
2478
- if (entry.isDirectory()) {
2479
- if (!SKIP_DIRS.has(entry.name)) removed += removeMacosJunk(path);
2480
- continue;
2278
+ const out = {};
2279
+ const lines = trimmed.split(/\r?\n/);
2280
+ for (let i = 0; i < lines.length; i++) {
2281
+ const line = lines[i].trim();
2282
+ if (!line || line.startsWith("#")) continue;
2283
+ const lineNumber = i + 1;
2284
+ const eq = line.indexOf("=");
2285
+ if (eq <= 0) {
2286
+ throw new Error(`Invalid dotenv assignment on line ${lineNumber}: expected KEY=value.`);
2481
2287
  }
2482
- if (entry.name === ".DS_Store" || entry.name.startsWith("._")) {
2483
- try {
2484
- rmSync3(path, { force: true });
2485
- removed++;
2486
- } catch {
2288
+ const key = line.slice(0, eq).trim();
2289
+ if (!SECRET_NAME_RE.test(key)) {
2290
+ throw new Error(
2291
+ `Invalid secret name on line ${lineNumber}: use letters, numbers, and underscores; start with a letter or underscore.`
2292
+ );
2293
+ }
2294
+ validateSecretName(key);
2295
+ const raw = line.slice(eq + 1).trim();
2296
+ const quote = raw[0] === '"' || raw[0] === "'" ? raw[0] : null;
2297
+ let value;
2298
+ if (!quote) {
2299
+ value = raw;
2300
+ } else if (endsWithUnescapedQuote(raw.slice(1), quote)) {
2301
+ const inner = raw.slice(1, -1);
2302
+ value = quote === '"' ? unescapeDotenv(inner) : inner;
2303
+ } else {
2304
+ const startLine = i + 1;
2305
+ const parts = [raw.slice(1)];
2306
+ let closed = false;
2307
+ while (++i < lines.length) {
2308
+ const cont = lines[i].trimEnd();
2309
+ if (endsWithUnescapedQuote(cont, quote)) {
2310
+ parts.push(cont.slice(0, -1));
2311
+ closed = true;
2312
+ break;
2313
+ }
2314
+ parts.push(lines[i]);
2315
+ }
2316
+ if (!closed) {
2317
+ throw new Error(
2318
+ `Unterminated quoted value for "${key}" (opens on line ${startLine}). Close the ${quote === '"' ? "double" : "single"} quote, or escape newlines as \\n on one line.`
2319
+ );
2487
2320
  }
2321
+ value = parts.join("\n");
2322
+ if (quote === '"') value = unescapeDotenv(value);
2488
2323
  }
2324
+ validateSecretValue(key, value);
2325
+ out[key] = value;
2489
2326
  }
2490
- return removed;
2327
+ return out;
2328
+ }
2329
+ function endsWithUnescapedQuote(s, q) {
2330
+ if (!s.endsWith(q)) return false;
2331
+ if (q === "'") return true;
2332
+ let backslashes = 0;
2333
+ for (let j = s.length - 2; j >= 0 && s[j] === "\\"; j--) backslashes++;
2334
+ return backslashes % 2 === 0;
2335
+ }
2336
+ function unescapeDotenv(v) {
2337
+ let out = "";
2338
+ for (let i = 0; i < v.length; i++) {
2339
+ if (v[i] === "\\" && i + 1 < v.length) {
2340
+ const c = v[++i];
2341
+ out += c === "n" ? "\n" : c === "r" ? "\r" : c;
2342
+ } else {
2343
+ out += v[i];
2344
+ }
2345
+ }
2346
+ return out;
2347
+ }
2348
+ function shellSingleQuote(value) {
2349
+ return `'${value.replace(/'/g, "'\\''")}'`;
2350
+ }
2351
+ function formatSecretsDownload(secrets, format) {
2352
+ const entries = Object.entries(secrets).sort(([a], [b]) => a.localeCompare(b));
2353
+ if (format === "json") {
2354
+ return JSON.stringify(Object.fromEntries(entries), null, 2) + "\n";
2355
+ }
2356
+ if (format === "shell") {
2357
+ return entries.map(([k, v]) => `export ${k}=${shellSingleQuote(v)}`).join("\n") + "\n";
2358
+ }
2359
+ return entries.map(([k, v]) => `${k}=${quoteDotenvValue(v)}`).join("\n") + "\n";
2360
+ }
2361
+ async function refreshSecretsCache(deployUrl, token, appId, wranglerEnv, selectedConfigName = defaultConfigNameForEnv(wranglerEnv)) {
2362
+ const configName = selectedConfigName;
2363
+ const pulled = await pullAppSecretsCache(deployUrl, token, appId, configName);
2364
+ const values = pulled?.values ?? {};
2365
+ const count = Object.keys(values).length;
2366
+ return {
2367
+ pulled,
2368
+ rendered: renderSecretsCache(values, { appId, configName }),
2369
+ // An empty store is the common case and not worth a status line.
2370
+ summary: count ? `Secrets: ${configName} (${count} secret${count === 1 ? "" : "s"})` : null
2371
+ };
2491
2372
  }
2492
2373
 
2493
2374
  // src/cli/lib/schema-lint.ts
2494
- import { existsSync as existsSync11 } from "fs";
2375
+ import { existsSync as existsSync10 } from "fs";
2495
2376
  import { join as join11 } from "path";
2496
2377
  import { pathToFileURL } from "url";
2497
2378
 
@@ -2552,7 +2433,7 @@ function lintSchema(schema) {
2552
2433
  // src/cli/lib/schema-lint.ts
2553
2434
  async function lintProjectSchemas(appDir) {
2554
2435
  const schemasPath = join11(appDir, "src", "schemas.ts");
2555
- if (!existsSync11(schemasPath)) return null;
2436
+ if (!existsSync10(schemasPath)) return null;
2556
2437
  let schemas;
2557
2438
  try {
2558
2439
  const esbuild = await import("esbuild");
@@ -2756,10 +2637,8 @@ var dev_default = defineDeepspaceCommand({
2756
2637
  if (appIdForSecrets) {
2757
2638
  try {
2758
2639
  const refreshed = await refreshSecretsCache(DEPLOY_URL, token, appIdForSecrets, wranglerEnv);
2759
- if (refreshed) {
2760
- generatedSecretsCache = refreshed.rendered;
2761
- if (refreshed.summary) say(refreshed.summary);
2762
- }
2640
+ generatedSecretsCache = refreshed.rendered;
2641
+ if (refreshed.summary) say(refreshed.summary);
2763
2642
  } catch (err) {
2764
2643
  throw new Refusal(
2765
2644
  `Failed to refresh app secrets: ${err instanceof Error ? err.message : String(err)}`,
@@ -2767,16 +2646,10 @@ var dev_default = defineDeepspaceCommand({
2767
2646
  );
2768
2647
  }
2769
2648
  }
2770
- const sharedDevVarsCache = generatedSecretsCache !== void 0;
2771
2649
  await writeDevVars(appDir, payload.sub, token, wranglerEnv, {
2772
- generatedSecretsCache,
2773
- sharedDevVarsCache
2650
+ generatedSecretsCache
2774
2651
  });
2775
- const wranglerConfig = prepareWranglerEnvConfig(
2776
- appDir,
2777
- wranglerEnv,
2778
- { sharedDevVarsCache }
2779
- );
2652
+ const wranglerConfig = prepareWranglerEnvConfig(appDir, wranglerEnv);
2780
2653
  preflightWindowsWorkerd(appDir);
2781
2654
  if (await isPortListening(port, host)) {
2782
2655
  const killArgv = port === DEFAULT_PORT ? ["deepspace", "dev", "kill"] : ["deepspace", "dev", "kill", "--port", String(port)];
@@ -2791,15 +2664,11 @@ Free it with \`${killCmd}\`, or start on another port: \`deepspace dev start --p
2791
2664
  }
2792
2665
  const url = `http://${host === DEFAULT_DEV_HOST ? "localhost" : host}:${port}`;
2793
2666
  say("Starting dev server...\n");
2794
- const vite = spawn(
2795
- "npx",
2796
- ["vite", "--port", String(port), "--strictPort", "--host", host],
2797
- {
2798
- cwd: appDir,
2799
- stdio: "inherit",
2800
- env: wranglerViteEnv(process.env, wranglerConfig, { DEEPSPACE_PORT: String(port) })
2801
- }
2802
- );
2667
+ const vite = spawn("npx", ["vite", "--port", String(port), "--strictPort", "--host", host], {
2668
+ cwd: appDir,
2669
+ stdio: "inherit",
2670
+ env: wranglerViteEnv(process.env, wranglerConfig, { DEEPSPACE_PORT: String(port) })
2671
+ });
2803
2672
  if (args.json) {
2804
2673
  void waitForPortListening(port, host, READINESS_TIMEOUT_MS).then((ready) => {
2805
2674
  console.log(
@@ -2844,7 +2713,7 @@ Free it with \`${killCmd}\`, or start on another port: \`deepspace dev start --p
2844
2713
 
2845
2714
  // src/cli/commands/kill.ts
2846
2715
  import { spawnSync as spawnSync2 } from "child_process";
2847
- import { readFileSync as readFileSync13, readdirSync as readdirSync3, readlinkSync } from "fs";
2716
+ import { readFileSync as readFileSync12, readdirSync as readdirSync3, readlinkSync } from "fs";
2848
2717
  import { setTimeout as wait } from "timers/promises";
2849
2718
  var SIGTERM_GRACE_MS = 1500;
2850
2719
  var IS_WIN = process.platform === "win32";
@@ -2995,7 +2864,7 @@ function listenerPidsViaProc(port) {
2995
2864
  for (const f of ["/proc/net/tcp", "/proc/net/tcp6"]) {
2996
2865
  let data;
2997
2866
  try {
2998
- data = readFileSync13(f, "utf-8");
2867
+ data = readFileSync12(f, "utf-8");
2999
2868
  } catch {
3000
2869
  continue;
3001
2870
  }
@@ -3154,10 +3023,10 @@ function ensurePlaywright(appDir) {
3154
3023
  }
3155
3024
 
3156
3025
  // src/cli/lib/test-account-service.ts
3157
- import { readFileSync as readFileSync15 } from "fs";
3026
+ import { readFileSync as readFileSync14 } from "fs";
3158
3027
 
3159
3028
  // src/testing/accounts.ts
3160
- import { existsSync as existsSync12, mkdirSync as mkdirSync5, readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "fs";
3029
+ import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync13, writeFileSync as writeFileSync7 } from "fs";
3161
3030
  import { homedir as homedir3 } from "os";
3162
3031
  import { join as join13 } from "path";
3163
3032
  var TEST_ACCOUNTS_PATH = join13(homedir3(), ".deepspace", "test-accounts.json");
@@ -3231,9 +3100,9 @@ function removeTestAccounts(ids, emails = [], scope = currentTestAccountScope())
3231
3100
  });
3232
3101
  }
3233
3102
  function readTestAccountStore() {
3234
- if (!existsSync12(TEST_ACCOUNTS_PATH)) return { version: 2, scopes: {}, unscoped: [] };
3103
+ if (!existsSync11(TEST_ACCOUNTS_PATH)) return { version: 2, scopes: {}, unscoped: [] };
3235
3104
  try {
3236
- const raw = JSON.parse(readFileSync14(TEST_ACCOUNTS_PATH, "utf-8"));
3105
+ const raw = JSON.parse(readFileSync13(TEST_ACCOUNTS_PATH, "utf-8"));
3237
3106
  if (Array.isArray(raw)) return { version: 2, scopes: {}, unscoped: validTestAccounts(raw) };
3238
3107
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
3239
3108
  return { version: 2, scopes: {}, unscoped: [] };
@@ -3271,7 +3140,7 @@ function saveTestAccountStore(store) {
3271
3140
  var SESSION_COOKIE2 = "__Secure-better-auth.session_token";
3272
3141
  var AUTH_URL4 = process.env.DEEPSPACE_AUTH_URL ?? PLATFORM_URLS.auth;
3273
3142
  function sessionCookie() {
3274
- const token = readFileSync15(SESSION_PATH, "utf-8").trim();
3143
+ const token = readFileSync14(SESSION_PATH, "utf-8").trim();
3275
3144
  return `${SESSION_COOKIE2}=${encodeURIComponent(token)}`;
3276
3145
  }
3277
3146
  async function fetchRemoteTestAccounts() {
@@ -3375,10 +3244,8 @@ var test_default = defineDeepspaceCommand({
3375
3244
  if (appIdForSecrets) {
3376
3245
  try {
3377
3246
  const refreshed = await refreshSecretsCache(DEPLOY_URL2, token, appIdForSecrets, wranglerEnv);
3378
- if (refreshed) {
3379
- generatedSecretsCache = refreshed.rendered;
3380
- if (refreshed.summary) say(refreshed.summary);
3381
- }
3247
+ generatedSecretsCache = refreshed.rendered;
3248
+ if (refreshed.summary) say(refreshed.summary);
3382
3249
  } catch (err) {
3383
3250
  throw new Refusal(
3384
3251
  `Failed to refresh app secrets: ${err instanceof Error ? err.message : String(err)}`,
@@ -3386,17 +3253,17 @@ var test_default = defineDeepspaceCommand({
3386
3253
  );
3387
3254
  }
3388
3255
  }
3389
- const sharedDevVarsCache = generatedSecretsCache !== void 0;
3390
3256
  await writeDevVars(appDir, ownerId, token, wranglerEnv, {
3391
- generatedSecretsCache,
3392
- sharedDevVarsCache
3257
+ generatedSecretsCache
3393
3258
  });
3394
3259
  if (suite !== "unit") {
3395
3260
  try {
3396
3261
  const { removed } = await syncTestAccountStore();
3397
3262
  if (removed > 0) say(`Removed ${removed} stale test account credential(s).`);
3398
3263
  } catch (error) {
3399
- say(`Warning: could not reconcile test accounts: ${error instanceof Error ? error.message : String(error)}`);
3264
+ say(
3265
+ `Warning: could not reconcile test accounts: ${error instanceof Error ? error.message : String(error)}`
3266
+ );
3400
3267
  }
3401
3268
  }
3402
3269
  if (suite !== "unit") {
@@ -3406,46 +3273,32 @@ var test_default = defineDeepspaceCommand({
3406
3273
  let exitCode = 0;
3407
3274
  switch (suite) {
3408
3275
  case "smoke":
3409
- exitCode = runPlaywright(
3410
- appDir,
3411
- ["tests/smoke.spec.ts"],
3412
- port,
3413
- wranglerEnv,
3414
- sharedDevVarsCache
3415
- );
3276
+ exitCode = runPlaywright(appDir, ["tests/smoke.spec.ts"], port, wranglerEnv);
3416
3277
  break;
3417
3278
  case "api":
3418
- exitCode = runPlaywright(
3419
- appDir,
3420
- ["tests/api.spec.ts"],
3421
- port,
3422
- wranglerEnv,
3423
- sharedDevVarsCache
3424
- );
3279
+ exitCode = runPlaywright(appDir, ["tests/api.spec.ts"], port, wranglerEnv);
3425
3280
  break;
3426
3281
  case "e2e":
3427
- exitCode = runPlaywright(appDir, [], port, wranglerEnv, sharedDevVarsCache);
3282
+ exitCode = runPlaywright(appDir, [], port, wranglerEnv);
3428
3283
  break;
3429
3284
  case "unit":
3430
3285
  exitCode = runVitest(appDir);
3431
3286
  break;
3432
3287
  case "all":
3433
3288
  exitCode = runVitest(appDir);
3434
- if (exitCode === 0)
3435
- exitCode = runPlaywright(appDir, [], port, wranglerEnv, sharedDevVarsCache);
3289
+ if (exitCode === 0) exitCode = runPlaywright(appDir, [], port, wranglerEnv);
3436
3290
  break;
3437
3291
  case "default":
3438
3292
  exitCode = runPlaywright(
3439
3293
  appDir,
3440
3294
  ["tests/smoke.spec.ts", "tests/api.spec.ts"],
3441
3295
  port,
3442
- wranglerEnv,
3443
- sharedDevVarsCache
3296
+ wranglerEnv
3444
3297
  );
3445
3298
  break;
3446
3299
  default:
3447
3300
  if (suite.endsWith(".spec.ts")) {
3448
- exitCode = runPlaywright(appDir, [suite], port, wranglerEnv, sharedDevVarsCache);
3301
+ exitCode = runPlaywright(appDir, [suite], port, wranglerEnv);
3449
3302
  } else {
3450
3303
  throw new Refusal(
3451
3304
  `Unknown test suite: ${suite}
@@ -3462,10 +3315,10 @@ Available: smoke, api, e2e, unit, all`,
3462
3315
  return { data: { suite, port, appDir, wranglerEnv: wranglerEnv ?? null } };
3463
3316
  }
3464
3317
  });
3465
- function runPlaywright(appDir, testFiles, port, wranglerEnv, sharedDevVarsCache = false) {
3318
+ function runPlaywright(appDir, testFiles, port, wranglerEnv) {
3466
3319
  let wranglerConfig;
3467
3320
  try {
3468
- wranglerConfig = prepareWranglerEnvConfig(appDir, wranglerEnv, { sharedDevVarsCache });
3321
+ wranglerConfig = prepareWranglerEnvConfig(appDir, wranglerEnv);
3469
3322
  } catch (err) {
3470
3323
  console.error(err instanceof Error ? err.message : String(err));
3471
3324
  return 1;
@@ -3997,33 +3850,33 @@ function describeFilesFailure(status3, body) {
3997
3850
  import * as p6 from "@clack/prompts";
3998
3851
  import { execSync as execSync2 } from "child_process";
3999
3852
  import { createHash as createHash2 } from "crypto";
4000
- import { existsSync as existsSync15, readFileSync as readFileSync18, readdirSync as readdirSync4 } from "fs";
3853
+ import { existsSync as existsSync14, readFileSync as readFileSync17, readdirSync as readdirSync4 } from "fs";
4001
3854
  import { dirname as dirname6, join as join16, resolve as resolve11 } from "path";
4002
3855
 
4003
3856
  // src/documentation/deploy.ts
4004
- import { existsSync as existsSync13, readFileSync as readFileSync16 } from "fs";
3857
+ import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
4005
3858
  import { join as join14 } from "path";
4006
3859
  function readDocumentationDeployManifest(appDir, clientDir) {
4007
- if (!existsSync13(join14(appDir, "documentation.json"))) return void 0;
3860
+ if (!existsSync12(join14(appDir, "documentation.json"))) return void 0;
4008
3861
  const manifestPath = join14(clientDir, "_documentation", "manifest.json");
4009
- if (!existsSync13(manifestPath)) {
3862
+ if (!existsSync12(manifestPath)) {
4010
3863
  throw new Error(
4011
3864
  "documentation.json exists but the build produced no documentation manifest. Ensure vite.config.ts includes deepSpaceDocumentation() from deepspace/documentation."
4012
3865
  );
4013
3866
  }
4014
- return JSON.parse(readFileSync16(manifestPath, "utf8"));
3867
+ return JSON.parse(readFileSync15(manifestPath, "utf8"));
4015
3868
  }
4016
3869
 
4017
3870
  // src/cli/commands/update/app-migrations.ts
4018
3871
  import {
4019
3872
  closeSync,
4020
3873
  constants,
4021
- existsSync as existsSync14,
3874
+ existsSync as existsSync13,
4022
3875
  fstatSync,
4023
3876
  ftruncateSync,
4024
3877
  lstatSync,
4025
3878
  openSync,
4026
- readFileSync as readFileSync17,
3879
+ readFileSync as readFileSync16,
4027
3880
  writeSync
4028
3881
  } from "fs";
4029
3882
  import { join as join15 } from "path";
@@ -4072,7 +3925,7 @@ var APP_MIGRATION_DEFINITIONS = SOURCE_MIGRATIONS.map(
4072
3925
  );
4073
3926
  function readAppliedAppMigrations(appDir) {
4074
3927
  const path = join15(appDir, APP_MIGRATIONS_MANIFEST);
4075
- if (!existsSync14(path)) return [];
3928
+ if (!existsSync13(path)) return [];
4076
3929
  const source = readRegularFile(path);
4077
3930
  let parsed;
4078
3931
  try {
@@ -4133,7 +3986,7 @@ function planAppMigrations(appDir) {
4133
3986
  if (pending.length > 0 && blockers.length === 0) {
4134
3987
  const manifestPath = `${appPrefix}${APP_MIGRATIONS_MANIFEST}`;
4135
3988
  const absoluteManifestPath = join15(repo, manifestPath);
4136
- const before = existsSync14(absoluteManifestPath) ? readRegularFile(absoluteManifestPath) : null;
3989
+ const before = existsSync13(absoluteManifestPath) ? readRegularFile(absoluteManifestPath) : null;
4137
3990
  const after = `${JSON.stringify([...appliedIds, ...pending.map(({ id }) => id)], null, 2)}
4138
3991
  `;
4139
3992
  if (before !== after) edits.push({ path: manifestPath, before, after });
@@ -4149,7 +4002,7 @@ function applyAppMigrationPlan(appDir, plan) {
4149
4002
  const repo = repoToplevel(appDir);
4150
4003
  for (const edit of plan.edits) {
4151
4004
  const absolutePath = join15(repo, edit.path);
4152
- const current = existsSync14(absolutePath) ? readRegularFile(absolutePath) : null;
4005
+ const current = existsSync13(absolutePath) ? readRegularFile(absolutePath) : null;
4153
4006
  if (current !== edit.before) {
4154
4007
  throw new Error(`Migration input changed after planning: ${edit.path}`);
4155
4008
  }
@@ -4168,7 +4021,7 @@ function readRegularFile(path) {
4168
4021
  if (!opened.isFile() || opened.dev !== after.dev || opened.ino !== after.ino) {
4169
4022
  throw new Error(`Migration input changed while opening: ${path}`);
4170
4023
  }
4171
- return readFileSync17(fd, "utf-8");
4024
+ return readFileSync16(fd, "utf-8");
4172
4025
  } finally {
4173
4026
  closeSync(fd);
4174
4027
  }
@@ -4194,7 +4047,7 @@ function writeRegularFile(path, before, after) {
4194
4047
  if (!opened.isFile() || opened.dev !== currentPath.dev || opened.ino !== currentPath.ino) {
4195
4048
  throw new Error(`Migration input changed while opening: ${path}`);
4196
4049
  }
4197
- if (readFileSync17(fd, "utf-8") !== before) {
4050
+ if (readFileSync16(fd, "utf-8") !== before) {
4198
4051
  throw new Error(`Migration input changed after planning: ${path}`);
4199
4052
  }
4200
4053
  ftruncateSync(fd, 0);
@@ -4278,7 +4131,7 @@ function declaredCompatibilityDate(config) {
4278
4131
  return typeof declared === "string" && /^\d{4}-\d{2}-\d{2}$/.test(declared) ? declared : null;
4279
4132
  }
4280
4133
  async function buildDeployBundle(options) {
4281
- const { appDir, appName, envName, sharedDevVarsCache, output, spinner: spinner2 } = options;
4134
+ const { appDir, appName, envName, output, spinner: spinner2 } = options;
4282
4135
  const junk = removeMacosJunk(appDir);
4283
4136
  if (junk > 0) p6.log.info(`Removed ${junk} macOS metadata file(s) (._*, .DS_Store)`);
4284
4137
  const lintFindings = await lintProjectSchemas(appDir);
@@ -4288,9 +4141,7 @@ async function buildDeployBundle(options) {
4288
4141
  spinner2.start("Building...");
4289
4142
  let preparedWranglerConfig;
4290
4143
  try {
4291
- preparedWranglerConfig = prepareWranglerEnvConfig(appDir, envName, {
4292
- sharedDevVarsCache
4293
- });
4144
+ preparedWranglerConfig = prepareWranglerEnvConfig(appDir, envName);
4294
4145
  const appDomain = DEEPSPACE_ENV === "staging" ? "spacestest.com" : "app.space";
4295
4146
  execSync2("npx vite build", {
4296
4147
  cwd: appDir,
@@ -4311,18 +4162,18 @@ async function buildDeployBundle(options) {
4311
4162
  }
4312
4163
  spinner2.stop("Built");
4313
4164
  const deployConfigPath = join16(appDir, ".wrangler", "deploy", "config.json");
4314
- if (!existsSync15(deployConfigPath)) {
4165
+ if (!existsSync14(deployConfigPath)) {
4315
4166
  output.die(
4316
4167
  "Build output config not found at .wrangler/deploy/config.json",
4317
4168
  "build_output_missing"
4318
4169
  );
4319
4170
  }
4320
- const deployConfig = JSON.parse(readFileSync18(deployConfigPath, "utf-8"));
4171
+ const deployConfig = JSON.parse(readFileSync17(deployConfigPath, "utf-8"));
4321
4172
  const outputWranglerPath = resolve11(dirname6(deployConfigPath), deployConfig.configPath);
4322
- if (!existsSync15(outputWranglerPath)) {
4173
+ if (!existsSync14(outputWranglerPath)) {
4323
4174
  output.die(`Output wrangler.json not found at ${outputWranglerPath}`, "build_output_missing");
4324
4175
  }
4325
- const outputConfig = JSON.parse(readFileSync18(outputWranglerPath, "utf-8"));
4176
+ const outputConfig = JSON.parse(readFileSync17(outputWranglerPath, "utf-8"));
4326
4177
  if (outputConfig.name && outputConfig.name !== appName) {
4327
4178
  const envHint = envName ? ` (--env ${envName})` : "";
4328
4179
  output.die(
@@ -4333,10 +4184,10 @@ async function buildDeployBundle(options) {
4333
4184
  const workerDir = dirname6(outputWranglerPath);
4334
4185
  const workerBundlePath = join16(workerDir, outputConfig.main);
4335
4186
  const clientDir = outputConfig.assets?.directory ? resolve11(workerDir, outputConfig.assets.directory) : null;
4336
- if (!existsSync15(workerBundlePath)) {
4187
+ if (!existsSync14(workerBundlePath)) {
4337
4188
  output.die(`Worker bundle not found at ${workerBundlePath}`, "build_output_missing");
4338
4189
  }
4339
- if (!clientDir || !existsSync15(clientDir)) {
4190
+ if (!clientDir || !existsSync14(clientDir)) {
4340
4191
  return output.die(`Client assets not found at ${clientDir}`, "build_output_missing");
4341
4192
  }
4342
4193
  try {
@@ -4349,7 +4200,7 @@ async function buildDeployBundle(options) {
4349
4200
  const assets = collectAssets(clientDir);
4350
4201
  const assetConfig = readDeployAssetConfig(clientDir);
4351
4202
  spinner2.stop(`Collected ${assets.length} assets`);
4352
- const workerJs = readFileSync18(workerBundlePath, "utf-8");
4203
+ const workerJs = readFileSync17(workerBundlePath, "utf-8");
4353
4204
  const doBindings = outputConfig.durable_objects?.bindings;
4354
4205
  const sqliteClasses = new Set(
4355
4206
  outputConfig.migrations?.flatMap((migration) => migration.new_sqlite_classes ?? []) ?? []
@@ -4428,7 +4279,7 @@ function collectAssets(dir) {
4428
4279
  if (entry.isDirectory()) {
4429
4280
  walk(fullPath, relativePath);
4430
4281
  } else {
4431
- const bytes = readFileSync18(fullPath);
4282
+ const bytes = readFileSync17(fullPath);
4432
4283
  assets.push({
4433
4284
  path: `/${relativePath}`,
4434
4285
  hash: createHash2("sha256").update(bytes).digest("hex"),
@@ -4443,10 +4294,13 @@ function collectAssets(dir) {
4443
4294
  }
4444
4295
  function readDeployAssetConfig(clientDir) {
4445
4296
  const config = {};
4446
- for (const [file, key] of [["_headers", "_headers"], ["_redirects", "_redirects"]]) {
4297
+ for (const [file, key] of [
4298
+ ["_headers", "_headers"],
4299
+ ["_redirects", "_redirects"]
4300
+ ]) {
4447
4301
  const filePath = join16(clientDir, file);
4448
- if (!existsSync15(filePath)) continue;
4449
- const content = readFileSync18(filePath, "utf8");
4302
+ if (!existsSync14(filePath)) continue;
4303
+ const content = readFileSync17(filePath, "utf8");
4450
4304
  if (content.trim()) config[key] = content;
4451
4305
  }
4452
4306
  return config;
@@ -4460,7 +4314,7 @@ function errorMessage(error) {
4460
4314
 
4461
4315
  // src/cli/commands/deploy/commerce.ts
4462
4316
  import * as p7 from "@clack/prompts";
4463
- import { existsSync as existsSync16 } from "fs";
4317
+ import { existsSync as existsSync15 } from "fs";
4464
4318
  import { join as join17 } from "path";
4465
4319
  import { pathToFileURL as pathToFileURL2 } from "url";
4466
4320
  var API_URL2 = process.env.DEEPSPACE_API_URL ?? PLATFORM_URLS.api;
@@ -4469,7 +4323,7 @@ function errorMessage2(error) {
4469
4323
  }
4470
4324
  async function loadDeclaration(appDir, sourceFile, outputFile, exportName) {
4471
4325
  const sourcePath = join17(appDir, "src", sourceFile);
4472
- if (!existsSync16(sourcePath)) return void 0;
4326
+ if (!existsSync15(sourcePath)) return void 0;
4473
4327
  const esbuild = await import("esbuild");
4474
4328
  const outputPath = join17(appDir, ".wrangler", "deploy", outputFile);
4475
4329
  await esbuild.build({
@@ -4564,7 +4418,7 @@ Stripe will keep charging them until you cancel. Cancel at the end of their curr
4564
4418
  }
4565
4419
  }
4566
4420
  async function syncSubscriptionPlans(appDir, appName, token, nonInteractive) {
4567
- if (!existsSync16(join17(appDir, "src", "subscriptions.ts"))) return;
4421
+ if (!existsSync15(join17(appDir, "src", "subscriptions.ts"))) return;
4568
4422
  let plans;
4569
4423
  try {
4570
4424
  plans = await loadDeclaration(
@@ -4619,7 +4473,7 @@ async function syncSubscriptionPlans(appDir, appName, token, nonInteractive) {
4619
4473
  }
4620
4474
  }
4621
4475
  async function syncOneTimeProducts(appDir, appName, token) {
4622
- if (!existsSync16(join17(appDir, "src", "products.ts"))) return;
4476
+ if (!existsSync15(join17(appDir, "src", "products.ts"))) return;
4623
4477
  let products;
4624
4478
  try {
4625
4479
  products = await loadDeclaration(
@@ -4721,7 +4575,7 @@ function createDeployOutput(json) {
4721
4575
 
4722
4576
  // src/cli/commands/deploy/request.ts
4723
4577
  import * as p10 from "@clack/prompts";
4724
- import { readFileSync as readFileSync19 } from "fs";
4578
+ import { readFileSync as readFileSync18 } from "fs";
4725
4579
 
4726
4580
  // src/cli/commands/deploy/repository.ts
4727
4581
  import * as p9 from "@clack/prompts";
@@ -4912,7 +4766,7 @@ function repoApi(deployUrl, token, appId) {
4912
4766
  }
4913
4767
 
4914
4768
  // src/cli/lib/vc-remote.ts
4915
- import { existsSync as existsSync17 } from "fs";
4769
+ import { existsSync as existsSync16 } from "fs";
4916
4770
  import { tmpdir } from "os";
4917
4771
  import { isAbsolute as isAbsolute3, relative, resolve as resolve12 } from "path";
4918
4772
  function spaceRemoteName(environment = DEEPSPACE_ENV) {
@@ -4967,7 +4821,7 @@ function helperEntryUsable(command) {
4967
4821
  const match = /^!(?:DEEPSPACE_ENV=(production|staging) )?'([^']*)' '([^']*)' git-credential$/.exec(command);
4968
4822
  const expectedEnvironment = DEEPSPACE_ENV === "production" ? void 0 : DEEPSPACE_ENV;
4969
4823
  if (!match || match[1] !== expectedEnvironment) return false;
4970
- return existsSync17(match[2]) && existsSync17(match[3]);
4824
+ return existsSync16(match[2]) && existsSync16(match[3]);
4971
4825
  }
4972
4826
  function credentialHelperCommand(environment = DEEPSPACE_ENV) {
4973
4827
  const entry = process.argv[1];
@@ -5554,7 +5408,7 @@ async function deployBuiltBundle(options) {
5554
5408
  if (bundle.customBindings.length) {
5555
5409
  form.append("bindingManifest", JSON.stringify(bundle.customBindings));
5556
5410
  }
5557
- if (secrets.names.length) form.append("userSecrets", JSON.stringify(secrets.values));
5411
+ if (secrets.authoritative) form.append("userSecrets", JSON.stringify(secrets.values));
5558
5412
  if (bundle.extraRoutes === true || bundle.extraRoutes.length) {
5559
5413
  form.append("extraRunWorkerFirst", JSON.stringify(bundle.extraRoutes));
5560
5414
  }
@@ -5746,7 +5600,7 @@ async function uploadDeployAssets(options) {
5746
5600
  // against the plan before it touches storage, and a stream body
5747
5601
  // would be sent chunked with no length at all; a retry also needs a
5748
5602
  // body it can send again.
5749
- body: readFileSync19(asset.sourcePath)
5603
+ body: readFileSync18(asset.sourcePath)
5750
5604
  })
5751
5605
  );
5752
5606
  if (!response.ok) {
@@ -5762,9 +5616,7 @@ async function uploadDeployAssets(options) {
5762
5616
  }
5763
5617
  };
5764
5618
  try {
5765
- await Promise.all(
5766
- Array.from({ length: Math.min(UPLOAD_CONCURRENCY, missing.length) }, worker)
5767
- );
5619
+ await Promise.all(Array.from({ length: Math.min(UPLOAD_CONCURRENCY, missing.length) }, worker));
5768
5620
  } catch (error) {
5769
5621
  spinner2.stop("Deploy failed");
5770
5622
  if (error instanceof AssetUploadError) {
@@ -5833,35 +5685,17 @@ async function postWithRetry(url, makeInit, {
5833
5685
 
5834
5686
  // src/cli/commands/deploy/secrets.ts
5835
5687
  import * as p11 from "@clack/prompts";
5836
- import { existsSync as existsSync18, readFileSync as readFileSync20 } from "fs";
5837
- function classifyDevVarsSecrets(options) {
5838
- const store = new Set(options.storeSecretNames);
5839
- const strayKeys = options.handEditedDevVarKeys.filter((key) => !store.has(key));
5840
- if (strayKeys.length === 0) return { kind: "ok" };
5841
- if (options.storeSecretNames.length === 0 && !options.allowMissing) {
5842
- return { kind: "block", strayKeys };
5843
- }
5844
- return { kind: "warn", strayKeys };
5845
- }
5846
5688
  async function loadDeploySecrets(options) {
5847
5689
  const { deployUrl, appDir, appId, envName, ownerId, token, output } = options;
5848
5690
  let linked = null;
5849
- let localOnlyDevVars = {};
5850
5691
  try {
5851
5692
  const refreshed = await refreshSecretsCache(deployUrl, token, appId, envName);
5852
- if (refreshed) {
5853
- linked = refreshed.pulled;
5854
- localOnlyDevVars = readCustomDevVars(
5855
- devVarsPathFor(appDir, envName, { sharedDevVarsCache: true }),
5856
- output
5857
- );
5858
- await writeDevVars(appDir, ownerId, token, envName, {
5859
- appId,
5860
- generatedSecretsCache: refreshed.rendered,
5861
- sharedDevVarsCache: true
5862
- });
5863
- if (refreshed.summary) p11.log.info(refreshed.summary);
5864
- }
5693
+ linked = refreshed.pulled;
5694
+ await writeDevVars(appDir, ownerId, token, envName, {
5695
+ appId,
5696
+ generatedSecretsCache: refreshed.rendered
5697
+ });
5698
+ if (refreshed.summary) p11.log.info(refreshed.summary);
5865
5699
  } catch (error) {
5866
5700
  if (error?.status === 403) {
5867
5701
  output.die(
@@ -5874,40 +5708,31 @@ async function loadDeploySecrets(options) {
5874
5708
  "secrets_refresh_failed"
5875
5709
  );
5876
5710
  }
5877
- return { linked, localOnlyDevVars };
5878
- }
5879
- function prepareDeploySecrets(options) {
5880
- const { appDir, envName, cache, allowMissing, customBindings, doManifest, output } = options;
5881
- const values = cache.linked ? { ...cache.linked.values } : {};
5882
- const devVarsPath = devVarsPathFor(appDir, envName, {
5883
- sharedDevVarsCache: cache.linked !== null
5884
- });
5885
- const handEditedDevVars = cache.linked ? cache.localOnlyDevVars : readCustomDevVars(devVarsPath, output);
5886
- const decision = classifyDevVarsSecrets({
5887
- storeSecretNames: Object.keys(values),
5888
- handEditedDevVarKeys: Object.keys(handEditedDevVars),
5889
- allowMissing
5890
- });
5891
- if (decision.kind === "block") {
5892
- const uploadArgv = [
5893
- "deepspace",
5894
- "secrets",
5895
- "upload",
5896
- ".dev.vars",
5897
- ...envName ? ["-e", envName] : []
5898
- ];
5711
+ if (!linked) {
5712
+ const configName = defaultConfigNameForEnv(envName);
5899
5713
  output.die(
5900
- `Refusing to deploy: the app store has no secrets, but .dev.vars has ${decision.strayKeys.join(", ")}. Deploying now would ship the app with no secrets \u2014 replacing any secrets a previous deploy set (they would be dropped from production). Upload them first, then redeploy. (Pass --allow-missing-secrets to deploy without them.)`,
5901
- "secrets_not_uploaded",
5902
- { action: { cwd: appDir, argv: uploadArgv } }
5903
- );
5904
- }
5905
- if (decision.kind === "warn") {
5906
- const stray = decision.strayKeys;
5907
- p11.log.warn(
5908
- `.dev.vars has ${stray.join(", ")} not in the app store \u2014 ${stray.length === 1 ? "it is" : "they are"} NOT deployed. \`.dev.vars\` is a local dev cache, not deploy config. Move ${stray.length === 1 ? "it" : "them"} into the store with \`deepspace secrets set KEY=value\` (or \`deepspace secrets upload .dev.vars\`), then redeploy.`
5714
+ `Secrets config "${configName}" does not exist. Create it explicitly before deploying; this distinguishes an intentional empty config from an uninitialized app and protects live Worker secrets during the platform rollout.`,
5715
+ "secrets_config_missing",
5716
+ {
5717
+ action: {
5718
+ cwd: appDir,
5719
+ argv: [
5720
+ "deepspace",
5721
+ "secrets",
5722
+ "configs",
5723
+ "create",
5724
+ configName,
5725
+ ...envName ? ["--env", envName] : []
5726
+ ]
5727
+ }
5728
+ }
5909
5729
  );
5910
5730
  }
5731
+ return { linked };
5732
+ }
5733
+ function prepareDeploySecrets(options) {
5734
+ const { cache, customBindings, doManifest, output } = options;
5735
+ const values = cache.linked ? { ...cache.linked.values } : {};
5911
5736
  const names = Object.keys(values);
5912
5737
  const declaredBindingNames = /* @__PURE__ */ new Set([
5913
5738
  ...customBindings.map((binding) => binding.name),
@@ -5928,17 +5753,7 @@ function prepareDeploySecrets(options) {
5928
5753
  }
5929
5754
  }
5930
5755
  if (names.length) p11.log.info(`App secrets: ${names.join(", ")}`);
5931
- return { values, names };
5932
- }
5933
- function readCustomDevVars(path, output) {
5934
- if (!existsSync18(path)) return {};
5935
- const section = extractCustomDevVars(stripGeneratedSecretsCache(readFileSync20(path, "utf-8")));
5936
- if (!section) return {};
5937
- try {
5938
- return parseDevVars(section);
5939
- } catch (error) {
5940
- output.die(`.dev.vars: ${errorMessage5(error)}`, "invalid_dev_vars");
5941
- }
5756
+ return { values, names, authoritative: cache.linked !== null };
5942
5757
  }
5943
5758
  function errorMessage5(error) {
5944
5759
  return error instanceof Error ? error.message : String(error);
@@ -5977,11 +5792,6 @@ var deploy_default = defineCommand3({
5977
5792
  description: "Platform admins only: claim an app name still inside its 30-day release cooldown. The previous owner permanently loses their reserved reclaim. Non-admin accounts are refused.",
5978
5793
  default: false
5979
5794
  },
5980
- "allow-missing-secrets": {
5981
- type: "boolean",
5982
- description: "Deploy even though hand-edited .dev.vars secrets are absent from the store (they will NOT be deployed, and any a previous deploy set are dropped).",
5983
- default: false
5984
- },
5985
5795
  // Citty maps --no-push to the negation of an argument named `push`.
5986
5796
  push: {
5987
5797
  type: "boolean",
@@ -6055,7 +5865,6 @@ var deploy_default = defineCommand3({
6055
5865
  appDir,
6056
5866
  appName,
6057
5867
  envName,
6058
- sharedDevVarsCache: secretsCache.linked !== null,
6059
5868
  output,
6060
5869
  spinner: spinner2
6061
5870
  });
@@ -6075,10 +5884,7 @@ var deploy_default = defineCommand3({
6075
5884
  sourceState
6076
5885
  });
6077
5886
  const secrets = prepareDeploySecrets({
6078
- appDir,
6079
- envName,
6080
5887
  cache: secretsCache,
6081
- allowMissing: Boolean(args["allow-missing-secrets"]),
6082
5888
  customBindings: bundle.customBindings,
6083
5889
  doManifest: bundle.doManifest,
6084
5890
  output
@@ -6818,7 +6624,7 @@ var usage_default = defineDeepspaceCommand({
6818
6624
  });
6819
6625
 
6820
6626
  // src/cli/commands/add.ts
6821
- import { existsSync as existsSync20, readFileSync as readFileSync22, realpathSync as realpathSync4 } from "fs";
6627
+ import { existsSync as existsSync18, readFileSync as readFileSync20, realpathSync as realpathSync4 } from "fs";
6822
6628
  import { dirname as dirname8, join as join19, resolve as resolve15 } from "path";
6823
6629
  import { pathToFileURL as pathToFileURL3 } from "url";
6824
6630
 
@@ -6826,12 +6632,12 @@ import { pathToFileURL as pathToFileURL3 } from "url";
6826
6632
  import {
6827
6633
  closeSync as closeSync2,
6828
6634
  copyFileSync,
6829
- existsSync as existsSync19,
6635
+ existsSync as existsSync17,
6830
6636
  fstatSync as fstatSync2,
6831
6637
  mkdirSync as mkdirSync6,
6832
6638
  mkdtempSync,
6833
6639
  openSync as openSync2,
6834
- readFileSync as readFileSync21,
6640
+ readFileSync as readFileSync19,
6835
6641
  readSync,
6836
6642
  readdirSync as readdirSync5,
6837
6643
  rmSync as rmSync4,
@@ -6846,9 +6652,9 @@ function featurePackageRoot(moduleUrl = import.meta.url) {
6846
6652
  let dir = dirname7(fileURLToPath(moduleUrl));
6847
6653
  for (; ; ) {
6848
6654
  const packagePath = join18(dir, "package.json");
6849
- if (existsSync19(packagePath)) {
6655
+ if (existsSync17(packagePath)) {
6850
6656
  try {
6851
- const pkg2 = JSON.parse(readFileSync21(packagePath, "utf-8"));
6657
+ const pkg2 = JSON.parse(readFileSync19(packagePath, "utf-8"));
6852
6658
  if (pkg2.name === "deepspace") return dir;
6853
6659
  } catch {
6854
6660
  }
@@ -6859,13 +6665,13 @@ function featurePackageRoot(moduleUrl = import.meta.url) {
6859
6665
  }
6860
6666
  }
6861
6667
  function hasFeatureCatalog(packageRoot = featurePackageRoot()) {
6862
- return existsSync19(join18(packageRoot, "features"));
6668
+ return existsSync17(join18(packageRoot, "features"));
6863
6669
  }
6864
6670
  function readFeatureConfig(id, packageRoot = featurePackageRoot()) {
6865
6671
  const configPath = join18(packageRoot, "features", id, "feature.json");
6866
- if (!existsSync19(configPath)) return null;
6672
+ if (!existsSync17(configPath)) return null;
6867
6673
  try {
6868
- const config = JSON.parse(readFileSync21(configPath, "utf-8"));
6674
+ const config = JSON.parse(readFileSync19(configPath, "utf-8"));
6869
6675
  if (typeof config.id !== "string" || typeof config.name !== "string" || !Array.isArray(config.files)) {
6870
6676
  return null;
6871
6677
  }
@@ -6876,7 +6682,7 @@ function readFeatureConfig(id, packageRoot = featurePackageRoot()) {
6876
6682
  }
6877
6683
  function listFeatureConfigs(packageRoot = featurePackageRoot()) {
6878
6684
  const featuresDir = join18(packageRoot, "features");
6879
- if (!existsSync19(featuresDir)) return [];
6685
+ if (!existsSync17(featuresDir)) return [];
6880
6686
  return readdirSync5(featuresDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => readFeatureConfig(entry.name, packageRoot)).filter((config) => config !== null && !config.internal).sort((a, b) => a.id.localeCompare(b.id));
6881
6687
  }
6882
6688
  function featureCatalog(packageRoot = featurePackageRoot()) {
@@ -6930,7 +6736,7 @@ function copyFile(src, dest) {
6930
6736
  function copyFileWithImportShift(src, dest, levels) {
6931
6737
  mkdirSync6(dirname7(dest), { recursive: true });
6932
6738
  const prefix = "../".repeat(levels);
6933
- const shifted = readFileSync21(src, "utf-8").replace(
6739
+ const shifted = readFileSync19(src, "utf-8").replace(
6934
6740
  /(from\s+|import\s+|import\()(['"])(\.\.\/)/g,
6935
6741
  `$1$2${prefix}$3`
6936
6742
  );
@@ -6946,9 +6752,9 @@ function installFiles(packageRoot, config, targetDir, emit) {
6946
6752
  const srcPath = join18(packageRoot, "features", config.id, file.src);
6947
6753
  const resolvedDest = resolveFeatureDestPath(file.dest, config.route);
6948
6754
  const destPath = join18(targetDir, resolvedDest);
6949
- const existed = existsSync19(destPath);
6755
+ const existed = existsSync17(destPath);
6950
6756
  if (existed) preexisting.push(resolvedDest);
6951
- if (!existsSync19(srcPath)) {
6757
+ if (!existsSync17(srcPath)) {
6952
6758
  emit(` Warning: source not found: ${file.src}`, "stderr");
6953
6759
  if (!existed) missing.push(resolvedDest);
6954
6760
  continue;
@@ -6986,10 +6792,10 @@ function unresolvedSchema(schema, reason, emit) {
6986
6792
  function integrateSchema(config, targetDir, emit) {
6987
6793
  if (!config.schema) return [];
6988
6794
  const schemasPath = join18(targetDir, "src", "schemas.ts");
6989
- if (!existsSync19(schemasPath)) {
6795
+ if (!existsSync17(schemasPath)) {
6990
6796
  return [unresolvedSchema(config.schema, "src/schemas.ts was not found.", emit)];
6991
6797
  }
6992
- let content = readFileSync21(schemasPath, "utf-8");
6798
+ let content = readFileSync19(schemasPath, "utf-8");
6993
6799
  const { exportName, importPath, spreadOperator } = config.schema;
6994
6800
  const importPattern = new RegExp(`^import\\s+\\{[^}]*\\b${exportName}\\b`, "m");
6995
6801
  if (importPattern.test(content)) {
@@ -7043,10 +6849,10 @@ function unresolvedActions(actions, reason, emit) {
7043
6849
  function integrateActions(config, targetDir, emit) {
7044
6850
  if (!config.actions?.length) return [];
7045
6851
  const actionsPath = join18(targetDir, "src", "actions", "index.ts");
7046
- if (!existsSync19(actionsPath)) {
6852
+ if (!existsSync17(actionsPath)) {
7047
6853
  return [unresolvedActions(config.actions, "src/actions/index.ts was not found.", emit)];
7048
6854
  }
7049
- let content = readFileSync21(actionsPath, "utf-8");
6855
+ let content = readFileSync19(actionsPath, "utf-8");
7050
6856
  const registryPattern = /export const actions:\s*Record<string,\s*ActionHandler(?:<[^>]*>)?>\s*=\s*\{/;
7051
6857
  if (!registryPattern.test(content)) {
7052
6858
  const reason = "src/actions/index.ts has an unexpected structure.";
@@ -7093,7 +6899,7 @@ function integrateCode(config, targetDir, emit) {
7093
6899
  const unresolved = [];
7094
6900
  for (const insertion of config.code ?? []) {
7095
6901
  const targetPath = join18(targetDir, insertion.file);
7096
- if (!existsSync19(targetPath)) {
6902
+ if (!existsSync17(targetPath)) {
7097
6903
  unresolved.push({
7098
6904
  area: "code",
7099
6905
  reason: `${insertion.file} was not found.`,
@@ -7101,7 +6907,7 @@ function integrateCode(config, targetDir, emit) {
7101
6907
  });
7102
6908
  continue;
7103
6909
  }
7104
- const original = readFileSync21(targetPath, "utf-8");
6910
+ const original = readFileSync19(targetPath, "utf-8");
7105
6911
  if (original.includes(insertion.insert.trim())) {
7106
6912
  emit(` Integration already present: ${insertion.file}`);
7107
6913
  continue;
@@ -7137,7 +6943,7 @@ ${content.slice(importIndex < 0 ? 0 : importIndex)}`;
7137
6943
  function integrateIgnore(config, targetDir) {
7138
6944
  if (!config.ignore?.length) return;
7139
6945
  const targetPath = join18(targetDir, ".gitignore");
7140
- const current = existsSync19(targetPath) ? readFileSync21(targetPath, "utf-8") : "";
6946
+ const current = existsSync17(targetPath) ? readFileSync19(targetPath, "utf-8") : "";
7141
6947
  const present = new Set(current.split(/\r?\n/).map((line) => line.trim()));
7142
6948
  const missing = config.ignore.filter((pattern) => !present.has(pattern));
7143
6949
  if (missing.length === 0) return;
@@ -7154,10 +6960,10 @@ function integrateWrangler(config, targetDir, emit) {
7154
6960
  ...runWorkerFirst ? ["Set assets.run_worker_first = true in wrangler.toml."] : [],
7155
6961
  ...rateLimits.map((binding) => `Add the ${binding.name} rate-limit binding to wrangler.toml.`)
7156
6962
  ];
7157
- if (!existsSync19(targetPath)) {
6963
+ if (!existsSync17(targetPath)) {
7158
6964
  return [{ area: "wrangler", reason: "wrangler.toml was not found.", steps }];
7159
6965
  }
7160
- let source = readFileSync21(targetPath, "utf-8");
6966
+ let source = readFileSync19(targetPath, "utf-8");
7161
6967
  if (runWorkerFirst && !/^\[assets\]\s*$/m.test(source)) {
7162
6968
  return [{ area: "wrangler", reason: "wrangler.toml has no [assets] section.", steps }];
7163
6969
  }
@@ -7234,7 +7040,7 @@ function rateLimitBindingBlock(scope, binding) {
7234
7040
  function integrateCss(packageRoot, config, targetDir, emit) {
7235
7041
  if (!config.css?.length) return [];
7236
7042
  const stylesPath = join18(targetDir, "src", "styles.css");
7237
- if (!existsSync19(stylesPath)) {
7043
+ if (!existsSync17(stylesPath)) {
7238
7044
  emit(" Warning: Cannot integrate CSS \u2014 src/styles.css not found");
7239
7045
  return [
7240
7046
  {
@@ -7246,12 +7052,12 @@ function integrateCss(packageRoot, config, targetDir, emit) {
7246
7052
  }
7247
7053
  ];
7248
7054
  }
7249
- let styles = readFileSync21(stylesPath, "utf-8");
7055
+ let styles = readFileSync19(stylesPath, "utf-8");
7250
7056
  let integrated = 0;
7251
7057
  const unresolved = [];
7252
7058
  for (const cssFile of config.css) {
7253
7059
  const srcPath = join18(packageRoot, "features", config.id, cssFile);
7254
- if (!existsSync19(srcPath)) {
7060
+ if (!existsSync17(srcPath)) {
7255
7061
  emit(` Warning: CSS source not found: ${cssFile}`, "stderr");
7256
7062
  unresolved.push({
7257
7063
  area: "css",
@@ -7260,7 +7066,7 @@ function integrateCss(packageRoot, config, targetDir, emit) {
7260
7066
  });
7261
7067
  continue;
7262
7068
  }
7263
- const css = readFileSync21(srcPath, "utf-8");
7069
+ const css = readFileSync19(srcPath, "utf-8");
7264
7070
  const fingerprint = css.match(/\.([\w-]+)\s*\{|@keyframes\s+([\w-]+)/);
7265
7071
  const marker = fingerprint?.[1] ?? fingerprint?.[2];
7266
7072
  if (marker && styles.includes(marker)) {
@@ -7288,10 +7094,10 @@ function unresolvedRoute(route, config, reason, emit) {
7288
7094
  function integrateRoute(config, targetDir, emit) {
7289
7095
  if (!config.route) return [];
7290
7096
  const navPath = join18(targetDir, "src", "nav.ts");
7291
- if (!existsSync19(navPath)) {
7097
+ if (!existsSync17(navPath)) {
7292
7098
  return [unresolvedRoute(config.route, config, "src/nav.ts was not found.", emit)];
7293
7099
  }
7294
- let content = readFileSync21(navPath, "utf-8");
7100
+ let content = readFileSync19(navPath, "utf-8");
7295
7101
  const routePath = config.route.path;
7296
7102
  if (content.includes(`'${routePath}'`)) {
7297
7103
  emit(` Nav already present: ${routePath}`);
@@ -7366,13 +7172,13 @@ function integrateDependencies(config, targetDir, runInstall, outputVisible, emi
7366
7172
  return { added: [], installRan: false, packageManager };
7367
7173
  }
7368
7174
  const packagePath = join18(targetDir, "package.json");
7369
- if (!existsSync19(packagePath)) {
7175
+ if (!existsSync17(packagePath)) {
7370
7176
  emit(" Could not patch package.json (file not found)");
7371
7177
  return { added: [], installRan: false, packageManager };
7372
7178
  }
7373
7179
  let pkg2;
7374
7180
  try {
7375
- pkg2 = JSON.parse(readFileSync21(packagePath, "utf-8"));
7181
+ pkg2 = JSON.parse(readFileSync19(packagePath, "utf-8"));
7376
7182
  } catch (error) {
7377
7183
  emit(
7378
7184
  ` Could not patch package.json (parse error: ${error instanceof Error ? error.message : String(error)})`
@@ -7432,7 +7238,7 @@ ${child.diagnosticTail}` : "";
7432
7238
  }
7433
7239
  function dependencySnapshot(targetDir) {
7434
7240
  try {
7435
- const pkg2 = JSON.parse(readFileSync21(join18(targetDir, "package.json"), "utf-8"));
7241
+ const pkg2 = JSON.parse(readFileSync19(join18(targetDir, "package.json"), "utf-8"));
7436
7242
  return {
7437
7243
  dependencies: pkg2.dependencies ?? {},
7438
7244
  devDependencies: pkg2.devDependencies ?? {}
@@ -7444,7 +7250,7 @@ function dependencySnapshot(targetDir) {
7444
7250
  function installedNavFeatures(packageRoot, targetDir) {
7445
7251
  return listFeatureConfigs(packageRoot).filter((feature) => feature.category === "nav" && feature.files.length > 0).filter((feature) => {
7446
7252
  const signature = resolveFeatureDestPath(feature.files[0].dest, feature.route);
7447
- return existsSync19(join18(targetDir, signature));
7253
+ return existsSync17(join18(targetDir, signature));
7448
7254
  }).map((feature) => feature.id);
7449
7255
  }
7450
7256
  function warnNavConflicts(packageRoot, config, targetDir, emit) {
@@ -7677,11 +7483,11 @@ function renderFeatureInfo(config) {
7677
7483
  return lines.join("\n");
7678
7484
  }
7679
7485
  function isDeepSpaceApp(dir) {
7680
- if (existsSync20(join19(dir, "wrangler.toml"))) return true;
7486
+ if (existsSync18(join19(dir, "wrangler.toml"))) return true;
7681
7487
  const packagePath = join19(dir, "package.json");
7682
- if (!existsSync20(packagePath)) return false;
7488
+ if (!existsSync18(packagePath)) return false;
7683
7489
  try {
7684
- const pkg2 = JSON.parse(readFileSync22(packagePath, "utf-8"));
7490
+ const pkg2 = JSON.parse(readFileSync20(packagePath, "utf-8"));
7685
7491
  return Boolean(pkg2.dependencies?.deepspace || pkg2.devDependencies?.deepspace);
7686
7492
  } catch {
7687
7493
  return false;
@@ -7691,7 +7497,7 @@ function installedPackageRoot(fromDir) {
7691
7497
  let dir = fromDir;
7692
7498
  for (; ; ) {
7693
7499
  const candidate = join19(dir, "node_modules", "deepspace");
7694
- if (existsSync20(join19(candidate, "package.json"))) return candidate;
7500
+ if (existsSync18(join19(candidate, "package.json"))) return candidate;
7695
7501
  const parent = dirname8(dir);
7696
7502
  if (parent === dir) return null;
7697
7503
  dir = parent;
@@ -7710,7 +7516,7 @@ async function targetInstaller(appDir) {
7710
7516
  }
7711
7517
  const entry = join19(installedRoot, "dist", "feature-installer.js");
7712
7518
  const catalog = join19(installedRoot, "features");
7713
- if (!existsSync20(entry) || !existsSync20(catalog)) {
7519
+ if (!existsSync18(entry) || !existsSync18(catalog)) {
7714
7520
  throw new Refusal(
7715
7521
  `The app's installed deepspace package does not contain the typed feature installer.
7716
7522
  Expected: ${entry}
@@ -7803,7 +7609,7 @@ var add_default = defineDeepspaceCommand({
7803
7609
  return { data: { features: featureCatalog(root) } };
7804
7610
  }
7805
7611
  const appDir = resolve15(args.dir ?? ".");
7806
- if (!existsSync20(appDir)) {
7612
+ if (!existsSync18(appDir)) {
7807
7613
  throw new Refusal(
7808
7614
  `Target directory not found: ${appDir}
7809
7615
  Create a new app first: deepspace app create <name>`,
@@ -8179,7 +7985,7 @@ var domain_default = defineCommand5({
8179
7985
  // src/cli/commands/app-files.ts
8180
7986
  import { defineCommand as defineCommand6 } from "citty";
8181
7987
  import { basename as basename4, resolve as resolve16 } from "path";
8182
- import { existsSync as existsSync21, statSync as statSync2 } from "fs";
7988
+ import { existsSync as existsSync19, statSync as statSync2 } from "fs";
8183
7989
 
8184
7990
  // src/cli/lib/app-files-api.ts
8185
7991
  import { createWriteStream, openAsBlob, statSync } from "fs";
@@ -8450,7 +8256,7 @@ var put = defineDeepspaceCommand({
8450
8256
  },
8451
8257
  async run({ args }) {
8452
8258
  const localPath = resolve16(String(args.file));
8453
- if (!existsSync21(localPath) || !statSync2(localPath).isFile()) {
8259
+ if (!existsSync19(localPath) || !statSync2(localPath).isFile()) {
8454
8260
  throw new Refusal(`No such file: ${args.file}`, "file_not_found");
8455
8261
  }
8456
8262
  const key = requireKey(args.key ?? basename4(localPath));
@@ -8920,7 +8726,7 @@ var transfer_default = defineCommand8({
8920
8726
  import { defineCommand as defineCommand9 } from "citty";
8921
8727
 
8922
8728
  // src/cli/commands/_invoke-impl.ts
8923
- import { readFileSync as readFileSync23 } from "fs";
8729
+ import { readFileSync as readFileSync21 } from "fs";
8924
8730
  import * as p15 from "@clack/prompts";
8925
8731
  var API_URL7 = process.env.DEEPSPACE_API_URL ?? PLATFORM_URLS.api;
8926
8732
  var DEFAULT_TIMEOUT_MS = 12e4;
@@ -8961,7 +8767,7 @@ function resolveBody(opts) {
8961
8767
  if (opts.body != null) {
8962
8768
  raw = opts.body;
8963
8769
  } else if (opts.bodyFile != null) {
8964
- raw = opts.bodyFile === "-" ? readFileSync23(0, "utf-8") : readFileSync23(opts.bodyFile, "utf-8");
8770
+ raw = opts.bodyFile === "-" ? readFileSync21(0, "utf-8") : readFileSync21(opts.bodyFile, "utf-8");
8965
8771
  } else {
8966
8772
  return "{}";
8967
8773
  }
@@ -9240,7 +9046,7 @@ var integrations_default = defineCommand9({
9240
9046
 
9241
9047
  // src/cli/commands/feedback.ts
9242
9048
  import * as p16 from "@clack/prompts";
9243
- import { existsSync as existsSync22, readFileSync as readFileSync24 } from "fs";
9049
+ import { existsSync as existsSync20, readFileSync as readFileSync22 } from "fs";
9244
9050
  import { dirname as dirname9, join as join20 } from "path";
9245
9051
  import { fileURLToPath as fileURLToPath2 } from "url";
9246
9052
  import { release } from "os";
@@ -9268,8 +9074,8 @@ function readCliVersion() {
9268
9074
  join20(here, "..", "..", "..", "package.json")
9269
9075
  ];
9270
9076
  for (const path of candidates) {
9271
- if (!existsSync22(path)) continue;
9272
- const pkg2 = JSON.parse(readFileSync24(path, "utf-8"));
9077
+ if (!existsSync20(path)) continue;
9078
+ const pkg2 = JSON.parse(readFileSync22(path, "utf-8"));
9273
9079
  if (pkg2.name === "deepspace" && pkg2.version) return pkg2.version;
9274
9080
  }
9275
9081
  } catch {
@@ -9465,7 +9271,7 @@ Context: deepspace ${payload.cliVersion} \xB7 ${payload.nodeVersion} \xB7 ${payl
9465
9271
 
9466
9272
  // src/cli/commands/secrets.ts
9467
9273
  import { defineCommand as defineCommand10 } from "citty";
9468
- import { readFileSync as readFileSync25 } from "fs";
9274
+ import { readFileSync as readFileSync23 } from "fs";
9469
9275
 
9470
9276
  // src/cli/lib/citty-args.ts
9471
9277
  function dedupePositionals(bound, rest) {
@@ -9735,7 +9541,7 @@ var upload = defineCommand10({
9735
9541
  async run({ args }) {
9736
9542
  try {
9737
9543
  const t = await resolveTarget(args);
9738
- const content = args.file === "-" ? readFileSync25(0, "utf-8") : readFileSync25(args.file, "utf-8");
9544
+ const content = args.file === "-" ? readFileSync23(0, "utf-8") : readFileSync23(args.file, "utf-8");
9739
9545
  const secrets = parseSecretsUpload(content);
9740
9546
  if (Object.keys(secrets).length === 0)
9741
9547
  fail(new InputError("No secrets found in the input.", "empty_input"));
@@ -9799,27 +9605,24 @@ var pull = defineCommand10({
9799
9605
  );
9800
9606
  const t = await resolveTarget(args);
9801
9607
  const ownerId = decodeJwtPayload(t.token).sub;
9802
- const pulled = await pullAppSecretsCache(DEPLOY_URL11, t.token, t.appId, t.configName);
9803
- if (!pulled) {
9804
- ok(
9805
- args.json === true,
9806
- { appId: t.appId, config: t.configName, pulled: 0 },
9807
- () => console.log(
9808
- `No secrets in ${t.configName} yet \u2014 nothing to pull. Whoever has them adds them with \`deepspace secrets set KEY=value\` (or \`secrets upload <file>\`); if the app truly needs none, you're ready as-is.`
9809
- )
9810
- );
9811
- return;
9812
- }
9608
+ const refreshed = await refreshSecretsCache(
9609
+ DEPLOY_URL11,
9610
+ t.token,
9611
+ t.appId,
9612
+ wranglerEnv,
9613
+ t.configName
9614
+ );
9813
9615
  await writeDevVars(appDir, ownerId, t.token, wranglerEnv, {
9814
9616
  appId: t.appId,
9815
- generatedSecretsCache: renderSecretsCache(pulled.values, pulled),
9816
- sharedDevVarsCache: true
9617
+ generatedSecretsCache: refreshed.rendered
9817
9618
  });
9818
- const count = Object.keys(pulled.values).length;
9619
+ const count = Object.keys(refreshed.pulled?.values ?? {}).length;
9819
9620
  ok(
9820
9621
  args.json === true,
9821
9622
  { appId: t.appId, config: t.configName, pulled: count },
9822
- () => console.log(`Pulled ${count} secrets (${t.configName}) into .dev.vars.`)
9623
+ () => console.log(
9624
+ refreshed.pulled ? `Pulled ${count} secrets (${t.configName}) into .dev.vars.` : `Config ${t.configName} does not exist yet; regenerated .dev.vars without app secrets.`
9625
+ )
9823
9626
  );
9824
9627
  } catch (err) {
9825
9628
  fail(err);
@@ -9991,7 +9794,7 @@ var init_default = defineDeepspaceCommand({
9991
9794
  // src/cli/commands/status.ts
9992
9795
  import { defineCommand as defineCommand11 } from "citty";
9993
9796
  import * as p17 from "@clack/prompts";
9994
- import { existsSync as existsSync23, readFileSync as readFileSync26 } from "fs";
9797
+ import { existsSync as existsSync21, readFileSync as readFileSync24 } from "fs";
9995
9798
  function timeAgo(iso) {
9996
9799
  const ms = Date.now() - Date.parse(iso);
9997
9800
  if (!Number.isFinite(ms) || ms < 0) return iso;
@@ -10076,7 +9879,7 @@ var status_default = defineCommand11({
10076
9879
  json.services = urls;
10077
9880
  if (urlOverrides.length > 0) json.urlOverrides = urlOverrides;
10078
9881
  }
10079
- facts.loggedIn = existsSync23(SESSION_PATH);
9882
+ facts.loggedIn = existsSync21(SESSION_PATH);
10080
9883
  let userId = null;
10081
9884
  if (!facts.loggedIn) {
10082
9885
  lines.push(["Session", "not logged in"]);
@@ -10085,7 +9888,7 @@ var status_default = defineCommand11({
10085
9888
  let who = "logged in";
10086
9889
  try {
10087
9890
  const payload = decodeJwtPayload(
10088
- readFileSync26(TOKEN_PATH, "utf-8").trim()
9891
+ readFileSync24(TOKEN_PATH, "utf-8").trim()
10089
9892
  );
10090
9893
  if (payload.email) who = payload.email;
10091
9894
  userId = payload.sub ?? null;
@@ -10634,12 +10437,7 @@ var pull_default = defineDeepspaceCommand({
10634
10437
  }
10635
10438
  spinner2?.message(`Fetching ${branch} from the cloud repo\u2026`);
10636
10439
  const trackingRef = spaceTrackingRef(branch);
10637
- runGitRemote(appDir, token, [
10638
- "fetch",
10639
- "--quiet",
10640
- SPACE_REMOTE,
10641
- `+${refName}:${trackingRef}`
10642
- ]);
10440
+ runGitRemote(appDir, token, ["fetch", "--quiet", SPACE_REMOTE, `+${refName}:${trackingRef}`]);
10643
10441
  const remoteOid = resolveCommit(appDir, trackingRef);
10644
10442
  if (!remoteOid) {
10645
10443
  spinner2?.stop("Fetch failed.");
@@ -10677,6 +10475,8 @@ var pull_default = defineDeepspaceCommand({
10677
10475
  integration = "fast_forwarded";
10678
10476
  }
10679
10477
  }
10478
+ } else if (isAncestor(appDir, remoteOid, localOid)) {
10479
+ integration = "local_ahead";
10680
10480
  } else {
10681
10481
  integration = "fetched_only_diverged";
10682
10482
  }
@@ -10690,10 +10490,15 @@ var pull_default = defineDeepspaceCommand({
10690
10490
  let line;
10691
10491
  let codeForState;
10692
10492
  let recoveryAction;
10493
+ let successAction;
10693
10494
  switch (integration) {
10694
10495
  case "up_to_date":
10695
10496
  line = `${branch} is already up to date.`;
10696
10497
  break;
10498
+ case "local_ahead":
10499
+ line = `${branch} already contains ${SPACE_REMOTE}/${branch} at ${remoteOid.slice(0, 10)} \u2014 nothing to pull. Local work is not on the cloud repo yet: run \`${pushCommand}\`.`;
10500
+ successAction = targetedVcAction("push", appDir, appId, branch);
10501
+ break;
10697
10502
  case "fast_forwarded":
10698
10503
  line = `Fast-forwarded ${branch} to ${remoteOid.slice(0, 10)}.`;
10699
10504
  break;
@@ -10742,7 +10547,8 @@ var pull_default = defineDeepspaceCommand({
10742
10547
  }
10743
10548
  const actionRequired = recoveryAction !== void 0;
10744
10549
  spinner2?.stop(codeForState ? `Fetched ${SPACE_REMOTE}/${branch}.` : line);
10745
- if (!args.json) p19.log.info(`App: ${appId} \xB7 ${SPACE_REMOTE}/${branch} at ${remoteOid.slice(0, 10)}`);
10550
+ if (!args.json)
10551
+ p19.log.info(`App: ${appId} \xB7 ${SPACE_REMOTE}/${branch} at ${remoteOid.slice(0, 10)}`);
10746
10552
  if (codeForState) {
10747
10553
  throw new Refusal(line, codeForState, {
10748
10554
  actionRequired,
@@ -10756,20 +10562,20 @@ var pull_default = defineDeepspaceCommand({
10756
10562
  }
10757
10563
  });
10758
10564
  }
10759
- return { data: { status: integration, appId, branch, remoteOid } };
10565
+ return { data: { status: integration, appId, branch, remoteOid }, action: successAction };
10760
10566
  }
10761
10567
  });
10762
10568
 
10763
10569
  // src/cli/commands/clone.ts
10764
10570
  import * as p20 from "@clack/prompts";
10765
- import { existsSync as existsSync24, readdirSync as readdirSync6, rmSync as rmSync5 } from "fs";
10571
+ import { existsSync as existsSync22, readdirSync as readdirSync6, rmSync as rmSync5 } from "fs";
10766
10572
  import { resolve as resolve17, basename as basename5, join as join21 } from "path";
10767
10573
  function cleanFailedCloneDir(dir) {
10768
10574
  let entries;
10769
10575
  try {
10770
10576
  entries = readdirSync6(dir);
10771
10577
  } catch {
10772
- return existsSync24(dir) ? ["<unreadable>"] : [];
10578
+ return existsSync22(dir) ? ["<unreadable>"] : [];
10773
10579
  }
10774
10580
  if (entries.length === 1 && entries[0] === ".git") {
10775
10581
  try {
@@ -10782,7 +10588,7 @@ function cleanFailedCloneDir(dir) {
10782
10588
  return entries;
10783
10589
  }
10784
10590
  function validateCloneTarget(dir) {
10785
- if (!existsSync24(dir)) return false;
10591
+ if (!existsSync22(dir)) return false;
10786
10592
  let entries;
10787
10593
  try {
10788
10594
  entries = readdirSync6(dir);
@@ -11258,7 +11064,7 @@ function printOverlaps(overlaps, json) {
11258
11064
 
11259
11065
  // src/cli/commands/workspace/local.ts
11260
11066
  import * as p24 from "@clack/prompts";
11261
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync27, realpathSync as realpathSync6, renameSync as renameSync2, statSync as statSync3, writeFileSync as writeFileSync9 } from "fs";
11067
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync25, realpathSync as realpathSync6, renameSync as renameSync2, statSync as statSync3, writeFileSync as writeFileSync9 } from "fs";
11262
11068
  import { dirname as dirname10, isAbsolute as isAbsolute5, join as join22, relative as relative2, sep as sep2 } from "path";
11263
11069
  var WORKSPACE_MARKER = "deepspace-workspace.json";
11264
11070
  function excludeWorktreeDir(appDir, dir) {
@@ -11316,7 +11122,7 @@ function isManagedWorkspaceWorktree(worktreeRoot, workspaceId) {
11316
11122
  const markerPath = worktreeMarkerPath(worktreeRoot);
11317
11123
  if (!markerPath) return false;
11318
11124
  try {
11319
- const marker = JSON.parse(readFileSync27(markerPath, "utf-8"));
11125
+ const marker = JSON.parse(readFileSync25(markerPath, "utf-8"));
11320
11126
  return marker.version === 1 && marker.workspaceId === workspaceId && marker.mode === "managed-linked";
11321
11127
  } catch {
11322
11128
  return false;
@@ -11756,7 +11562,7 @@ var newWorkspaceCommand = defineDeepspaceCommand({
11756
11562
 
11757
11563
  // src/cli/commands/workspace/attach.ts
11758
11564
  import * as p26 from "@clack/prompts";
11759
- import { existsSync as existsSync25, readdirSync as readdirSync7, realpathSync as realpathSync7, rmSync as rmSync6 } from "fs";
11565
+ import { existsSync as existsSync23, readdirSync as readdirSync7, realpathSync as realpathSync7, rmSync as rmSync6 } from "fs";
11760
11566
  import { isAbsolute as isAbsolute7, resolve as resolve19 } from "path";
11761
11567
  function samePath(a, b) {
11762
11568
  const real = (p35) => {
@@ -11776,7 +11582,7 @@ function finishedWorkspaceMessage(id, view) {
11776
11582
  return `Workspace ${id} is ${workspace.status} \u2014 its ref is retained briefly (fetch ${workspace.ref} to inspect), but attach only works on active workspaces.`;
11777
11583
  }
11778
11584
  function cleanFailedFreshAttachDir(dir) {
11779
- if (!existsSync25(dir)) return [];
11585
+ if (!existsSync23(dir)) return [];
11780
11586
  let entries;
11781
11587
  try {
11782
11588
  entries = readdirSync7(dir);
@@ -11858,7 +11664,7 @@ var attachWorkspaceCommand = defineDeepspaceCommand({
11858
11664
  const localTip = resolveCommit(appDir, `refs/heads/${branch}`);
11859
11665
  if (localTip) {
11860
11666
  let existing = resolveWorkspaceWorktree(listWorktrees(appDir), id);
11861
- if (existing && !existsSync25(existing)) {
11667
+ if (existing && !existsSync23(existing)) {
11862
11668
  runGit(appDir, ["worktree", "prune"], { allowFail: true });
11863
11669
  existing = null;
11864
11670
  }
@@ -11976,7 +11782,7 @@ var attachWorkspaceCommand = defineDeepspaceCommand({
11976
11782
  process.cwd(),
11977
11783
  (typeof args.dir === "string" ? args.dir.trim() : "") || `ws-${id.slice(3).toLowerCase()}`
11978
11784
  );
11979
- if (existsSync25(dir)) {
11785
+ if (existsSync23(dir)) {
11980
11786
  spinner2?.stop("Target exists.");
11981
11787
  throw new Refusal(`${dir} already exists \u2014 pass a different directory.`, "dir_exists");
11982
11788
  }
@@ -12313,13 +12119,13 @@ import * as p30 from "@clack/prompts";
12313
12119
 
12314
12120
  // src/cli/lib/validation.ts
12315
12121
  import { sync as spawnSync7 } from "cross-spawn";
12316
- import { readFileSync as readFileSync28 } from "fs";
12122
+ import { readFileSync as readFileSync26 } from "fs";
12317
12123
  import { join as join23 } from "path";
12318
12124
  var MAX_SUMMARY_CHARS = 4e3;
12319
12125
  var MAX_OUTPUT_BUFFER = 64 * 1024 * 1024;
12320
12126
  function configuredCommand(appDir) {
12321
12127
  try {
12322
- const pkg2 = JSON.parse(readFileSync28(join23(appDir, "package.json"), "utf-8"));
12128
+ const pkg2 = JSON.parse(readFileSync26(join23(appDir, "package.json"), "utf-8"));
12323
12129
  if (pkg2.scripts?.validate) return `${detectPackageManager(appDir)} run --silent validate`;
12324
12130
  } catch {
12325
12131
  }
@@ -13384,7 +13190,7 @@ function sameRefs(left, right) {
13384
13190
 
13385
13191
  // src/cli/commands/update.ts
13386
13192
  import * as p34 from "@clack/prompts";
13387
- import { existsSync as existsSync26, readFileSync as readFileSync29, writeFileSync as writeFileSync10 } from "fs";
13193
+ import { existsSync as existsSync24, readFileSync as readFileSync27, writeFileSync as writeFileSync10 } from "fs";
13388
13194
  import { join as join24, resolve as resolve20 } from "path";
13389
13195
  var REGISTRY = "https://registry.npmjs.org/deepspace/latest";
13390
13196
  var DOCS_URL = "https://documentation.deep.space/cli-reference/commands";
@@ -13400,18 +13206,18 @@ function gapIsTooWide(from, to) {
13400
13206
  }
13401
13207
  function readAppSdkVersion(appDir) {
13402
13208
  const installed = join24(appDir, "node_modules", "deepspace", "package.json");
13403
- if (existsSync26(installed)) {
13404
- const parsed2 = JSON.parse(readFileSync29(installed, "utf8"));
13209
+ if (existsSync24(installed)) {
13210
+ const parsed2 = JSON.parse(readFileSync27(installed, "utf8"));
13405
13211
  if (typeof parsed2.version === "string") return parsed2.version;
13406
13212
  }
13407
13213
  const manifest = join24(appDir, "package.json");
13408
- if (!existsSync26(manifest)) return null;
13409
- const parsed = JSON.parse(readFileSync29(manifest, "utf8"));
13214
+ if (!existsSync24(manifest)) return null;
13215
+ const parsed = JSON.parse(readFileSync27(manifest, "utf8"));
13410
13216
  return parsed.dependencies?.deepspace ?? null;
13411
13217
  }
13412
13218
  function pinSdkVersion(appDir, version) {
13413
13219
  const path = join24(appDir, "package.json");
13414
- const source = readFileSync29(path, "utf8");
13220
+ const source = readFileSync27(path, "utf8");
13415
13221
  const updated = source.replace(
13416
13222
  /("deepspace"\s*:\s*")[^"]+(")/,
13417
13223
  (_match, head, tail) => `${head}^${version}${tail}`
@@ -13557,8 +13363,8 @@ var update_default = defineDeepspaceCommand({
13557
13363
 
13558
13364
  // src/cli/cli.ts
13559
13365
  var __dirname = dirname11(fileURLToPath3(import.meta.url));
13560
- var pkg = JSON.parse(readFileSync30(join25(__dirname, "..", "package.json"), "utf-8"));
13561
- if (process.platform === "linux" && !process.env.SSL_CERT_FILE && existsSync27("/etc/ssl/certs/ca-certificates.crt")) {
13366
+ var pkg = JSON.parse(readFileSync28(join25(__dirname, "..", "package.json"), "utf-8"));
13367
+ if (process.platform === "linux" && !process.env.SSL_CERT_FILE && existsSync25("/etc/ssl/certs/ca-certificates.crt")) {
13562
13368
  process.env.SSL_CERT_FILE = "/etc/ssl/certs/ca-certificates.crt";
13563
13369
  }
13564
13370
  var create2 = defineCommand14({
@@ -13689,6 +13495,46 @@ Run \`${unknown.helpPath.join(" ")} --help\` for the command list.`
13689
13495
  }
13690
13496
  process.exit(1);
13691
13497
  }
13498
+ function selectedLeaf(argv) {
13499
+ let command = main;
13500
+ let remaining = argv;
13501
+ const path = [];
13502
+ const commandPrefixArgs = [];
13503
+ for (; ; ) {
13504
+ const subCommands = command.subCommands;
13505
+ if (!subCommands || Object.keys(subCommands).length === 0) {
13506
+ return { command, rawArgs: [...commandPrefixArgs, ...remaining], path };
13507
+ }
13508
+ const index = remaining.findIndex((argument) => !argument.startsWith("-"));
13509
+ if (index < 0) return { command, rawArgs: [...commandPrefixArgs, ...remaining], path };
13510
+ const name = remaining[index];
13511
+ const entry = Object.entries(subCommands).find(([key, child]) => {
13512
+ if (key === name) return true;
13513
+ const alias = child.meta?.alias;
13514
+ return (Array.isArray(alias) ? alias : alias ? [alias] : []).includes(name);
13515
+ });
13516
+ if (!entry) return null;
13517
+ commandPrefixArgs.push(...remaining.slice(0, index));
13518
+ path.push(entry[0]);
13519
+ command = entry[1];
13520
+ remaining = remaining.slice(index + 1);
13521
+ }
13522
+ }
13523
+ function assertKnownOptions(argv) {
13524
+ const selected = selectedLeaf(argv);
13525
+ if (!selected || selected.path.join(" ") === "app create") return;
13526
+ const args = selected.command.args ?? {};
13527
+ const unknown = unknownOptionNamesFromRaw(selected.rawArgs, args);
13528
+ if (unknown.length === 0) return;
13529
+ const commandName = selected.path.join(" ") || "deepspace";
13530
+ const message = unknownOptionMessage(commandName, unknown, args);
13531
+ if (argv.includes("--json")) {
13532
+ console.log(JSON.stringify({ ok: false, code: "unknown_option", error: message }));
13533
+ } else {
13534
+ console.error(withSlug(message, "unknown_option"));
13535
+ }
13536
+ process.exit(1);
13537
+ }
13692
13538
  var rawArgs = process.argv.slice(2);
13693
13539
  var wantsHelp = rawArgs.some((arg) => arg === "--help" || arg === "-h");
13694
13540
  if (DEEPSPACE_ENV === "invalid") {
@@ -13700,6 +13546,9 @@ if (DEEPSPACE_ENV === "invalid") {
13700
13546
  }
13701
13547
  process.exit(1);
13702
13548
  }
13549
+ if (!wantsHelp && !(rawArgs.length === 1 && ["--version", "-v"].includes(rawArgs[0]))) {
13550
+ assertKnownOptions(rawArgs);
13551
+ }
13703
13552
  if (rawArgs.includes("--json") && !wantsHelp) {
13704
13553
  const unknown = findUnknownCommand(rawArgs, commandTree());
13705
13554
  if (unknown) {