deepspace 0.17.0 → 0.19.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: def.jsonDescription ?? "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" }] };
1466
- }
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
- }
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
- }
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 {
1565
1482
  }
1566
1483
  }
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
- );
1561
+ var generatedConfigSeq = 0;
1562
+ function resolveAppNameForEnv(config, envName) {
1563
+ if (!envName) {
1564
+ return resolveAppName(config.name);
1676
1565
  }
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
- );
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
+ };
1684
1572
  }
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
- );
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
+ };
1689
1578
  }
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`);
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
+ };
1695
1584
  }
1585
+ return resolveAppName(envBlock.name);
1696
1586
  }
1697
- function defaultConfigNameForEnv(wranglerEnv) {
1698
- return wranglerEnv ?? "prd";
1587
+ function devVarsPathFor(appDir) {
1588
+ return join7(appDir, ".dev.vars");
1699
1589
  }
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 ?? {}
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`
1600
+ );
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")
1608
+ );
1609
+ return {
1610
+ configPath,
1611
+ cleanup() {
1612
+ rmSync2(configPath, { force: true });
1707
1613
  }
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;
1614
+ };
1615
+ }
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;
1715
1621
  }
1716
- return body;
1622
+ return env;
1717
1623
  }
1718
- function listSecrets(deployUrl, token, appId, configName) {
1719
- return secretsApi(deployUrl, token, `/${appId}/configs/${encodeURIComponent(configName)}`);
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;
1720
1646
  }
1721
- function fetchSecretsValues(deployUrl, token, appId, configName) {
1722
- return secretsApi(deployUrl, token, `/${appId}/configs/${encodeURIComponent(configName)}/values`);
1647
+ function asTomlRecord(value) {
1648
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
1723
1649
  }
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
- );
1650
+ function safeGeneratedConfigSegment(value) {
1651
+ return value.replace(/[^A-Za-z0-9._-]/gu, "_");
1730
1652
  }
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 }) }
1737
- );
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");
1665
+ try {
1666
+ return parseToml3(raw);
1667
+ } catch (err) {
1668
+ throw new WranglerConfigError(path, err);
1669
+ }
1738
1670
  }
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" }
1745
- );
1671
+ function hasWranglerConfig(appDir) {
1672
+ return existsSync6(join7(appDir, "wrangler.toml"));
1746
1673
  }
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 }) }
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."
1753
1686
  );
1754
1687
  }
1755
- function listConfigs(deployUrl, token, appId) {
1756
- return secretsApi(deployUrl, token, `/${appId}/configs`);
1757
- }
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
- });
1763
- }
1764
- function deleteConfig(deployUrl, token, appId, configName) {
1765
- return secretsApi(deployUrl, token, `/${appId}/configs/${encodeURIComponent(configName)}`, {
1766
- method: "DELETE"
1767
- });
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
+ );
1768
1711
  }
1769
- async function pullAppSecretsCache(deployUrl, token, appId, configName) {
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) {
1770
1721
  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 };
1781
- } catch (err) {
1782
- if (err?.status === 404) return null;
1783
- throw err;
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 {
1784
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";
1785
1731
  }
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}"`;
1732
+ function installCommand(appDir) {
1733
+ return `${detectPackageManager(appDir)} install`;
1790
1734
  }
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])}`);
1735
+
1736
+ // src/cli/lib/install-status.ts
1737
+ function installState(appDir) {
1738
+ if (resolvesDeepspace(appDir)) return "ready";
1739
+ const sentinelDir = join9(appDir, ".deepspace");
1740
+ if (existsSync8(join9(sentinelDir, "install.err"))) return "failed";
1741
+ if (existsSync8(join9(sentinelDir, "install.started"))) {
1742
+ if (!existsSync8(join9(sentinelDir, "install.done")) && installWorkerAlive(sentinelDir)) {
1743
+ return "installing";
1744
+ }
1745
+ return "failed";
1801
1746
  }
1802
- return lines.join("\n");
1747
+ return "missing";
1803
1748
  }
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";
1749
+ function tailHint(logPath, platform = process.platform) {
1750
+ return platform === "win32" ? `Get-Content -Wait ${logPath}` : `tail -f ${logPath}`;
1808
1751
  }
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;
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
+ );
1825
1767
  }
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);
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
+ );
1864
1776
  }
1865
- validateSecretValue(key, value);
1866
- out[key] = value;
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
+ );
1867
1782
  }
1868
- return out;
1869
- }
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;
1876
- }
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];
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"] }
1885
1788
  }
1886
- }
1887
- return out;
1888
- }
1889
- function shellSingleQuote(value) {
1890
- return `'${value.replace(/'/g, "'\\''")}'`;
1789
+ );
1891
1790
  }
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";
1791
+ function resolvesDeepspace(fromDir) {
1792
+ let dir = canonicalPath2(fromDir);
1793
+ let checkoutRoot = null;
1794
+ try {
1795
+ checkoutRoot = canonicalPath2(repoToplevel(dir));
1796
+ } catch {
1896
1797
  }
1897
- if (format === "shell") {
1898
- return entries.map(([k, v]) => `export ${k}=${shellSingleQuote(v)}`).join("\n") + "\n";
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;
1899
1804
  }
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
- };
1913
1805
  }
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 });
1806
+ function canonicalPath2(path) {
1919
1807
  try {
1920
- chmodSync(path, 384);
1808
+ return realpathSync3(path);
1921
1809
  } catch {
1810
+ return resolve6(path);
1922
1811
  }
1923
1812
  }
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)` };
1947
- }
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)` };
1950
- }
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
- };
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;
2082
+ const browserBinding = pluckString(outputConfig.browser, "binding");
2083
+ if (browserBinding) out.push({ type: "browser_rendering", name: browserBinding });
2084
+ for (const a of asObjectArray(outputConfig.analytics_engine_datasets)) {
2085
+ const name = pluckString(a, "binding");
2086
+ const dataset = pluckString(a, "dataset");
2087
+ if (name) out.push({ type: "analytics_engine", name, dataset: dataset ?? void 0 });
2088
+ }
2089
+ for (const h of asObjectArray(outputConfig.hyperdrive)) {
2090
+ const name = pluckString(h, "binding");
2091
+ const id = pluckString(h, "id");
2092
+ if (name && id) out.push({ type: "hyperdrive", name, id });
2093
+ }
2094
+ for (const r of asObjectArray(outputConfig.ratelimits)) {
2095
+ const name = pluckString(r, "name");
2096
+ const namespace_id = pluckString(r, "namespace_id");
2097
+ const simple = isObject(r.simple) ? r.simple : null;
2098
+ const limit = pluckNumber(simple, "limit");
2099
+ const period = pluckNumber(simple, "period");
2100
+ if (!name || !namespace_id || limit === null || period !== 10 && period !== 60) continue;
2101
+ out.push({ type: "ratelimit", name, namespace_id, simple: { limit, period } });
2102
+ }
2103
+ return out;
2102
2104
  }
2103
- function asTomlRecord(value) {
2104
- return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
2105
+ function isObject(v) {
2106
+ return v !== null && typeof v === "object" && !Array.isArray(v);
2105
2107
  }
2106
- function safeGeneratedConfigSegment(value) {
2107
- return value.replace(/[^A-Za-z0-9._-]/gu, "_");
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;
2108
2112
  }
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";
2116
- }
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);
2125
- }
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;
2126
2117
  }
2127
- function hasWranglerConfig(appDir) {
2128
- return existsSync6(join7(appDir, "wrangler.toml"));
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;
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
+ );
2150
+ }
2151
+ return name;
2196
2152
  }
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;
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`);
2244
2156
  }
2245
- return result;
2246
2157
  }
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;
2158
+ function defaultConfigNameForEnv(wranglerEnv) {
2159
+ return wranglerEnv ?? "prd";
2160
+ }
2161
+ async function secretsApi(deployUrl, token, path, init = {}) {
2162
+ const res = await fetch(`${deployUrl}/api/secrets${path}`, {
2163
+ ...init,
2164
+ headers: {
2165
+ Authorization: `Bearer ${token}`,
2166
+ "Content-Type": "application/json",
2167
+ ...init.headers ?? {}
2260
2168
  }
2261
- out += char;
2169
+ });
2170
+ const body = await res.json().catch(() => ({}));
2171
+ if (!res.ok) {
2172
+ const err = new Error(body.error ?? `Secrets request failed (${res.status})`);
2173
+ err.status = res.status;
2174
+ err.apiPath = path;
2175
+ throw err;
2262
2176
  }
2263
- return out;
2177
+ return body;
2178
+ }
2179
+ function listSecrets(deployUrl, token, appId, configName) {
2180
+ return secretsApi(deployUrl, token, `/${appId}/configs/${encodeURIComponent(configName)}`);
2181
+ }
2182
+ function fetchSecretsValues(deployUrl, token, appId, configName) {
2183
+ return secretsApi(deployUrl, token, `/${appId}/configs/${encodeURIComponent(configName)}/values`);
2184
+ }
2185
+ function getSecretPlain(deployUrl, token, appId, configName, key) {
2186
+ return secretsApi(
2187
+ deployUrl,
2188
+ token,
2189
+ `/${appId}/configs/${encodeURIComponent(configName)}/secrets/${encodeURIComponent(key)}?plain=true`
2190
+ );
2191
+ }
2192
+ function setSecret(deployUrl, token, appId, configName, key, value) {
2193
+ return secretsApi(
2194
+ deployUrl,
2195
+ token,
2196
+ `/${appId}/configs/${encodeURIComponent(configName)}/secrets/${encodeURIComponent(key)}`,
2197
+ { method: "PUT", body: JSON.stringify({ value }) }
2198
+ );
2199
+ }
2200
+ function deleteSecret(deployUrl, token, appId, configName, key) {
2201
+ return secretsApi(
2202
+ deployUrl,
2203
+ token,
2204
+ `/${appId}/configs/${encodeURIComponent(configName)}/secrets/${encodeURIComponent(key)}`,
2205
+ { method: "DELETE" }
2206
+ );
2264
2207
  }
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."
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 }) }
2270
2214
  );
2271
2215
  }
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
2216
+ function listConfigs(deployUrl, token, appId) {
2217
+ return secretsApi(deployUrl, token, `/${appId}/configs`);
2218
+ }
2219
+ function createConfig(deployUrl, token, appId, configName, copyFrom) {
2220
+ return secretsApi(deployUrl, token, `/${appId}/configs`, {
2221
+ method: "POST",
2222
+ body: JSON.stringify({ name: configName, ...copyFrom ? { copyFrom } : {} })
2292
2223
  });
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
- `;
2310
- }
2311
- writeSecretFileSync(devVarsPath, body);
2312
2224
  }
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) {
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) {
2322
2231
  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 {
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 };
2242
+ } catch (err) {
2243
+ if (err?.status === 404) return null;
2244
+ throw err;
2327
2245
  }
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";
2332
2246
  }
2333
- function installCommand(appDir) {
2334
- return `${detectPackageManager(appDir)} install`;
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}"`;
2335
2251
  }
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";
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])}`);
2347
2258
  }
2348
- return "missing";
2349
- }
2350
- function tailHint(logPath, platform = process.platform) {
2351
- return platform === "win32" ? `Get-Content -Wait ${logPath}` : `tail -f ${logPath}`;
2259
+ return lines.join("\n");
2352
2260
  }
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
- );
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;
2368
2277
  }
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"] } }
2278
+ const out = {};
2279
+ const lines = trimmed.split(/\r?\n/);
2280
+ for (let i = 0; i < lines.length; i++) {
2281
+ const line = lines[i].trim();
2282
+ if (!line || line.startsWith("#")) continue;
2283
+ const lineNumber = i + 1;
2284
+ const eq = line.indexOf("=");
2285
+ if (eq <= 0) {
2286
+ throw new Error(`Invalid dotenv assignment on line ${lineNumber}: expected KEY=value.`);
2287
+ }
2288
+ const key = line.slice(0, eq).trim();
2289
+ if (!SECRET_NAME_RE.test(key)) {
2290
+ throw new Error(
2291
+ `Invalid secret name on line ${lineNumber}: use letters, numbers, and underscores; start with a letter or underscore.`
2376
2292
  );
2377
2293
  }
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"] }
2294
+ validateSecretName(key);
2295
+ const raw = line.slice(eq + 1).trim();
2296
+ const quote = raw[0] === '"' || raw[0] === "'" ? raw[0] : null;
2297
+ let value;
2298
+ if (!quote) {
2299
+ value = raw;
2300
+ } else if (endsWithUnescapedQuote(raw.slice(1), quote)) {
2301
+ const inner = raw.slice(1, -1);
2302
+ value = quote === '"' ? unescapeDotenv(inner) : inner;
2303
+ } else {
2304
+ const startLine = i + 1;
2305
+ const parts = [raw.slice(1)];
2306
+ let closed = false;
2307
+ while (++i < lines.length) {
2308
+ const cont = lines[i].trimEnd();
2309
+ if (endsWithUnescapedQuote(cont, quote)) {
2310
+ parts.push(cont.slice(0, -1));
2311
+ closed = true;
2312
+ break;
2313
+ }
2314
+ parts.push(lines[i]);
2315
+ }
2316
+ if (!closed) {
2317
+ throw new Error(
2318
+ `Unterminated quoted value for "${key}" (opens on line ${startLine}). Close the ${quote === '"' ? "double" : "single"} quote, or escape newlines as \\n on one line.`
2319
+ );
2320
+ }
2321
+ value = parts.join("\n");
2322
+ if (quote === '"') value = unescapeDotenv(value);
2389
2323
  }
2390
- );
2391
- }
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
- }
2406
- }
2407
- function canonicalPath2(path) {
2408
- try {
2409
- return realpathSync3(path);
2410
- } catch {
2411
- return resolve6(path);
2412
- }
2413
- }
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;
2419
- try {
2420
- process.kill(pid, 0);
2421
- return true;
2422
- } catch (err) {
2423
- return err.code === "EPERM";
2324
+ validateSecretValue(key, value);
2325
+ out[key] = value;
2424
2326
  }
2327
+ return out;
2425
2328
  }
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
- );
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;
2438
2335
  }
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
- );
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
+ }
2461
2345
  }
2346
+ return out;
2462
2347
  }
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;
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";
2475
2355
  }
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;
2481
- }
2482
- if (entry.name === ".DS_Store" || entry.name.startsWith("._")) {
2483
- try {
2484
- rmSync3(path, { force: true });
2485
- removed++;
2486
- } catch {
2487
- }
2488
- }
2356
+ if (format === "shell") {
2357
+ return entries.map(([k, v]) => `export ${k}=${shellSingleQuote(v)}`).join("\n") + "\n";
2489
2358
  }
2490
- return removed;
2359
+ return entries.map(([k, v]) => `${k}=${quoteDotenvValue(v)}`).join("\n") + "\n";
2360
+ }
2361
+ async function refreshSecretsCache(deployUrl, token, appId, wranglerEnv, selectedConfigName = defaultConfigNameForEnv(wranglerEnv)) {
2362
+ const configName = selectedConfigName;
2363
+ const pulled = await pullAppSecretsCache(deployUrl, token, appId, configName);
2364
+ const values = pulled?.values ?? {};
2365
+ const count = Object.keys(values).length;
2366
+ return {
2367
+ pulled,
2368
+ rendered: renderSecretsCache(values, { appId, configName }),
2369
+ // An empty store is the common case and not worth a status line.
2370
+ summary: count ? `Secrets: ${configName} (${count} secret${count === 1 ? "" : "s"})` : null
2371
+ };
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
 
@@ -2531,7 +2412,7 @@ function lintSchema(schema) {
2531
2412
  if (schema.ownerField) {
2532
2413
  const col = schema.columns.find((c) => c.name === schema.ownerField);
2533
2414
  const spoofableRoles = Object.entries(schema.permissions).filter(
2534
- ([role, permissions]) => role !== "owner" && role !== "admin" && permissions.create === true
2415
+ ([role, permissions]) => role !== "owner" && role !== "admin" && permissions.create === true && permissions.update !== "unclaimed-or-own"
2535
2416
  ).map(([role]) => role);
2536
2417
  if (col && !col.userBound && spoofableRoles.length > 0) {
2537
2418
  warnings.push(
@@ -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");
@@ -3176,7 +3045,7 @@ function normalizeTestAccountScope(value) {
3176
3045
  }
3177
3046
  function loadAllTestAccounts(scope = currentTestAccountScope()) {
3178
3047
  const store = readTestAccountStore();
3179
- return mergeTestAccounts(store.scopes[scope] ?? [], store.unscoped);
3048
+ return store.scopes[scope] ?? [];
3180
3049
  }
3181
3050
  function reconcileTestAccountStore(remote, scope = currentTestAccountScope()) {
3182
3051
  const result = reconcileTestAccountScopes(readTestAccountStore(), remote, scope);
@@ -3185,24 +3054,20 @@ function reconcileTestAccountStore(remote, scope = currentTestAccountScope()) {
3185
3054
  }
3186
3055
  function reconcileTestAccountScopes(store, remote, scope) {
3187
3056
  const scoped = store.scopes[scope] ?? [];
3188
- const current = reconcileTestAccounts(scoped, remote);
3189
- const migrated = reconcileTestAccounts(store.unscoped, remote);
3190
- const accounts = mergeTestAccounts(current, migrated);
3057
+ const accounts = reconcileTestAccounts(scoped, remote);
3191
3058
  return {
3192
3059
  accounts,
3193
- removed: Math.max(0, scoped.length - current.length),
3060
+ removed: Math.max(0, scoped.length - accounts.length),
3194
3061
  store: {
3195
3062
  version: 2,
3196
- scopes: { ...store.scopes, [scope]: accounts },
3197
- unscoped: store.unscoped.filter((account) => !remote.some((item) => accountMatchesRemote(account, item)))
3063
+ scopes: { ...store.scopes, [scope]: accounts }
3198
3064
  }
3199
3065
  };
3200
3066
  }
3201
3067
  function reconcileTestAccounts(local, remote) {
3202
3068
  const localById = new Map(local.filter((account) => account.id).map((account) => [account.id, account]));
3203
- const legacyByEmail = new Map(local.filter((account) => !account.id).map((account) => [account.email, account]));
3204
3069
  return remote.flatMap((account) => {
3205
- const stored = localById.get(account.id) ?? legacyByEmail.get(account.email);
3070
+ const stored = localById.get(account.id);
3206
3071
  return stored ? [{ ...stored, ...account }] : [];
3207
3072
  });
3208
3073
  }
@@ -3213,10 +3078,7 @@ function upsertTestAccount(account, scope = currentTestAccountScope()) {
3213
3078
  );
3214
3079
  saveTestAccountStore({
3215
3080
  version: 2,
3216
- scopes: { ...store.scopes, [scope]: [...accounts, account] },
3217
- unscoped: store.unscoped.filter(
3218
- (stored) => stored.id !== account.id && stored.email !== account.email
3219
- )
3081
+ scopes: { ...store.scopes, [scope]: [...accounts, account] }
3220
3082
  });
3221
3083
  }
3222
3084
  function removeTestAccounts(ids, emails = [], scope = currentTestAccountScope()) {
@@ -3226,41 +3088,31 @@ function removeTestAccounts(ids, emails = [], scope = currentTestAccountScope())
3226
3088
  const store = readTestAccountStore();
3227
3089
  saveTestAccountStore({
3228
3090
  version: 2,
3229
- scopes: { ...store.scopes, [scope]: (store.scopes[scope] ?? []).filter(keep) },
3230
- unscoped: store.unscoped.filter(keep)
3091
+ scopes: { ...store.scopes, [scope]: (store.scopes[scope] ?? []).filter(keep) }
3231
3092
  });
3232
3093
  }
3233
3094
  function readTestAccountStore() {
3234
- if (!existsSync12(TEST_ACCOUNTS_PATH)) return { version: 2, scopes: {}, unscoped: [] };
3095
+ if (!existsSync11(TEST_ACCOUNTS_PATH)) return { version: 2, scopes: {} };
3235
3096
  try {
3236
- const raw = JSON.parse(readFileSync14(TEST_ACCOUNTS_PATH, "utf-8"));
3237
- if (Array.isArray(raw)) return { version: 2, scopes: {}, unscoped: validTestAccounts(raw) };
3238
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
3239
- return { version: 2, scopes: {}, unscoped: [] };
3097
+ const raw = JSON.parse(readFileSync13(TEST_ACCOUNTS_PATH, "utf-8"));
3098
+ if (Array.isArray(raw)) return { version: 2, scopes: {} };
3099
+ if (!raw || typeof raw !== "object") {
3100
+ return { version: 2, scopes: {} };
3240
3101
  }
3241
3102
  const candidate = raw;
3242
3103
  const scopes = candidate.scopes && typeof candidate.scopes === "object" && !Array.isArray(candidate.scopes) ? Object.fromEntries(Object.entries(candidate.scopes).map(([key, value]) => [key, validTestAccounts(value)])) : {};
3243
- return { version: 2, scopes, unscoped: validTestAccounts(candidate.unscoped) };
3104
+ return { version: 2, scopes };
3244
3105
  } catch {
3245
- return { version: 2, scopes: {}, unscoped: [] };
3106
+ return { version: 2, scopes: {} };
3246
3107
  }
3247
3108
  }
3248
3109
  function validTestAccounts(value) {
3249
3110
  if (!Array.isArray(value)) return [];
3250
- return value.filter((item) => Boolean(
3251
- item && typeof item === "object" && typeof item.email === "string" && typeof item.password === "string"
3252
- ));
3253
- }
3254
- function mergeTestAccounts(...groups) {
3255
- const accounts = /* @__PURE__ */ new Map();
3256
- for (const account of groups.flat()) {
3257
- const key = account.id ? `id:${account.id}` : `email:${account.email}`;
3258
- if (!accounts.has(key)) accounts.set(key, account);
3259
- }
3260
- return [...accounts.values()];
3261
- }
3262
- function accountMatchesRemote(local, remote) {
3263
- return local.id ? local.id === remote.id : local.email === remote.email;
3111
+ return value.filter(
3112
+ (item) => Boolean(
3113
+ item && typeof item === "object" && typeof item.id === "string" && typeof item.email === "string" && typeof item.password === "string"
3114
+ )
3115
+ );
3264
3116
  }
3265
3117
  function saveTestAccountStore(store) {
3266
3118
  mkdirSync5(join13(homedir3(), ".deepspace"), { recursive: true });
@@ -3271,7 +3123,7 @@ function saveTestAccountStore(store) {
3271
3123
  var SESSION_COOKIE2 = "__Secure-better-auth.session_token";
3272
3124
  var AUTH_URL4 = process.env.DEEPSPACE_AUTH_URL ?? PLATFORM_URLS.auth;
3273
3125
  function sessionCookie() {
3274
- const token = readFileSync15(SESSION_PATH, "utf-8").trim();
3126
+ const token = readFileSync14(SESSION_PATH, "utf-8").trim();
3275
3127
  return `${SESSION_COOKIE2}=${encodeURIComponent(token)}`;
3276
3128
  }
3277
3129
  async function fetchRemoteTestAccounts() {
@@ -3318,11 +3170,24 @@ async function syncTestAccountStore() {
3318
3170
 
3319
3171
  // src/cli/commands/test.ts
3320
3172
  var DEPLOY_URL2 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
3173
+ var PLAYWRIGHT_OUTPUT_DIR = ".deepspace/test-results";
3174
+ function playwrightTestArgs(testFiles) {
3175
+ return [
3176
+ "playwright",
3177
+ "test",
3178
+ "--config",
3179
+ "tests/playwright.config.ts",
3180
+ "--output",
3181
+ PLAYWRIGHT_OUTPUT_DIR,
3182
+ ...testFiles
3183
+ ];
3184
+ }
3321
3185
  var test_default = defineDeepspaceCommand({
3322
3186
  meta: {
3323
3187
  name: "test",
3324
3188
  description: "Run tests for your DeepSpace app"
3325
3189
  },
3190
+ jsonDescription: "Stream test output; emit the final JSON result on the last line",
3326
3191
  args: {
3327
3192
  suite: {
3328
3193
  type: "positional",
@@ -3375,10 +3240,8 @@ var test_default = defineDeepspaceCommand({
3375
3240
  if (appIdForSecrets) {
3376
3241
  try {
3377
3242
  const refreshed = await refreshSecretsCache(DEPLOY_URL2, token, appIdForSecrets, wranglerEnv);
3378
- if (refreshed) {
3379
- generatedSecretsCache = refreshed.rendered;
3380
- if (refreshed.summary) say(refreshed.summary);
3381
- }
3243
+ generatedSecretsCache = refreshed.rendered;
3244
+ if (refreshed.summary) say(refreshed.summary);
3382
3245
  } catch (err) {
3383
3246
  throw new Refusal(
3384
3247
  `Failed to refresh app secrets: ${err instanceof Error ? err.message : String(err)}`,
@@ -3386,17 +3249,17 @@ var test_default = defineDeepspaceCommand({
3386
3249
  );
3387
3250
  }
3388
3251
  }
3389
- const sharedDevVarsCache = generatedSecretsCache !== void 0;
3390
3252
  await writeDevVars(appDir, ownerId, token, wranglerEnv, {
3391
- generatedSecretsCache,
3392
- sharedDevVarsCache
3253
+ generatedSecretsCache
3393
3254
  });
3394
3255
  if (suite !== "unit") {
3395
3256
  try {
3396
3257
  const { removed } = await syncTestAccountStore();
3397
3258
  if (removed > 0) say(`Removed ${removed} stale test account credential(s).`);
3398
3259
  } catch (error) {
3399
- say(`Warning: could not reconcile test accounts: ${error instanceof Error ? error.message : String(error)}`);
3260
+ say(
3261
+ `Warning: could not reconcile test accounts: ${error instanceof Error ? error.message : String(error)}`
3262
+ );
3400
3263
  }
3401
3264
  }
3402
3265
  if (suite !== "unit") {
@@ -3406,46 +3269,32 @@ var test_default = defineDeepspaceCommand({
3406
3269
  let exitCode = 0;
3407
3270
  switch (suite) {
3408
3271
  case "smoke":
3409
- exitCode = runPlaywright(
3410
- appDir,
3411
- ["tests/smoke.spec.ts"],
3412
- port,
3413
- wranglerEnv,
3414
- sharedDevVarsCache
3415
- );
3272
+ exitCode = runPlaywright(appDir, ["tests/smoke.spec.ts"], port, wranglerEnv);
3416
3273
  break;
3417
3274
  case "api":
3418
- exitCode = runPlaywright(
3419
- appDir,
3420
- ["tests/api.spec.ts"],
3421
- port,
3422
- wranglerEnv,
3423
- sharedDevVarsCache
3424
- );
3275
+ exitCode = runPlaywright(appDir, ["tests/api.spec.ts"], port, wranglerEnv);
3425
3276
  break;
3426
3277
  case "e2e":
3427
- exitCode = runPlaywright(appDir, [], port, wranglerEnv, sharedDevVarsCache);
3278
+ exitCode = runPlaywright(appDir, [], port, wranglerEnv);
3428
3279
  break;
3429
3280
  case "unit":
3430
3281
  exitCode = runVitest(appDir);
3431
3282
  break;
3432
3283
  case "all":
3433
3284
  exitCode = runVitest(appDir);
3434
- if (exitCode === 0)
3435
- exitCode = runPlaywright(appDir, [], port, wranglerEnv, sharedDevVarsCache);
3285
+ if (exitCode === 0) exitCode = runPlaywright(appDir, [], port, wranglerEnv);
3436
3286
  break;
3437
3287
  case "default":
3438
3288
  exitCode = runPlaywright(
3439
3289
  appDir,
3440
3290
  ["tests/smoke.spec.ts", "tests/api.spec.ts"],
3441
3291
  port,
3442
- wranglerEnv,
3443
- sharedDevVarsCache
3292
+ wranglerEnv
3444
3293
  );
3445
3294
  break;
3446
3295
  default:
3447
3296
  if (suite.endsWith(".spec.ts")) {
3448
- exitCode = runPlaywright(appDir, [suite], port, wranglerEnv, sharedDevVarsCache);
3297
+ exitCode = runPlaywright(appDir, [suite], port, wranglerEnv);
3449
3298
  } else {
3450
3299
  throw new Refusal(
3451
3300
  `Unknown test suite: ${suite}
@@ -3462,24 +3311,23 @@ Available: smoke, api, e2e, unit, all`,
3462
3311
  return { data: { suite, port, appDir, wranglerEnv: wranglerEnv ?? null } };
3463
3312
  }
3464
3313
  });
3465
- function runPlaywright(appDir, testFiles, port, wranglerEnv, sharedDevVarsCache = false) {
3314
+ function runPlaywright(appDir, testFiles, port, wranglerEnv) {
3466
3315
  let wranglerConfig;
3467
3316
  try {
3468
- wranglerConfig = prepareWranglerEnvConfig(appDir, wranglerEnv, { sharedDevVarsCache });
3317
+ wranglerConfig = prepareWranglerEnvConfig(appDir, wranglerEnv);
3469
3318
  } catch (err) {
3470
3319
  console.error(err instanceof Error ? err.message : String(err));
3471
3320
  return 1;
3472
3321
  }
3473
3322
  try {
3474
- const result = spawnSync4(
3475
- "npx",
3476
- ["playwright", "test", "--config", "tests/playwright.config.ts", ...testFiles],
3477
- {
3478
- cwd: appDir,
3479
- stdio: "inherit",
3480
- env: wranglerViteEnv(process.env, wranglerConfig, { DEEPSPACE_PORT: String(port) })
3481
- }
3482
- );
3323
+ const result = spawnSync4("npx", playwrightTestArgs(testFiles), {
3324
+ cwd: appDir,
3325
+ stdio: "inherit",
3326
+ env: wranglerViteEnv(process.env, wranglerConfig, {
3327
+ DEEPSPACE_PORT: String(port),
3328
+ PLAYWRIGHT_HTML_OUTPUT_DIR: ".deepspace/playwright-report"
3329
+ })
3330
+ });
3483
3331
  return result.status ?? 1;
3484
3332
  } finally {
3485
3333
  wranglerConfig.cleanup();
@@ -3997,33 +3845,33 @@ function describeFilesFailure(status3, body) {
3997
3845
  import * as p6 from "@clack/prompts";
3998
3846
  import { execSync as execSync2 } from "child_process";
3999
3847
  import { createHash as createHash2 } from "crypto";
4000
- import { existsSync as existsSync15, readFileSync as readFileSync18, readdirSync as readdirSync4 } from "fs";
3848
+ import { chmodSync as chmodSync2, existsSync as existsSync14, lstatSync as lstatSync2, readFileSync as readFileSync17, readdirSync as readdirSync4 } from "fs";
4001
3849
  import { dirname as dirname6, join as join16, resolve as resolve11 } from "path";
4002
3850
 
4003
3851
  // src/documentation/deploy.ts
4004
- import { existsSync as existsSync13, readFileSync as readFileSync16 } from "fs";
3852
+ import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
4005
3853
  import { join as join14 } from "path";
4006
3854
  function readDocumentationDeployManifest(appDir, clientDir) {
4007
- if (!existsSync13(join14(appDir, "documentation.json"))) return void 0;
3855
+ if (!existsSync12(join14(appDir, "documentation.json"))) return void 0;
4008
3856
  const manifestPath = join14(clientDir, "_documentation", "manifest.json");
4009
- if (!existsSync13(manifestPath)) {
3857
+ if (!existsSync12(manifestPath)) {
4010
3858
  throw new Error(
4011
3859
  "documentation.json exists but the build produced no documentation manifest. Ensure vite.config.ts includes deepSpaceDocumentation() from deepspace/documentation."
4012
3860
  );
4013
3861
  }
4014
- return JSON.parse(readFileSync16(manifestPath, "utf8"));
3862
+ return JSON.parse(readFileSync15(manifestPath, "utf8"));
4015
3863
  }
4016
3864
 
4017
3865
  // src/cli/commands/update/app-migrations.ts
4018
3866
  import {
4019
3867
  closeSync,
4020
3868
  constants,
4021
- existsSync as existsSync14,
3869
+ existsSync as existsSync13,
4022
3870
  fstatSync,
4023
3871
  ftruncateSync,
4024
3872
  lstatSync,
4025
3873
  openSync,
4026
- readFileSync as readFileSync17,
3874
+ readFileSync as readFileSync16,
4027
3875
  writeSync
4028
3876
  } from "fs";
4029
3877
  import { join as join15 } from "path";
@@ -4032,6 +3880,7 @@ import { join as join15 } from "path";
4032
3880
  var APP_MIGRATION_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/;
4033
3881
  var MAX_APP_MIGRATION_IDS = 256;
4034
3882
  var WORKER_OWNED_NOT_FOUND_MIGRATION_ID = "2026-08-worker-owned-not-found";
3883
+ var SECURE_ROOM_BOUNDARIES_MIGRATION_ID = "2026-08-secure-room-boundaries";
4035
3884
  function validateAppMigrationIds(value) {
4036
3885
  if (!Array.isArray(value)) {
4037
3886
  return { valid: false, reason: "app migrations must be a JSON string array" };
@@ -4065,6 +3914,12 @@ var SOURCE_MIGRATIONS = [
4065
3914
  description: "Let the worker answer misses, so deleted files 404 instead of serving HTML",
4066
3915
  transform: migrateWorkerOwnedNotFound,
4067
3916
  findBlockers: findWorkerOwnedNotFoundBlockers
3917
+ },
3918
+ {
3919
+ id: SECURE_ROOM_BOUNDARIES_MIGRATION_ID,
3920
+ description: "Secure room identity, job mutations, and production debug routes",
3921
+ transform: migrateSecureRoomBoundaries,
3922
+ findBlockers: findSecureRoomBoundaryBlockers
4068
3923
  }
4069
3924
  ];
4070
3925
  var APP_MIGRATION_DEFINITIONS = SOURCE_MIGRATIONS.map(
@@ -4072,7 +3927,7 @@ var APP_MIGRATION_DEFINITIONS = SOURCE_MIGRATIONS.map(
4072
3927
  );
4073
3928
  function readAppliedAppMigrations(appDir) {
4074
3929
  const path = join15(appDir, APP_MIGRATIONS_MANIFEST);
4075
- if (!existsSync14(path)) return [];
3930
+ if (!existsSync13(path)) return [];
4076
3931
  const source = readRegularFile(path);
4077
3932
  let parsed;
4078
3933
  try {
@@ -4133,7 +3988,7 @@ function planAppMigrations(appDir) {
4133
3988
  if (pending.length > 0 && blockers.length === 0) {
4134
3989
  const manifestPath = `${appPrefix}${APP_MIGRATIONS_MANIFEST}`;
4135
3990
  const absoluteManifestPath = join15(repo, manifestPath);
4136
- const before = existsSync14(absoluteManifestPath) ? readRegularFile(absoluteManifestPath) : null;
3991
+ const before = existsSync13(absoluteManifestPath) ? readRegularFile(absoluteManifestPath) : null;
4137
3992
  const after = `${JSON.stringify([...appliedIds, ...pending.map(({ id }) => id)], null, 2)}
4138
3993
  `;
4139
3994
  if (before !== after) edits.push({ path: manifestPath, before, after });
@@ -4149,7 +4004,7 @@ function applyAppMigrationPlan(appDir, plan) {
4149
4004
  const repo = repoToplevel(appDir);
4150
4005
  for (const edit of plan.edits) {
4151
4006
  const absolutePath = join15(repo, edit.path);
4152
- const current = existsSync14(absolutePath) ? readRegularFile(absolutePath) : null;
4007
+ const current = existsSync13(absolutePath) ? readRegularFile(absolutePath) : null;
4153
4008
  if (current !== edit.before) {
4154
4009
  throw new Error(`Migration input changed after planning: ${edit.path}`);
4155
4010
  }
@@ -4168,7 +4023,7 @@ function readRegularFile(path) {
4168
4023
  if (!opened.isFile() || opened.dev !== after.dev || opened.ino !== after.ino) {
4169
4024
  throw new Error(`Migration input changed while opening: ${path}`);
4170
4025
  }
4171
- return readFileSync17(fd, "utf-8");
4026
+ return readFileSync16(fd, "utf-8");
4172
4027
  } finally {
4173
4028
  closeSync(fd);
4174
4029
  }
@@ -4194,7 +4049,7 @@ function writeRegularFile(path, before, after) {
4194
4049
  if (!opened.isFile() || opened.dev !== currentPath.dev || opened.ino !== currentPath.ino) {
4195
4050
  throw new Error(`Migration input changed while opening: ${path}`);
4196
4051
  }
4197
- if (readFileSync17(fd, "utf-8") !== before) {
4052
+ if (readFileSync16(fd, "utf-8") !== before) {
4198
4053
  throw new Error(`Migration input changed after planning: ${path}`);
4199
4054
  }
4200
4055
  ftruncateSync(fd, 0);
@@ -4208,6 +4063,248 @@ function isSourcePath(repoPath, appPrefix) {
4208
4063
  if (repoPath.endsWith("wrangler.toml")) return true;
4209
4064
  return /\.(?:[cm]?[jt]sx?)$/.test(repoPath);
4210
4065
  }
4066
+ var LEGACY_AUTHENTICATED_ROOM_HELPER = `const IDENTITY_QUERY_PARAMS = ['userId', 'userName', 'userEmail', 'userImageUrl', 'role'] as const
4067
+
4068
+ function authenticatedRoomUrl(
4069
+ requestUrl: string,
4070
+ auth: VerifyResult | null,
4071
+ extraParams?: (auth: VerifyResult) => Record<string, string>,
4072
+ ): URL {
4073
+ const doUrl = new URL(requestUrl)
4074
+ doUrl.searchParams.delete('token')
4075
+ for (const key of IDENTITY_QUERY_PARAMS) {
4076
+ doUrl.searchParams.delete(key)
4077
+ }
4078
+
4079
+ if (!auth) return doUrl
4080
+
4081
+ doUrl.searchParams.set('userId', auth.userId)
4082
+ if (auth.claims.name) doUrl.searchParams.set('userName', auth.claims.name)
4083
+ if (auth.claims.email) doUrl.searchParams.set('userEmail', auth.claims.email)
4084
+ if (auth.claims.image) doUrl.searchParams.set('userImageUrl', auth.claims.image)
4085
+ if (extraParams) {
4086
+ for (const [key, value] of Object.entries(extraParams(auth))) {
4087
+ doUrl.searchParams.set(key, value)
4088
+ }
4089
+ }
4090
+ return doUrl
4091
+ }`;
4092
+ var IDENTITY_ROUTE_REPLACEMENTS = [
4093
+ [
4094
+ `import { verifyJwt } from 'deepspace/worker'`,
4095
+ `import { authenticatedRoomRequest, verifyJwt } from 'deepspace/worker'`,
4096
+ 1
4097
+ ],
4098
+ [LEGACY_AUTHENTICATED_ROOM_HELPER, "", 1],
4099
+ [
4100
+ `function wsRoute(
4101
+ doNamespace: (env: Env) => DurableObjectNamespace,
4102
+ extraParams?: (auth: VerifyResult) => Record<string, string>,
4103
+ ) {`,
4104
+ `function wsRoute(
4105
+ doNamespace: (env: Env) => DurableObjectNamespace,
4106
+ extraIdentity?: (auth: VerifyResult) => { role?: string },
4107
+ ) {`,
4108
+ 1
4109
+ ],
4110
+ [
4111
+ ` const doUrl = authenticatedRoomUrl(c.req.url, auth, extraParams)`,
4112
+ ` const roomRequest = authenticatedRoomRequest(
4113
+ c.req.raw,
4114
+ auth,
4115
+ auth ? extraIdentity?.(auth) : undefined,
4116
+ )`,
4117
+ 1
4118
+ ],
4119
+ [
4120
+ ` const doUrl = authenticatedRoomUrl(c.req.url, auth, () => ({ role }))`,
4121
+ ` const roomRequest = authenticatedRoomRequest(c.req.raw, auth, { role })`,
4122
+ 1
4123
+ ],
4124
+ [
4125
+ ` return stub.fetch(new Request(doUrl.toString(), c.req.raw))`,
4126
+ ` return stub.fetch(roomRequest)`,
4127
+ 2
4128
+ ]
4129
+ ];
4130
+ function occurrences(source, pattern) {
4131
+ return source.split(pattern).length - 1;
4132
+ }
4133
+ var SECURE_JOB_ROOM_CONSTRUCTOR = ` constructor(state: DurableObjectState, env: Env) {
4134
+ super(state, env, {
4135
+ authorizeWrite: async (user) => {
4136
+ if (user.userId.startsWith('anon-')) return false
4137
+ const role = await resolveAppRole(env, user.userId)
4138
+ return role === 'member' || role === 'admin'
4139
+ },
4140
+ })
4141
+ }`;
4142
+ var APP_JOB_ROOM_CLASS = `export class AppJobRoom extends JobRoom<Env> {`;
4143
+ var SECURE_APP_JOB_ROOM = `${APP_JOB_ROOM_CLASS}
4144
+ ${SECURE_JOB_ROOM_CONSTRUCTOR}`;
4145
+ function hasSecureAppJobRoom(source) {
4146
+ return occurrences(source, "class AppJobRoom") === 1 && source.includes(SECURE_APP_JOB_ROOM);
4147
+ }
4148
+ var WORKER_JOB_REPLACEMENTS = [
4149
+ [
4150
+ `import type { DOBindings, DOManifest, Job, JobContext } from 'deepspace/worker'`,
4151
+ `import { resolveAppRole } from 'deepspace/worker'
4152
+ import type { DOBindings, DOManifest, Job, JobContext } from 'deepspace/worker'`
4153
+ ],
4154
+ [
4155
+ `export class AppJobRoom extends JobRoom<Env> {
4156
+ constructor(state: DurableObjectState, env: Env) {
4157
+ super(state, env)
4158
+ }`,
4159
+ `export class AppJobRoom extends JobRoom<Env> {
4160
+ ${SECURE_JOB_ROOM_CONSTRUCTOR}`
4161
+ ]
4162
+ ];
4163
+ var HTTP_ROLE_IMPORT = `import { resolveAppRole } from 'deepspace/worker'`;
4164
+ var HTTP_ROLE_IMPORT_SEAM = `import type { AppContext, Env } from '../../worker.js'`;
4165
+ var DEBUG_ROUTE_SEAM = ` app.all('/api/debug/*', async (c) => {
4166
+ if (c.env.ALLOW_DEBUG_ROUTES !== 'true') {
4167
+ return c.notFound()
4168
+ }
4169
+ const stub = c.env.RECORD_ROOMS.get(`;
4170
+ var DOUBLE_QUOTED_DEBUG_ROUTE_SEAM = DEBUG_ROUTE_SEAM.replace(
4171
+ "app.all('/api/debug/*'",
4172
+ 'app.all("/api/debug/*"'
4173
+ );
4174
+ var SECURE_DEBUG_ROUTE_SEAM = ` app.all('/api/debug/*', async (c) => {
4175
+ if (c.env.ALLOW_DEBUG_ROUTES !== 'true') {
4176
+ return c.notFound()
4177
+ }
4178
+ const auth = await resolveAuth(c.req.raw, c.env)
4179
+ if (!auth) return c.json({ error: 'unauthorized' }, 401)
4180
+ if ((await resolveAppRole(c.env, auth.userId)) !== 'admin') {
4181
+ return c.json({ error: 'forbidden' }, 403)
4182
+ }
4183
+ const stub = c.env.RECORD_ROOMS.get(`;
4184
+ var DOUBLE_QUOTED_SECURE_DEBUG_ROUTE_SEAM = SECURE_DEBUG_ROUTE_SEAM.replace(
4185
+ "app.all('/api/debug/*'",
4186
+ 'app.all("/api/debug/*"'
4187
+ );
4188
+ var DEBUG_ROUTE_REPLACEMENTS = [
4189
+ [DEBUG_ROUTE_SEAM, SECURE_DEBUG_ROUTE_SEAM],
4190
+ [DOUBLE_QUOTED_DEBUG_ROUTE_SEAM, DOUBLE_QUOTED_SECURE_DEBUG_ROUTE_SEAM]
4191
+ ];
4192
+ var DEBUG_ROUTE_PATTERN = /\bapp\.all\(\s*(['"])\/api\/debug\/\*\1\s*,/;
4193
+ function debugRouteOccurrences(source) {
4194
+ return source.match(new RegExp(DEBUG_ROUTE_PATTERN.source, "g"))?.length ?? 0;
4195
+ }
4196
+ function secureDebugRouteOccurrences(source) {
4197
+ return DEBUG_ROUTE_REPLACEMENTS.reduce(
4198
+ (count, [, secure]) => count + occurrences(source, secure),
4199
+ 0
4200
+ );
4201
+ }
4202
+ function hasSecureDebugRoute(source) {
4203
+ return debugRouteOccurrences(source) === 1 && secureDebugRouteOccurrences(source) === 1 && occurrences(source, HTTP_ROLE_IMPORT) === 1;
4204
+ }
4205
+ function migrateSecureRoomBoundaries(source, file) {
4206
+ if (file.endsWith("src/server/realtime-routes.ts")) {
4207
+ return migrateRealtimeRoomBoundaries(source);
4208
+ }
4209
+ if (file.endsWith("worker.ts")) return migrateWorkerJobAuthorization(source);
4210
+ if (file.endsWith("src/server/http-routes.ts")) return migrateDebugRouteAuthorization(source);
4211
+ return { source, replacements: 0 };
4212
+ }
4213
+ function migrateRealtimeRoomBoundaries(source) {
4214
+ let migrated = source;
4215
+ let replacements = 0;
4216
+ if (!migrated.includes("authenticatedRoomRequest(")) {
4217
+ if (IDENTITY_ROUTE_REPLACEMENTS.some(
4218
+ ([before, , expected]) => occurrences(migrated, before) !== expected
4219
+ )) {
4220
+ return { source, replacements: 0 };
4221
+ }
4222
+ for (const [before, after, expected] of IDENTITY_ROUTE_REPLACEMENTS) {
4223
+ migrated = migrated.replaceAll(before, after);
4224
+ replacements += expected;
4225
+ }
4226
+ }
4227
+ return { source: migrated, replacements };
4228
+ }
4229
+ function migrateWorkerJobAuthorization(source) {
4230
+ if (!source.includes("class AppJobRoom")) return { source, replacements: 0 };
4231
+ if (hasSecureAppJobRoom(source)) return { source, replacements: 0 };
4232
+ if (WORKER_JOB_REPLACEMENTS.some(([before]) => occurrences(source, before) !== 1)) {
4233
+ return { source, replacements: 0 };
4234
+ }
4235
+ let migrated = source;
4236
+ for (const [before, after] of WORKER_JOB_REPLACEMENTS) migrated = migrated.replace(before, after);
4237
+ return { source: migrated, replacements: WORKER_JOB_REPLACEMENTS.length };
4238
+ }
4239
+ function migrateDebugRouteAuthorization(source) {
4240
+ if (debugRouteOccurrences(source) === 0 || hasSecureDebugRoute(source)) {
4241
+ return { source, replacements: 0 };
4242
+ }
4243
+ const matchingRoutes = DEBUG_ROUTE_REPLACEMENTS.filter(
4244
+ ([before2]) => occurrences(source, before2) === 1
4245
+ );
4246
+ if (debugRouteOccurrences(source) !== 1 || matchingRoutes.length !== 1 || occurrences(source, HTTP_ROLE_IMPORT) !== 0 || occurrences(source, HTTP_ROLE_IMPORT_SEAM) !== 1) {
4247
+ return { source, replacements: 0 };
4248
+ }
4249
+ const [before, after] = matchingRoutes[0];
4250
+ return {
4251
+ source: source.replace(HTTP_ROLE_IMPORT_SEAM, `${HTTP_ROLE_IMPORT_SEAM}
4252
+ ${HTTP_ROLE_IMPORT}`).replace(before, after),
4253
+ replacements: 2
4254
+ };
4255
+ }
4256
+ function findSecureRoomBoundaryBlockers(source, file) {
4257
+ if (file.endsWith("worker.ts") && source.includes("class AppJobRoom")) {
4258
+ if (!hasSecureAppJobRoom(source)) {
4259
+ return [
4260
+ roomBoundaryBlocker(
4261
+ file,
4262
+ source,
4263
+ "class AppJobRoom",
4264
+ "Configure AppJobRoom with authorizeWrite using the current users.role."
4265
+ )
4266
+ ];
4267
+ }
4268
+ return [];
4269
+ }
4270
+ if (file.endsWith("src/server/http-routes.ts") && debugRouteOccurrences(source) > 0) {
4271
+ if (!hasSecureDebugRoute(source)) {
4272
+ const route = source.match(DEBUG_ROUTE_PATTERN)?.[0] ?? "/api/debug/*";
4273
+ return [
4274
+ roomBoundaryBlocker(
4275
+ file,
4276
+ source,
4277
+ route,
4278
+ "Require verified owner/admin access inside the enabled debug-route handler."
4279
+ )
4280
+ ];
4281
+ }
4282
+ return [];
4283
+ }
4284
+ if (!file.endsWith("src/server/realtime-routes.ts")) return [];
4285
+ const lines = source.split("\n");
4286
+ let index = lines.findIndex(
4287
+ (line) => line.includes("authenticatedRoomUrl") || /searchParams\.(?:set|append)\(\s*['"](?:userId|userName|userEmail|userImageUrl|role)['"]/.test(
4288
+ line
4289
+ )
4290
+ );
4291
+ if (index < 0 && /\bauth\.(?:userId|claims)\b/.test(source)) {
4292
+ index = lines.findIndex((line) => /searchParams\.(?:set|append)\s*\(/.test(line));
4293
+ }
4294
+ if (index < 0) return [];
4295
+ return [
4296
+ {
4297
+ migrationId: SECURE_ROOM_BOUNDARIES_MIGRATION_ID,
4298
+ file,
4299
+ line: index + 1,
4300
+ message: "This customized room proxy still places verified user identity in its internal URL. Replace that forwarding code with authenticatedRoomRequest() from deepspace/worker before the Durable Object fetch."
4301
+ }
4302
+ ];
4303
+ }
4304
+ function roomBoundaryBlocker(file, source, seam, message) {
4305
+ const line = source.slice(0, source.indexOf(seam)).split("\n").length;
4306
+ return { migrationId: SECURE_ROOM_BOUNDARIES_MIGRATION_ID, file, line, message };
4307
+ }
4211
4308
  function migrateWorkerOwnedNotFound(source, file) {
4212
4309
  let replacements = 0;
4213
4310
  if (file.endsWith("wrangler.toml")) {
@@ -4273,12 +4370,22 @@ function findWorkerOwnedNotFoundBlockers(source, file) {
4273
4370
 
4274
4371
  // src/cli/commands/deploy/build.ts
4275
4372
  var RESERVED_RUN_WORKER_FIRST2 = new Set(RESERVED_RUN_WORKER_FIRST);
4373
+ function secureBuildDevVars(workerDir) {
4374
+ const path = join16(workerDir, ".dev.vars");
4375
+ if (!existsSync14(path)) return false;
4376
+ const stat = lstatSync2(path);
4377
+ if (!stat.isFile() || stat.isSymbolicLink()) {
4378
+ throw new Error(`Refusing unsafe build secret path: ${path}`);
4379
+ }
4380
+ chmodSync2(path, 384);
4381
+ return true;
4382
+ }
4276
4383
  function declaredCompatibilityDate(config) {
4277
4384
  const declared = config.compatibility_date;
4278
4385
  return typeof declared === "string" && /^\d{4}-\d{2}-\d{2}$/.test(declared) ? declared : null;
4279
4386
  }
4280
4387
  async function buildDeployBundle(options) {
4281
- const { appDir, appName, envName, sharedDevVarsCache, output, spinner: spinner2 } = options;
4388
+ const { appDir, appName, envName, output, spinner: spinner2 } = options;
4282
4389
  const junk = removeMacosJunk(appDir);
4283
4390
  if (junk > 0) p6.log.info(`Removed ${junk} macOS metadata file(s) (._*, .DS_Store)`);
4284
4391
  const lintFindings = await lintProjectSchemas(appDir);
@@ -4288,9 +4395,7 @@ async function buildDeployBundle(options) {
4288
4395
  spinner2.start("Building...");
4289
4396
  let preparedWranglerConfig;
4290
4397
  try {
4291
- preparedWranglerConfig = prepareWranglerEnvConfig(appDir, envName, {
4292
- sharedDevVarsCache
4293
- });
4398
+ preparedWranglerConfig = prepareWranglerEnvConfig(appDir, envName);
4294
4399
  const appDomain = DEEPSPACE_ENV === "staging" ? "spacestest.com" : "app.space";
4295
4400
  execSync2("npx vite build", {
4296
4401
  cwd: appDir,
@@ -4311,18 +4416,18 @@ async function buildDeployBundle(options) {
4311
4416
  }
4312
4417
  spinner2.stop("Built");
4313
4418
  const deployConfigPath = join16(appDir, ".wrangler", "deploy", "config.json");
4314
- if (!existsSync15(deployConfigPath)) {
4419
+ if (!existsSync14(deployConfigPath)) {
4315
4420
  output.die(
4316
4421
  "Build output config not found at .wrangler/deploy/config.json",
4317
4422
  "build_output_missing"
4318
4423
  );
4319
4424
  }
4320
- const deployConfig = JSON.parse(readFileSync18(deployConfigPath, "utf-8"));
4425
+ const deployConfig = JSON.parse(readFileSync17(deployConfigPath, "utf-8"));
4321
4426
  const outputWranglerPath = resolve11(dirname6(deployConfigPath), deployConfig.configPath);
4322
- if (!existsSync15(outputWranglerPath)) {
4427
+ if (!existsSync14(outputWranglerPath)) {
4323
4428
  output.die(`Output wrangler.json not found at ${outputWranglerPath}`, "build_output_missing");
4324
4429
  }
4325
- const outputConfig = JSON.parse(readFileSync18(outputWranglerPath, "utf-8"));
4430
+ const outputConfig = JSON.parse(readFileSync17(outputWranglerPath, "utf-8"));
4326
4431
  if (outputConfig.name && outputConfig.name !== appName) {
4327
4432
  const envHint = envName ? ` (--env ${envName})` : "";
4328
4433
  output.die(
@@ -4331,12 +4436,17 @@ async function buildDeployBundle(options) {
4331
4436
  );
4332
4437
  }
4333
4438
  const workerDir = dirname6(outputWranglerPath);
4439
+ try {
4440
+ secureBuildDevVars(workerDir);
4441
+ } catch (error) {
4442
+ output.die(errorMessage(error), "build_output_unsafe");
4443
+ }
4334
4444
  const workerBundlePath = join16(workerDir, outputConfig.main);
4335
4445
  const clientDir = outputConfig.assets?.directory ? resolve11(workerDir, outputConfig.assets.directory) : null;
4336
- if (!existsSync15(workerBundlePath)) {
4446
+ if (!existsSync14(workerBundlePath)) {
4337
4447
  output.die(`Worker bundle not found at ${workerBundlePath}`, "build_output_missing");
4338
4448
  }
4339
- if (!clientDir || !existsSync15(clientDir)) {
4449
+ if (!clientDir || !existsSync14(clientDir)) {
4340
4450
  return output.die(`Client assets not found at ${clientDir}`, "build_output_missing");
4341
4451
  }
4342
4452
  try {
@@ -4349,7 +4459,7 @@ async function buildDeployBundle(options) {
4349
4459
  const assets = collectAssets(clientDir);
4350
4460
  const assetConfig = readDeployAssetConfig(clientDir);
4351
4461
  spinner2.stop(`Collected ${assets.length} assets`);
4352
- const workerJs = readFileSync18(workerBundlePath, "utf-8");
4462
+ const workerJs = readFileSync17(workerBundlePath, "utf-8");
4353
4463
  const doBindings = outputConfig.durable_objects?.bindings;
4354
4464
  const sqliteClasses = new Set(
4355
4465
  outputConfig.migrations?.flatMap((migration) => migration.new_sqlite_classes ?? []) ?? []
@@ -4428,7 +4538,7 @@ function collectAssets(dir) {
4428
4538
  if (entry.isDirectory()) {
4429
4539
  walk(fullPath, relativePath);
4430
4540
  } else {
4431
- const bytes = readFileSync18(fullPath);
4541
+ const bytes = readFileSync17(fullPath);
4432
4542
  assets.push({
4433
4543
  path: `/${relativePath}`,
4434
4544
  hash: createHash2("sha256").update(bytes).digest("hex"),
@@ -4443,10 +4553,13 @@ function collectAssets(dir) {
4443
4553
  }
4444
4554
  function readDeployAssetConfig(clientDir) {
4445
4555
  const config = {};
4446
- for (const [file, key] of [["_headers", "_headers"], ["_redirects", "_redirects"]]) {
4556
+ for (const [file, key] of [
4557
+ ["_headers", "_headers"],
4558
+ ["_redirects", "_redirects"]
4559
+ ]) {
4447
4560
  const filePath = join16(clientDir, file);
4448
- if (!existsSync15(filePath)) continue;
4449
- const content = readFileSync18(filePath, "utf8");
4561
+ if (!existsSync14(filePath)) continue;
4562
+ const content = readFileSync17(filePath, "utf8");
4450
4563
  if (content.trim()) config[key] = content;
4451
4564
  }
4452
4565
  return config;
@@ -4460,7 +4573,7 @@ function errorMessage(error) {
4460
4573
 
4461
4574
  // src/cli/commands/deploy/commerce.ts
4462
4575
  import * as p7 from "@clack/prompts";
4463
- import { existsSync as existsSync16 } from "fs";
4576
+ import { existsSync as existsSync15 } from "fs";
4464
4577
  import { join as join17 } from "path";
4465
4578
  import { pathToFileURL as pathToFileURL2 } from "url";
4466
4579
  var API_URL2 = process.env.DEEPSPACE_API_URL ?? PLATFORM_URLS.api;
@@ -4469,7 +4582,7 @@ function errorMessage2(error) {
4469
4582
  }
4470
4583
  async function loadDeclaration(appDir, sourceFile, outputFile, exportName) {
4471
4584
  const sourcePath = join17(appDir, "src", sourceFile);
4472
- if (!existsSync16(sourcePath)) return void 0;
4585
+ if (!existsSync15(sourcePath)) return void 0;
4473
4586
  const esbuild = await import("esbuild");
4474
4587
  const outputPath = join17(appDir, ".wrangler", "deploy", outputFile);
4475
4588
  await esbuild.build({
@@ -4564,7 +4677,7 @@ Stripe will keep charging them until you cancel. Cancel at the end of their curr
4564
4677
  }
4565
4678
  }
4566
4679
  async function syncSubscriptionPlans(appDir, appName, token, nonInteractive) {
4567
- if (!existsSync16(join17(appDir, "src", "subscriptions.ts"))) return;
4680
+ if (!existsSync15(join17(appDir, "src", "subscriptions.ts"))) return;
4568
4681
  let plans;
4569
4682
  try {
4570
4683
  plans = await loadDeclaration(
@@ -4601,7 +4714,7 @@ async function syncSubscriptionPlans(appDir, appName, token, nonInteractive) {
4601
4714
  spinner2.stop(`Synced ${plans.length} plan${plans.length === 1 ? "" : "s"} to Stripe`);
4602
4715
  if (body.connectMissing) {
4603
4716
  p7.log.warn(
4604
- "Stripe Connect not yet set up. Customers can subscribe and earnings will queue in your platform balance until you finish onboarding at /earnings on the dashboard. The first payout after you connect will sweep everything that accumulated in the meantime."
4717
+ "Stripe Connect is not ready. Paid checkout will remain unavailable until the app owner finishes onboarding at /earnings on the dashboard."
4605
4718
  );
4606
4719
  }
4607
4720
  const changes = (body.planChanges ?? []).filter((change) => change.affectedSubscribers > 0);
@@ -4619,7 +4732,7 @@ async function syncSubscriptionPlans(appDir, appName, token, nonInteractive) {
4619
4732
  }
4620
4733
  }
4621
4734
  async function syncOneTimeProducts(appDir, appName, token) {
4622
- if (!existsSync16(join17(appDir, "src", "products.ts"))) return;
4735
+ if (!existsSync15(join17(appDir, "src", "products.ts"))) return;
4623
4736
  let products;
4624
4737
  try {
4625
4738
  products = await loadDeclaration(
@@ -4721,7 +4834,7 @@ function createDeployOutput(json) {
4721
4834
 
4722
4835
  // src/cli/commands/deploy/request.ts
4723
4836
  import * as p10 from "@clack/prompts";
4724
- import { readFileSync as readFileSync19 } from "fs";
4837
+ import { readFileSync as readFileSync18 } from "fs";
4725
4838
 
4726
4839
  // src/cli/commands/deploy/repository.ts
4727
4840
  import * as p9 from "@clack/prompts";
@@ -4912,7 +5025,7 @@ function repoApi(deployUrl, token, appId) {
4912
5025
  }
4913
5026
 
4914
5027
  // src/cli/lib/vc-remote.ts
4915
- import { existsSync as existsSync17 } from "fs";
5028
+ import { existsSync as existsSync16 } from "fs";
4916
5029
  import { tmpdir } from "os";
4917
5030
  import { isAbsolute as isAbsolute3, relative, resolve as resolve12 } from "path";
4918
5031
  function spaceRemoteName(environment = DEEPSPACE_ENV) {
@@ -4967,7 +5080,7 @@ function helperEntryUsable(command) {
4967
5080
  const match = /^!(?:DEEPSPACE_ENV=(production|staging) )?'([^']*)' '([^']*)' git-credential$/.exec(command);
4968
5081
  const expectedEnvironment = DEEPSPACE_ENV === "production" ? void 0 : DEEPSPACE_ENV;
4969
5082
  if (!match || match[1] !== expectedEnvironment) return false;
4970
- return existsSync17(match[2]) && existsSync17(match[3]);
5083
+ return existsSync16(match[2]) && existsSync16(match[3]);
4971
5084
  }
4972
5085
  function credentialHelperCommand(environment = DEEPSPACE_ENV) {
4973
5086
  const entry = process.argv[1];
@@ -5242,7 +5355,16 @@ function selectGitHubRemote(cwd, options = {}) {
5242
5355
  return matching.find((remote) => remote.name === "origin") ?? (matching.length === 1 ? matching[0] : null);
5243
5356
  }
5244
5357
  function remotePublicRefs(cwd, remote) {
5245
- const output = runGit(cwd, ["ls-remote", "--refs", remote]).stdout.toString("utf-8");
5358
+ let output;
5359
+ try {
5360
+ output = runGit(cwd, ["ls-remote", "--refs", remote]).stdout.toString("utf-8");
5361
+ } catch (error) {
5362
+ if (!isGitCredentialFailure(error)) throw error;
5363
+ throw new GitError(
5364
+ "Git could not authenticate the remote. In a container, forward an SSH agent or use an ephemeral Git credential helper, then retry. Do not put tokens in remote URLs.",
5365
+ "github_credentials_required"
5366
+ );
5367
+ }
5246
5368
  return output.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => {
5247
5369
  const [oid, name] = line.split(/\s+/, 2);
5248
5370
  return { oid, name };
@@ -5250,6 +5372,12 @@ function remotePublicRefs(cwd, remote) {
5250
5372
  (ref) => /^[0-9a-f]{40}$/.test(ref.oid) && (ref.name.startsWith("refs/heads/") || ref.name.startsWith("refs/tags/"))
5251
5373
  );
5252
5374
  }
5375
+ function isGitCredentialFailure(error) {
5376
+ if (!(error instanceof Error)) return false;
5377
+ return /authentication failed|could not read username|permission denied \(publickey\)|repository not found|terminal prompts disabled/i.test(
5378
+ error.message
5379
+ );
5380
+ }
5253
5381
  function remoteBranchOid(cwd, remote, branch) {
5254
5382
  return remotePublicRefs(cwd, remote).find((ref) => ref.name === `refs/heads/${branch}`)?.oid ?? null;
5255
5383
  }
@@ -5291,6 +5419,25 @@ function setAppSource(deployUrl, token, appId, input) {
5291
5419
  }
5292
5420
 
5293
5421
  // src/cli/commands/deploy/repository.ts
5422
+ function preflightDeployRepository(options) {
5423
+ const { appDir, push, source } = options;
5424
+ if (!push || source?.provider === "github") return null;
5425
+ try {
5426
+ assertSyncableRepo(appDir);
5427
+ if (!source && listGitHubRemotes(appDir).length > 0) {
5428
+ return {
5429
+ code: "source_unclaimed",
5430
+ error: "This app has a GitHub remote but no claimed source. Choose once with `deepspace app source github` (manual GitHub ownership) or `deepspace app source deepspace` (packaged DeepSpace source), then deploy again."
5431
+ };
5432
+ }
5433
+ const branch = currentBranch(appDir);
5434
+ if (!isWorkTreeClean(appDir)) return dirtyWorktreeRefusal(branch);
5435
+ if (!branch) return detachedHeadRefusal();
5436
+ return null;
5437
+ } catch (error) {
5438
+ return deployRepositoryFailure(error, appDir);
5439
+ }
5440
+ }
5294
5441
  async function syncDeployRepository(options) {
5295
5442
  const { deployUrl, appDir, appId, token, push, ignoreStale, output, sourceState } = options;
5296
5443
  let commitOid = null;
@@ -5311,21 +5458,12 @@ async function syncDeployRepository(options) {
5311
5458
  return { commitOid, recoverable, deployKey, source, sourceRevision, baseReleaseId };
5312
5459
  }
5313
5460
  try {
5314
- assertSyncableRepo(appDir);
5315
- if (!source && listGitHubRemotes(appDir).length > 0) {
5316
- output.die(
5317
- "This app has a GitHub remote but no claimed source. Choose once with `deepspace app source github` (manual GitHub ownership) or `deepspace app source deepspace` (packaged DeepSpace source), then deploy again.",
5318
- "source_unclaimed"
5319
- );
5320
- }
5461
+ const preflight = preflightDeployRepository({ appDir, push, source });
5462
+ if (preflight) output.die(preflight.error, preflight.code);
5321
5463
  const sourceRemote = spaceRemoteName();
5322
5464
  ensureSpaceRemote(appDir, appId, sourceRemote);
5323
5465
  const branch = currentBranch(appDir);
5324
5466
  const workspaceBranchId = workspaceIdFromBranch(branch);
5325
- if (!isWorkTreeClean(appDir)) {
5326
- const refusal = dirtyWorktreeRefusal(branch);
5327
- output.die(refusal.error, refusal.code);
5328
- }
5329
5467
  if (!branch) {
5330
5468
  const refusal = detachedHeadRefusal();
5331
5469
  output.die(refusal.error, refusal.code);
@@ -5427,12 +5565,7 @@ function classifyRemoteState(appDir, token, branch, localTip, remote) {
5427
5565
  let remoteTip = null;
5428
5566
  const remoteRef = `refs/remotes/${remote}/${branch}`;
5429
5567
  try {
5430
- runGitRemote(appDir, token, [
5431
- "fetch",
5432
- "--quiet",
5433
- remote,
5434
- `+refs/heads/${branch}:${remoteRef}`
5435
- ]);
5568
+ runGitRemote(appDir, token, ["fetch", "--quiet", remote, `+refs/heads/${branch}:${remoteRef}`]);
5436
5569
  remoteTip = resolveCommit(appDir, remoteRef);
5437
5570
  return {
5438
5571
  remoteTip,
@@ -5554,7 +5687,7 @@ async function deployBuiltBundle(options) {
5554
5687
  if (bundle.customBindings.length) {
5555
5688
  form.append("bindingManifest", JSON.stringify(bundle.customBindings));
5556
5689
  }
5557
- if (secrets.names.length) form.append("userSecrets", JSON.stringify(secrets.values));
5690
+ if (secrets.authoritative) form.append("userSecrets", JSON.stringify(secrets.values));
5558
5691
  if (bundle.extraRoutes === true || bundle.extraRoutes.length) {
5559
5692
  form.append("extraRunWorkerFirst", JSON.stringify(bundle.extraRoutes));
5560
5693
  }
@@ -5746,7 +5879,7 @@ async function uploadDeployAssets(options) {
5746
5879
  // against the plan before it touches storage, and a stream body
5747
5880
  // would be sent chunked with no length at all; a retry also needs a
5748
5881
  // body it can send again.
5749
- body: readFileSync19(asset.sourcePath)
5882
+ body: readFileSync18(asset.sourcePath)
5750
5883
  })
5751
5884
  );
5752
5885
  if (!response.ok) {
@@ -5762,9 +5895,7 @@ async function uploadDeployAssets(options) {
5762
5895
  }
5763
5896
  };
5764
5897
  try {
5765
- await Promise.all(
5766
- Array.from({ length: Math.min(UPLOAD_CONCURRENCY, missing.length) }, worker)
5767
- );
5898
+ await Promise.all(Array.from({ length: Math.min(UPLOAD_CONCURRENCY, missing.length) }, worker));
5768
5899
  } catch (error) {
5769
5900
  spinner2.stop("Deploy failed");
5770
5901
  if (error instanceof AssetUploadError) {
@@ -5833,35 +5964,17 @@ async function postWithRetry(url, makeInit, {
5833
5964
 
5834
5965
  // src/cli/commands/deploy/secrets.ts
5835
5966
  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
5967
  async function loadDeploySecrets(options) {
5847
5968
  const { deployUrl, appDir, appId, envName, ownerId, token, output } = options;
5848
5969
  let linked = null;
5849
- let localOnlyDevVars = {};
5850
5970
  try {
5851
5971
  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
- }
5972
+ linked = refreshed.pulled;
5973
+ await writeDevVars(appDir, ownerId, token, envName, {
5974
+ appId,
5975
+ generatedSecretsCache: refreshed.rendered
5976
+ });
5977
+ if (refreshed.summary) p11.log.info(refreshed.summary);
5865
5978
  } catch (error) {
5866
5979
  if (error?.status === 403) {
5867
5980
  output.die(
@@ -5874,40 +5987,31 @@ async function loadDeploySecrets(options) {
5874
5987
  "secrets_refresh_failed"
5875
5988
  );
5876
5989
  }
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
- ];
5990
+ if (!linked) {
5991
+ const configName = defaultConfigNameForEnv(envName);
5899
5992
  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.`
5993
+ `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.`,
5994
+ "secrets_config_missing",
5995
+ {
5996
+ action: {
5997
+ cwd: appDir,
5998
+ argv: [
5999
+ "deepspace",
6000
+ "secrets",
6001
+ "configs",
6002
+ "create",
6003
+ configName,
6004
+ ...envName ? ["--env", envName] : []
6005
+ ]
6006
+ }
6007
+ }
5909
6008
  );
5910
6009
  }
6010
+ return { linked };
6011
+ }
6012
+ function prepareDeploySecrets(options) {
6013
+ const { cache, customBindings, doManifest, output } = options;
6014
+ const values = cache.linked ? { ...cache.linked.values } : {};
5911
6015
  const names = Object.keys(values);
5912
6016
  const declaredBindingNames = /* @__PURE__ */ new Set([
5913
6017
  ...customBindings.map((binding) => binding.name),
@@ -5928,17 +6032,7 @@ function prepareDeploySecrets(options) {
5928
6032
  }
5929
6033
  }
5930
6034
  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
- }
6035
+ return { values, names, authoritative: cache.linked !== null };
5942
6036
  }
5943
6037
  function errorMessage5(error) {
5944
6038
  return error instanceof Error ? error.message : String(error);
@@ -5977,11 +6071,6 @@ var deploy_default = defineCommand3({
5977
6071
  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
6072
  default: false
5979
6073
  },
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
6074
  // Citty maps --no-push to the negation of an argument named `push`.
5986
6075
  push: {
5987
6076
  type: "boolean",
@@ -6050,12 +6139,19 @@ var deploy_default = defineCommand3({
6050
6139
  output
6051
6140
  });
6052
6141
  const sourceState = await getAppSource(DEPLOY_URL3, token, appId);
6142
+ const repositoryPreflight = preflightDeployRepository({
6143
+ appDir,
6144
+ push: args.push !== false,
6145
+ source: sourceState.source
6146
+ });
6147
+ if (repositoryPreflight) {
6148
+ output.die(repositoryPreflight.error, repositoryPreflight.code);
6149
+ }
6053
6150
  const spinner2 = createSpinner();
6054
6151
  const bundle = await buildDeployBundle({
6055
6152
  appDir,
6056
6153
  appName,
6057
6154
  envName,
6058
- sharedDevVarsCache: secretsCache.linked !== null,
6059
6155
  output,
6060
6156
  spinner: spinner2
6061
6157
  });
@@ -6075,10 +6171,7 @@ var deploy_default = defineCommand3({
6075
6171
  sourceState
6076
6172
  });
6077
6173
  const secrets = prepareDeploySecrets({
6078
- appDir,
6079
- envName,
6080
6174
  cache: secretsCache,
6081
- allowMissing: Boolean(args["allow-missing-secrets"]),
6082
6175
  customBindings: bundle.customBindings,
6083
6176
  doManifest: bundle.doManifest,
6084
6177
  output
@@ -6516,11 +6609,17 @@ function parseSince(input, now = Date.now()) {
6516
6609
  } else {
6517
6610
  ts = Date.parse(trimmed);
6518
6611
  if (Number.isNaN(ts)) {
6519
- throw new InputError(`Invalid --since "${input}" \u2014 use 30s, 15m, 2h, 7d, or an ISO timestamp.`, "invalid_since");
6612
+ throw new InputError(
6613
+ `Invalid --since "${input}" \u2014 use 30s, 15m, 2h, 7d, or an ISO timestamp.`,
6614
+ "invalid_since"
6615
+ );
6520
6616
  }
6521
6617
  }
6522
6618
  if (now - ts > RETENTION_MS) {
6523
- throw new InputError("Logs are retained for 7 days \u2014 --since can reach back at most 7d.", "since_out_of_range");
6619
+ throw new InputError(
6620
+ "Logs are retained for 7 days \u2014 --since can reach back at most 7d.",
6621
+ "since_out_of_range"
6622
+ );
6524
6623
  }
6525
6624
  return ts;
6526
6625
  }
@@ -6651,13 +6750,19 @@ var logs_default = defineCommand4({
6651
6750
  const token = await ensureToken();
6652
6751
  const appId = await resolveAppTarget(DEPLOY_URL6, token, args.app, { wranglerEnv: envArg });
6653
6752
  if (args.level && !LOG_LEVELS.includes(args.level)) {
6654
- throw new InputError(`Invalid --level "${args.level}". Use: ${LOG_LEVELS.join(", ")}`, "invalid_level");
6753
+ throw new InputError(
6754
+ `Invalid --level "${args.level}". Use: ${LOG_LEVELS.join(", ")}`,
6755
+ "invalid_level"
6756
+ );
6655
6757
  }
6656
6758
  let limit;
6657
6759
  if (args.limit !== void 0) {
6658
6760
  limit = Number(args.limit);
6659
6761
  if (!Number.isInteger(limit) || limit < 1 || limit > 500) {
6660
- throw new InputError("Invalid --limit \u2014 expected an integer between 1 and 500.", "invalid_limit");
6762
+ throw new InputError(
6763
+ "Invalid --limit \u2014 expected an integer between 1 and 500.",
6764
+ "invalid_limit"
6765
+ );
6661
6766
  }
6662
6767
  }
6663
6768
  const initialSince = args.since ? parseSince(args.since) : Date.now() - DEFAULT_WINDOW_MS;
@@ -6673,7 +6778,9 @@ var logs_default = defineCommand4({
6673
6778
  };
6674
6779
  const print = (events) => {
6675
6780
  for (const e of events) {
6676
- process.stdout.write(args.json ? JSON.stringify(e) + "\n" : formatEvent(e, stdoutColor()) + "\n");
6781
+ process.stdout.write(
6782
+ args.json ? JSON.stringify(e) + "\n" : formatEvent(e, stdoutColor()) + "\n"
6783
+ );
6677
6784
  }
6678
6785
  };
6679
6786
  const first = await fetchPage(initialSince, args.follow ? followLimit : limit, token);
@@ -6681,15 +6788,25 @@ var logs_default = defineCommand4({
6681
6788
  if (!args.follow) {
6682
6789
  if (first.events.length === 0) {
6683
6790
  if (args.json) {
6684
- process.stdout.write(JSON.stringify({ type: "meta", count: 0, window: windowLabel }) + "\n");
6791
+ process.stdout.write(
6792
+ JSON.stringify({ type: "meta", count: 0, window: windowLabel }) + "\n"
6793
+ );
6685
6794
  } else {
6686
6795
  console.log(
6687
6796
  `No logs in the last ${windowLabel} for ${appId}. New logs can take up to a minute to appear; retention is 7 days.`
6688
6797
  );
6689
6798
  }
6690
6799
  } else if (first.truncated) {
6691
- if (args.json) process.stdout.write(JSON.stringify({ type: "meta", truncated: true }) + "\n");
6692
- else console.error(`(showing the newest ${first.events.length} events \u2014 narrow with --since/--level/--search)`);
6800
+ if (args.json)
6801
+ process.stdout.write(JSON.stringify({ type: "meta", truncated: true }) + "\n");
6802
+ else if (args.search)
6803
+ console.error(
6804
+ `(showing the newest ${first.events.length} matches \u2014 search scans at most the newest 500 level-filtered events per fetch; narrow with --since/--level or raise --limit)`
6805
+ );
6806
+ else
6807
+ console.error(
6808
+ `(showing the newest ${first.events.length} events \u2014 narrow with --since/--level)`
6809
+ );
6693
6810
  }
6694
6811
  return;
6695
6812
  }
@@ -6709,7 +6826,11 @@ var logs_default = defineCommand4({
6709
6826
  process.stdout.write(JSON.stringify({ type: "meta", truncated: true }) + "\n");
6710
6827
  } else {
6711
6828
  console.error(
6712
- paint("33", `(more than ${followLimit} events in the initial ${windowLabel} window \u2014 some older events not shown; narrow with --since/--level/--search)`, stderrColor())
6829
+ paint(
6830
+ "33",
6831
+ args.search ? `(search scanned only the newest ${MAX_LIMIT} level-filtered events in the initial ${windowLabel} window \u2014 older matches may be omitted; narrow with --since/--level)` : `(more than ${followLimit} events in the initial ${windowLabel} window \u2014 some older events not shown; narrow with --since/--level)`,
6832
+ stderrColor()
6833
+ )
6713
6834
  );
6714
6835
  }
6715
6836
  }
@@ -6723,21 +6844,27 @@ var logs_default = defineCommand4({
6723
6844
  if (err instanceof ApiError && [401, 403, 404].includes(err.status)) throw err;
6724
6845
  backoff = Math.min(backoff ? backoff * 2 : MIN_POLL_MS, 3e4);
6725
6846
  const msg = err instanceof Error ? err.message : String(err);
6726
- console.error(paint("2", `(logs fetch failed: ${msg} \u2014 retrying in ${backoff / 1e3}s)`, stderrColor()));
6847
+ console.error(
6848
+ paint("2", `(logs fetch failed: ${msg} \u2014 retrying in ${backoff / 1e3}s)`, stderrColor())
6849
+ );
6727
6850
  continue;
6728
6851
  }
6729
6852
  backoff = 0;
6730
6853
  interval = Math.max(page.pollIntervalMs || 0, MIN_POLL_MS);
6731
6854
  const fresh = seen.fresh(page.events);
6732
6855
  print(fresh);
6733
- const realDrop = fresh.length >= followLimit;
6856
+ const realDrop = fresh.length >= followLimit || Boolean(args.search && page.truncated);
6734
6857
  if (realDrop && !warnedTruncated) {
6735
6858
  warnedTruncated = true;
6736
6859
  if (args.json) {
6737
6860
  process.stdout.write(JSON.stringify({ type: "meta", truncated: true }) + "\n");
6738
6861
  } else {
6739
6862
  console.error(
6740
- paint("33", `(burst exceeded ${followLimit} events/poll \u2014 some may be dropped; narrow with --level/--search)`, stderrColor())
6863
+ paint(
6864
+ "33",
6865
+ args.search ? `(search scanned only the newest ${MAX_LIMIT} level-filtered events this poll \u2014 older matches may be omitted; narrow with --level)` : `(burst exceeded ${followLimit} events/poll \u2014 some may be dropped; narrow with --level)`,
6866
+ stderrColor()
6867
+ )
6741
6868
  );
6742
6869
  }
6743
6870
  } else if (!realDrop) {
@@ -6818,7 +6945,7 @@ var usage_default = defineDeepspaceCommand({
6818
6945
  });
6819
6946
 
6820
6947
  // src/cli/commands/add.ts
6821
- import { existsSync as existsSync20, readFileSync as readFileSync22, realpathSync as realpathSync4 } from "fs";
6948
+ import { existsSync as existsSync18, readFileSync as readFileSync20, realpathSync as realpathSync4 } from "fs";
6822
6949
  import { dirname as dirname8, join as join19, resolve as resolve15 } from "path";
6823
6950
  import { pathToFileURL as pathToFileURL3 } from "url";
6824
6951
 
@@ -6826,12 +6953,12 @@ import { pathToFileURL as pathToFileURL3 } from "url";
6826
6953
  import {
6827
6954
  closeSync as closeSync2,
6828
6955
  copyFileSync,
6829
- existsSync as existsSync19,
6956
+ existsSync as existsSync17,
6830
6957
  fstatSync as fstatSync2,
6831
6958
  mkdirSync as mkdirSync6,
6832
6959
  mkdtempSync,
6833
6960
  openSync as openSync2,
6834
- readFileSync as readFileSync21,
6961
+ readFileSync as readFileSync19,
6835
6962
  readSync,
6836
6963
  readdirSync as readdirSync5,
6837
6964
  rmSync as rmSync4,
@@ -6846,9 +6973,9 @@ function featurePackageRoot(moduleUrl = import.meta.url) {
6846
6973
  let dir = dirname7(fileURLToPath(moduleUrl));
6847
6974
  for (; ; ) {
6848
6975
  const packagePath = join18(dir, "package.json");
6849
- if (existsSync19(packagePath)) {
6976
+ if (existsSync17(packagePath)) {
6850
6977
  try {
6851
- const pkg2 = JSON.parse(readFileSync21(packagePath, "utf-8"));
6978
+ const pkg2 = JSON.parse(readFileSync19(packagePath, "utf-8"));
6852
6979
  if (pkg2.name === "deepspace") return dir;
6853
6980
  } catch {
6854
6981
  }
@@ -6859,13 +6986,13 @@ function featurePackageRoot(moduleUrl = import.meta.url) {
6859
6986
  }
6860
6987
  }
6861
6988
  function hasFeatureCatalog(packageRoot = featurePackageRoot()) {
6862
- return existsSync19(join18(packageRoot, "features"));
6989
+ return existsSync17(join18(packageRoot, "features"));
6863
6990
  }
6864
6991
  function readFeatureConfig(id, packageRoot = featurePackageRoot()) {
6865
6992
  const configPath = join18(packageRoot, "features", id, "feature.json");
6866
- if (!existsSync19(configPath)) return null;
6993
+ if (!existsSync17(configPath)) return null;
6867
6994
  try {
6868
- const config = JSON.parse(readFileSync21(configPath, "utf-8"));
6995
+ const config = JSON.parse(readFileSync19(configPath, "utf-8"));
6869
6996
  if (typeof config.id !== "string" || typeof config.name !== "string" || !Array.isArray(config.files)) {
6870
6997
  return null;
6871
6998
  }
@@ -6876,7 +7003,7 @@ function readFeatureConfig(id, packageRoot = featurePackageRoot()) {
6876
7003
  }
6877
7004
  function listFeatureConfigs(packageRoot = featurePackageRoot()) {
6878
7005
  const featuresDir = join18(packageRoot, "features");
6879
- if (!existsSync19(featuresDir)) return [];
7006
+ if (!existsSync17(featuresDir)) return [];
6880
7007
  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
7008
  }
6882
7009
  function featureCatalog(packageRoot = featurePackageRoot()) {
@@ -6930,7 +7057,7 @@ function copyFile(src, dest) {
6930
7057
  function copyFileWithImportShift(src, dest, levels) {
6931
7058
  mkdirSync6(dirname7(dest), { recursive: true });
6932
7059
  const prefix = "../".repeat(levels);
6933
- const shifted = readFileSync21(src, "utf-8").replace(
7060
+ const shifted = readFileSync19(src, "utf-8").replace(
6934
7061
  /(from\s+|import\s+|import\()(['"])(\.\.\/)/g,
6935
7062
  `$1$2${prefix}$3`
6936
7063
  );
@@ -6946,9 +7073,9 @@ function installFiles(packageRoot, config, targetDir, emit) {
6946
7073
  const srcPath = join18(packageRoot, "features", config.id, file.src);
6947
7074
  const resolvedDest = resolveFeatureDestPath(file.dest, config.route);
6948
7075
  const destPath = join18(targetDir, resolvedDest);
6949
- const existed = existsSync19(destPath);
7076
+ const existed = existsSync17(destPath);
6950
7077
  if (existed) preexisting.push(resolvedDest);
6951
- if (!existsSync19(srcPath)) {
7078
+ if (!existsSync17(srcPath)) {
6952
7079
  emit(` Warning: source not found: ${file.src}`, "stderr");
6953
7080
  if (!existed) missing.push(resolvedDest);
6954
7081
  continue;
@@ -6986,10 +7113,10 @@ function unresolvedSchema(schema, reason, emit) {
6986
7113
  function integrateSchema(config, targetDir, emit) {
6987
7114
  if (!config.schema) return [];
6988
7115
  const schemasPath = join18(targetDir, "src", "schemas.ts");
6989
- if (!existsSync19(schemasPath)) {
7116
+ if (!existsSync17(schemasPath)) {
6990
7117
  return [unresolvedSchema(config.schema, "src/schemas.ts was not found.", emit)];
6991
7118
  }
6992
- let content = readFileSync21(schemasPath, "utf-8");
7119
+ let content = readFileSync19(schemasPath, "utf-8");
6993
7120
  const { exportName, importPath, spreadOperator } = config.schema;
6994
7121
  const importPattern = new RegExp(`^import\\s+\\{[^}]*\\b${exportName}\\b`, "m");
6995
7122
  if (importPattern.test(content)) {
@@ -7043,10 +7170,10 @@ function unresolvedActions(actions, reason, emit) {
7043
7170
  function integrateActions(config, targetDir, emit) {
7044
7171
  if (!config.actions?.length) return [];
7045
7172
  const actionsPath = join18(targetDir, "src", "actions", "index.ts");
7046
- if (!existsSync19(actionsPath)) {
7173
+ if (!existsSync17(actionsPath)) {
7047
7174
  return [unresolvedActions(config.actions, "src/actions/index.ts was not found.", emit)];
7048
7175
  }
7049
- let content = readFileSync21(actionsPath, "utf-8");
7176
+ let content = readFileSync19(actionsPath, "utf-8");
7050
7177
  const registryPattern = /export const actions:\s*Record<string,\s*ActionHandler(?:<[^>]*>)?>\s*=\s*\{/;
7051
7178
  if (!registryPattern.test(content)) {
7052
7179
  const reason = "src/actions/index.ts has an unexpected structure.";
@@ -7093,7 +7220,7 @@ function integrateCode(config, targetDir, emit) {
7093
7220
  const unresolved = [];
7094
7221
  for (const insertion of config.code ?? []) {
7095
7222
  const targetPath = join18(targetDir, insertion.file);
7096
- if (!existsSync19(targetPath)) {
7223
+ if (!existsSync17(targetPath)) {
7097
7224
  unresolved.push({
7098
7225
  area: "code",
7099
7226
  reason: `${insertion.file} was not found.`,
@@ -7101,7 +7228,7 @@ function integrateCode(config, targetDir, emit) {
7101
7228
  });
7102
7229
  continue;
7103
7230
  }
7104
- const original = readFileSync21(targetPath, "utf-8");
7231
+ const original = readFileSync19(targetPath, "utf-8");
7105
7232
  if (original.includes(insertion.insert.trim())) {
7106
7233
  emit(` Integration already present: ${insertion.file}`);
7107
7234
  continue;
@@ -7137,7 +7264,7 @@ ${content.slice(importIndex < 0 ? 0 : importIndex)}`;
7137
7264
  function integrateIgnore(config, targetDir) {
7138
7265
  if (!config.ignore?.length) return;
7139
7266
  const targetPath = join18(targetDir, ".gitignore");
7140
- const current = existsSync19(targetPath) ? readFileSync21(targetPath, "utf-8") : "";
7267
+ const current = existsSync17(targetPath) ? readFileSync19(targetPath, "utf-8") : "";
7141
7268
  const present = new Set(current.split(/\r?\n/).map((line) => line.trim()));
7142
7269
  const missing = config.ignore.filter((pattern) => !present.has(pattern));
7143
7270
  if (missing.length === 0) return;
@@ -7154,10 +7281,10 @@ function integrateWrangler(config, targetDir, emit) {
7154
7281
  ...runWorkerFirst ? ["Set assets.run_worker_first = true in wrangler.toml."] : [],
7155
7282
  ...rateLimits.map((binding) => `Add the ${binding.name} rate-limit binding to wrangler.toml.`)
7156
7283
  ];
7157
- if (!existsSync19(targetPath)) {
7284
+ if (!existsSync17(targetPath)) {
7158
7285
  return [{ area: "wrangler", reason: "wrangler.toml was not found.", steps }];
7159
7286
  }
7160
- let source = readFileSync21(targetPath, "utf-8");
7287
+ let source = readFileSync19(targetPath, "utf-8");
7161
7288
  if (runWorkerFirst && !/^\[assets\]\s*$/m.test(source)) {
7162
7289
  return [{ area: "wrangler", reason: "wrangler.toml has no [assets] section.", steps }];
7163
7290
  }
@@ -7234,7 +7361,7 @@ function rateLimitBindingBlock(scope, binding) {
7234
7361
  function integrateCss(packageRoot, config, targetDir, emit) {
7235
7362
  if (!config.css?.length) return [];
7236
7363
  const stylesPath = join18(targetDir, "src", "styles.css");
7237
- if (!existsSync19(stylesPath)) {
7364
+ if (!existsSync17(stylesPath)) {
7238
7365
  emit(" Warning: Cannot integrate CSS \u2014 src/styles.css not found");
7239
7366
  return [
7240
7367
  {
@@ -7246,12 +7373,12 @@ function integrateCss(packageRoot, config, targetDir, emit) {
7246
7373
  }
7247
7374
  ];
7248
7375
  }
7249
- let styles = readFileSync21(stylesPath, "utf-8");
7376
+ let styles = readFileSync19(stylesPath, "utf-8");
7250
7377
  let integrated = 0;
7251
7378
  const unresolved = [];
7252
7379
  for (const cssFile of config.css) {
7253
7380
  const srcPath = join18(packageRoot, "features", config.id, cssFile);
7254
- if (!existsSync19(srcPath)) {
7381
+ if (!existsSync17(srcPath)) {
7255
7382
  emit(` Warning: CSS source not found: ${cssFile}`, "stderr");
7256
7383
  unresolved.push({
7257
7384
  area: "css",
@@ -7260,7 +7387,7 @@ function integrateCss(packageRoot, config, targetDir, emit) {
7260
7387
  });
7261
7388
  continue;
7262
7389
  }
7263
- const css = readFileSync21(srcPath, "utf-8");
7390
+ const css = readFileSync19(srcPath, "utf-8");
7264
7391
  const fingerprint = css.match(/\.([\w-]+)\s*\{|@keyframes\s+([\w-]+)/);
7265
7392
  const marker = fingerprint?.[1] ?? fingerprint?.[2];
7266
7393
  if (marker && styles.includes(marker)) {
@@ -7288,10 +7415,10 @@ function unresolvedRoute(route, config, reason, emit) {
7288
7415
  function integrateRoute(config, targetDir, emit) {
7289
7416
  if (!config.route) return [];
7290
7417
  const navPath = join18(targetDir, "src", "nav.ts");
7291
- if (!existsSync19(navPath)) {
7418
+ if (!existsSync17(navPath)) {
7292
7419
  return [unresolvedRoute(config.route, config, "src/nav.ts was not found.", emit)];
7293
7420
  }
7294
- let content = readFileSync21(navPath, "utf-8");
7421
+ let content = readFileSync19(navPath, "utf-8");
7295
7422
  const routePath = config.route.path;
7296
7423
  if (content.includes(`'${routePath}'`)) {
7297
7424
  emit(` Nav already present: ${routePath}`);
@@ -7366,13 +7493,13 @@ function integrateDependencies(config, targetDir, runInstall, outputVisible, emi
7366
7493
  return { added: [], installRan: false, packageManager };
7367
7494
  }
7368
7495
  const packagePath = join18(targetDir, "package.json");
7369
- if (!existsSync19(packagePath)) {
7496
+ if (!existsSync17(packagePath)) {
7370
7497
  emit(" Could not patch package.json (file not found)");
7371
7498
  return { added: [], installRan: false, packageManager };
7372
7499
  }
7373
7500
  let pkg2;
7374
7501
  try {
7375
- pkg2 = JSON.parse(readFileSync21(packagePath, "utf-8"));
7502
+ pkg2 = JSON.parse(readFileSync19(packagePath, "utf-8"));
7376
7503
  } catch (error) {
7377
7504
  emit(
7378
7505
  ` Could not patch package.json (parse error: ${error instanceof Error ? error.message : String(error)})`
@@ -7432,7 +7559,7 @@ ${child.diagnosticTail}` : "";
7432
7559
  }
7433
7560
  function dependencySnapshot(targetDir) {
7434
7561
  try {
7435
- const pkg2 = JSON.parse(readFileSync21(join18(targetDir, "package.json"), "utf-8"));
7562
+ const pkg2 = JSON.parse(readFileSync19(join18(targetDir, "package.json"), "utf-8"));
7436
7563
  return {
7437
7564
  dependencies: pkg2.dependencies ?? {},
7438
7565
  devDependencies: pkg2.devDependencies ?? {}
@@ -7444,7 +7571,7 @@ function dependencySnapshot(targetDir) {
7444
7571
  function installedNavFeatures(packageRoot, targetDir) {
7445
7572
  return listFeatureConfigs(packageRoot).filter((feature) => feature.category === "nav" && feature.files.length > 0).filter((feature) => {
7446
7573
  const signature = resolveFeatureDestPath(feature.files[0].dest, feature.route);
7447
- return existsSync19(join18(targetDir, signature));
7574
+ return existsSync17(join18(targetDir, signature));
7448
7575
  }).map((feature) => feature.id);
7449
7576
  }
7450
7577
  function warnNavConflicts(packageRoot, config, targetDir, emit) {
@@ -7677,11 +7804,11 @@ function renderFeatureInfo(config) {
7677
7804
  return lines.join("\n");
7678
7805
  }
7679
7806
  function isDeepSpaceApp(dir) {
7680
- if (existsSync20(join19(dir, "wrangler.toml"))) return true;
7807
+ if (existsSync18(join19(dir, "wrangler.toml"))) return true;
7681
7808
  const packagePath = join19(dir, "package.json");
7682
- if (!existsSync20(packagePath)) return false;
7809
+ if (!existsSync18(packagePath)) return false;
7683
7810
  try {
7684
- const pkg2 = JSON.parse(readFileSync22(packagePath, "utf-8"));
7811
+ const pkg2 = JSON.parse(readFileSync20(packagePath, "utf-8"));
7685
7812
  return Boolean(pkg2.dependencies?.deepspace || pkg2.devDependencies?.deepspace);
7686
7813
  } catch {
7687
7814
  return false;
@@ -7691,7 +7818,7 @@ function installedPackageRoot(fromDir) {
7691
7818
  let dir = fromDir;
7692
7819
  for (; ; ) {
7693
7820
  const candidate = join19(dir, "node_modules", "deepspace");
7694
- if (existsSync20(join19(candidate, "package.json"))) return candidate;
7821
+ if (existsSync18(join19(candidate, "package.json"))) return candidate;
7695
7822
  const parent = dirname8(dir);
7696
7823
  if (parent === dir) return null;
7697
7824
  dir = parent;
@@ -7710,7 +7837,7 @@ async function targetInstaller(appDir) {
7710
7837
  }
7711
7838
  const entry = join19(installedRoot, "dist", "feature-installer.js");
7712
7839
  const catalog = join19(installedRoot, "features");
7713
- if (!existsSync20(entry) || !existsSync20(catalog)) {
7840
+ if (!existsSync18(entry) || !existsSync18(catalog)) {
7714
7841
  throw new Refusal(
7715
7842
  `The app's installed deepspace package does not contain the typed feature installer.
7716
7843
  Expected: ${entry}
@@ -7803,7 +7930,7 @@ var add_default = defineDeepspaceCommand({
7803
7930
  return { data: { features: featureCatalog(root) } };
7804
7931
  }
7805
7932
  const appDir = resolve15(args.dir ?? ".");
7806
- if (!existsSync20(appDir)) {
7933
+ if (!existsSync18(appDir)) {
7807
7934
  throw new Refusal(
7808
7935
  `Target directory not found: ${appDir}
7809
7936
  Create a new app first: deepspace app create <name>`,
@@ -8179,7 +8306,7 @@ var domain_default = defineCommand5({
8179
8306
  // src/cli/commands/app-files.ts
8180
8307
  import { defineCommand as defineCommand6 } from "citty";
8181
8308
  import { basename as basename4, resolve as resolve16 } from "path";
8182
- import { existsSync as existsSync21, statSync as statSync2 } from "fs";
8309
+ import { existsSync as existsSync19, statSync as statSync2 } from "fs";
8183
8310
 
8184
8311
  // src/cli/lib/app-files-api.ts
8185
8312
  import { createWriteStream, openAsBlob, statSync } from "fs";
@@ -8450,7 +8577,7 @@ var put = defineDeepspaceCommand({
8450
8577
  },
8451
8578
  async run({ args }) {
8452
8579
  const localPath = resolve16(String(args.file));
8453
- if (!existsSync21(localPath) || !statSync2(localPath).isFile()) {
8580
+ if (!existsSync19(localPath) || !statSync2(localPath).isFile()) {
8454
8581
  throw new Refusal(`No such file: ${args.file}`, "file_not_found");
8455
8582
  }
8456
8583
  const key = requireKey(args.key ?? basename4(localPath));
@@ -8920,7 +9047,7 @@ var transfer_default = defineCommand8({
8920
9047
  import { defineCommand as defineCommand9 } from "citty";
8921
9048
 
8922
9049
  // src/cli/commands/_invoke-impl.ts
8923
- import { readFileSync as readFileSync23 } from "fs";
9050
+ import { readFileSync as readFileSync21 } from "fs";
8924
9051
  import * as p15 from "@clack/prompts";
8925
9052
  var API_URL7 = process.env.DEEPSPACE_API_URL ?? PLATFORM_URLS.api;
8926
9053
  var DEFAULT_TIMEOUT_MS = 12e4;
@@ -8961,7 +9088,7 @@ function resolveBody(opts) {
8961
9088
  if (opts.body != null) {
8962
9089
  raw = opts.body;
8963
9090
  } else if (opts.bodyFile != null) {
8964
- raw = opts.bodyFile === "-" ? readFileSync23(0, "utf-8") : readFileSync23(opts.bodyFile, "utf-8");
9091
+ raw = opts.bodyFile === "-" ? readFileSync21(0, "utf-8") : readFileSync21(opts.bodyFile, "utf-8");
8965
9092
  } else {
8966
9093
  return "{}";
8967
9094
  }
@@ -9240,7 +9367,7 @@ var integrations_default = defineCommand9({
9240
9367
 
9241
9368
  // src/cli/commands/feedback.ts
9242
9369
  import * as p16 from "@clack/prompts";
9243
- import { existsSync as existsSync22, readFileSync as readFileSync24 } from "fs";
9370
+ import { existsSync as existsSync20, readFileSync as readFileSync22 } from "fs";
9244
9371
  import { dirname as dirname9, join as join20 } from "path";
9245
9372
  import { fileURLToPath as fileURLToPath2 } from "url";
9246
9373
  import { release } from "os";
@@ -9268,8 +9395,8 @@ function readCliVersion() {
9268
9395
  join20(here, "..", "..", "..", "package.json")
9269
9396
  ];
9270
9397
  for (const path of candidates) {
9271
- if (!existsSync22(path)) continue;
9272
- const pkg2 = JSON.parse(readFileSync24(path, "utf-8"));
9398
+ if (!existsSync20(path)) continue;
9399
+ const pkg2 = JSON.parse(readFileSync22(path, "utf-8"));
9273
9400
  if (pkg2.name === "deepspace" && pkg2.version) return pkg2.version;
9274
9401
  }
9275
9402
  } catch {
@@ -9465,7 +9592,7 @@ Context: deepspace ${payload.cliVersion} \xB7 ${payload.nodeVersion} \xB7 ${payl
9465
9592
 
9466
9593
  // src/cli/commands/secrets.ts
9467
9594
  import { defineCommand as defineCommand10 } from "citty";
9468
- import { readFileSync as readFileSync25 } from "fs";
9595
+ import { readFileSync as readFileSync23 } from "fs";
9469
9596
 
9470
9597
  // src/cli/lib/citty-args.ts
9471
9598
  function dedupePositionals(bound, rest) {
@@ -9562,7 +9689,7 @@ function ok(json, data, human) {
9562
9689
  function warnIfDebugRoutesEnabled(secrets) {
9563
9690
  if (secrets.ALLOW_DEBUG_ROUTES !== "true") return false;
9564
9691
  console.warn(
9565
- "\n\u26A0 ALLOW_DEBUG_ROUTES=true exposes an UNAUTHENTICATED debug API on the deployed app \u2014\n anyone who can reach /api/debug/* can read and mutate any record. Redeploy to apply;\n `deepspace secrets delete ALLOW_DEBUG_ROUTES` (then redeploy) turns it back off."
9692
+ "\n\u26A0 ALLOW_DEBUG_ROUTES=true enables the owner/admin debug API on the deployed app \u2014\n those users can read and mutate any app record through /api/debug/*. Redeploy to apply;\n `deepspace secrets delete ALLOW_DEBUG_ROUTES` (then redeploy) turns it back off."
9566
9693
  );
9567
9694
  return true;
9568
9695
  }
@@ -9735,7 +9862,7 @@ var upload = defineCommand10({
9735
9862
  async run({ args }) {
9736
9863
  try {
9737
9864
  const t = await resolveTarget(args);
9738
- const content = args.file === "-" ? readFileSync25(0, "utf-8") : readFileSync25(args.file, "utf-8");
9865
+ const content = args.file === "-" ? readFileSync23(0, "utf-8") : readFileSync23(args.file, "utf-8");
9739
9866
  const secrets = parseSecretsUpload(content);
9740
9867
  if (Object.keys(secrets).length === 0)
9741
9868
  fail(new InputError("No secrets found in the input.", "empty_input"));
@@ -9799,27 +9926,24 @@ var pull = defineCommand10({
9799
9926
  );
9800
9927
  const t = await resolveTarget(args);
9801
9928
  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
- }
9929
+ const refreshed = await refreshSecretsCache(
9930
+ DEPLOY_URL11,
9931
+ t.token,
9932
+ t.appId,
9933
+ wranglerEnv,
9934
+ t.configName
9935
+ );
9813
9936
  await writeDevVars(appDir, ownerId, t.token, wranglerEnv, {
9814
9937
  appId: t.appId,
9815
- generatedSecretsCache: renderSecretsCache(pulled.values, pulled),
9816
- sharedDevVarsCache: true
9938
+ generatedSecretsCache: refreshed.rendered
9817
9939
  });
9818
- const count = Object.keys(pulled.values).length;
9940
+ const count = Object.keys(refreshed.pulled?.values ?? {}).length;
9819
9941
  ok(
9820
9942
  args.json === true,
9821
9943
  { appId: t.appId, config: t.configName, pulled: count },
9822
- () => console.log(`Pulled ${count} secrets (${t.configName}) into .dev.vars.`)
9944
+ () => console.log(
9945
+ refreshed.pulled ? `Pulled ${count} secrets (${t.configName}) into .dev.vars.` : `Config ${t.configName} does not exist yet; regenerated .dev.vars without app secrets.`
9946
+ )
9823
9947
  );
9824
9948
  } catch (err) {
9825
9949
  fail(err);
@@ -9991,7 +10115,7 @@ var init_default = defineDeepspaceCommand({
9991
10115
  // src/cli/commands/status.ts
9992
10116
  import { defineCommand as defineCommand11 } from "citty";
9993
10117
  import * as p17 from "@clack/prompts";
9994
- import { existsSync as existsSync23, readFileSync as readFileSync26 } from "fs";
10118
+ import { existsSync as existsSync21, readFileSync as readFileSync24 } from "fs";
9995
10119
  function timeAgo(iso) {
9996
10120
  const ms = Date.now() - Date.parse(iso);
9997
10121
  if (!Number.isFinite(ms) || ms < 0) return iso;
@@ -10076,7 +10200,7 @@ var status_default = defineCommand11({
10076
10200
  json.services = urls;
10077
10201
  if (urlOverrides.length > 0) json.urlOverrides = urlOverrides;
10078
10202
  }
10079
- facts.loggedIn = existsSync23(SESSION_PATH);
10203
+ facts.loggedIn = existsSync21(SESSION_PATH);
10080
10204
  let userId = null;
10081
10205
  if (!facts.loggedIn) {
10082
10206
  lines.push(["Session", "not logged in"]);
@@ -10085,7 +10209,7 @@ var status_default = defineCommand11({
10085
10209
  let who = "logged in";
10086
10210
  try {
10087
10211
  const payload = decodeJwtPayload(
10088
- readFileSync26(TOKEN_PATH, "utf-8").trim()
10212
+ readFileSync24(TOKEN_PATH, "utf-8").trim()
10089
10213
  );
10090
10214
  if (payload.email) who = payload.email;
10091
10215
  userId = payload.sub ?? null;
@@ -10634,12 +10758,7 @@ var pull_default = defineDeepspaceCommand({
10634
10758
  }
10635
10759
  spinner2?.message(`Fetching ${branch} from the cloud repo\u2026`);
10636
10760
  const trackingRef = spaceTrackingRef(branch);
10637
- runGitRemote(appDir, token, [
10638
- "fetch",
10639
- "--quiet",
10640
- SPACE_REMOTE,
10641
- `+${refName}:${trackingRef}`
10642
- ]);
10761
+ runGitRemote(appDir, token, ["fetch", "--quiet", SPACE_REMOTE, `+${refName}:${trackingRef}`]);
10643
10762
  const remoteOid = resolveCommit(appDir, trackingRef);
10644
10763
  if (!remoteOid) {
10645
10764
  spinner2?.stop("Fetch failed.");
@@ -10677,6 +10796,8 @@ var pull_default = defineDeepspaceCommand({
10677
10796
  integration = "fast_forwarded";
10678
10797
  }
10679
10798
  }
10799
+ } else if (isAncestor(appDir, remoteOid, localOid)) {
10800
+ integration = "local_ahead";
10680
10801
  } else {
10681
10802
  integration = "fetched_only_diverged";
10682
10803
  }
@@ -10690,10 +10811,15 @@ var pull_default = defineDeepspaceCommand({
10690
10811
  let line;
10691
10812
  let codeForState;
10692
10813
  let recoveryAction;
10814
+ let successAction;
10693
10815
  switch (integration) {
10694
10816
  case "up_to_date":
10695
10817
  line = `${branch} is already up to date.`;
10696
10818
  break;
10819
+ case "local_ahead":
10820
+ 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}\`.`;
10821
+ successAction = targetedVcAction("push", appDir, appId, branch);
10822
+ break;
10697
10823
  case "fast_forwarded":
10698
10824
  line = `Fast-forwarded ${branch} to ${remoteOid.slice(0, 10)}.`;
10699
10825
  break;
@@ -10742,7 +10868,8 @@ var pull_default = defineDeepspaceCommand({
10742
10868
  }
10743
10869
  const actionRequired = recoveryAction !== void 0;
10744
10870
  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)}`);
10871
+ if (!args.json)
10872
+ p19.log.info(`App: ${appId} \xB7 ${SPACE_REMOTE}/${branch} at ${remoteOid.slice(0, 10)}`);
10746
10873
  if (codeForState) {
10747
10874
  throw new Refusal(line, codeForState, {
10748
10875
  actionRequired,
@@ -10756,20 +10883,20 @@ var pull_default = defineDeepspaceCommand({
10756
10883
  }
10757
10884
  });
10758
10885
  }
10759
- return { data: { status: integration, appId, branch, remoteOid } };
10886
+ return { data: { status: integration, appId, branch, remoteOid }, action: successAction };
10760
10887
  }
10761
10888
  });
10762
10889
 
10763
10890
  // src/cli/commands/clone.ts
10764
10891
  import * as p20 from "@clack/prompts";
10765
- import { existsSync as existsSync24, readdirSync as readdirSync6, rmSync as rmSync5 } from "fs";
10892
+ import { existsSync as existsSync22, readdirSync as readdirSync6, rmSync as rmSync5 } from "fs";
10766
10893
  import { resolve as resolve17, basename as basename5, join as join21 } from "path";
10767
10894
  function cleanFailedCloneDir(dir) {
10768
10895
  let entries;
10769
10896
  try {
10770
10897
  entries = readdirSync6(dir);
10771
10898
  } catch {
10772
- return existsSync24(dir) ? ["<unreadable>"] : [];
10899
+ return existsSync22(dir) ? ["<unreadable>"] : [];
10773
10900
  }
10774
10901
  if (entries.length === 1 && entries[0] === ".git") {
10775
10902
  try {
@@ -10782,7 +10909,7 @@ function cleanFailedCloneDir(dir) {
10782
10909
  return entries;
10783
10910
  }
10784
10911
  function validateCloneTarget(dir) {
10785
- if (!existsSync24(dir)) return false;
10912
+ if (!existsSync22(dir)) return false;
10786
10913
  let entries;
10787
10914
  try {
10788
10915
  entries = readdirSync6(dir);
@@ -11258,7 +11385,7 @@ function printOverlaps(overlaps, json) {
11258
11385
 
11259
11386
  // src/cli/commands/workspace/local.ts
11260
11387
  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";
11388
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync25, realpathSync as realpathSync6, renameSync as renameSync2, statSync as statSync3, writeFileSync as writeFileSync9 } from "fs";
11262
11389
  import { dirname as dirname10, isAbsolute as isAbsolute5, join as join22, relative as relative2, sep as sep2 } from "path";
11263
11390
  var WORKSPACE_MARKER = "deepspace-workspace.json";
11264
11391
  function excludeWorktreeDir(appDir, dir) {
@@ -11316,7 +11443,7 @@ function isManagedWorkspaceWorktree(worktreeRoot, workspaceId) {
11316
11443
  const markerPath = worktreeMarkerPath(worktreeRoot);
11317
11444
  if (!markerPath) return false;
11318
11445
  try {
11319
- const marker = JSON.parse(readFileSync27(markerPath, "utf-8"));
11446
+ const marker = JSON.parse(readFileSync25(markerPath, "utf-8"));
11320
11447
  return marker.version === 1 && marker.workspaceId === workspaceId && marker.mode === "managed-linked";
11321
11448
  } catch {
11322
11449
  return false;
@@ -11331,6 +11458,11 @@ function appDirInWorktree(appDir, worktreeRoot) {
11331
11458
  const appRelative = relative2(realpathSync6(repoToplevel(appDir)), realpathSync6(appDir));
11332
11459
  return appRelative ? join22(worktreeRoot, appRelative) : worktreeRoot;
11333
11460
  }
11461
+ function primaryAppDir(appDir) {
11462
+ const primary = listWorktrees(appDir)[0];
11463
+ if (!primary) throw new Error("Git did not report a primary worktree");
11464
+ return appDirInWorktree(appDir, primary.path);
11465
+ }
11334
11466
  function materializeWorkspaceWorktree(appDir, worktreeRoot, branch, startPoint, workspaceId, options = {}) {
11335
11467
  const createdOid = resolveCommit(appDir, startPoint);
11336
11468
  if (!createdOid) throw new Error(`Could not resolve workspace start point ${startPoint}`);
@@ -11756,7 +11888,7 @@ var newWorkspaceCommand = defineDeepspaceCommand({
11756
11888
 
11757
11889
  // src/cli/commands/workspace/attach.ts
11758
11890
  import * as p26 from "@clack/prompts";
11759
- import { existsSync as existsSync25, readdirSync as readdirSync7, realpathSync as realpathSync7, rmSync as rmSync6 } from "fs";
11891
+ import { existsSync as existsSync23, readdirSync as readdirSync7, realpathSync as realpathSync7, rmSync as rmSync6 } from "fs";
11760
11892
  import { isAbsolute as isAbsolute7, resolve as resolve19 } from "path";
11761
11893
  function samePath(a, b) {
11762
11894
  const real = (p35) => {
@@ -11776,7 +11908,7 @@ function finishedWorkspaceMessage(id, view) {
11776
11908
  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
11909
  }
11778
11910
  function cleanFailedFreshAttachDir(dir) {
11779
- if (!existsSync25(dir)) return [];
11911
+ if (!existsSync23(dir)) return [];
11780
11912
  let entries;
11781
11913
  try {
11782
11914
  entries = readdirSync7(dir);
@@ -11858,7 +11990,7 @@ var attachWorkspaceCommand = defineDeepspaceCommand({
11858
11990
  const localTip = resolveCommit(appDir, `refs/heads/${branch}`);
11859
11991
  if (localTip) {
11860
11992
  let existing = resolveWorkspaceWorktree(listWorktrees(appDir), id);
11861
- if (existing && !existsSync25(existing)) {
11993
+ if (existing && !existsSync23(existing)) {
11862
11994
  runGit(appDir, ["worktree", "prune"], { allowFail: true });
11863
11995
  existing = null;
11864
11996
  }
@@ -11976,7 +12108,7 @@ var attachWorkspaceCommand = defineDeepspaceCommand({
11976
12108
  process.cwd(),
11977
12109
  (typeof args.dir === "string" ? args.dir.trim() : "") || `ws-${id.slice(3).toLowerCase()}`
11978
12110
  );
11979
- if (existsSync25(dir)) {
12111
+ if (existsSync23(dir)) {
11980
12112
  spinner2?.stop("Target exists.");
11981
12113
  throw new Refusal(`${dir} already exists \u2014 pass a different directory.`, "dir_exists");
11982
12114
  }
@@ -12313,13 +12445,13 @@ import * as p30 from "@clack/prompts";
12313
12445
 
12314
12446
  // src/cli/lib/validation.ts
12315
12447
  import { sync as spawnSync7 } from "cross-spawn";
12316
- import { readFileSync as readFileSync28 } from "fs";
12448
+ import { readFileSync as readFileSync26 } from "fs";
12317
12449
  import { join as join23 } from "path";
12318
12450
  var MAX_SUMMARY_CHARS = 4e3;
12319
12451
  var MAX_OUTPUT_BUFFER = 64 * 1024 * 1024;
12320
12452
  function configuredCommand(appDir) {
12321
12453
  try {
12322
- const pkg2 = JSON.parse(readFileSync28(join23(appDir, "package.json"), "utf-8"));
12454
+ const pkg2 = JSON.parse(readFileSync26(join23(appDir, "package.json"), "utf-8"));
12323
12455
  if (pkg2.scripts?.validate) return `${detectPackageManager(appDir)} run --silent validate`;
12324
12456
  } catch {
12325
12457
  }
@@ -12357,6 +12489,11 @@ function conflictMarkerFiles(diffCheckOutput) {
12357
12489
  }
12358
12490
  return [...files];
12359
12491
  }
12492
+ function pullAfterLandAction(primaryDir, intoBranch, landedOid) {
12493
+ if (currentBranch(primaryDir) !== intoBranch) return null;
12494
+ if (resolveCommit(primaryDir, "HEAD") === landedOid) return null;
12495
+ return { cwd: primaryDir, argv: ["deepspace", "pull"] };
12496
+ }
12360
12497
  function landResumeArgv(args) {
12361
12498
  const argv = ["deepspace", "workspace", "land"];
12362
12499
  const workspace = args.workspace?.trim();
@@ -12635,6 +12772,7 @@ var landWorkspaceCommand = defineDeepspaceCommand({
12635
12772
  );
12636
12773
  }
12637
12774
  spinner2?.stop(`Landed ${id} into ${intoBranch} at ${landedOid.slice(0, 10)}.`);
12775
+ const primaryDir = primaryAppDir(appDir);
12638
12776
  const cleanup = keepWorktree ? null : cleanupWorkspaceLocal(appDir, id, intoBranch, { expectedBranchOid: landedOid });
12639
12777
  const inOwnWorktree = !cleanup && inOwnLinkedWorktree(appDir, id);
12640
12778
  const retainedWorktree = cleanup?.worktreeRetained ?? (keepWorktree || inOwnWorktree ? appDir : null);
@@ -12652,7 +12790,7 @@ var landWorkspaceCommand = defineDeepspaceCommand({
12652
12790
  p30.log.info(`Retained checkout: ${retainedWorktree}`);
12653
12791
  }
12654
12792
  if (cleanup?.error) {
12655
- const action = cleanupAction({
12793
+ const action2 = cleanupAction({
12656
12794
  worktreeDir: cleanup.worktreeDir,
12657
12795
  branch: cleanup.branch
12658
12796
  });
@@ -12663,10 +12801,11 @@ var landWorkspaceCommand = defineDeepspaceCommand({
12663
12801
  branch: cleanup.branch
12664
12802
  }),
12665
12803
  "cleanup_incomplete",
12666
- { actionRequired: true, action, extra: data }
12804
+ { actionRequired: true, action: action2, extra: data }
12667
12805
  );
12668
12806
  }
12669
- return { data };
12807
+ const action = pullAfterLandAction(primaryDir, intoBranch, landedOid);
12808
+ return { data, ...action ? { action } : {} };
12670
12809
  }
12671
12810
  });
12672
12811
 
@@ -13106,7 +13245,7 @@ var source_default = defineDeepspaceCommand({
13106
13245
  args: {
13107
13246
  provider: {
13108
13247
  type: "positional",
13109
- description: "Source provider: github or deepspace (omit to show)",
13248
+ description: "Source authority: deepspace is platform-hosted and commit-first; github deploys the local working tree without Git sync (omit to show)",
13110
13249
  required: false
13111
13250
  },
13112
13251
  app: {
@@ -13384,7 +13523,7 @@ function sameRefs(left, right) {
13384
13523
 
13385
13524
  // src/cli/commands/update.ts
13386
13525
  import * as p34 from "@clack/prompts";
13387
- import { existsSync as existsSync26, readFileSync as readFileSync29, writeFileSync as writeFileSync10 } from "fs";
13526
+ import { existsSync as existsSync24, readFileSync as readFileSync27, writeFileSync as writeFileSync10 } from "fs";
13388
13527
  import { join as join24, resolve as resolve20 } from "path";
13389
13528
  var REGISTRY = "https://registry.npmjs.org/deepspace/latest";
13390
13529
  var DOCS_URL = "https://documentation.deep.space/cli-reference/commands";
@@ -13400,18 +13539,18 @@ function gapIsTooWide(from, to) {
13400
13539
  }
13401
13540
  function readAppSdkVersion(appDir) {
13402
13541
  const installed = join24(appDir, "node_modules", "deepspace", "package.json");
13403
- if (existsSync26(installed)) {
13404
- const parsed2 = JSON.parse(readFileSync29(installed, "utf8"));
13542
+ if (existsSync24(installed)) {
13543
+ const parsed2 = JSON.parse(readFileSync27(installed, "utf8"));
13405
13544
  if (typeof parsed2.version === "string") return parsed2.version;
13406
13545
  }
13407
13546
  const manifest = join24(appDir, "package.json");
13408
- if (!existsSync26(manifest)) return null;
13409
- const parsed = JSON.parse(readFileSync29(manifest, "utf8"));
13547
+ if (!existsSync24(manifest)) return null;
13548
+ const parsed = JSON.parse(readFileSync27(manifest, "utf8"));
13410
13549
  return parsed.dependencies?.deepspace ?? null;
13411
13550
  }
13412
13551
  function pinSdkVersion(appDir, version) {
13413
13552
  const path = join24(appDir, "package.json");
13414
- const source = readFileSync29(path, "utf8");
13553
+ const source = readFileSync27(path, "utf8");
13415
13554
  const updated = source.replace(
13416
13555
  /("deepspace"\s*:\s*")[^"]+(")/,
13417
13556
  (_match, head, tail) => `${head}^${version}${tail}`
@@ -13557,8 +13696,8 @@ var update_default = defineDeepspaceCommand({
13557
13696
 
13558
13697
  // src/cli/cli.ts
13559
13698
  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")) {
13699
+ var pkg = JSON.parse(readFileSync28(join25(__dirname, "..", "package.json"), "utf-8"));
13700
+ if (process.platform === "linux" && !process.env.SSL_CERT_FILE && existsSync25("/etc/ssl/certs/ca-certificates.crt")) {
13562
13701
  process.env.SSL_CERT_FILE = "/etc/ssl/certs/ca-certificates.crt";
13563
13702
  }
13564
13703
  var create2 = defineCommand14({
@@ -13689,6 +13828,46 @@ Run \`${unknown.helpPath.join(" ")} --help\` for the command list.`
13689
13828
  }
13690
13829
  process.exit(1);
13691
13830
  }
13831
+ function selectedLeaf(argv) {
13832
+ let command = main;
13833
+ let remaining = argv;
13834
+ const path = [];
13835
+ const commandPrefixArgs = [];
13836
+ for (; ; ) {
13837
+ const subCommands = command.subCommands;
13838
+ if (!subCommands || Object.keys(subCommands).length === 0) {
13839
+ return { command, rawArgs: [...commandPrefixArgs, ...remaining], path };
13840
+ }
13841
+ const index = remaining.findIndex((argument) => !argument.startsWith("-"));
13842
+ if (index < 0) return { command, rawArgs: [...commandPrefixArgs, ...remaining], path };
13843
+ const name = remaining[index];
13844
+ const entry = Object.entries(subCommands).find(([key, child]) => {
13845
+ if (key === name) return true;
13846
+ const alias = child.meta?.alias;
13847
+ return (Array.isArray(alias) ? alias : alias ? [alias] : []).includes(name);
13848
+ });
13849
+ if (!entry) return null;
13850
+ commandPrefixArgs.push(...remaining.slice(0, index));
13851
+ path.push(entry[0]);
13852
+ command = entry[1];
13853
+ remaining = remaining.slice(index + 1);
13854
+ }
13855
+ }
13856
+ function assertKnownOptions(argv) {
13857
+ const selected = selectedLeaf(argv);
13858
+ if (!selected || selected.path.join(" ") === "app create") return;
13859
+ const args = selected.command.args ?? {};
13860
+ const unknown = unknownOptionNamesFromRaw(selected.rawArgs, args);
13861
+ if (unknown.length === 0) return;
13862
+ const commandName = selected.path.join(" ") || "deepspace";
13863
+ const message = unknownOptionMessage(commandName, unknown, args);
13864
+ if (argv.includes("--json")) {
13865
+ console.log(JSON.stringify({ ok: false, code: "unknown_option", error: message }));
13866
+ } else {
13867
+ console.error(withSlug(message, "unknown_option"));
13868
+ }
13869
+ process.exit(1);
13870
+ }
13692
13871
  var rawArgs = process.argv.slice(2);
13693
13872
  var wantsHelp = rawArgs.some((arg) => arg === "--help" || arg === "-h");
13694
13873
  if (DEEPSPACE_ENV === "invalid") {
@@ -13700,6 +13879,9 @@ if (DEEPSPACE_ENV === "invalid") {
13700
13879
  }
13701
13880
  process.exit(1);
13702
13881
  }
13882
+ if (!wantsHelp && !(rawArgs.length === 1 && ["--version", "-v"].includes(rawArgs[0]))) {
13883
+ assertKnownOptions(rawArgs);
13884
+ }
13703
13885
  if (rawArgs.includes("--json") && !wantsHelp) {
13704
13886
  const unknown = findUnknownCommand(rawArgs, commandTree());
13705
13887
  if (unknown) {