deepspace 0.24.1 → 0.25.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
@@ -8,12 +8,43 @@ import {
8
8
  renderUsage
9
9
  } from "citty";
10
10
  import { sync as spawnSync8 } from "cross-spawn";
11
- import { readFileSync as readFileSync27, existsSync as existsSync29 } from "fs";
11
+ import { readFileSync as readFileSync27, existsSync as existsSync30 } from "fs";
12
12
  import { dirname as dirname13, join as join29 } from "path";
13
13
  import { fileURLToPath as fileURLToPath3 } from "url";
14
14
 
15
15
  // src/cli/lib/spinner.ts
16
16
  import * as p from "@clack/prompts";
17
+
18
+ // src/cli/lib/cli-format.ts
19
+ var ANSI_RE = /\[[0-9;]*m/g;
20
+ function stripAnsi(s) {
21
+ return s.replace(ANSI_RE, "");
22
+ }
23
+ function forTerminal(s) {
24
+ return process.stdout.isTTY ? s : stripAnsi(s);
25
+ }
26
+ function shQuote(s) {
27
+ return `'${s.replace(/'/g, "'\\''")}'`;
28
+ }
29
+ function humanCommand(argv) {
30
+ return argv.map((arg) => /^[A-Za-z0-9_./=:@-]+$/.test(arg) ? arg : shQuote(arg)).join(" ");
31
+ }
32
+ function displayText(value) {
33
+ return value.replace(/[\u0000-\u001f\u007f-\u009f]/g, (ch) => {
34
+ if (ch === " ") return " ";
35
+ const code = ch.codePointAt(0);
36
+ return `\\x${code.toString(16).padStart(2, "0")}`;
37
+ }).replace(/[\p{Cf}\p{Zl}\p{Zp}]/gu, (ch) => {
38
+ if (ch === "\u200C" || ch === "\u200D") return ch;
39
+ const code = ch.codePointAt(0);
40
+ return `\\u{${code.toString(16)}}`;
41
+ });
42
+ }
43
+ function displayLines(value) {
44
+ return value.split("\n").map((line) => displayText(line)).join("\n");
45
+ }
46
+
47
+ // src/cli/lib/spinner.ts
17
48
  var activeSpinner = null;
18
49
  function createSpinner() {
19
50
  const impl = process.stdout.isTTY ? p.spinner() : {
@@ -30,13 +61,13 @@ function createSpinner() {
30
61
  const wrapper = {
31
62
  start: (msg) => {
32
63
  activeSpinner = wrapper;
33
- impl.start(msg);
64
+ impl.start(msg === void 0 ? msg : displayLines(msg));
34
65
  },
35
66
  stop: (msg) => {
36
- impl.stop(msg);
67
+ impl.stop(msg === void 0 ? msg : displayLines(msg));
37
68
  if (activeSpinner === wrapper) activeSpinner = null;
38
69
  },
39
- message: (msg) => impl.message(msg)
70
+ message: (msg) => impl.message(msg === void 0 ? msg : displayLines(msg))
40
71
  };
41
72
  return wrapper;
42
73
  }
@@ -48,23 +79,6 @@ function stopActiveSpinner() {
48
79
  import * as p2 from "@clack/prompts";
49
80
  import { readFileSync, realpathSync } from "fs";
50
81
  import { dirname, isAbsolute, resolve } from "path";
51
-
52
- // src/cli/lib/cli-format.ts
53
- var ANSI_RE = /\[[0-9;]*m/g;
54
- function stripAnsi(s) {
55
- return s.replace(ANSI_RE, "");
56
- }
57
- function forTerminal(s) {
58
- return process.stdout.isTTY ? s : stripAnsi(s);
59
- }
60
- function shQuote(s) {
61
- return `'${s.replace(/'/g, "'\\''")}'`;
62
- }
63
- function humanCommand(argv) {
64
- return argv.map((arg) => /^[A-Za-z0-9_./=:@-]+$/.test(arg) ? arg : shQuote(arg)).join(" ");
65
- }
66
-
67
- // src/cli/lib/output.ts
68
82
  function resolveActionArgv(argv, cwd) {
69
83
  if (argv[0] !== "deepspace") return argv;
70
84
  const entry = (cwd ? installedCliEntry(cwd) : null) ?? cliEntryPath();
@@ -122,9 +136,10 @@ function withSlug(msg, code) {
122
136
  return msg.trimEnd().endsWith(tag) ? msg : `${msg.trimEnd()} ${tag}`;
123
137
  }
124
138
  function displayArg(arg) {
125
- if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(arg)) return arg;
126
- if (process.platform === "win32") return `"${arg.replaceAll('"', '""')}"`;
127
- return shQuote(arg);
139
+ const safe = displayText(arg);
140
+ if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(safe)) return safe;
141
+ if (process.platform === "win32") return `"${safe.replaceAll('"', '""')}"`;
142
+ return shQuote(safe);
128
143
  }
129
144
  function printAction(action) {
130
145
  assertExecutableAction(action);
@@ -171,7 +186,7 @@ async function fetchWithTransientRetry(url, makeInit, {
171
186
  // src/cli/lib/api.ts
172
187
  var ApiError = class extends Error {
173
188
  constructor(message, status3, code, apiPath, details) {
174
- super(message);
189
+ super(displayLines(message));
175
190
  this.status = status3;
176
191
  this.code = code;
177
192
  this.apiPath = apiPath;
@@ -232,7 +247,7 @@ async function apiFetch(baseUrl, token, path, init, isRetry = false) {
232
247
  }
233
248
  } catch {
234
249
  }
235
- if (!code && res.status === 404 && text2.trim() === "Not found") {
250
+ if (!code && res.status === 404 && msg.trim() === "Not found") {
236
251
  code = "unrecognized_service";
237
252
  msg = `The deploy service at ${baseUrl} doesn't recognize this request \u2014 check DEEPSPACE_DEPLOY_URL (is this the service the app lives on?); if it is, the server may be older than this CLI.`;
238
253
  }
@@ -286,17 +301,23 @@ function isTransientFailure(error) {
286
301
  // src/cli/lib/cli-errors.ts
287
302
  var InputError = class extends Error {
288
303
  constructor(message, code) {
289
- super(message);
304
+ super(displayLines(message));
290
305
  this.code = code;
291
306
  }
292
307
  };
308
+ function isMissingCwdError(err) {
309
+ if (!(err instanceof Error)) return false;
310
+ const errno = err;
311
+ if (errno.code !== "ENOENT") return false;
312
+ return errno.syscall === "uv_cwd";
313
+ }
293
314
  var Refusal = class extends Error {
294
315
  code;
295
316
  action;
296
317
  actionRequired;
297
318
  extra;
298
319
  constructor(message, code, opts = {}) {
299
- super(message);
320
+ super(displayLines(message));
300
321
  this.name = "Refusal";
301
322
  this.code = code;
302
323
  this.action = opts.action ? executableAction(opts.action) : void 0;
@@ -338,6 +359,7 @@ var CliExit = class extends Error {
338
359
  };
339
360
  function errorCode(err) {
340
361
  if (err instanceof ApiError || err instanceof InputError || err instanceof Refusal) return err.code;
362
+ if (isMissingCwdError(err)) return "worktree_missing";
341
363
  if (err instanceof Error && err.name === "GitError") return err.code ?? "git_error";
342
364
  if (err instanceof Error && err.name === "WranglerConfigError")
343
365
  return err.code ?? "invalid_config";
@@ -356,6 +378,10 @@ var API_ERROR_HINTS = {
356
378
  invite_email_failed: "The invite itself was not created and the charge was voided \u2014 nothing to undo. Retrying helps only if the send was transient; check the address is real and deliverable first. An email is needed ONLY for someone with no DeepSpace account yet: an existing user is added instantly, so having them sign in once and re-running this skips email entirely. If a retry now reports `already_invited` with no email sent, cancel the stuck invite (`deepspace app collaborators cancel <email>`) and invite again."
357
379
  };
358
380
  function formatCliError(err) {
381
+ if (isMissingCwdError(err)) {
382
+ const pwd = process.env.PWD;
383
+ return `The directory this command was run from no longer exists${pwd ? ` (${pwd})` : ""} \u2014 it was deleted or moved while the shell was still in it. Move somewhere that exists (\`cd\` to the main checkout). If it was a workspace worktree that a land, merge, or drop removed, \`deepspace workspace attach <ws_\u2026>\` re-materializes it there.`;
384
+ }
359
385
  const message = err instanceof Error ? err.message : String(err);
360
386
  const slug = errorCode(err);
361
387
  const hint = slug ? API_ERROR_HINTS[slug] : void 0;
@@ -363,11 +389,11 @@ function formatCliError(err) {
363
389
  ${hint}` : message;
364
390
  }
365
391
  function renderCliError(err) {
392
+ stopActiveSpinner();
366
393
  if (err instanceof CliExit) {
367
394
  process.exitCode = err.exitCode;
368
395
  return;
369
396
  }
370
- stopActiveSpinner();
371
397
  const message = formatCliError(err);
372
398
  const slug = errorCode(err);
373
399
  if (process.argv.includes("--json")) {
@@ -529,6 +555,9 @@ function editDistance(a, b) {
529
555
  dp[i][j - 1] + 1,
530
556
  dp[i - 1][j - 1] + (left[i - 1] === right[j - 1] ? 0 : 1)
531
557
  );
558
+ if (i > 1 && j > 1 && left[i - 1] === right[j - 2] && left[i - 2] === right[j - 1]) {
559
+ dp[i][j] = Math.min(dp[i][j], dp[i - 2][j - 2] + 1);
560
+ }
532
561
  }
533
562
  }
534
563
  return dp[left.length][right.length];
@@ -548,15 +577,21 @@ function closestCommandPath(input, table) {
548
577
  );
549
578
  return ranked[0]?.path ?? null;
550
579
  }
580
+ function takeLeadingPositionals(rest) {
581
+ const stop = rest.findIndex((token) => token.startsWith("-"));
582
+ return stop === -1 ? rest : rest.slice(0, stop);
583
+ }
551
584
  function findUnknownCommand(argv, rootCommands) {
552
585
  let table = rootCommands;
553
586
  const accepted = [];
554
- for (const token of argv) {
587
+ for (const [index, token] of argv.entries()) {
555
588
  if (!table || token.startsWith("-")) return null;
556
589
  const command = table[token];
557
590
  if (!command) {
591
+ const exactAll = commandPaths(table).filter((path) => path.at(-1) === token);
592
+ const exactHit = exactAll.length === 1 ? exactAll[0] : null;
558
593
  const split = splitTokenPath(token, table);
559
- const relativeSuggestion = split ?? closestCommandPath(token, table);
594
+ const relativeSuggestion = exactHit ?? split ?? closestCommandPath(token, table);
560
595
  if (!relativeSuggestion) return null;
561
596
  const guessed = relativeSuggestion.at(-1) ?? "";
562
597
  return {
@@ -564,6 +599,11 @@ function findUnknownCommand(argv, rootCommands) {
564
599
  helpPath: ["deepspace", ...accepted],
565
600
  suggestion: ["deepspace", ...accepted, ...relativeSuggestion],
566
601
  executable: split !== null && !DESTRUCTIVE_VERBS.has(guessed),
602
+ // POSITIONALS ONLY. The remainder exists so the corrected verb keeps
603
+ // the arguments it needs; carrying flags too meant `auth logn --email
604
+ // a@b --password hunter2` echoed the password into `action.argv`, and
605
+ // agents persist envelopes. Cut at the first `-` token.
606
+ remainder: takeLeadingPositionals(argv.slice(index + 1)),
567
607
  ...split ? { quotedToken: true } : {}
568
608
  };
569
609
  }
@@ -1352,6 +1392,20 @@ function hasWranglerConfig(appDir) {
1352
1392
  }
1353
1393
 
1354
1394
  // src/cli/lib/app-identity.ts
1395
+ function declaredAppIds(cwd = process.cwd()) {
1396
+ const appDir = resolve2(cwd);
1397
+ if (!hasWranglerConfig(appDir)) return /* @__PURE__ */ new Set();
1398
+ let cfg;
1399
+ try {
1400
+ cfg = readWranglerConfig(appDir);
1401
+ } catch {
1402
+ return /* @__PURE__ */ new Set();
1403
+ }
1404
+ const sections = [cfg.vars, ...Object.values(cfg.env ?? {}).map((env) => env?.vars)];
1405
+ return new Set(
1406
+ sections.map((vars) => vars?.DEEPSPACE_APP_ID).filter((id) => typeof id === "string" && APP_ID_RE.test(id))
1407
+ );
1408
+ }
1355
1409
  var APP_ID_PLACEHOLDER = "__APP_ID__";
1356
1410
  function readAppId(cwd = process.cwd(), wranglerEnv) {
1357
1411
  const appDir = resolve2(cwd);
@@ -1437,7 +1491,7 @@ function findChildApps(dir) {
1437
1491
 
1438
1492
  // src/cli/lib/launch-config.ts
1439
1493
  import {
1440
- existsSync as existsSync7,
1494
+ existsSync as existsSync8,
1441
1495
  readFileSync as readFileSync7,
1442
1496
  realpathSync as realpathSync2,
1443
1497
  writeFileSync as writeFileSync4,
@@ -1448,16 +1502,17 @@ import {
1448
1502
  import { join as join6, resolve as resolve5, dirname as dirname5, basename, isAbsolute as isAbsolute2, sep } from "path";
1449
1503
 
1450
1504
  // src/cli/lib/git/repository.ts
1451
- import { appendFileSync, existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync6 } from "fs";
1505
+ import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync6 } from "fs";
1452
1506
  import { dirname as dirname4, join as join5, resolve as resolve4 } from "path";
1453
1507
 
1454
1508
  // src/cli/lib/git/process.ts
1455
1509
  import { sync as spawnSync } from "cross-spawn";
1510
+ import { existsSync as existsSync6 } from "fs";
1456
1511
  var MAX_GIT_BUFFER = 512 * 1024 * 1024;
1457
1512
  var GIT_TIMEOUT_MS = 3e5;
1458
1513
  var GitError = class extends Error {
1459
1514
  constructor(message, code) {
1460
- super(message);
1515
+ super(displayLines(message));
1461
1516
  this.code = code;
1462
1517
  this.name = "GitError";
1463
1518
  }
@@ -1489,7 +1544,13 @@ function runGit(cwd, args, opts = {}) {
1489
1544
  }
1490
1545
  if (result.error) {
1491
1546
  const code = result.error.code;
1492
- if (code === "ENOENT") {
1547
+ if (code === "ENOENT" || code === "ENOTDIR") {
1548
+ if (code === "ENOTDIR" || !existsSync6(cwd)) {
1549
+ throw new GitError(
1550
+ `The directory this command runs in is not there: ${cwd}. If it was a workspace worktree, prune it (\`git worktree prune\`) and re-attach.`,
1551
+ "worktree_missing"
1552
+ );
1553
+ }
1493
1554
  throw new GitError(
1494
1555
  "git is not installed or not on PATH \u2014 install git and retry.",
1495
1556
  "git_not_installed"
@@ -1583,7 +1644,7 @@ function assertNoOperationInProgress(cwd) {
1583
1644
  ["REVERT_HEAD", "revert", "git revert --continue", "git revert --abort"],
1584
1645
  ["rebase-merge", "rebase", "git rebase --continue", "git rebase --abort"],
1585
1646
  ["rebase-apply", "rebase", "git rebase --continue", "git rebase --abort"]
1586
- ].find(([marker]) => existsSync6(join5(resolve4(cwd, gitDir), marker)));
1647
+ ].find(([marker]) => existsSync7(join5(resolve4(cwd, gitDir), marker)));
1587
1648
  if (!inProgress) return;
1588
1649
  const [, kind, finish, abort] = inProgress;
1589
1650
  throw new InputError(
@@ -1594,6 +1655,18 @@ function assertNoOperationInProgress(cwd) {
1594
1655
  function repoToplevel(cwd) {
1595
1656
  return gitLine(cwd, ["rev-parse", "--show-toplevel"]);
1596
1657
  }
1658
+ function interruptedGitOperation(cwd) {
1659
+ const gitDir = runGit(cwd, ["rev-parse", "--git-path", "HEAD"], { allowFail: true });
1660
+ if (gitDir.status !== 0) return null;
1661
+ const dir = dirname4(resolve4(cwd, gitDir.stdout.toString("utf-8").trim()));
1662
+ if (existsSync7(join5(dir, "rebase-apply", "applying"))) return "am";
1663
+ if (existsSync7(join5(dir, "rebase-merge")) || existsSync7(join5(dir, "rebase-apply"))) return "rebase";
1664
+ if (existsSync7(join5(dir, "MERGE_HEAD"))) return "merge";
1665
+ if (existsSync7(join5(dir, "CHERRY_PICK_HEAD"))) return "cherry-pick";
1666
+ if (existsSync7(join5(dir, "REVERT_HEAD"))) return "revert";
1667
+ if (existsSync7(join5(dir, "BISECT_LOG"))) return "bisect";
1668
+ return null;
1669
+ }
1597
1670
  function currentBranch(cwd) {
1598
1671
  const result = runGit(cwd, ["symbolic-ref", "--short", "-q", "HEAD"], { allowFail: true });
1599
1672
  const name = result.stdout.toString("utf-8").trim();
@@ -1609,6 +1682,9 @@ function resolveCommit(cwd, ref) {
1609
1682
  function isWorkTreeClean(cwd) {
1610
1683
  return gitLine(cwd, ["status", "--porcelain"]) === "";
1611
1684
  }
1685
+ function hasUnmergedEntries(cwd) {
1686
+ return gitLine(cwd, ["ls-files", "-u"]) !== "";
1687
+ }
1612
1688
  function updateRef(cwd, ref, oid) {
1613
1689
  runGit(cwd, ["update-ref", ref, oid]);
1614
1690
  }
@@ -1665,7 +1741,7 @@ function listWorktrees(cwd) {
1665
1741
  current.branch = ref.startsWith("refs/heads/") ? ref.slice("refs/heads/".length) : ref;
1666
1742
  }
1667
1743
  }
1668
- return worktrees;
1744
+ return worktrees.filter((worktree) => existsSync7(worktree.path));
1669
1745
  }
1670
1746
  function ensureLocalExclude(cwd, pattern) {
1671
1747
  const commonDir = resolve4(cwd, gitLine(cwd, ["rev-parse", "--git-common-dir"]));
@@ -1704,7 +1780,7 @@ function writeLaunchFile(launchPath, config) {
1704
1780
  }
1705
1781
  function readLaunchFile(launchPath) {
1706
1782
  const empty = { version: "0.0.1", configurations: [] };
1707
- if (!existsSync7(launchPath)) return empty;
1783
+ if (!existsSync8(launchPath)) return empty;
1708
1784
  const raw = readFileSync7(launchPath, "utf-8").trim();
1709
1785
  if (!raw) return empty;
1710
1786
  const parsed = JSON.parse(raw);
@@ -1815,13 +1891,13 @@ function upsertWorktreeLaunchConfig(mainRepoRoot, worktreeName, appDir, opts) {
1815
1891
  const worktreesRoot = join6(mainRepoRoot, ".claude", "worktrees") + sep;
1816
1892
  config.configurations = config.configurations.filter((c) => {
1817
1893
  if (c?.name === entryName) return false;
1818
- const isStaleWorktreeEntry = typeof c?.name === "string" && c.name.startsWith(WORKTREE_ENTRY_PREFIX) && typeof c.cwd === "string" && isAbsolute2(c.cwd) && c.cwd.startsWith(worktreesRoot) && !existsSync7(c.cwd);
1894
+ const isStaleWorktreeEntry = typeof c?.name === "string" && c.name.startsWith(WORKTREE_ENTRY_PREFIX) && typeof c.cwd === "string" && isAbsolute2(c.cwd) && c.cwd.startsWith(worktreesRoot) && !existsSync8(c.cwd);
1819
1895
  return !isStaleWorktreeEntry;
1820
1896
  });
1821
1897
  let port = opts.port;
1822
1898
  if (opts.probePort) {
1823
1899
  const taken = new Set(
1824
- config.configurations.map((c) => Number(c?.port)).filter((p35) => Number.isInteger(p35) && p35 > 0)
1900
+ config.configurations.map((c) => Number(c?.port)).filter((p36) => Number.isInteger(p36) && p36 > 0)
1825
1901
  );
1826
1902
  for (let i = 0; i < WORKTREE_PORT_RANGE && taken.has(port); i++) {
1827
1903
  port = WORKTREE_PORT_MIN + (port - WORKTREE_PORT_MIN + 1) % WORKTREE_PORT_RANGE;
@@ -1865,6 +1941,29 @@ function resolveWorktreePort(appDir) {
1865
1941
  // src/cli/lib/repo-api.ts
1866
1942
  import { randomUUID } from "crypto";
1867
1943
 
1944
+ // src/cli/lib/workspace-id.ts
1945
+ var ULID_RE = /^[0-9A-HJKMNP-TV-Z]{26}$/i;
1946
+ var WORKSPACE_ID_RE = /^ws_[0-9A-HJKMNP-TV-Z]{26}$/;
1947
+ function isWorkspaceId(id) {
1948
+ return WORKSPACE_ID_RE.test(id);
1949
+ }
1950
+ function workspaceBranchName(workspaceId) {
1951
+ return `ws/${workspaceId.replace(/^ws_/, "").toLowerCase()}`;
1952
+ }
1953
+ function workspaceIdFromBranch(branch) {
1954
+ if (!branch || !branch.startsWith("ws/")) return null;
1955
+ const ulid = branch.slice(3);
1956
+ if (!ULID_RE.test(ulid)) return null;
1957
+ return `ws_${ulid.toUpperCase()}`;
1958
+ }
1959
+ function isSelectedWorkspaceCheckout(branch, workspaceId) {
1960
+ return workspaceIdFromBranch(branch) === workspaceId;
1961
+ }
1962
+ function resolveWorkspaceWorktree(worktrees, workspaceId) {
1963
+ const branch = workspaceBranchName(workspaceId);
1964
+ return worktrees.find((worktree) => worktree.branch === branch)?.path ?? null;
1965
+ }
1966
+
1868
1967
  // src/cli/lib/source-api.ts
1869
1968
  function githubSourceRefusal(appId, repository) {
1870
1969
  return new Refusal(
@@ -1898,6 +1997,10 @@ function setAppSource(deployUrl, token, appId, input) {
1898
1997
  }
1899
1998
 
1900
1999
  // src/cli/lib/repo-api.ts
2000
+ function isWorkspaceRef(ref) {
2001
+ const prefix = "refs/deepspace/ws/";
2002
+ return ref.startsWith(prefix) && isWorkspaceId(ref.slice(prefix.length));
2003
+ }
1901
2004
  function releaseSourceLabel(release2) {
1902
2005
  if (release2.commitOid) return `commit ${release2.commitOid.slice(0, 10)}`;
1903
2006
  if (release2.source?.provider === "github") return `GitHub \xB7 ${release2.source.repository}`;
@@ -1937,12 +2040,22 @@ function repoApi(deployUrl, token, appId) {
1937
2040
  "invalid_response",
1938
2041
  `${base}${path}`
1939
2042
  );
2043
+ const normalizeWorkspace = (view, path) => {
2044
+ if (view === null || typeof view !== "object") return;
2045
+ const workspace = view.workspace;
2046
+ if (workspace === null || typeof workspace !== "object") return;
2047
+ const fields = workspace;
2048
+ if (typeof fields.ref !== "string" || !isWorkspaceRef(fields.ref)) throw shapeError(path);
2049
+ if (typeof fields.task === "string") fields.task = displayText(fields.task);
2050
+ };
1940
2051
  const assertShape = (r, spec, path) => {
1941
2052
  if (r === null || typeof r !== "object") throw shapeError(path);
1942
2053
  const obj = r;
1943
2054
  for (const [key, kind] of Object.entries(spec)) {
1944
2055
  if (!KIND[kind](obj[key])) throw shapeError(path);
1945
2056
  }
2057
+ normalizeWorkspace(obj.view, path);
2058
+ if (Array.isArray(obj.views)) for (const view of obj.views) normalizeWorkspace(view, path);
1946
2059
  return r;
1947
2060
  };
1948
2061
  const getShape = async (path, spec) => assertShape(await call(path), spec, path);
@@ -1964,6 +2077,9 @@ function repoApi(deployUrl, token, appId) {
1964
2077
  createWorkspace(args) {
1965
2078
  return postShape("/workspaces", args, { view: "object" });
1966
2079
  },
2080
+ /** `truncated` ⇒ the server had more rows than `limit` and stopped. Without
2081
+ * it a capped page is indistinguishable from the whole set, so a caller
2082
+ * reading "3 workspaces" cannot tell that a fourth exists. */
1967
2083
  listWorkspaces(opts) {
1968
2084
  const params = new URLSearchParams();
1969
2085
  if (opts?.all) params.set("all", "1");
@@ -2241,11 +2357,11 @@ async function writeDevVars(appDir, ownerId, callerJwt, wranglerEnv, opts = {})
2241
2357
  }
2242
2358
 
2243
2359
  // src/cli/lib/install-status.ts
2244
- import { existsSync as existsSync9, readFileSync as readFileSync9, realpathSync as realpathSync3 } from "fs";
2360
+ import { existsSync as existsSync10, readFileSync as readFileSync9, realpathSync as realpathSync3 } from "fs";
2245
2361
  import { dirname as dirname6, join as join8, resolve as resolve6 } from "path";
2246
2362
 
2247
2363
  // src/cli/lib/package-manager.ts
2248
- import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
2364
+ import { existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
2249
2365
  import { join as join7 } from "path";
2250
2366
  function detectPackageManager(appDir) {
2251
2367
  try {
@@ -2254,9 +2370,9 @@ function detectPackageManager(appDir) {
2254
2370
  if (name === "npm" || name === "pnpm" || name === "yarn" || name === "bun") return name;
2255
2371
  } catch {
2256
2372
  }
2257
- if (existsSync8(join7(appDir, "pnpm-lock.yaml"))) return "pnpm";
2258
- if (existsSync8(join7(appDir, "yarn.lock"))) return "yarn";
2259
- if (existsSync8(join7(appDir, "bun.lock")) || existsSync8(join7(appDir, "bun.lockb"))) return "bun";
2373
+ if (existsSync9(join7(appDir, "pnpm-lock.yaml"))) return "pnpm";
2374
+ if (existsSync9(join7(appDir, "yarn.lock"))) return "yarn";
2375
+ if (existsSync9(join7(appDir, "bun.lock")) || existsSync9(join7(appDir, "bun.lockb"))) return "bun";
2260
2376
  return "npm";
2261
2377
  }
2262
2378
  function installCommand(appDir) {
@@ -2267,9 +2383,9 @@ function installCommand(appDir) {
2267
2383
  function installState(appDir) {
2268
2384
  if (resolvesDeepspace(appDir)) return "ready";
2269
2385
  const sentinelDir = join8(appDir, ".deepspace");
2270
- if (existsSync9(join8(sentinelDir, "install.err"))) return "failed";
2271
- if (existsSync9(join8(sentinelDir, "install.started"))) {
2272
- if (!existsSync9(join8(sentinelDir, "install.done")) && installWorkerAlive(sentinelDir)) {
2386
+ if (existsSync10(join8(sentinelDir, "install.err"))) return "failed";
2387
+ if (existsSync10(join8(sentinelDir, "install.started"))) {
2388
+ if (!existsSync10(join8(sentinelDir, "install.done")) && installWorkerAlive(sentinelDir)) {
2273
2389
  return "installing";
2274
2390
  }
2275
2391
  return "failed";
@@ -2286,25 +2402,25 @@ function ensureInstallReady(appDir) {
2286
2402
  const startedPath = join8(sentinelDir, "install.started");
2287
2403
  const donePath = join8(sentinelDir, "install.done");
2288
2404
  const logPath = join8(sentinelDir, "install.log");
2289
- if (existsSync9(errPath)) {
2405
+ if (existsSync10(errPath)) {
2290
2406
  const detail = readFileSync9(errPath, "utf-8").trimEnd();
2291
- const log29 = existsSync9(logPath) ? ` Full log: ${logPath}.` : "";
2407
+ const log29 = existsSync10(logPath) ? ` Full log: ${logPath}.` : "";
2292
2408
  throw new Refusal(
2293
2409
  `Dependency install failed: ${detail}.${log29} Run \`npm install\` (or \`bun install\`) manually, then retry.`,
2294
2410
  "install_failed",
2295
2411
  { action: { cwd: appDir, argv: [detectPackageManager(appDir), "install"] } }
2296
2412
  );
2297
2413
  }
2298
- if (existsSync9(startedPath) && !existsSync9(donePath)) {
2414
+ if (existsSync10(startedPath) && !existsSync10(donePath)) {
2299
2415
  if (!installWorkerAlive(sentinelDir)) {
2300
- const log29 = existsSync9(logPath) ? ` See what happened: ${logPath}.` : "";
2416
+ const log29 = existsSync10(logPath) ? ` See what happened: ${logPath}.` : "";
2301
2417
  throw new Refusal(
2302
2418
  `The dependency install is no longer running and never finished.${log29} Run \`npm install\` (or \`bun install\`) manually, then retry.`,
2303
2419
  "install_failed",
2304
2420
  { action: { cwd: appDir, argv: [detectPackageManager(appDir), "install"] } }
2305
2421
  );
2306
2422
  }
2307
- const tail = existsSync9(logPath) ? ` Tail progress: ${tailHint(logPath)}.` : "";
2423
+ const tail = existsSync10(logPath) ? ` Tail progress: ${tailHint(logPath)}.` : "";
2308
2424
  throw new Refusal(
2309
2425
  `Dependencies are still installing.${tail} Retry once it finishes.`,
2310
2426
  "install_in_progress"
@@ -2326,7 +2442,7 @@ function resolvesDeepspace(fromDir) {
2326
2442
  } catch {
2327
2443
  }
2328
2444
  for (; ; ) {
2329
- if (existsSync9(join8(dir, "node_modules", "deepspace", "package.json"))) return true;
2445
+ if (existsSync10(join8(dir, "node_modules", "deepspace", "package.json"))) return true;
2330
2446
  if (dir === checkoutRoot) return false;
2331
2447
  const parent = dirname6(dir);
2332
2448
  if (parent === dir) return false;
@@ -2342,7 +2458,7 @@ function canonicalPath2(path) {
2342
2458
  }
2343
2459
  function installWorkerAlive(sentinelDir) {
2344
2460
  const pidPath = join8(sentinelDir, "install.pid");
2345
- if (!existsSync9(pidPath)) return false;
2461
+ if (!existsSync10(pidPath)) return false;
2346
2462
  return processAlive(Number(readFileSync9(pidPath, "utf-8").trim()));
2347
2463
  }
2348
2464
  function processAlive(pid) {
@@ -2357,7 +2473,7 @@ function processAlive(pid) {
2357
2473
 
2358
2474
  // src/cli/lib/preflight.ts
2359
2475
  import { execFileSync } from "child_process";
2360
- import { existsSync as existsSync10 } from "fs";
2476
+ import { existsSync as existsSync11 } from "fs";
2361
2477
  import * as nodeModule from "module";
2362
2478
  import { resolve as resolve7 } from "path";
2363
2479
  function preflightNodeVersion(command, hasRegisterHooks = typeof nodeModule.registerHooks === "function", version = process.versions.node) {
@@ -2380,7 +2496,7 @@ function preflightWindowsWorkerd(appDir) {
2380
2496
  "bin",
2381
2497
  "workerd.exe"
2382
2498
  );
2383
- if (!existsSync10(workerdExe)) return;
2499
+ if (!existsSync11(workerdExe)) return;
2384
2500
  try {
2385
2501
  execFileSync(workerdExe, ["--help"], { stdio: "pipe", timeout: 1e4 });
2386
2502
  return;
@@ -2731,28 +2847,34 @@ var CONFIG_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
2731
2847
  var MAX_SECRET_VALUE_BYTES = 32 * 1024;
2732
2848
  function validateConfigName(configName) {
2733
2849
  if (!CONFIG_NAME_RE.test(configName)) {
2734
- throw new Error(
2735
- `Invalid config "${configName}". Use letters, numbers, underscores, or hyphens; start with a letter or number.`
2850
+ throw new InputError(
2851
+ `Invalid config "${configName}". Use letters, numbers, underscores, or hyphens; start with a letter or number.`,
2852
+ "invalid_config_name"
2736
2853
  );
2737
2854
  }
2738
2855
  return configName;
2739
2856
  }
2740
2857
  function validateSecretName(name) {
2741
2858
  if (!SECRET_NAME_RE.test(name)) {
2742
- throw new Error(
2743
- `Invalid secret name "${name}". Use letters, numbers, and underscores; start with a letter or underscore.`
2859
+ throw new InputError(
2860
+ `Invalid secret name "${name}". Use letters, numbers, and underscores; start with a letter or underscore.`,
2861
+ "invalid_secret_name"
2744
2862
  );
2745
2863
  }
2746
2864
  if (RESERVED_SECRET_NAMES.has(name)) {
2747
- throw new Error(
2748
- `"${name}" is managed by the DeepSpace SDK and can't be set as an app secret \u2014 the platform injects it at deploy time.`
2865
+ throw new InputError(
2866
+ `"${name}" is managed by the DeepSpace SDK and can't be set as an app secret \u2014 the platform injects it at deploy time.`,
2867
+ "reserved_secret_name"
2749
2868
  );
2750
2869
  }
2751
2870
  return name;
2752
2871
  }
2753
2872
  function validateSecretValue(key, value) {
2754
2873
  if (new TextEncoder().encode(value).byteLength > MAX_SECRET_VALUE_BYTES) {
2755
- throw new Error(`Secret "${key}" exceeds ${MAX_SECRET_VALUE_BYTES} bytes`);
2874
+ throw new InputError(
2875
+ `Secret "${key}" exceeds ${MAX_SECRET_VALUE_BYTES} bytes`,
2876
+ "secret_too_large"
2877
+ );
2756
2878
  }
2757
2879
  }
2758
2880
  function defaultConfigNameForEnv(wranglerEnv) {
@@ -2835,7 +2957,9 @@ async function pullAppSecretsCache(deployUrl, token, appId, configName) {
2835
2957
  }
2836
2958
  return { appId, configName, values: secrets };
2837
2959
  } catch (err) {
2838
- if (err?.status === 404) return null;
2960
+ if (err instanceof ApiError && err.status === 404 && err.code !== "unrecognized_service") {
2961
+ return null;
2962
+ }
2839
2963
  throw err;
2840
2964
  }
2841
2965
  }
@@ -2857,14 +2981,27 @@ function parseSecretsUpload(content) {
2857
2981
  const trimmed = content.trim();
2858
2982
  if (!trimmed) return {};
2859
2983
  if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
2860
- const parsed = JSON.parse(trimmed);
2984
+ let parsed;
2985
+ try {
2986
+ parsed = JSON.parse(trimmed);
2987
+ } catch {
2988
+ throw new InputError(
2989
+ "The input starts with { or [ but is not valid JSON. Pass a JSON object of KEY:value strings, or a dotenv file.",
2990
+ "invalid_format"
2991
+ );
2992
+ }
2861
2993
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
2862
- throw new Error("JSON upload must be an object of KEY=value strings.");
2994
+ throw new InputError("JSON upload must be an object of KEY=value strings.", "invalid_format");
2863
2995
  }
2864
2996
  const out2 = {};
2865
2997
  for (const [key, value] of Object.entries(parsed)) {
2866
2998
  validateSecretName(key);
2867
- if (typeof value !== "string") throw new Error(`Secret "${key}" must be a string`);
2999
+ if (typeof value !== "string") {
3000
+ throw new InputError(
3001
+ `Secret "${key}" must be a string \u2014 got ${Array.isArray(value) ? "an array" : typeof value}.`,
3002
+ "invalid_format"
3003
+ );
3004
+ }
2868
3005
  validateSecretValue(key, value);
2869
3006
  out2[key] = value;
2870
3007
  }
@@ -2909,8 +3046,9 @@ function parseSecretsUpload(content) {
2909
3046
  parts.push(lines[i]);
2910
3047
  }
2911
3048
  if (!closed) {
2912
- throw new Error(
2913
- `Unterminated quoted value for "${key}" (opens on line ${startLine}). Close the ${quote === '"' ? "double" : "single"} quote, or escape newlines as \\n on one line.`
3049
+ throw new InputError(
3050
+ `Unterminated quoted value for "${key}" (opens on line ${startLine}). Close the ${quote === '"' ? "double" : "single"} quote, or escape newlines as \\n on one line.`,
3051
+ "invalid_format"
2914
3052
  );
2915
3053
  }
2916
3054
  value = parts.join("\n");
@@ -2967,7 +3105,7 @@ async function refreshSecretsCache(deployUrl, token, appId, wranglerEnv, selecte
2967
3105
  }
2968
3106
 
2969
3107
  // src/cli/lib/schema-lint.ts
2970
- import { existsSync as existsSync11 } from "fs";
3108
+ import { existsSync as existsSync12 } from "fs";
2971
3109
  import { join as join10 } from "path";
2972
3110
  import { pathToFileURL } from "url";
2973
3111
 
@@ -3001,7 +3139,7 @@ function lintSchema(schema) {
3001
3139
  const offendingRoles = Object.entries(schema.permissions).filter(([, perms]) => perms.read === true).map(([role]) => role);
3002
3140
  if (offendingRoles.length > 0) {
3003
3141
  const usesAware = Object.values(schema.permissions).some(
3004
- (p35) => typeof p35.read === "string" && VISIBILITY_AWARE.includes(p35.read)
3142
+ (p36) => typeof p36.read === "string" && VISIBILITY_AWARE.includes(p36.read)
3005
3143
  );
3006
3144
  if (!usesAware) {
3007
3145
  warnings.push(
@@ -3037,7 +3175,7 @@ function lintSchemaSet(schemas) {
3037
3175
  const warnings = [];
3038
3176
  if (!schemas.some((schema) => schema.name === "team_members")) {
3039
3177
  for (const schema of schemas) {
3040
- const teamRoles = Object.entries(schema.permissions).filter(([, p35]) => [p35.read, p35.update, p35.delete].includes("team")).map(([role]) => role);
3178
+ const teamRoles = Object.entries(schema.permissions).filter(([, p36]) => [p36.read, p36.update, p36.delete].includes("team")).map(([role]) => role);
3041
3179
  if (teamRoles.length > 0) {
3042
3180
  warnings.push(
3043
3181
  `[${schema.name}] role(s) ${teamRoles.join(", ")} use the 'team' permission level, but no 'team_members' collection is registered. 'team' resolves membership from team_members (teamId, userId, status), so without it those roles are denied everything \u2014 silently. Register a team_members schema or use another level.`
@@ -3051,7 +3189,7 @@ function lintSchemaSet(schemas) {
3051
3189
  // src/cli/lib/schema-lint.ts
3052
3190
  async function lintProjectSchemas(appDir) {
3053
3191
  const schemasPath = join10(appDir, "src", "schemas.ts");
3054
- if (!existsSync11(schemasPath)) return null;
3192
+ if (!existsSync12(schemasPath)) return null;
3055
3193
  let schemas;
3056
3194
  try {
3057
3195
  const esbuild = await import("esbuild");
@@ -3678,7 +3816,7 @@ function ensurePlaywright(appDir) {
3678
3816
  import { readFileSync as readFileSync12 } from "fs";
3679
3817
 
3680
3818
  // src/testing/accounts.ts
3681
- import { existsSync as existsSync12, mkdirSync as mkdirSync5, readFileSync as readFileSync11 } from "fs";
3819
+ import { existsSync as existsSync13, mkdirSync as mkdirSync5, readFileSync as readFileSync11 } from "fs";
3682
3820
  import { homedir as homedir2 } from "os";
3683
3821
  import { join as join12 } from "path";
3684
3822
  var TEST_ACCOUNTS_PATH = join12(homedir2(), ".deepspace", "test-accounts.json");
@@ -3746,7 +3884,7 @@ function removeTestAccounts(ids, emails = [], scope = currentTestAccountScope())
3746
3884
  });
3747
3885
  }
3748
3886
  function readTestAccountStore() {
3749
- if (!existsSync12(TEST_ACCOUNTS_PATH)) return { version: 2, scopes: {} };
3887
+ if (!existsSync13(TEST_ACCOUNTS_PATH)) return { version: 2, scopes: {} };
3750
3888
  try {
3751
3889
  const raw = JSON.parse(readFileSync11(TEST_ACCOUNTS_PATH, "utf-8"));
3752
3890
  if (Array.isArray(raw)) return { version: 2, scopes: {} };
@@ -4714,7 +4852,7 @@ function describeFilesFailure(status3, body) {
4714
4852
  import * as p6 from "@clack/prompts";
4715
4853
  import { execSync as execSync2 } from "child_process";
4716
4854
  import { createHash as createHash2 } from "crypto";
4717
- import { existsSync as existsSync16, readFileSync as readFileSync15, readdirSync as readdirSync6 } from "fs";
4855
+ import { existsSync as existsSync17, readFileSync as readFileSync15, readdirSync as readdirSync6 } from "fs";
4718
4856
  import { dirname as dirname7, isAbsolute as isAbsolute3, join as join17, relative as relative2, resolve as resolve13, sep as sep3 } from "path";
4719
4857
 
4720
4858
  // src/build/app-id.ts
@@ -4732,11 +4870,11 @@ var APP_ID_ADOPTION_STEPS = `Point the client at the build-injected id \u2014 th
4732
4870
  `).join("") + APP_ID_ADOPTION_TOGETHER;
4733
4871
 
4734
4872
  // src/build/plugin.ts
4735
- import { existsSync as existsSync13, lstatSync, readdirSync as readdirSync5, unlinkSync as unlinkSync2 } from "fs";
4873
+ import { existsSync as existsSync14, lstatSync, readdirSync as readdirSync5, unlinkSync as unlinkSync2 } from "fs";
4736
4874
  import { join as join14, resolve as resolve12 } from "path";
4737
4875
  function removeBuildDevVars(workerDir) {
4738
4876
  const path = join14(workerDir, ".dev.vars");
4739
- if (!existsSync13(path)) return false;
4877
+ if (!existsSync14(path)) return false;
4740
4878
  if (!lstatSync(path).isFile()) {
4741
4879
  throw new Error(`Refusing unsafe build secret path: ${path}`);
4742
4880
  }
@@ -4745,12 +4883,12 @@ function removeBuildDevVars(workerDir) {
4745
4883
  }
4746
4884
 
4747
4885
  // src/documentation/deploy.ts
4748
- import { existsSync as existsSync14, readFileSync as readFileSync13 } from "fs";
4886
+ import { existsSync as existsSync15, readFileSync as readFileSync13 } from "fs";
4749
4887
  import { join as join15 } from "path";
4750
4888
  function readDocumentationDeployManifest(appDir, clientDir) {
4751
- if (!existsSync14(join15(appDir, "documentation.json"))) return void 0;
4889
+ if (!existsSync15(join15(appDir, "documentation.json"))) return void 0;
4752
4890
  const manifestPath = join15(clientDir, "_documentation", "manifest.json");
4753
- if (!existsSync14(manifestPath)) {
4891
+ if (!existsSync15(manifestPath)) {
4754
4892
  throw new Error(
4755
4893
  "documentation.json exists but the build produced no documentation manifest. Ensure vite.config.ts includes deepSpaceDocumentation() from deepspace/documentation."
4756
4894
  );
@@ -4759,7 +4897,7 @@ function readDocumentationDeployManifest(appDir, clientDir) {
4759
4897
  }
4760
4898
 
4761
4899
  // src/cli/commands/update/app-migrations.ts
4762
- import { existsSync as existsSync15, readFileSync as readFileSync14 } from "fs";
4900
+ import { existsSync as existsSync16, readFileSync as readFileSync14 } from "fs";
4763
4901
  import { join as join16 } from "path";
4764
4902
 
4765
4903
  // src/shared/protocol/app-migrations.ts
@@ -4835,7 +4973,7 @@ var APP_MIGRATION_DEFINITIONS = APP_MIGRATION_GUIDANCE.map(({ id, description })
4835
4973
  }));
4836
4974
  function readAppliedAppMigrations(appDir) {
4837
4975
  const path = join16(appDir, APP_MIGRATIONS_MANIFEST);
4838
- if (!existsSync15(path)) return [];
4976
+ if (!existsSync16(path)) return [];
4839
4977
  let parsed;
4840
4978
  try {
4841
4979
  parsed = JSON.parse(readFileSync14(path, "utf8"));
@@ -4898,7 +5036,7 @@ async function buildDeployBundle(options) {
4898
5036
  }
4899
5037
  spinner2.stop(`Built (${Math.round((Date.now() - buildStarted) / 1e3)}s)`);
4900
5038
  const deployConfigPath = join17(appDir, ".wrangler", "deploy", "config.json");
4901
- if (!existsSync16(deployConfigPath)) {
5039
+ if (!existsSync17(deployConfigPath)) {
4902
5040
  output.die(
4903
5041
  "Build output config not found at .wrangler/deploy/config.json",
4904
5042
  "build_output_missing"
@@ -4906,7 +5044,7 @@ async function buildDeployBundle(options) {
4906
5044
  }
4907
5045
  const deployConfig = JSON.parse(readFileSync15(deployConfigPath, "utf-8"));
4908
5046
  const outputWranglerPath = resolve13(dirname7(deployConfigPath), deployConfig.configPath);
4909
- if (!existsSync16(outputWranglerPath)) {
5047
+ if (!existsSync17(outputWranglerPath)) {
4910
5048
  output.die(`Output wrangler.json not found at ${outputWranglerPath}`, "build_output_missing");
4911
5049
  }
4912
5050
  const outputConfig = JSON.parse(readFileSync15(outputWranglerPath, "utf-8"));
@@ -4925,10 +5063,10 @@ async function buildDeployBundle(options) {
4925
5063
  }
4926
5064
  const workerBundlePath = join17(workerDir, outputConfig.main);
4927
5065
  const clientDir = outputConfig.assets?.directory ? resolve13(workerDir, outputConfig.assets.directory) : null;
4928
- if (!existsSync16(workerBundlePath)) {
5066
+ if (!existsSync17(workerBundlePath)) {
4929
5067
  output.die(`Worker bundle not found at ${workerBundlePath}`, "build_output_missing");
4930
5068
  }
4931
- if (!clientDir || !existsSync16(clientDir)) {
5069
+ if (!clientDir || !existsSync17(clientDir)) {
4932
5070
  return output.die(`Client assets not found at ${clientDir}`, "build_output_missing");
4933
5071
  }
4934
5072
  try {
@@ -5116,7 +5254,7 @@ function readDeployAssetConfig(clientDir) {
5116
5254
  ["_redirects", "_redirects"]
5117
5255
  ]) {
5118
5256
  const filePath = join17(clientDir, file);
5119
- if (!existsSync16(filePath)) continue;
5257
+ if (!existsSync17(filePath)) continue;
5120
5258
  const content = readFileSync15(filePath, "utf8");
5121
5259
  if (content.trim()) config[key] = content;
5122
5260
  }
@@ -5131,7 +5269,7 @@ function errorMessage(error) {
5131
5269
 
5132
5270
  // src/cli/commands/deploy/commerce.ts
5133
5271
  import * as p7 from "@clack/prompts";
5134
- import { existsSync as existsSync17 } from "fs";
5272
+ import { existsSync as existsSync18 } from "fs";
5135
5273
  import { join as join18 } from "path";
5136
5274
  import { pathToFileURL as pathToFileURL2 } from "url";
5137
5275
 
@@ -5185,7 +5323,7 @@ function errorMessage2(error) {
5185
5323
  }
5186
5324
  async function loadDeclaration(appDir, sourceFile, outputFile, exportName) {
5187
5325
  const sourcePath = join18(appDir, "src", sourceFile);
5188
- if (!existsSync17(sourcePath)) return void 0;
5326
+ if (!existsSync18(sourcePath)) return void 0;
5189
5327
  const esbuild = await import("esbuild");
5190
5328
  const outputPath = join18(appDir, ".wrangler", "deploy", outputFile);
5191
5329
  await esbuild.build({
@@ -5280,7 +5418,7 @@ Stripe will keep charging them until you cancel. Cancel at the end of their curr
5280
5418
  }
5281
5419
  }
5282
5420
  async function syncSubscriptionPlans(appDir, appName, token, nonInteractive) {
5283
- if (!existsSync17(join18(appDir, "src", "subscriptions.ts"))) return;
5421
+ if (!existsSync18(join18(appDir, "src", "subscriptions.ts"))) return;
5284
5422
  let plans;
5285
5423
  try {
5286
5424
  plans = await loadDeclaration(
@@ -5335,7 +5473,7 @@ async function syncSubscriptionPlans(appDir, appName, token, nonInteractive) {
5335
5473
  }
5336
5474
  }
5337
5475
  async function syncOneTimeProducts(appDir, appName, token) {
5338
- if (!existsSync17(join18(appDir, "src", "products.ts"))) return;
5476
+ if (!existsSync18(join18(appDir, "src", "products.ts"))) return;
5339
5477
  let products;
5340
5478
  try {
5341
5479
  products = await loadDeclaration(
@@ -5447,92 +5585,8 @@ import { readFileSync as readFileSync16 } from "fs";
5447
5585
  // src/cli/commands/deploy/repository.ts
5448
5586
  import * as p9 from "@clack/prompts";
5449
5587
 
5450
- // src/cli/lib/git/safety.ts
5451
- var SECRET_FILE_RE = /(^|\/)(\.dev\.vars(\.[^/]*)?|\.env(\.[^/]*)?|\.npmrc|\.envrc|\.mcp\.json)$/i;
5452
- var SECRET_TEMPLATE_RE = /\.(example|sample|template)$/i;
5453
- function findOversizedObjects(cwd, capBytes, limit = 3) {
5454
- try {
5455
- const revisionObjects = runGit(cwd, ["rev-list", "--objects", "--all"], { allowFail: true });
5456
- if (revisionObjects.status !== 0) return [];
5457
- const pathByOid = /* @__PURE__ */ new Map();
5458
- for (const line of revisionObjects.stdout.toString("utf-8").split("\n")) {
5459
- const separator = line.indexOf(" ");
5460
- if (separator > 0) pathByOid.set(line.slice(0, separator), line.slice(separator + 1));
5461
- }
5462
- const sizedObjects = runGit(
5463
- cwd,
5464
- [
5465
- "cat-file",
5466
- "--batch-all-objects",
5467
- "--batch-check=%(objectname) %(objecttype) %(objectsize)"
5468
- ],
5469
- { allowFail: true }
5470
- );
5471
- if (sizedObjects.status !== 0) return [];
5472
- const oversized = [];
5473
- for (const line of sizedObjects.stdout.toString("utf-8").split("\n")) {
5474
- const [oid, type, size] = line.split(" ");
5475
- if (type !== "blob") continue;
5476
- const bytes = Number(size);
5477
- const path = pathByOid.get(oid);
5478
- if (path && Number.isFinite(bytes) && bytes > capBytes) oversized.push({ path, bytes });
5479
- }
5480
- return oversized.sort((left, right) => right.bytes - left.bytes).slice(0, limit);
5481
- } catch {
5482
- return [];
5483
- }
5484
- }
5485
- function statusFiles(cwd) {
5486
- const records = splitNulFields(
5487
- runGit(cwd, [
5488
- "-c",
5489
- "core.quotePath=false",
5490
- "status",
5491
- "--porcelain",
5492
- "--untracked-files=all",
5493
- "-z"
5494
- ]).stdout
5495
- );
5496
- const files = [];
5497
- for (let index = 0; index < records.length; index++) {
5498
- const record = records[index];
5499
- files.push(record.slice(3));
5500
- if (/[RC]/.test(record.slice(0, 2))) {
5501
- index++;
5502
- if (/R/.test(record.slice(0, 2)) && index < records.length) files.push(records[index]);
5503
- }
5504
- }
5505
- return [
5506
- ...new Set(
5507
- files.filter((path) => !(SECRET_FILE_RE.test(path) && !SECRET_TEMPLATE_RE.test(path)))
5508
- )
5509
- ];
5510
- }
5511
- function trackedSecretFiles(cwd, ref) {
5512
- const result = runGit(
5513
- repoToplevel(cwd),
5514
- ["-c", "core.quotePath=false", "ls-tree", "-r", "--name-only", "-z", ref],
5515
- { allowFail: true }
5516
- );
5517
- if (result.status !== 0) return [];
5518
- return splitNulFields(result.stdout).filter(
5519
- (path) => SECRET_FILE_RE.test(path) && !SECRET_TEMPLATE_RE.test(path)
5520
- );
5521
- }
5522
- var SECRET_IN_HISTORY_CODE = "secret_in_history";
5523
- function committedSecretRefusal(cwd, commitOid, options) {
5524
- if (!commitOid) return null;
5525
- const files = trackedSecretFiles(cwd, commitOid);
5526
- if (files.length === 0) return null;
5527
- return {
5528
- files,
5529
- code: SECRET_IN_HISTORY_CODE,
5530
- message: `Refusing to ${options.action}: the commit being published carries secret file(s) \u2014 ${files.join(", ")}. These hold local secrets and must not reach the cloud repo, but pushing this commit would upload them through its history. Untrack with \`git rm --cached ${shQuote(files[0])}\`, ensure it's .gitignored, commit, then ${options.then}.`
5531
- };
5532
- }
5533
-
5534
5588
  // src/cli/lib/vc-remote.ts
5535
- import { existsSync as existsSync18 } from "fs";
5589
+ import { existsSync as existsSync19 } from "fs";
5536
5590
  import { tmpdir } from "os";
5537
5591
  import { isAbsolute as isAbsolute4, relative as relative3, resolve as resolve14 } from "path";
5538
5592
  function spaceRemoteName(environment = DEEPSPACE_ENV) {
@@ -5587,7 +5641,7 @@ function helperEntryUsable(command) {
5587
5641
  const match = /^!(?:DEEPSPACE_ENV=(production|staging) )?'([^']*)' '([^']*)' git-credential$/.exec(command);
5588
5642
  const expectedEnvironment = DEEPSPACE_ENV === "production" ? void 0 : DEEPSPACE_ENV;
5589
5643
  if (!match || match[1] !== expectedEnvironment) return false;
5590
- return existsSync18(match[2]) && existsSync18(match[3]);
5644
+ return existsSync19(match[2]) && existsSync19(match[3]);
5591
5645
  }
5592
5646
  function credentialHelperCommand(environment = DEEPSPACE_ENV) {
5593
5647
  const entry = process.argv[1];
@@ -5632,23 +5686,31 @@ function installCredentialHelper(cwd, url) {
5632
5686
  } catch {
5633
5687
  }
5634
5688
  }
5635
- function ensureSpaceRemote(cwd, appId, remote = SPACE_REMOTE) {
5689
+ function ensureSpaceRemote(cwd, appId, remote = SPACE_REMOTE, token) {
5636
5690
  const url = repoUrl(appId);
5637
5691
  const current = runGit(cwd, ["remote", "get-url", remote], { allowFail: true });
5638
- if (current.status !== 0) {
5692
+ const existing = current.status === 0 ? current.stdout.toString("utf-8").trim() : null;
5693
+ if (appIdFromRepoUrl(existing ?? "") !== appId) {
5694
+ const owned = declaredAppIds(cwd);
5695
+ if (owned.size > 0 && !owned.has(appId)) {
5696
+ throw new InputError(
5697
+ `This checkout declares ${[...owned].join(", ")}, but this command targets ${appId}. Pointing its \`${remote}\` remote at another app would send every later plain \`git push ${remote}\` to the wrong repo. Work on ${appId} from its own checkout (\`deepspace clone ${appId}\` into a new directory), or fork THIS checkout into a brand-new app of your own with \`deepspace app init --new-id\`.`,
5698
+ "app_checkout_mismatch"
5699
+ );
5700
+ }
5701
+ }
5702
+ if (token) ensureGitIdentity(cwd, token);
5703
+ if (existing === null) {
5639
5704
  runGit(cwd, ["remote", "add", remote, url]);
5640
- } else {
5641
- const existing = current.stdout.toString("utf-8").trim();
5642
- if (existing !== url) {
5643
- const priorApp = appIdFromRepoUrl(existing);
5644
- if (priorApp && priorApp !== appId && !process.argv.includes("--json")) {
5645
- process.stderr.write(
5646
- `warning: remote '${remote}' was pointed at app ${priorApp}; re-aiming it at ${appId}.
5705
+ } else if (existing !== url) {
5706
+ const priorApp = appIdFromRepoUrl(existing);
5707
+ if (priorApp && priorApp !== appId && !process.argv.includes("--json")) {
5708
+ process.stderr.write(
5709
+ `warning: remote '${remote}' was pointed at app ${priorApp}; re-aiming it at ${appId}.
5647
5710
  `
5648
- );
5649
- }
5650
- runGit(cwd, ["remote", "set-url", remote, url]);
5711
+ );
5651
5712
  }
5713
+ runGit(cwd, ["remote", "set-url", remote, url]);
5652
5714
  }
5653
5715
  installCredentialHelper(cwd, url);
5654
5716
  return url;
@@ -5665,6 +5727,9 @@ function ensureGitIdentity(cwd, token) {
5665
5727
  const missingName = !configured("user.name");
5666
5728
  const missingEmail = !configured("user.email");
5667
5729
  if (!missingName && !missingEmail) return;
5730
+ if (runGit(cwd, ["config", "--get", "--type=bool", "user.useConfigOnly"], { allowFail: true }).stdout.toString("utf-8").trim() === "true") {
5731
+ return;
5732
+ }
5668
5733
  const payload = decodeJwtPayload(token);
5669
5734
  if (!payload.email) return;
5670
5735
  if (missingName) {
@@ -5675,6 +5740,14 @@ function ensureGitIdentity(cwd, token) {
5675
5740
  if (missingEmail) {
5676
5741
  runGit(cwd, ["config", "--local", "user.email", payload.email], { allowFail: true });
5677
5742
  }
5743
+ const wrote = [...missingEmail ? ["user.email"] : [], ...missingName ? ["user.name"] : []];
5744
+ const dir = /^[A-Za-z0-9_./:@-]+$/.test(cwd) ? cwd : shQuote(cwd);
5745
+ const undo = wrote.map((key) => `git -C ${dir} config --local --unset ${key}`).join(" && ");
5746
+ process.stderr.write(
5747
+ `Git identity for this checkout set from your DeepSpace login (${payload.email}).
5748
+ Undo with: ${undo}
5749
+ `
5750
+ );
5678
5751
  } catch {
5679
5752
  }
5680
5753
  }
@@ -5683,74 +5756,287 @@ function appIdFromRepoUrl(url) {
5683
5756
  return match ? decodeURIComponent(match[1]) : null;
5684
5757
  }
5685
5758
 
5686
- // src/cli/lib/vc-push.ts
5687
- var CAS_CONFLICT_REASON = /stale ref|fetch first|no such ref|atomic push failed/i;
5688
- function parsePushPorcelain(out) {
5689
- const results = [];
5690
- for (const line of out.split("\n")) {
5691
- const match = /^([ +\-*=!])\t([^\t]*)\t(.*)$/.exec(line);
5692
- if (!match) continue;
5693
- const [, flag, refspec, summary] = match;
5694
- const colon = refspec.indexOf(":");
5695
- const localRef = colon >= 0 ? refspec.slice(0, colon) : refspec;
5696
- const remoteRef = colon >= 0 ? refspec.slice(colon + 1) : refspec;
5697
- let status3;
5698
- let reason;
5699
- if (flag === "=") {
5700
- status3 = "up_to_date";
5701
- } else if (flag === "!") {
5702
- reason = /\((.*)\)\s*$/.exec(summary)?.[1];
5703
- if (!summary.startsWith("[remote rejected]")) {
5704
- status3 = "non_fast_forward";
5705
- } else if (reason && CAS_CONFLICT_REASON.test(reason)) {
5706
- status3 = "ref_conflict";
5707
- } else {
5708
- status3 = "rejected";
5709
- }
5710
- } else {
5711
- status3 = "committed";
5759
+ // src/cli/lib/git/safety.ts
5760
+ var SECRET_FILE_RE = /(^|\/)(\.dev\.vars(\.[^/]*)?|\.env(\.[^/]*)?|\.npmrc|\.envrc|\.mcp\.json)$/i;
5761
+ var SECRET_TEMPLATE_RE = /\.(example|sample|template)$/i;
5762
+ function findOversizedObjects(cwd, capBytes, limit = 3) {
5763
+ try {
5764
+ const revisionObjects = runGit(cwd, ["rev-list", "--objects", "--all"], { allowFail: true });
5765
+ if (revisionObjects.status !== 0) return [];
5766
+ const pathByOid = /* @__PURE__ */ new Map();
5767
+ for (const line of revisionObjects.stdout.toString("utf-8").split("\n")) {
5768
+ const separator = line.indexOf(" ");
5769
+ if (separator > 0) pathByOid.set(line.slice(0, separator), line.slice(separator + 1));
5712
5770
  }
5713
- results.push({ status: status3, localRef, remoteRef, summary, ...reason ? { reason } : {} });
5714
- }
5715
- return results;
5716
- }
5717
- function isThinPackRejection(result) {
5718
- return result.status === "rejected" && /thin (?:push|pack)|--no-thin/i.test(result.reason ?? result.summary ?? "");
5719
- }
5720
- function isRecoverablePushFailure(status3) {
5721
- return status3 === "non_fast_forward" || status3 === "ref_conflict";
5722
- }
5723
- function classifyPushTransportFailure(error, cwd) {
5724
- const message = error instanceof Error ? error.message : String(error);
5725
- if (/(?:HTTP |error: )409\b/i.test(message)) {
5726
- return {
5727
- code: "app_quota_exceeded",
5728
- error: `A first push to this new app hit your plan's active-app quota. Use \`deepspace app list\` to choose an app to undeploy, or upgrade your plan, then retry.`
5729
- };
5730
- }
5731
- if (/(?:HTTP |error: )429\b/i.test(message)) {
5732
- return {
5733
- code: "rate_limited",
5771
+ const sizedObjects = runGit(
5772
+ cwd,
5773
+ [
5774
+ "cat-file",
5775
+ "--batch-all-objects",
5776
+ "--batch-check=%(objectname) %(objecttype) %(objectsize)"
5777
+ ],
5778
+ { allowFail: true }
5779
+ );
5780
+ if (sizedObjects.status !== 0) return [];
5781
+ const oversized = [];
5782
+ for (const line of sizedObjects.stdout.toString("utf-8").split("\n")) {
5783
+ const [oid, type, size] = line.split(" ");
5784
+ if (type !== "blob") continue;
5785
+ const bytes = Number(size);
5786
+ const path = pathByOid.get(oid);
5787
+ if (path && Number.isFinite(bytes) && bytes > capBytes) oversized.push({ path, bytes });
5788
+ }
5789
+ return oversized.sort((left, right) => right.bytes - left.bytes).slice(0, limit);
5790
+ } catch {
5791
+ return [];
5792
+ }
5793
+ }
5794
+ function statusFiles(cwd) {
5795
+ const records = splitNulFields(
5796
+ runGit(cwd, [
5797
+ "-c",
5798
+ "core.quotePath=false",
5799
+ "status",
5800
+ "--porcelain",
5801
+ "--untracked-files=all",
5802
+ "-z"
5803
+ ]).stdout
5804
+ );
5805
+ const files = [];
5806
+ for (let index = 0; index < records.length; index++) {
5807
+ const record = records[index];
5808
+ files.push(record.slice(3));
5809
+ if (/[RC]/.test(record.slice(0, 2))) {
5810
+ index++;
5811
+ if (/R/.test(record.slice(0, 2)) && index < records.length) files.push(records[index]);
5812
+ }
5813
+ }
5814
+ return [
5815
+ ...new Set(
5816
+ files.filter((path) => !(SECRET_FILE_RE.test(path) && !SECRET_TEMPLATE_RE.test(path)))
5817
+ )
5818
+ ];
5819
+ }
5820
+ function trackedSecretFiles(cwd, ref) {
5821
+ const result = runGit(
5822
+ repoToplevel(cwd),
5823
+ ["-c", "core.quotePath=false", "ls-tree", "-r", "--name-only", "-z", ref],
5824
+ { allowFail: true }
5825
+ );
5826
+ if (result.status !== 0) return [];
5827
+ return splitNulFields(result.stdout).filter(
5828
+ (path) => SECRET_FILE_RE.test(path) && !SECRET_TEMPLATE_RE.test(path)
5829
+ );
5830
+ }
5831
+ function pushRange(root, base, tip) {
5832
+ if (base) return ["--root", `${base}..${tip}`];
5833
+ const listed = runGit(
5834
+ root,
5835
+ ["for-each-ref", "--format=%(refname)", `refs/remotes/${SPACE_REMOTE}/`],
5836
+ { allowFail: true }
5837
+ );
5838
+ const spaceRefs = listed.status === 0 ? listed.stdout.toString("utf-8").split("\n").map((ref) => ref.trim()).filter(Boolean) : [];
5839
+ return spaceRefs.length === 0 ? ["--root", tip] : ["--root", tip, "--not", ...spaceRefs];
5840
+ }
5841
+ function secretFilesInPushRange(cwd, base, tip) {
5842
+ const root = repoToplevel(cwd);
5843
+ const found = new Set(trackedSecretFiles(root, tip));
5844
+ const result = runGit(
5845
+ root,
5846
+ [
5847
+ "-c",
5848
+ "core.quotePath=false",
5849
+ "log",
5850
+ // Rename detection is on by default, so `git mv notes.txt .env` is
5851
+ // reported as R100 and never as an addition — while the blob still
5852
+ // rides the pack and the server still refuses it.
5853
+ "--no-renames",
5854
+ "--diff-filter=A",
5855
+ "--name-only",
5856
+ "--pretty=format:",
5857
+ "-z",
5858
+ ...pushRange(root, base, tip)
5859
+ ],
5860
+ { allowFail: true }
5861
+ );
5862
+ if (result.status !== 0) {
5863
+ return [...found];
5864
+ }
5865
+ for (const path of splitNulFields(result.stdout)) {
5866
+ if (path && SECRET_FILE_RE.test(path) && !SECRET_TEMPLATE_RE.test(path)) found.add(path);
5867
+ }
5868
+ return [...found];
5869
+ }
5870
+ var SECRET_IN_HISTORY_CODE = "secret_in_history";
5871
+ function secretRecoverySentence(files, then) {
5872
+ const first = shQuote(files[0]);
5873
+ return `Untracking alone will not fix this: the file is already in the commits being sent, so its contents upload through history either way. Rewrite the history that carries it \u2014 \`git rebase -i\` back past the commit that added ${first}, or \`git reset --soft\` to before it and re-commit without the file \u2014 and add it to .gitignore, then ${then}. Treat any credential it held as exposed and rotate it.`;
5874
+ }
5875
+ function committedSecretRefusal(cwd, commitOid, options) {
5876
+ if (!commitOid) return null;
5877
+ const files = options.base === void 0 ? trackedSecretFiles(cwd, commitOid) : secretFilesInPushRange(cwd, options.base, commitOid);
5878
+ if (files.length === 0) return null;
5879
+ return {
5880
+ files,
5881
+ code: SECRET_IN_HISTORY_CODE,
5882
+ message: `Refusing to ${options.action}: the commit being published carries secret file(s) \u2014 ${files.join(", ")}. These hold local secrets and must not reach the cloud repo, but publishing this commit would upload them through its history. ` + secretRecoverySentence(files, options.then)
5883
+ };
5884
+ }
5885
+ function unmergedIndexRefusal(cwd, opts) {
5886
+ const marker = interruptedGitOperation(cwd);
5887
+ const unmerged = hasUnmergedEntries(cwd);
5888
+ const operation = marker === "bisect" ? null : marker;
5889
+ if (operation === null && !unmerged) return null;
5890
+ if (operation !== null && !unmerged) {
5891
+ return {
5892
+ operation,
5893
+ code: "git_operation_in_progress",
5894
+ message: `A ${operation} is in progress in this checkout \u2014 finish it (\`git ${operation} --continue\`) or abort it (\`git ${operation} --abort\`), then re-run \`${opts.resume}\`. If it stopped at a conflict, anything already staged can still carry \`<<<<<<<\` markers; staging is not resolving.`
5895
+ };
5896
+ }
5897
+ if (operation === "merge" || operation === null) {
5898
+ return {
5899
+ operation,
5900
+ code: "merge_conflict",
5901
+ message: (operation === "merge" ? `${opts.ours} is already in progress here, stopped at unresolved conflicts. ` : `The index has unresolved conflicts with no operation to continue \u2014 a squash merge or a stash pop. `) + `Resolve the conflicted files (remove the \`<<<<<<<\` markers), \`git add\` them, ` + (operation === "merge" ? `\`git commit\`, then re-run \`${opts.resume}\`. To abandon the merge instead: \`git merge --abort\`.` : `\`git commit\`, then re-run \`${opts.resume}\`. To discard them instead: \`git reset --merge\`.`)
5902
+ };
5903
+ }
5904
+ return {
5905
+ operation,
5906
+ code: "git_operation_in_progress",
5907
+ message: `A ${operation} is unfinished in this checkout with unresolved conflicts \u2014 finish it (\`git ${operation} --continue\` after resolving, or \`git ${operation} --abort\`), then re-run \`${opts.resume}\`.`
5908
+ };
5909
+ }
5910
+
5911
+ // src/cli/lib/vc-push.ts
5912
+ function parseRefusalCode(reason) {
5913
+ const match = /^([a-z_]+): (.*)$/s.exec(reason);
5914
+ if (!match) return null;
5915
+ const remainder = match[2];
5916
+ const cut = remainder.indexOf(" \u2014 ");
5917
+ return cut === -1 ? { code: match[1], sentence: remainder } : {
5918
+ code: match[1],
5919
+ sentence: remainder.slice(0, cut),
5920
+ detail: remainder.slice(cut + " \u2014 ".length)
5921
+ };
5922
+ }
5923
+ function parsePushPorcelain(out) {
5924
+ const results = [];
5925
+ for (const line of out.split("\n")) {
5926
+ const match = /^([ +\-*=!])\t([^\t]*)\t(.*)$/.exec(line);
5927
+ if (!match) continue;
5928
+ const [, flag, refspec, summary] = match;
5929
+ const colon = refspec.indexOf(":");
5930
+ const localRef = colon >= 0 ? refspec.slice(0, colon) : refspec;
5931
+ const remoteRef = colon >= 0 ? refspec.slice(colon + 1) : refspec;
5932
+ let status3;
5933
+ let reason;
5934
+ let code;
5935
+ if (flag === "=") {
5936
+ status3 = "up_to_date";
5937
+ } else if (flag === "!") {
5938
+ reason = /\((.*)\)\s*$/.exec(summary)?.[1];
5939
+ code = reason ? parseRefusalCode(reason)?.code ?? void 0 : void 0;
5940
+ if (!summary.startsWith("[remote rejected]")) {
5941
+ status3 = "non_fast_forward";
5942
+ } else if (code === "stale_ref") {
5943
+ status3 = "ref_conflict";
5944
+ } else {
5945
+ status3 = "rejected";
5946
+ }
5947
+ } else {
5948
+ status3 = "committed";
5949
+ }
5950
+ results.push({
5951
+ status: status3,
5952
+ localRef,
5953
+ remoteRef,
5954
+ summary,
5955
+ ...reason ? { reason } : {},
5956
+ ...code ? { code } : {}
5957
+ });
5958
+ }
5959
+ return results;
5960
+ }
5961
+ function isThinPackRejection(result) {
5962
+ return result.status === "rejected" && result.code === "thin_pack";
5963
+ }
5964
+ function isRecoverablePushFailure(status3) {
5965
+ return status3 === "non_fast_forward" || status3 === "ref_conflict";
5966
+ }
5967
+ function gitHttpStatus(message) {
5968
+ for (const line of message.split("\n")) {
5969
+ if (/^\s*remote:/i.test(line)) continue;
5970
+ const match = /(?:HTTP |error: )(\d{3})\b/i.exec(line);
5971
+ if (match) return Number(match[1]);
5972
+ }
5973
+ return null;
5974
+ }
5975
+ function classifyPushTransportFailure(error, cwd) {
5976
+ const message = error instanceof Error ? error.message : String(error);
5977
+ const status3 = gitHttpStatus(message);
5978
+ if (status3 === 409) {
5979
+ return {
5980
+ code: "app_quota_exceeded",
5981
+ error: `A first push to this new app hit your plan's active-app quota. Use \`deepspace app list\` to choose an app to undeploy, or upgrade your plan, then retry.`
5982
+ };
5983
+ }
5984
+ if (status3 === 429) {
5985
+ return {
5986
+ code: "rate_limited",
5734
5987
  error: "The cloud repo rate-limited this push. Wait a few seconds, then retry."
5735
5988
  };
5736
5989
  }
5737
- if (/(?:HTTP |error: )413\b/i.test(message)) {
5990
+ if (status3 === 413) {
5738
5991
  return { code: "push_too_large", error: pushTooLargeMessage(cwd) };
5739
5992
  }
5740
- if (/(?:HTTP |error: )422\b/i.test(message)) {
5993
+ if (status3 === 422) {
5741
5994
  return {
5742
5995
  code: "source_managed_by_github",
5743
5996
  error: "This app's source is managed by GitHub, so pushes to the DeepSpace repo are refused. Push to the GitHub repository instead \u2014 `deepspace app status` names it."
5744
5997
  };
5745
5998
  }
5999
+ if (status3 === 401) {
6000
+ return {
6001
+ code: "not_authenticated",
6002
+ error: "The cloud repo rejected the push as unauthenticated \u2014 the session likely expired mid-push. Run `deepspace auth login`, then push again. Nothing was applied."
6003
+ };
6004
+ }
6005
+ if (status3 === 403) {
6006
+ return {
6007
+ code: "forbidden",
6008
+ error: "The cloud repo refused this push: you do not have write access to that app, or its id belongs to another account (`deepspace app init --new-id` mints your own). Nothing was applied."
6009
+ };
6010
+ }
6011
+ if (status3 === null && /unexpected disconnect|Empty reply from server|curl (?:18|52|56)/i.test(
6012
+ message
6013
+ )) {
6014
+ return {
6015
+ code: "push_outcome_unknown",
6016
+ error: "The connection dropped while the push was in flight with no reply from the server, so whether the cloud repo applied it is unknown \u2014 it may have landed. Re-run this push before anything else: an already-applied push reports `up_to_date`. Do NOT force-push or reset until you have."
6017
+ };
6018
+ }
5746
6019
  return null;
5747
6020
  }
6021
+ function representativeResult(results) {
6022
+ const refused = results.filter(
6023
+ (result) => result.status !== "committed" && result.status !== "up_to_date"
6024
+ );
6025
+ return refused.find((result) => result.code !== "not_attempted") ?? refused[0] ?? results[0];
6026
+ }
5748
6027
  function pushToSpace(cwd, token, refspec, opts = {}) {
5749
6028
  const doPush = (extra) => {
5750
6029
  const result = runGitRemote(
5751
6030
  cwd,
5752
6031
  token,
5753
- ["push", "--porcelain", ...opts.force ? ["--force"] : [], ...extra, opts.remote ?? SPACE_REMOTE, refspec],
6032
+ [
6033
+ "push",
6034
+ "--porcelain",
6035
+ ...opts.force ? ["--force"] : [],
6036
+ ...extra,
6037
+ opts.remote ?? SPACE_REMOTE,
6038
+ refspec
6039
+ ],
5754
6040
  { allowFail: true }
5755
6041
  );
5756
6042
  const parsed = parsePushPorcelain(result.stdout.toString("utf-8"));
@@ -5760,12 +6046,11 @@ function pushToSpace(cwd, token, refspec, opts = {}) {
5760
6046
  stderr || `git push exited ${result.status} without reporting a ref status`
5761
6047
  );
5762
6048
  }
5763
- return parsed[0];
6049
+ return representativeResult(parsed);
5764
6050
  };
5765
6051
  const first = doPush([]);
5766
6052
  return isThinPackRejection(first) ? doPush(["--no-thin"]) : first;
5767
6053
  }
5768
- var OVERSIZED_PUSH_RE = /too large|exceeds|\bLFS\b/i;
5769
6054
  var SERVER_OBJECT_CAP_BYTES = 20 * 1024 * 1024;
5770
6055
  var SERVER_PACK_CAP_BYTES = 32 * 1024 * 1024;
5771
6056
  var PUSH_CEILINGS = `${formatBytes(SERVER_OBJECT_CAP_BYTES)} per file, ${formatBytes(SERVER_PACK_CAP_BYTES)} per push`;
@@ -5791,39 +6076,83 @@ function pushTooLargeMessage(cwd) {
5791
6076
  }
5792
6077
  return `${ceilings} ${fixForObjects(objects)}`;
5793
6078
  }
5794
- function pushFailureMessage(label, push, cwd) {
5795
- const reason = push.reason ?? push.summary ?? "";
5796
- const detail = `${push.status}${reason ? `: ${reason}` : ""}`;
5797
- if (OVERSIZED_PUSH_RE.test(reason)) {
5798
- return `${label} failed (${detail}) \u2014 an object exceeds the server's size cap, so retrying cannot succeed. ${oversizedPushFix(cwd)}`;
5799
- }
5800
- if (/^missing necessary objects — retry the push$/i.test(reason)) {
5801
- return `${label} failed (${detail}). Retry; if it persists, report it with \`deepspace feedback\`.`;
6079
+ var GENERIC_REJECTION = {
6080
+ code: "rejected",
6081
+ message: `The server rejected this push; correct the reported history/ref problem, or report it with \`deepspace feedback\` if the reason is unclear.`
6082
+ };
6083
+ var REFUSAL_ADVICE = {
6084
+ push_too_large: ({ cwd }) => ({
6085
+ code: "push_too_large",
6086
+ message: `An object exceeds the server's size cap, so retrying cannot succeed. ${oversizedPushFix(cwd)}`
6087
+ }),
6088
+ repo_full: () => ({
6089
+ code: "repo_full",
6090
+ message: `The cloud repo is at its size ceiling, so retrying cannot succeed. Large objects already committed upload through their history, so untracking them is not enough \u2014 ${removeFromHistoryAdvice(null)}`
6091
+ }),
6092
+ secret_committed: ({ detail }) => {
6093
+ const files = (detail ?? "").split(", ").map((name) => name.trim()).filter(Boolean);
6094
+ return {
6095
+ code: SECRET_IN_HISTORY_CODE,
6096
+ message: files.length > 0 ? secretRecoverySentence(files, "push again") : "A secret file is committed in the history being pushed. Rewrite that history to drop it (untracking alone is not enough), then push again."
6097
+ };
6098
+ },
6099
+ missing_objects: () => ({
6100
+ code: "missing_objects",
6101
+ message: "Retry; if it persists, report it with `deepspace feedback`."
6102
+ }),
6103
+ thin_pack: () => ({
6104
+ code: "thin_pack",
6105
+ // `pushToSpace` already retried once with `--no-thin`; reaching a caller
6106
+ // means that retry also failed.
6107
+ message: "Retry with `git push --no-thin`; if it persists, report it with `deepspace feedback`."
6108
+ }),
6109
+ // No `stale_ref` entry: it is encoded once, in `parsePushPorcelain`, as the
6110
+ // `ref_conflict` STATUS. That is what drives the pull action and exit 2, and
6111
+ // it is why no caller can reach this table with that code.
6112
+ funny_refname: () => ({
6113
+ code: "funny_refname",
6114
+ message: "Rename the branch to a valid git ref name, then push again."
6115
+ }),
6116
+ workspace_creator: () => ({
6117
+ code: "workspace_creator",
6118
+ message: "Another agent owns that workspace ref. Publish your own with `deepspace workspace sync`."
6119
+ }),
6120
+ internal_ref: () => ({
6121
+ code: "internal_ref",
6122
+ message: "That ref namespace is managed by DeepSpace. Push a branch instead."
6123
+ }),
6124
+ bad_tip: () => ({
6125
+ code: "bad_tip",
6126
+ message: "Point the branch at a commit (not a tag or tree object), then push again."
6127
+ }),
6128
+ unpacker_error: () => ({
6129
+ code: "unpacker_error",
6130
+ message: "The server could not unpack this push. Report it with `deepspace feedback` \u2014 a plain retry sends the identical pack."
6131
+ }),
6132
+ // The server has no more specific code; its own sentence is the whole fact,
6133
+ // so this reads exactly like an untagged rejection.
6134
+ push_failed: () => GENERIC_REJECTION,
6135
+ // Reaching here means `representativeResult` found nothing better — every
6136
+ // ref reported "not attempted", so the refusal that stopped the push was
6137
+ // never reported. Re-running would produce the same incomplete report, so
6138
+ // say what is actually true rather than prescribing a loop.
6139
+ not_attempted: () => ({
6140
+ code: "rejected",
6141
+ message: 'The push is atomic and every ref reports "not attempted", so the server did not report the refusal that stopped it. That should not happen \u2014 send this output with `deepspace feedback`.'
6142
+ })
6143
+ };
6144
+ function classifyRejection(reason, cwd) {
6145
+ const parsed = parseRefusalCode(reason);
6146
+ if (parsed && Object.hasOwn(REFUSAL_ADVICE, parsed.code)) {
6147
+ return REFUSAL_ADVICE[parsed.code]({ detail: parsed.detail, cwd });
5802
6148
  }
5803
- return `${label} failed (${detail}). The server rejected this push; correct the reported history/ref problem, or report it with \`deepspace feedback\` if the reason is unclear.`;
5804
- }
5805
-
5806
- // src/cli/lib/workspace-id.ts
5807
- var ULID_RE = /^[0-9A-HJKMNP-TV-Z]{26}$/i;
5808
- var WORKSPACE_ID_RE = /^ws_[0-9A-HJKMNP-TV-Z]{26}$/;
5809
- function isWorkspaceId(id) {
5810
- return WORKSPACE_ID_RE.test(id);
6149
+ return GENERIC_REJECTION;
5811
6150
  }
5812
- function workspaceBranchName(workspaceId) {
5813
- return `ws/${workspaceId.replace(/^ws_/, "").toLowerCase()}`;
5814
- }
5815
- function workspaceIdFromBranch(branch) {
5816
- if (!branch || !branch.startsWith("ws/")) return null;
5817
- const ulid = branch.slice(3);
5818
- if (!ULID_RE.test(ulid)) return null;
5819
- return `ws_${ulid.toUpperCase()}`;
5820
- }
5821
- function isSelectedWorkspaceCheckout(branch, workspaceId) {
5822
- return workspaceIdFromBranch(branch) === workspaceId;
5823
- }
5824
- function resolveWorkspaceWorktree(worktrees, workspaceId) {
5825
- const branch = workspaceBranchName(workspaceId);
5826
- return worktrees.find((worktree) => worktree.branch === branch)?.path ?? null;
6151
+ function pushFailureMessage(subject, push, cwd) {
6152
+ const reason = push.reason ?? push.summary ?? "";
6153
+ const sentence = parseRefusalCode(reason)?.sentence ?? reason;
6154
+ const detail = `${push.status}${sentence ? `: ${sentence}` : ""}`;
6155
+ return `${subject} failed (${detail}). ${classifyRejection(reason, cwd).message}`;
5827
6156
  }
5828
6157
 
5829
6158
  // src/cli/lib/source-control.ts
@@ -5924,7 +6253,7 @@ function preflightDeployRepository(options) {
5924
6253
  }
5925
6254
  assertNoOperationInProgress(appDir);
5926
6255
  const branch = currentBranch(appDir);
5927
- if (!isWorkTreeClean(appDir)) return dirtyWorktreeRefusal(branch);
6256
+ if (!isWorkTreeClean(appDir)) return dirtyWorktreeRefusal(branch, appDir);
5928
6257
  if (!branch) return detachedHeadRefusal();
5929
6258
  return null;
5930
6259
  } catch (error) {
@@ -5999,7 +6328,7 @@ async function syncDeployRepository(options) {
5999
6328
  p9.log.info(`Workspace ${workspaceBranchId} is synced at ${tip.slice(0, 10)}.`);
6000
6329
  } else if (secretFiles.length > 0) {
6001
6330
  p9.log.warn(
6002
- `Skipping cloud-repo push \u2014 the branch tracks secret file(s): ${secretFiles.join(", ")}. Untrack with \`git rm --cached ${secretFiles[0]}\` (add to .gitignore), re-commit, then \`deepspace push\`. Without the push this deploy carries no source lineage: if the live release has one, the server refuses it as stale (--ignore-stale replaces it anyway), and this release's source isn't recoverable until the push works.`
6331
+ `Skipping cloud-repo push \u2014 the branch tracks secret file(s): ${secretFiles.join(", ")}. ` + secretRecoverySentence(secretFiles, "`deepspace push`") + ` Without the push this deploy carries no source lineage: if the live release has one, the server refuses it as stale (--ignore-stale replaces it anyway), and this release's source isn't recoverable until the push works.`
6003
6332
  );
6004
6333
  } else {
6005
6334
  const pushResult = await pushWithTransientRetry(
@@ -6117,7 +6446,14 @@ function workspaceDeployLineage(status3, publishedTip, localTip) {
6117
6446
  if (status3 !== "active") return "inactive";
6118
6447
  return publishedTip === localTip ? "recoverable" : "unsynced";
6119
6448
  }
6120
- function dirtyWorktreeRefusal(branch) {
6449
+ function dirtyWorktreeRefusal(branch, cwd) {
6450
+ const conflicted = cwd ? unmergedIndexRefusal(cwd, { ours: "A merge", resume: "deepspace deploy" }) : null;
6451
+ if (conflicted) {
6452
+ return {
6453
+ code: conflicted.code,
6454
+ error: `${conflicted.message} A deploy records the commit it ships, so committing an unresolved merge would ship the markers.`
6455
+ };
6456
+ }
6121
6457
  const workspaceId = workspaceIdFromBranch(branch);
6122
6458
  return {
6123
6459
  code: "dirty_worktree",
@@ -6350,7 +6686,7 @@ async function deployBuiltBundle(options) {
6350
6686
  await bail(
6351
6687
  formatDeployWorkerError(response.status, body.error, body.code),
6352
6688
  "Deploy failed",
6353
- body.code
6689
+ body.code ?? "deploy_failed"
6354
6690
  );
6355
6691
  }
6356
6692
  p10.log.info(`Platform commit: ${Math.round((Date.now() - commitStarted) / 1e3)}s`);
@@ -7227,6 +7563,7 @@ var RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
7227
7563
  var DEFAULT_WINDOW_MS = 15 * 60 * 1e3;
7228
7564
  var MIN_POLL_MS = 2500;
7229
7565
  var FOLLOW_LAG_MS = 9e4;
7566
+ var LOG_RETENTION_DAYS = 7;
7230
7567
  function followInitialLimit(limit, maxLimit) {
7231
7568
  return limit ?? maxLimit;
7232
7569
  }
@@ -7306,23 +7643,23 @@ function formatEvent(e, color, now = Date.now()) {
7306
7643
  if (e.eventType === "exception" && e.exception) {
7307
7644
  tag = paint("31", "ERROR", color);
7308
7645
  const where = e.request ? ` \u2014 ${e.request.method} ${e.request.path}` : "";
7309
- body = `${e.exception.name}: ${e.exception.message}${where}`;
7646
+ body = displayLines(`${e.exception.name}: ${e.exception.message}${where}`);
7310
7647
  if (e.exception.stack) {
7311
7648
  const lines = e.exception.stack.split("\n");
7312
7649
  const firstIsHeader = lines.length > 0 && !/^\s*at\s/.test(lines[0]) && lines[0].includes(e.exception.message);
7313
7650
  const frames = (firstIsHeader ? lines.slice(1) : lines).map((l) => " " + l.trim()).filter((l) => l.trim().length > 0);
7314
- if (frames.length) body += "\n" + paint("2", frames.join("\n"), color);
7651
+ if (frames.length) body += "\n" + paint("2", displayLines(frames.join("\n")), color);
7315
7652
  }
7316
7653
  } else if (e.eventType === "request" && e.request) {
7317
7654
  tag = paint("36", "REQ ", color);
7318
7655
  const status3 = e.request.status !== void 0 ? ` ${e.request.status}` : "";
7319
7656
  const outcome = e.outcome && e.outcome !== "ok" ? ` (${e.outcome})` : "";
7320
- body = `${e.request.method} ${e.request.path}${status3}${outcome}`;
7657
+ body = displayLines(`${e.request.method} ${e.request.path}${status3}${outcome}`);
7321
7658
  } else {
7322
7659
  const style = LEVEL_STYLE[e.level];
7323
7660
  tag = (e.level.toUpperCase() + " ").slice(0, 5);
7324
7661
  if (style) tag = paint(style, tag, color);
7325
- body = style ? paint(style, e.message, color) : e.message;
7662
+ body = style ? paint(style, displayLines(e.message), color) : displayLines(e.message);
7326
7663
  }
7327
7664
  const client = e.source === "client" ? paint("35", "CLIENT", color) + " " : "";
7328
7665
  return `${time} ${client}${tag} ${body}`;
@@ -7433,16 +7770,33 @@ var logs_default = defineCommand4({
7433
7770
  }
7434
7771
  };
7435
7772
  const first = await fetchPage(initialSince, args.follow ? followLimit : limit, token);
7773
+ if (args.follow && args.json) {
7774
+ process.stdout.write(
7775
+ JSON.stringify({
7776
+ type: "ready",
7777
+ appId,
7778
+ window: windowLabel,
7779
+ count: first.events.length,
7780
+ retentionDays: LOG_RETENTION_DAYS
7781
+ }) + "\n"
7782
+ );
7783
+ }
7436
7784
  print(first.events);
7437
7785
  if (!args.follow) {
7438
7786
  if (first.events.length === 0) {
7439
7787
  if (args.json) {
7440
7788
  process.stdout.write(
7441
- JSON.stringify({ type: "meta", count: 0, window: windowLabel }) + "\n"
7789
+ JSON.stringify({
7790
+ type: "meta",
7791
+ count: 0,
7792
+ window: windowLabel,
7793
+ appId,
7794
+ retentionDays: LOG_RETENTION_DAYS
7795
+ }) + "\n"
7442
7796
  );
7443
7797
  } else {
7444
7798
  console.log(
7445
- `No logs in the last ${windowLabel} for ${appId}. New logs can take up to a minute to appear; retention is 7 days.`
7799
+ `No logs in the last ${windowLabel} for ${appId}. New logs can take up to a minute to appear; retention is ${LOG_RETENTION_DAYS} days.`
7446
7800
  );
7447
7801
  }
7448
7802
  } else if (first.truncated) {
@@ -7606,7 +7960,7 @@ var usage_default = defineDeepspaceCommand({
7606
7960
  });
7607
7961
 
7608
7962
  // src/cli/commands/add.ts
7609
- import { existsSync as existsSync20, readFileSync as readFileSync19, realpathSync as realpathSync4 } from "fs";
7963
+ import { existsSync as existsSync21, readFileSync as readFileSync19, realpathSync as realpathSync4 } from "fs";
7610
7964
  import { dirname as dirname10, join as join22, resolve as resolve17 } from "path";
7611
7965
  import { pathToFileURL as pathToFileURL3 } from "url";
7612
7966
 
@@ -7614,7 +7968,7 @@ import { pathToFileURL as pathToFileURL3 } from "url";
7614
7968
  import {
7615
7969
  closeSync,
7616
7970
  copyFileSync,
7617
- existsSync as existsSync19,
7971
+ existsSync as existsSync20,
7618
7972
  fstatSync,
7619
7973
  mkdirSync as mkdirSync7,
7620
7974
  mkdtempSync,
@@ -7634,7 +7988,7 @@ function featurePackageRoot(moduleUrl = import.meta.url) {
7634
7988
  let dir = dirname9(fileURLToPath(moduleUrl));
7635
7989
  for (; ; ) {
7636
7990
  const packagePath = join21(dir, "package.json");
7637
- if (existsSync19(packagePath)) {
7991
+ if (existsSync20(packagePath)) {
7638
7992
  try {
7639
7993
  const pkg2 = JSON.parse(readFileSync18(packagePath, "utf-8"));
7640
7994
  if (pkg2.name === "deepspace") return dir;
@@ -7647,11 +8001,11 @@ function featurePackageRoot(moduleUrl = import.meta.url) {
7647
8001
  }
7648
8002
  }
7649
8003
  function hasFeatureCatalog(packageRoot = featurePackageRoot()) {
7650
- return existsSync19(join21(packageRoot, "features"));
8004
+ return existsSync20(join21(packageRoot, "features"));
7651
8005
  }
7652
8006
  function readFeatureConfig(id, packageRoot = featurePackageRoot()) {
7653
8007
  const configPath = join21(packageRoot, "features", id, "feature.json");
7654
- if (!existsSync19(configPath)) return null;
8008
+ if (!existsSync20(configPath)) return null;
7655
8009
  try {
7656
8010
  const config = JSON.parse(readFileSync18(configPath, "utf-8"));
7657
8011
  if (typeof config.id !== "string" || typeof config.name !== "string" || !Array.isArray(config.files)) {
@@ -7664,7 +8018,7 @@ function readFeatureConfig(id, packageRoot = featurePackageRoot()) {
7664
8018
  }
7665
8019
  function listFeatureConfigs(packageRoot = featurePackageRoot()) {
7666
8020
  const featuresDir = join21(packageRoot, "features");
7667
- if (!existsSync19(featuresDir)) return [];
8021
+ if (!existsSync20(featuresDir)) return [];
7668
8022
  return readdirSync7(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));
7669
8023
  }
7670
8024
  function featureCatalog(packageRoot = featurePackageRoot()) {
@@ -7734,9 +8088,9 @@ function installFiles(packageRoot, config, targetDir, emit) {
7734
8088
  const srcPath = join21(packageRoot, "features", config.id, file.src);
7735
8089
  const resolvedDest = resolveFeatureDestPath(file.dest, config.route);
7736
8090
  const destPath = join21(targetDir, resolvedDest);
7737
- const existed = existsSync19(destPath);
8091
+ const existed = existsSync20(destPath);
7738
8092
  if (existed) preexisting.push(resolvedDest);
7739
- if (!existsSync19(srcPath)) {
8093
+ if (!existsSync20(srcPath)) {
7740
8094
  emit(` Warning: source not found: ${file.src}`, "stderr");
7741
8095
  if (!existed) missing.push(resolvedDest);
7742
8096
  continue;
@@ -7774,7 +8128,7 @@ function unresolvedSchema(schema, reason, emit) {
7774
8128
  function integrateSchema(config, targetDir, emit) {
7775
8129
  if (!config.schema) return [];
7776
8130
  const schemasPath = join21(targetDir, "src", "schemas.ts");
7777
- if (!existsSync19(schemasPath)) {
8131
+ if (!existsSync20(schemasPath)) {
7778
8132
  return [unresolvedSchema(config.schema, "src/schemas.ts was not found.", emit)];
7779
8133
  }
7780
8134
  let content = readFileSync18(schemasPath, "utf-8");
@@ -7831,7 +8185,7 @@ function unresolvedActions(actions, reason, emit) {
7831
8185
  function integrateActions(config, targetDir, emit) {
7832
8186
  if (!config.actions?.length) return [];
7833
8187
  const actionsPath = join21(targetDir, "src", "actions", "index.ts");
7834
- if (!existsSync19(actionsPath)) {
8188
+ if (!existsSync20(actionsPath)) {
7835
8189
  return [unresolvedActions(config.actions, "src/actions/index.ts was not found.", emit)];
7836
8190
  }
7837
8191
  let content = readFileSync18(actionsPath, "utf-8");
@@ -7881,7 +8235,7 @@ function integrateCode(config, targetDir, emit) {
7881
8235
  const unresolved = [];
7882
8236
  for (const insertion of config.code ?? []) {
7883
8237
  const targetPath = join21(targetDir, insertion.file);
7884
- if (!existsSync19(targetPath)) {
8238
+ if (!existsSync20(targetPath)) {
7885
8239
  unresolved.push({
7886
8240
  area: "code",
7887
8241
  reason: `${insertion.file} was not found.`,
@@ -7927,7 +8281,7 @@ ${content.slice(
7927
8281
  function integrateIgnore(config, targetDir) {
7928
8282
  if (!config.ignore?.length) return;
7929
8283
  const targetPath = join21(targetDir, ".gitignore");
7930
- const current = existsSync19(targetPath) ? readFileSync18(targetPath, "utf-8") : "";
8284
+ const current = existsSync20(targetPath) ? readFileSync18(targetPath, "utf-8") : "";
7931
8285
  const present = new Set(current.split(/\r?\n/).map((line) => line.trim()));
7932
8286
  const missing = config.ignore.filter((pattern) => !present.has(pattern));
7933
8287
  if (missing.length === 0) return;
@@ -7944,7 +8298,7 @@ function integrateWrangler(config, targetDir, emit) {
7944
8298
  ...runWorkerFirst ? ["Set assets.run_worker_first = true in wrangler.toml."] : [],
7945
8299
  ...rateLimits.map((binding) => `Add the ${binding.name} rate-limit binding to wrangler.toml.`)
7946
8300
  ];
7947
- if (!existsSync19(targetPath)) {
8301
+ if (!existsSync20(targetPath)) {
7948
8302
  return [{ area: "wrangler", reason: "wrangler.toml was not found.", steps }];
7949
8303
  }
7950
8304
  let source = readFileSync18(targetPath, "utf-8");
@@ -8024,7 +8378,7 @@ function rateLimitBindingBlock(scope, binding) {
8024
8378
  function integrateCss(packageRoot, config, targetDir, emit) {
8025
8379
  if (!config.css?.length) return [];
8026
8380
  const stylesPath = join21(targetDir, "src", "styles.css");
8027
- if (!existsSync19(stylesPath)) {
8381
+ if (!existsSync20(stylesPath)) {
8028
8382
  emit(" Warning: Cannot integrate CSS \u2014 src/styles.css not found");
8029
8383
  return [
8030
8384
  {
@@ -8041,7 +8395,7 @@ function integrateCss(packageRoot, config, targetDir, emit) {
8041
8395
  const unresolved = [];
8042
8396
  for (const cssFile of config.css) {
8043
8397
  const srcPath = join21(packageRoot, "features", config.id, cssFile);
8044
- if (!existsSync19(srcPath)) {
8398
+ if (!existsSync20(srcPath)) {
8045
8399
  emit(` Warning: CSS source not found: ${cssFile}`, "stderr");
8046
8400
  unresolved.push({
8047
8401
  area: "css",
@@ -8085,7 +8439,7 @@ function routeRolesSuffix(route) {
8085
8439
  function integrateRoute(config, targetDir, emit) {
8086
8440
  if (!config.route) return [];
8087
8441
  const navPath = join21(targetDir, "src", "nav.ts");
8088
- if (!existsSync19(navPath)) {
8442
+ if (!existsSync20(navPath)) {
8089
8443
  return [unresolvedRoute(config.route, config, "src/nav.ts was not found.", emit)];
8090
8444
  }
8091
8445
  let content = readFileSync18(navPath, "utf-8");
@@ -8163,7 +8517,7 @@ function integrateDependencies(config, targetDir, runInstall, outputVisible, emi
8163
8517
  return { added: [], installRan: false, packageManager };
8164
8518
  }
8165
8519
  const packagePath = join21(targetDir, "package.json");
8166
- if (!existsSync19(packagePath)) {
8520
+ if (!existsSync20(packagePath)) {
8167
8521
  emit(" Could not patch package.json (file not found)");
8168
8522
  return { added: [], installRan: false, packageManager };
8169
8523
  }
@@ -8241,7 +8595,7 @@ function dependencySnapshot(targetDir) {
8241
8595
  function installedNavFeatures(packageRoot, targetDir) {
8242
8596
  return listFeatureConfigs(packageRoot).filter((feature) => feature.category === "nav" && feature.files.length > 0).filter((feature) => {
8243
8597
  const signature = resolveFeatureDestPath(feature.files[0].dest, feature.route);
8244
- return existsSync19(join21(targetDir, signature));
8598
+ return existsSync20(join21(targetDir, signature));
8245
8599
  }).map((feature) => feature.id);
8246
8600
  }
8247
8601
  function warnNavConflicts(packageRoot, config, targetDir, emit) {
@@ -8474,9 +8828,9 @@ function renderFeatureInfo(config) {
8474
8828
  return lines.join("\n");
8475
8829
  }
8476
8830
  function isDeepSpaceApp(dir) {
8477
- if (existsSync20(join22(dir, "wrangler.toml"))) return true;
8831
+ if (existsSync21(join22(dir, "wrangler.toml"))) return true;
8478
8832
  const packagePath = join22(dir, "package.json");
8479
- if (!existsSync20(packagePath)) return false;
8833
+ if (!existsSync21(packagePath)) return false;
8480
8834
  try {
8481
8835
  const pkg2 = JSON.parse(readFileSync19(packagePath, "utf-8"));
8482
8836
  return Boolean(pkg2.dependencies?.deepspace || pkg2.devDependencies?.deepspace);
@@ -8488,7 +8842,7 @@ function installedPackageRoot(fromDir) {
8488
8842
  let dir = fromDir;
8489
8843
  for (; ; ) {
8490
8844
  const candidate = join22(dir, "node_modules", "deepspace");
8491
- if (existsSync20(join22(candidate, "package.json"))) return candidate;
8845
+ if (existsSync21(join22(candidate, "package.json"))) return candidate;
8492
8846
  const parent = dirname10(dir);
8493
8847
  if (parent === dir) return null;
8494
8848
  dir = parent;
@@ -8507,7 +8861,7 @@ async function targetInstaller(appDir) {
8507
8861
  }
8508
8862
  const entry = join22(installedRoot, "dist", "feature-installer.js");
8509
8863
  const catalog = join22(installedRoot, "features");
8510
- if (!existsSync20(entry) || !existsSync20(catalog)) {
8864
+ if (!existsSync21(entry) || !existsSync21(catalog)) {
8511
8865
  throw new Refusal(
8512
8866
  `The app's installed deepspace package does not contain the typed feature installer.
8513
8867
  Expected: ${entry}
@@ -8600,7 +8954,7 @@ var add_default = defineDeepspaceCommand({
8600
8954
  return { data: { features: featureCatalog(root) } };
8601
8955
  }
8602
8956
  const appDir = resolve17(args.dir ?? ".");
8603
- if (!existsSync20(appDir)) {
8957
+ if (!existsSync21(appDir)) {
8604
8958
  throw new Refusal(
8605
8959
  `Target directory not found: ${appDir}
8606
8960
  Create a new app first: deepspace app create <name>`,
@@ -8976,7 +9330,7 @@ var domain_default = defineCommand5({
8976
9330
  // src/cli/commands/app-files.ts
8977
9331
  import { defineCommand as defineCommand6 } from "citty";
8978
9332
  import { basename as basename4, resolve as resolve18 } from "path";
8979
- import { existsSync as existsSync21, statSync as statSync2 } from "fs";
9333
+ import { existsSync as existsSync22, statSync as statSync2 } from "fs";
8980
9334
 
8981
9335
  // src/cli/lib/app-files-api.ts
8982
9336
  import { createWriteStream, openAsBlob, statSync } from "fs";
@@ -9255,7 +9609,7 @@ var put = defineDeepspaceCommand({
9255
9609
  },
9256
9610
  async run({ args }) {
9257
9611
  const localPath = resolve18(String(args.file));
9258
- if (!existsSync21(localPath) || !statSync2(localPath).isFile()) {
9612
+ if (!existsSync22(localPath) || !statSync2(localPath).isFile()) {
9259
9613
  throw new Refusal(`No such file: ${args.file}`, "file_not_found");
9260
9614
  }
9261
9615
  const key = requireKey(args.key ?? basename4(localPath));
@@ -9428,8 +9782,8 @@ var list4 = defineDeepspaceCommand({
9428
9782
  }
9429
9783
  if (pending.length) {
9430
9784
  console.log(`PENDING INVITES ON ${app2}`);
9431
- for (const p35 of pending) {
9432
- console.log(` ${p35.email} (expires ${new Date(p35.expiresAt).toLocaleDateString()})`);
9785
+ for (const p36 of pending) {
9786
+ console.log(` ${p36.email} (expires ${new Date(p36.expiresAt).toLocaleDateString()})`);
9433
9787
  }
9434
9788
  }
9435
9789
  }
@@ -9535,7 +9889,7 @@ var cancel3 = defineDeepspaceCommand({
9535
9889
  `/api/app-collaborators/${encodeURIComponent(app2)}`
9536
9890
  );
9537
9891
  const target2 = email.trim().toLowerCase();
9538
- const found = pending.find((p35) => p35.email.toLowerCase() === target2);
9892
+ const found = pending.find((p36) => p36.email.toLowerCase() === target2);
9539
9893
  if (!found) {
9540
9894
  throw new Refusal(`${email} has no pending invite on ${app2}`, "no_pending_invite", {
9541
9895
  action: cliAction("deepspace", "app", "collaborators", "list", "--app", app2)
@@ -10137,7 +10491,7 @@ var integrations_default = defineCommand9({
10137
10491
 
10138
10492
  // src/cli/commands/feedback.ts
10139
10493
  import * as p16 from "@clack/prompts";
10140
- import { existsSync as existsSync22, readFileSync as readFileSync21 } from "fs";
10494
+ import { existsSync as existsSync23, readFileSync as readFileSync21 } from "fs";
10141
10495
  import { dirname as dirname11, join as join23 } from "path";
10142
10496
  import { fileURLToPath as fileURLToPath2 } from "url";
10143
10497
  import { release } from "os";
@@ -10165,7 +10519,7 @@ function readCliVersion() {
10165
10519
  join23(here, "..", "..", "..", "package.json")
10166
10520
  ];
10167
10521
  for (const path of candidates) {
10168
- if (!existsSync22(path)) continue;
10522
+ if (!existsSync23(path)) continue;
10169
10523
  const pkg2 = JSON.parse(readFileSync21(path, "utf-8"));
10170
10524
  if (pkg2.name === "deepspace" && pkg2.version) return pkg2.version;
10171
10525
  }
@@ -10362,6 +10716,7 @@ Context: deepspace ${payload.cliVersion} \xB7 ${payload.nodeVersion} \xB7 ${payl
10362
10716
 
10363
10717
  // src/cli/commands/secrets.ts
10364
10718
  import { defineCommand as defineCommand10 } from "citty";
10719
+ import * as p17 from "@clack/prompts";
10365
10720
  import { readFileSync as readFileSync22 } from "fs";
10366
10721
 
10367
10722
  // src/cli/lib/citty-args.ts
@@ -10429,9 +10784,6 @@ async function resolveTarget(args) {
10429
10784
  const appId = await resolveAppTarget(DEPLOY_URL11, token, args.app, { wranglerEnv });
10430
10785
  return { appId, configName, token };
10431
10786
  }
10432
- function fail(err) {
10433
- renderCliError(err);
10434
- }
10435
10787
  function ok(json, data, human) {
10436
10788
  if (json) console.log(JSON.stringify({ ok: true, ...data }));
10437
10789
  else human();
@@ -10457,22 +10809,18 @@ var list6 = defineCommand10({
10457
10809
  }
10458
10810
  },
10459
10811
  async run({ args }) {
10460
- try {
10461
- const t = await resolveTarget(args);
10462
- const { secrets } = await listSecrets(DEPLOY_URL11, t.token, t.appId, t.configName);
10463
- if (args.json) {
10464
- console.log(JSON.stringify({ ok: true, appId: t.appId, config: t.configName, secrets }));
10465
- return;
10466
- }
10467
- if (secrets.length === 0) {
10468
- console.log(`No secrets in ${t.configName}.`);
10469
- return;
10470
- }
10471
- for (const s of secrets) {
10472
- console.log(args["only-names"] ? s.key : `${s.key} (v${s.version}, ${s.updatedAt})`);
10473
- }
10474
- } catch (err) {
10475
- fail(err);
10812
+ const t = await resolveTarget(args);
10813
+ const { secrets } = await listSecrets(DEPLOY_URL11, t.token, t.appId, t.configName);
10814
+ if (args.json) {
10815
+ console.log(JSON.stringify({ ok: true, appId: t.appId, config: t.configName, secrets }));
10816
+ return;
10817
+ }
10818
+ if (secrets.length === 0) {
10819
+ console.log(`No secrets in ${t.configName}.`);
10820
+ return;
10821
+ }
10822
+ for (const s of secrets) {
10823
+ console.log(args["only-names"] ? s.key : `${s.key} (v${s.version}, ${s.updatedAt})`);
10476
10824
  }
10477
10825
  }
10478
10826
  });
@@ -10483,46 +10831,42 @@ var set = defineCommand10({
10483
10831
  secret: { type: "positional", description: "KEY=value (repeatable)", required: true }
10484
10832
  },
10485
10833
  async run({ args }) {
10486
- try {
10487
- const t = await resolveTarget(args);
10488
- const pairs = dedupePositionals(args.secret, args._);
10489
- const secrets = {};
10490
- const dupes = [];
10491
- for (const pair of pairs) {
10492
- const eq = pair.indexOf("=");
10493
- if (eq <= 0) throw new InputError(`Expected KEY=value, got "${pair}"`, "invalid_pair");
10494
- const key = validateSecretName(pair.slice(0, eq));
10495
- const value = pair.slice(eq + 1);
10496
- validateSecretValue(key, value);
10497
- if (key in secrets && !dupes.includes(key)) dupes.push(key);
10498
- secrets[key] = value;
10499
- }
10500
- if (dupes.length) {
10501
- console.warn(
10502
- `Warning: ${dupes.join(", ")} given more than once \u2014 kept the last value for each.`
10503
- );
10504
- }
10505
- if (Object.keys(secrets).length === 1) {
10506
- const [[k, v]] = Object.entries(secrets);
10507
- await setSecret(DEPLOY_URL11, t.token, t.appId, t.configName, k, v);
10508
- } else {
10509
- await uploadSecrets(DEPLOY_URL11, t.token, t.appId, t.configName, secrets, false);
10510
- }
10511
- const n = Object.keys(secrets).length;
10512
- ok(
10513
- args.json === true,
10514
- { appId: t.appId, config: t.configName, set: Object.keys(secrets), ...APPLIES_AT_DEPLOY },
10515
- () => {
10516
- console.log(`Set ${n} secret${n === 1 ? "" : "s"} in ${t.configName}.`);
10517
- if (!warnIfDebugRoutesEnabled(secrets)) {
10518
- console.log(APPLY_HINT);
10519
- console.log(LOCAL_DEV_HINT);
10520
- }
10521
- }
10834
+ const t = await resolveTarget(args);
10835
+ const pairs = dedupePositionals(args.secret, args._);
10836
+ const secrets = {};
10837
+ const dupes = [];
10838
+ for (const pair of pairs) {
10839
+ const eq = pair.indexOf("=");
10840
+ if (eq <= 0) throw new InputError(`Expected KEY=value, got "${pair}"`, "invalid_pair");
10841
+ const key = validateSecretName(pair.slice(0, eq));
10842
+ const value = pair.slice(eq + 1);
10843
+ validateSecretValue(key, value);
10844
+ if (key in secrets && !dupes.includes(key)) dupes.push(key);
10845
+ secrets[key] = value;
10846
+ }
10847
+ if (dupes.length) {
10848
+ console.warn(
10849
+ `Warning: ${dupes.join(", ")} given more than once \u2014 kept the last value for each.`
10522
10850
  );
10523
- } catch (err) {
10524
- fail(err);
10525
10851
  }
10852
+ if (Object.keys(secrets).length === 1) {
10853
+ const [[k, v]] = Object.entries(secrets);
10854
+ await setSecret(DEPLOY_URL11, t.token, t.appId, t.configName, k, v);
10855
+ } else {
10856
+ await uploadSecrets(DEPLOY_URL11, t.token, t.appId, t.configName, secrets, false);
10857
+ }
10858
+ const n = Object.keys(secrets).length;
10859
+ ok(
10860
+ args.json === true,
10861
+ { appId: t.appId, config: t.configName, set: Object.keys(secrets), ...APPLIES_AT_DEPLOY },
10862
+ () => {
10863
+ console.log(`Set ${n} secret${n === 1 ? "" : "s"} in ${t.configName}.`);
10864
+ if (!warnIfDebugRoutesEnabled(secrets)) {
10865
+ console.log(APPLY_HINT);
10866
+ console.log(LOCAL_DEV_HINT);
10867
+ }
10868
+ }
10869
+ );
10526
10870
  }
10527
10871
  });
10528
10872
  var get2 = defineCommand10({
@@ -10533,32 +10877,34 @@ var get2 = defineCommand10({
10533
10877
  plain: { type: "boolean", description: "Print the plaintext value", default: false }
10534
10878
  },
10535
10879
  async run({ args }) {
10536
- try {
10537
- const t = await resolveTarget(args);
10538
- const key = validateSecretName(args.key);
10539
- if (args.plain) {
10540
- const { value } = await getSecretPlain(DEPLOY_URL11, t.token, t.appId, t.configName, key);
10541
- process.stdout.write(process.stdout.isTTY ? value + "\n" : value);
10542
- return;
10543
- }
10544
- const { secrets } = await listSecrets(DEPLOY_URL11, t.token, t.appId, t.configName);
10545
- const item = secrets.find((s) => s.key === key);
10546
- if (!item)
10547
- throw new InputError(`Secret "${key}" not found in ${t.configName}`, "secret_not_found");
10548
- ok(
10549
- args.json === true,
10550
- {
10551
- appId: t.appId,
10552
- config: t.configName,
10553
- key: item.key,
10554
- version: item.version,
10555
- updatedAt: item.updatedAt
10556
- },
10557
- () => console.log(`${item.key} (v${item.version}, ${item.updatedAt})`)
10880
+ if (args.plain && args.json === true) {
10881
+ throw new InputError(
10882
+ "--plain writes the raw secret value, which is not JSON. Use --plain to capture the value, or --json for the metadata envelope \u2014 not both.",
10883
+ "invalid_flags"
10558
10884
  );
10559
- } catch (err) {
10560
- fail(err);
10561
10885
  }
10886
+ const t = await resolveTarget(args);
10887
+ const key = validateSecretName(args.key);
10888
+ if (args.plain) {
10889
+ const { value } = await getSecretPlain(DEPLOY_URL11, t.token, t.appId, t.configName, key);
10890
+ process.stdout.write(process.stdout.isTTY ? value + "\n" : value);
10891
+ return;
10892
+ }
10893
+ const { secrets } = await listSecrets(DEPLOY_URL11, t.token, t.appId, t.configName);
10894
+ const item = secrets.find((s) => s.key === key);
10895
+ if (!item)
10896
+ throw new InputError(`Secret "${key}" not found in ${t.configName}`, "secret_not_found");
10897
+ ok(
10898
+ args.json === true,
10899
+ {
10900
+ appId: t.appId,
10901
+ config: t.configName,
10902
+ key: item.key,
10903
+ version: item.version,
10904
+ updatedAt: item.updatedAt
10905
+ },
10906
+ () => console.log(`${item.key} (v${item.version}, ${item.updatedAt})`)
10907
+ );
10562
10908
  }
10563
10909
  });
10564
10910
  var del2 = defineCommand10({
@@ -10568,36 +10914,33 @@ var del2 = defineCommand10({
10568
10914
  key: { type: "positional", description: "Secret name (repeatable)", required: true }
10569
10915
  },
10570
10916
  async run({ args }) {
10571
- try {
10572
- const t = await resolveTarget(args);
10573
- const keys = dedupePositionals(args.key, args._);
10574
- let deleted = 0;
10575
- let absent = 0;
10576
- for (const key of keys) {
10577
- const name = validateSecretName(key);
10578
- try {
10579
- const result = await deleteSecret(DEPLOY_URL11, t.token, t.appId, t.configName, name);
10580
- if (result.deleted === false) absent++;
10581
- else deleted++;
10582
- } catch (err) {
10583
- if (err?.status === 404) absent++;
10584
- else throw err;
10585
- }
10917
+ const t = await resolveTarget(args);
10918
+ const keys = dedupePositionals(args.key, args._);
10919
+ let deleted = 0;
10920
+ let absent = 0;
10921
+ for (const key of keys) {
10922
+ const name = validateSecretName(key);
10923
+ try {
10924
+ const result = await deleteSecret(DEPLOY_URL11, t.token, t.appId, t.configName, name);
10925
+ if (result.deleted === false) absent++;
10926
+ else deleted++;
10927
+ } catch (err) {
10928
+ if (err instanceof ApiError && err.status === 404 && err.code !== "unrecognized_service") {
10929
+ absent++;
10930
+ } else throw err;
10586
10931
  }
10587
- const note3 = absent > 0 ? ` (${absent} already absent)` : "";
10588
- ok(
10589
- args.json === true,
10590
- { appId: t.appId, config: t.configName, deleted, absent, ...APPLIES_AT_DEPLOY },
10591
- () => {
10592
- console.log(
10593
- `Deleted ${deleted} secret${deleted === 1 ? "" : "s"} from ${t.configName}.${note3}`
10594
- );
10595
- if (deleted > 0) console.log(APPLY_HINT);
10596
- }
10597
- );
10598
- } catch (err) {
10599
- fail(err);
10600
10932
  }
10933
+ const note3 = absent > 0 ? ` (${absent} already absent)` : "";
10934
+ ok(
10935
+ args.json === true,
10936
+ { appId: t.appId, config: t.configName, deleted, absent, ...APPLIES_AT_DEPLOY },
10937
+ () => {
10938
+ console.log(
10939
+ `Deleted ${deleted} secret${deleted === 1 ? "" : "s"} from ${t.configName}.${note3}`
10940
+ );
10941
+ if (deleted > 0) console.log(APPLY_HINT);
10942
+ }
10943
+ );
10601
10944
  }
10602
10945
  });
10603
10946
  var upload = defineCommand10({
@@ -10616,30 +10959,26 @@ var upload = defineCommand10({
10616
10959
  }
10617
10960
  },
10618
10961
  async run({ args }) {
10619
- try {
10620
- const t = await resolveTarget(args);
10621
- const secrets = parseSecretsUpload(await readUploadSource(args.file));
10622
- if (Object.keys(secrets).length === 0)
10623
- throw new InputError("No secrets found in the input.", "empty_input");
10624
- await uploadSecrets(DEPLOY_URL11, t.token, t.appId, t.configName, secrets, args.replace);
10625
- const uploaded = Object.keys(secrets);
10626
- ok(
10627
- args.json === true,
10628
- {
10629
- appId: t.appId,
10630
- config: t.configName,
10631
- uploaded,
10632
- replaced: args.replace === true,
10633
- ...APPLIES_AT_DEPLOY
10634
- },
10635
- () => {
10636
- console.log(`Uploaded ${uploaded.length} secrets to ${t.configName}.`);
10637
- if (!warnIfDebugRoutesEnabled(secrets)) console.log(APPLY_HINT);
10638
- }
10639
- );
10640
- } catch (err) {
10641
- fail(err);
10642
- }
10962
+ const t = await resolveTarget(args);
10963
+ const secrets = parseSecretsUpload(await readUploadSource(String(args.file)));
10964
+ if (Object.keys(secrets).length === 0)
10965
+ throw new InputError("No secrets found in the input.", "empty_input");
10966
+ await uploadSecrets(DEPLOY_URL11, t.token, t.appId, t.configName, secrets, args.replace);
10967
+ const uploaded = Object.keys(secrets);
10968
+ ok(
10969
+ args.json === true,
10970
+ {
10971
+ appId: t.appId,
10972
+ config: t.configName,
10973
+ uploaded,
10974
+ replaced: args.replace === true,
10975
+ ...APPLIES_AT_DEPLOY
10976
+ },
10977
+ () => {
10978
+ console.log(`Uploaded ${uploaded.length} secrets to ${t.configName}.`);
10979
+ if (!warnIfDebugRoutesEnabled(secrets)) console.log(APPLY_HINT);
10980
+ }
10981
+ );
10643
10982
  }
10644
10983
  });
10645
10984
  async function readUploadSource(file) {
@@ -10655,10 +10994,11 @@ async function readUploadSource(file) {
10655
10994
  try {
10656
10995
  return readFileSync22(file, "utf-8");
10657
10996
  } catch (err) {
10658
- if (err.code !== "ENOENT") throw err;
10997
+ const errno = err.code;
10998
+ if (typeof errno !== "string" || errno.length === 0) throw err;
10659
10999
  throw new InputError(
10660
- `No such file: ${file}. Pass the path of a dotenv or JSON file to upload, or \`-\` to read it from stdin.`,
10661
- "file_not_found"
11000
+ errno === "ENOENT" ? `No such file: ${file}. Pass the path of a dotenv or JSON file to upload, or \`-\` to read it from stdin.` : `Cannot read ${file} (${errno}).`,
11001
+ errno === "ENOENT" ? "file_not_found" : "file_unreadable"
10662
11002
  );
10663
11003
  }
10664
11004
  }
@@ -10666,6 +11006,17 @@ var download = defineCommand10({
10666
11006
  meta: { name: "download", description: "Download a config\u2019s secrets (dotenv/json/shell)" },
10667
11007
  args: {
10668
11008
  ...COMMON_ARGS,
11009
+ // `download` writes a FILE FORMAT to stdout, so `--json` has no meaning
11010
+ // here and is REFUSED rather than described: rewording it was not enough,
11011
+ // because the flag still silently emitted dotenv — zero JSON documents on
11012
+ // a success path, with the plaintext secrets themselves as the output a
11013
+ // parser choked on. `secrets get --plain --json` already refuses the same
11014
+ // contradiction.
11015
+ json: {
11016
+ type: "boolean",
11017
+ description: "Not supported here \u2014 secrets are a stream; use --format json for JSON output",
11018
+ default: false
11019
+ },
10669
11020
  format: {
10670
11021
  type: "string",
10671
11022
  description: "dotenv (default) | json | shell",
@@ -10673,79 +11024,73 @@ var download = defineCommand10({
10673
11024
  }
10674
11025
  },
10675
11026
  async run({ args }) {
10676
- try {
10677
- const t = await resolveTarget(args);
10678
- const format = args.format;
10679
- if (!["dotenv", "json", "shell"].includes(format)) {
10680
- throw new InputError(
10681
- `Unknown format "${args.format}" \u2014 use dotenv, json, or shell.`,
10682
- "invalid_format"
10683
- );
10684
- }
10685
- const { secrets } = await fetchSecretsValues(DEPLOY_URL11, t.token, t.appId, t.configName);
10686
- process.stdout.write(formatSecretsDownload(secrets, format));
10687
- } catch (err) {
10688
- fail(err);
11027
+ if (args.json === true) {
11028
+ throw new InputError(
11029
+ "--json is not supported by `secrets download`: it writes the secrets themselves to stdout, not an envelope. Use `--format json` for JSON-shaped secrets, or `secrets list --json` for the metadata envelope.",
11030
+ "invalid_flags"
11031
+ );
11032
+ }
11033
+ const t = await resolveTarget(args);
11034
+ const format = args.format;
11035
+ if (!["dotenv", "json", "shell"].includes(format)) {
11036
+ throw new InputError(
11037
+ `Unknown format "${args.format}" \u2014 use dotenv, json, or shell.`,
11038
+ "invalid_format"
11039
+ );
10689
11040
  }
11041
+ const { secrets } = await fetchSecretsValues(DEPLOY_URL11, t.token, t.appId, t.configName);
11042
+ process.stdout.write(formatSecretsDownload(secrets, format));
10690
11043
  }
10691
11044
  });
10692
11045
  var pull = defineCommand10({
10693
11046
  meta: { name: "pull", description: "Refresh the .dev.vars cache from the app store" },
10694
11047
  args: { ...COMMON_ARGS },
10695
11048
  async run({ args }) {
10696
- try {
10697
- const wranglerEnv = args.env?.trim() || void 0;
10698
- const appDir = findAppDir();
10699
- if (!appDir)
10700
- throw new InputError(
10701
- "Run from a DeepSpace app directory (one containing wrangler.toml).",
10702
- "not_in_app_repo"
10703
- );
10704
- const t = await resolveTarget(args);
10705
- const ownerId = decodeJwtPayload(t.token).sub;
10706
- const refreshed = await refreshSecretsCache(
10707
- DEPLOY_URL11,
10708
- t.token,
10709
- t.appId,
10710
- wranglerEnv,
10711
- t.configName
10712
- );
10713
- await writeDevVars(appDir, ownerId, t.token, wranglerEnv, {
10714
- appId: t.appId,
10715
- generatedSecretsCache: refreshed.rendered
10716
- });
10717
- const count = Object.keys(refreshed.pulled?.values ?? {}).length;
10718
- ok(
10719
- args.json === true,
10720
- { appId: t.appId, config: t.configName, pulled: count },
10721
- () => console.log(
10722
- refreshed.pulled ? `Pulled ${count} secrets (${t.configName}) into .dev.vars.` : `Config ${t.configName} does not exist yet; regenerated .dev.vars without app secrets.`
10723
- )
11049
+ const wranglerEnv = args.env?.trim() || void 0;
11050
+ const appDir = findAppDir();
11051
+ if (!appDir)
11052
+ throw new InputError(
11053
+ "Run from a DeepSpace app directory (one containing wrangler.toml).",
11054
+ "not_in_app_repo"
10724
11055
  );
10725
- } catch (err) {
10726
- fail(err);
10727
- }
11056
+ const t = await resolveTarget(args);
11057
+ const ownerId = decodeJwtPayload(t.token).sub;
11058
+ const refreshed = await refreshSecretsCache(
11059
+ DEPLOY_URL11,
11060
+ t.token,
11061
+ t.appId,
11062
+ wranglerEnv,
11063
+ t.configName
11064
+ );
11065
+ await writeDevVars(appDir, ownerId, t.token, wranglerEnv, {
11066
+ appId: t.appId,
11067
+ generatedSecretsCache: refreshed.rendered
11068
+ });
11069
+ const count = Object.keys(refreshed.pulled?.values ?? {}).length;
11070
+ ok(
11071
+ args.json === true,
11072
+ { appId: t.appId, config: t.configName, pulled: count },
11073
+ () => console.log(
11074
+ refreshed.pulled ? `Pulled ${count} secrets (${t.configName}) into .dev.vars.` : `Config ${t.configName} does not exist yet; regenerated .dev.vars without app secrets.`
11075
+ )
11076
+ );
10728
11077
  }
10729
11078
  });
10730
11079
  var configsList = defineCommand10({
10731
11080
  meta: { name: "list", description: "List the app\u2019s configs" },
10732
11081
  args: { ...COMMON_ARGS },
10733
11082
  async run({ args }) {
10734
- try {
10735
- const t = await resolveTarget(args);
10736
- const { configs: configs2 } = await listConfigs(DEPLOY_URL11, t.token, t.appId);
10737
- ok(args.json === true, { appId: t.appId, configs: configs2 }, () => {
10738
- if (configs2.length === 0) {
10739
- console.log("No configs yet \u2014 the first `secrets set` creates one.");
10740
- return;
10741
- }
10742
- for (const cfg of configs2) {
10743
- console.log(`${cfg.name} (${cfg.secretCount ?? 0} secrets, updated ${cfg.updatedAt})`);
10744
- }
10745
- });
10746
- } catch (err) {
10747
- fail(err);
10748
- }
11083
+ const t = await resolveTarget(args);
11084
+ const { configs: configs2 } = await listConfigs(DEPLOY_URL11, t.token, t.appId);
11085
+ ok(args.json === true, { appId: t.appId, configs: configs2 }, () => {
11086
+ if (configs2.length === 0) {
11087
+ console.log("No configs yet \u2014 the first `secrets set` creates one.");
11088
+ return;
11089
+ }
11090
+ for (const cfg of configs2) {
11091
+ console.log(`${cfg.name} (${cfg.secretCount ?? 0} secrets, updated ${cfg.updatedAt})`);
11092
+ }
11093
+ });
10749
11094
  }
10750
11095
  });
10751
11096
  var configsCreate = defineCommand10({
@@ -10756,55 +11101,80 @@ var configsCreate = defineCommand10({
10756
11101
  "copy-from": { type: "string", description: "Copy secrets from this config", required: false }
10757
11102
  },
10758
11103
  async run({ args }) {
10759
- try {
10760
- const t = await resolveTarget(args);
10761
- const name = validateConfigName(args.name);
10762
- const copyFrom = args["copy-from"] || void 0;
10763
- const { configs: configs2 } = await listConfigs(DEPLOY_URL11, t.token, t.appId);
10764
- if (configs2.some((c) => c.name === name)) {
10765
- if (copyFrom) {
10766
- throw new InputError(
10767
- `Config "${name}" already exists \u2014 refusing to copy "${copyFrom}" over it. Delete it first, or pick a new name.`,
10768
- "config_exists"
10769
- );
10770
- }
10771
- ok(
10772
- args.json === true,
10773
- { appId: t.appId, config: name, created: false },
10774
- () => console.log(`Config ${name} already exists.`)
11104
+ const t = await resolveTarget(args);
11105
+ const name = validateConfigName(args.name);
11106
+ const copyFrom = args["copy-from"] || void 0;
11107
+ const { configs: configs2 } = await listConfigs(DEPLOY_URL11, t.token, t.appId);
11108
+ if (configs2.some((c) => c.name === name)) {
11109
+ if (copyFrom) {
11110
+ throw new InputError(
11111
+ `Config "${name}" already exists \u2014 refusing to copy "${copyFrom}" over it. Delete it first, or pick a new name.`,
11112
+ "config_exists"
10775
11113
  );
10776
- return;
10777
11114
  }
10778
- await createConfig(DEPLOY_URL11, t.token, t.appId, name, copyFrom);
10779
11115
  ok(
10780
11116
  args.json === true,
10781
- { appId: t.appId, config: name, created: true, ...copyFrom ? { copyFrom } : {} },
10782
- () => console.log(`Created ${name}.`)
11117
+ { appId: t.appId, config: name, created: false },
11118
+ () => console.log(`Config ${name} already exists.`)
10783
11119
  );
10784
- } catch (err) {
10785
- fail(err);
11120
+ return;
10786
11121
  }
11122
+ await createConfig(DEPLOY_URL11, t.token, t.appId, name, copyFrom);
11123
+ ok(
11124
+ args.json === true,
11125
+ { appId: t.appId, config: name, created: true, ...copyFrom ? { copyFrom } : {} },
11126
+ () => console.log(`Created ${name}.`)
11127
+ );
10787
11128
  }
10788
11129
  });
10789
11130
  var configsDelete = defineCommand10({
10790
11131
  meta: { name: "delete", description: "Delete a config and all of its secrets" },
10791
11132
  args: {
10792
11133
  ...COMMON_ARGS,
10793
- name: { type: "positional", description: "Config name", required: true }
11134
+ name: { type: "positional", description: "Config name", required: true },
11135
+ yes: {
11136
+ type: "boolean",
11137
+ alias: "y",
11138
+ description: "Skip the confirmation (required for --json / non-interactive)",
11139
+ default: false
11140
+ }
10794
11141
  },
10795
11142
  async run({ args }) {
10796
- try {
10797
- const t = await resolveTarget(args);
10798
- const name = validateConfigName(args.name);
10799
- await deleteConfig(DEPLOY_URL11, t.token, t.appId, name);
10800
- ok(
10801
- args.json === true,
10802
- { appId: t.appId, config: name, deleted: true },
10803
- () => console.log(`Deleted ${name}.`)
11143
+ const t = await resolveTarget(args);
11144
+ const name = validateConfigName(args.name);
11145
+ if (args.yes !== true && (args.json === true || !process.stdin.isTTY)) {
11146
+ throw new InputError(
11147
+ `Deleting config ${name} removes every secret in it permanently. Re-run with --yes to confirm.`,
11148
+ "confirmation_required"
10804
11149
  );
10805
- } catch (err) {
10806
- fail(err);
10807
11150
  }
11151
+ let secrets = [];
11152
+ let counted = true;
11153
+ try {
11154
+ secrets = (await listSecrets(DEPLOY_URL11, t.token, t.appId, name)).secrets;
11155
+ } catch (error) {
11156
+ if (args.yes !== true) throw error;
11157
+ counted = false;
11158
+ }
11159
+ const count = secrets.length;
11160
+ if (args.yes !== true) {
11161
+ const detail = count === 0 ? `Config ${name} is empty.` : `Config ${name} holds ${count} secret${count === 1 ? "" : "s"}: ${secrets.map((secret) => secret.key).slice(0, 8).join(", ")}${count > 8 ? `, \u2026` : ""}.`;
11162
+ const confirmed = await p17.confirm({
11163
+ message: `${detail} Deleting it removes ${count === 1 ? "that secret" : "them all"} permanently.`,
11164
+ initialValue: false
11165
+ });
11166
+ if (p17.isCancel(confirmed) || !confirmed) {
11167
+ throw new InputError("Cancelled.", "confirmation_required");
11168
+ }
11169
+ }
11170
+ await deleteConfig(DEPLOY_URL11, t.token, t.appId, name);
11171
+ ok(
11172
+ args.json === true,
11173
+ { appId: t.appId, config: name, deleted: true, secretsDeleted: counted ? count : null },
11174
+ () => console.log(
11175
+ counted ? `Deleted ${name} and ${count} secret${count === 1 ? "" : "s"}.` : `Deleted ${name}.`
11176
+ )
11177
+ );
10808
11178
  }
10809
11179
  });
10810
11180
  var configs = defineCommand10({
@@ -10826,11 +11196,11 @@ var secrets_default = defineCommand10({
10826
11196
  });
10827
11197
 
10828
11198
  // src/cli/commands/init.ts
10829
- import { existsSync as existsSync23 } from "fs";
11199
+ import { existsSync as existsSync24 } from "fs";
10830
11200
  import { join as join24 } from "path";
10831
11201
  var DEPLOY_URL12 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
10832
11202
  function wranglerConfigUncommitted(appDir) {
10833
- if (!existsSync23(join24(appDir, ".git"))) return false;
11203
+ if (!existsSync24(join24(appDir, ".git"))) return false;
10834
11204
  const dryRun = runGit(appDir, ["commit", "--dry-run", "-m", "x", "--", "wrangler.toml"], {
10835
11205
  allowFail: true
10836
11206
  });
@@ -10838,7 +11208,7 @@ function wranglerConfigUncommitted(appDir) {
10838
11208
  }
10839
11209
  function commitScaffoldIfUnborn(appDir, token) {
10840
11210
  try {
10841
- if (!existsSync23(join24(appDir, ".git"))) return false;
11211
+ if (!existsSync24(join24(appDir, ".git"))) return false;
10842
11212
  if (runGit(appDir, ["rev-parse", "--verify", "HEAD"], { allowFail: true }).status === 0) {
10843
11213
  return false;
10844
11214
  }
@@ -10989,8 +11359,8 @@ var init_default = defineDeepspaceCommand({
10989
11359
 
10990
11360
  // src/cli/commands/status.ts
10991
11361
  import { defineCommand as defineCommand11 } from "citty";
10992
- import * as p17 from "@clack/prompts";
10993
- import { existsSync as existsSync24, readFileSync as readFileSync23 } from "fs";
11362
+ import * as p18 from "@clack/prompts";
11363
+ import { existsSync as existsSync25, readFileSync as readFileSync23 } from "fs";
10994
11364
  function timeAgo(iso) {
10995
11365
  const ms = Date.now() - Date.parse(iso);
10996
11366
  if (!Number.isFinite(ms) || ms < 0) return iso;
@@ -11002,6 +11372,7 @@ function timeAgo(iso) {
11002
11372
  return `${Math.floor(h / 24)}d ago`;
11003
11373
  }
11004
11374
  var UNREACHABLE = "(unreachable)";
11375
+ var sessionError = (error) => ({ state: "unavailable", error, code: "not_authenticated" });
11005
11376
  var observeRemote = (promise) => promise.then(
11006
11377
  (value) => ({ ok: true, value }),
11007
11378
  (error) => ({ ok: false, error })
@@ -11082,13 +11453,14 @@ var status_default = defineCommand11({
11082
11453
  ]);
11083
11454
  if (urlOverrides.length > 0) json.urlOverrides = urlOverrides;
11084
11455
  }
11085
- facts.loggedIn = existsSync24(SESSION_PATH);
11456
+ facts.loggedIn = existsSync25(SESSION_PATH) || existsSync25(TOKEN_PATH);
11086
11457
  let userId = null;
11087
11458
  if (!facts.loggedIn) {
11088
11459
  lines.push(["Session", "not logged in"]);
11089
11460
  json.loggedIn = false;
11461
+ json.sessionError = sessionError("Not logged in. Run `deepspace auth login` first.");
11090
11462
  } else {
11091
- let who = "logged in";
11463
+ let who = null;
11092
11464
  try {
11093
11465
  const payload = decodeJwtPayload(
11094
11466
  readFileSync23(TOKEN_PATH, "utf-8").trim()
@@ -11097,9 +11469,12 @@ var status_default = defineCommand11({
11097
11469
  userId = payload.sub ?? null;
11098
11470
  } catch {
11099
11471
  }
11100
- lines.push(["Session", `${who} \xB7 renews on use (re-login only after 30 idle days)`]);
11472
+ lines.push([
11473
+ "Session",
11474
+ `${who ?? "logged in"} \xB7 renews on use (re-login only after 30 idle days)`
11475
+ ]);
11101
11476
  json.loggedIn = true;
11102
- json.user = who;
11477
+ if (who) json.user = who;
11103
11478
  }
11104
11479
  const appDir = findAppDir();
11105
11480
  if (!appDir) {
@@ -11158,6 +11533,9 @@ var status_default = defineCommand11({
11158
11533
  lines.push(["Cloud", "session expired \u2014 run `deepspace auth login`"]);
11159
11534
  facts.loggedIn = false;
11160
11535
  json.sessionExpired = true;
11536
+ json.loggedIn = false;
11537
+ delete json.user;
11538
+ json.sessionError = sessionError("Session expired. Run `deepspace auth login`.");
11161
11539
  }
11162
11540
  }
11163
11541
  if (token) {
@@ -11245,9 +11623,13 @@ var status_default = defineCommand11({
11245
11623
  const refs = refsResult.ok ? refsResult.value : void 0;
11246
11624
  const trunkRef = refs?.refs.find((r) => `refs/heads/${branch}` === r.name);
11247
11625
  const defaultBranch = refs?.head && refs.head.startsWith("refs/heads/") ? refs.head.slice("refs/heads/".length) : null;
11626
+ if (!githubSource && refsResult.ok && defaultBranch) json.defaultBranch = defaultBranch;
11248
11627
  if (!facts.workspaceId && branch) {
11249
11628
  const isTrunk = defaultBranch === null ? null : branch === defaultBranch;
11250
11629
  const label = isTrunk === false ? "Branch sync" : "Trunk";
11630
+ if (isTrunk === false && defaultBranch && !githubSource) {
11631
+ lines.push(["Trunk", `${defaultBranch} (this checkout is on ${branch})`]);
11632
+ }
11251
11633
  if (githubSource) {
11252
11634
  lines.push([label, `${branch} is owned through normal Git/GitHub`]);
11253
11635
  json.trunk = { state: "external", provider: "github", branch, isTrunk };
@@ -11276,12 +11658,12 @@ var status_default = defineCommand11({
11276
11658
  return;
11277
11659
  }
11278
11660
  const width = Math.max(...lines.map(([k]) => k.length));
11279
- for (const [k, v] of lines) p17.log.message(`${k.padEnd(width)} ${v}`);
11661
+ for (const [k, v] of lines) p18.log.message(displayLines(`${k.padEnd(width)} ${v}`));
11280
11662
  }
11281
11663
  });
11282
11664
 
11283
11665
  // src/cli/commands/push.ts
11284
- import * as p18 from "@clack/prompts";
11666
+ import * as p19 from "@clack/prompts";
11285
11667
  function pullRecoveryAction(cwd, appId, branch) {
11286
11668
  return {
11287
11669
  cwd,
@@ -11298,12 +11680,26 @@ function forcePushOrphansWork(lastPushed, remoteTip, tipOid, tipContainsRemoteTi
11298
11680
  if (remoteTip === lastPushed) return false;
11299
11681
  return !tipContainsRemoteTip;
11300
11682
  }
11683
+ function fetchRemoteTip(appDir, token, branch) {
11684
+ try {
11685
+ runGitRemote(appDir, token, [
11686
+ "fetch",
11687
+ "--quiet",
11688
+ "--refmap=",
11689
+ SPACE_REMOTE,
11690
+ `refs/heads/${branch}`
11691
+ ]);
11692
+ return resolveCommit(appDir, "FETCH_HEAD");
11693
+ } catch {
11694
+ return null;
11695
+ }
11696
+ }
11301
11697
  function workspaceBranchPushRefusal(branch) {
11302
11698
  const id = workspaceIdFromBranch(branch);
11303
- if (!id) return null;
11699
+ if (!id && !branch?.startsWith("ws/")) return null;
11304
11700
  return {
11305
11701
  code: "workspace_branch",
11306
- error: `"${branch}" is a workspace branch (${id}). A plain push moves only the visible ref and bypasses workspace coordination \u2014 the workspace tip, overlap warnings, and activity would go stale. Publish it with \`deepspace workspace sync\` instead.`
11702
+ error: `"${branch}" is in the workspace branch namespace${id ? ` (${id})` : ""}. A plain push moves only the visible ref and bypasses workspace coordination \u2014 the workspace tip, overlap warnings, and activity would go stale. Publish it with \`deepspace workspace sync\` instead.`
11307
11703
  };
11308
11704
  }
11309
11705
  var push_default = defineDeepspaceCommand({
@@ -11326,7 +11722,7 @@ var push_default = defineDeepspaceCommand({
11326
11722
  },
11327
11723
  force: {
11328
11724
  type: "boolean",
11329
- description: "Allow a non-fast-forward ref move to rewrite history you previously pushed with `deepspace push`. Guarded: refuses whenever the remote tip is a commit your branch does not contain, so no work is silently dropped (a first force from a fresh clone/pull is refused until you re-integrate that tip).",
11725
+ description: "Allow a non-fast-forward ref move to rewrite history you previously pushed with `deepspace push`. Guarded by a per-checkout push record: allowed when THIS checkout published the cloud tip with `deepspace push` (your own amend/rebase), refused when the cloud tip is any other commit your branch does not contain \u2014 so a peer's work is never silently dropped. A plain `git push space` leaves no record, so a force over a tip published that way is refused until you re-integrate it, as is a first force from a fresh clone/pull.",
11330
11726
  default: false
11331
11727
  }
11332
11728
  },
@@ -11390,7 +11786,14 @@ var push_default = defineDeepspaceCommand({
11390
11786
  const { appId: appId2 } = await resolveTarget3();
11391
11787
  const checkoutPath = selectedBranchCheckout(appDir, branch);
11392
11788
  const workspaceId = workspaceIdFromBranch(branch);
11393
- if (!workspaceId) throw new Error(`Workspace branch did not contain an id: ${branch}`);
11789
+ if (!workspaceId) {
11790
+ spinner2?.stop("Workspace branch.");
11791
+ throw new Refusal(
11792
+ `${wsRefusal.error} "${branch}" carries no workspace id, so nothing can publish it \u2014 rename it out of the \`ws/\` namespace (\`git branch -m ${shQuote(branch)} <name>\`) and push that.`,
11793
+ wsRefusal.code,
11794
+ { extra: { appId: appId2, branch } }
11795
+ );
11796
+ }
11394
11797
  const syncCommand = `deepspace workspace sync --app ${shQuote(appId2)} --workspace ${shQuote(workspaceId)}`;
11395
11798
  const quotedSyncCommand = `\`${syncCommand}\``;
11396
11799
  const message = checkoutPath ? `${wsRefusal.error} Run ${quotedSyncCommand} from its checkout at ${checkoutPath}.` : `${wsRefusal.error} ${branch} exists locally but is not checked out. Check it out in a clean worktree, then run ${quotedSyncCommand}.`;
@@ -11405,16 +11808,23 @@ var push_default = defineDeepspaceCommand({
11405
11808
  extra: { appId: appId2, branch, ...checkoutPath ? { worktreePath: checkoutPath } : {} }
11406
11809
  });
11407
11810
  }
11408
- const secretFiles = trackedSecretFiles(appDir, tipOid);
11811
+ const secretFiles = secretFilesInPushRange(
11812
+ appDir,
11813
+ resolveCommit(appDir, `refs/remotes/${SPACE_REMOTE}/${branch}`) ?? resolveCommit(appDir, spacePrivateRef(`pushed/${branch}`)),
11814
+ tipOid
11815
+ );
11409
11816
  if (secretFiles.length > 0) {
11410
11817
  const checkoutPath = selectedBranchCheckout(appDir, branch);
11411
- const checkoutNote = checkoutPath ? "" : ` Check out ${shQuote(branch)} in a clean worktree before untracking it.`;
11818
+ const checkoutNote = checkoutPath ? "" : ` Check out ${shQuote(branch)} in a clean worktree to do it.`;
11412
11819
  throw new Refusal(
11413
- `Refusing to push: the branch tracks secret file(s) \u2014 ${secretFiles.join(", ")}. These hold local secrets and must not reach the cloud repo. Untrack with \`git rm --cached ${shQuote(secretFiles[0])}\`, ensure it's .gitignored, commit, then push.` + checkoutNote,
11820
+ `Refusing to push: the commits being sent carry secret file(s) \u2014 ${secretFiles.join(", ")}. These hold local secrets and must not reach the cloud repo. ` + secretRecoverySentence(secretFiles, "push") + checkoutNote,
11414
11821
  SECRET_IN_HISTORY_CODE,
11415
11822
  {
11416
- action: checkoutPath ? { cwd: checkoutPath, argv: ["git", "rm", "--cached", secretFiles[0]] } : void 0,
11417
- extra: { branch, ...checkoutPath ? { worktreePath: checkoutPath } : {} }
11823
+ extra: {
11824
+ branch,
11825
+ files: secretFiles,
11826
+ ...checkoutPath ? { worktreePath: checkoutPath } : {}
11827
+ }
11418
11828
  }
11419
11829
  );
11420
11830
  }
@@ -11441,11 +11851,11 @@ var push_default = defineDeepspaceCommand({
11441
11851
  throw githubSourceRefusal(appId, sourceState.source.repository);
11442
11852
  }
11443
11853
  const pullRecoveryCwd = selectedBranchCheckout(appDir, branch) ?? appDir;
11444
- ensureSpaceRemote(appDir, appId);
11854
+ ensureSpaceRemote(appDir, appId, void 0, token);
11445
11855
  if (force) {
11446
11856
  spinner2?.message(`Checking the cloud ${branch} tip before force-push\u2026`);
11447
11857
  const lastPushed = resolveCommit(appDir, spacePrivateRef(`pushed/${branch}`));
11448
- let remoteTip = null;
11858
+ let remoteTip2 = null;
11449
11859
  try {
11450
11860
  runGitRemote(appDir, token, [
11451
11861
  "fetch",
@@ -11454,7 +11864,7 @@ var push_default = defineDeepspaceCommand({
11454
11864
  SPACE_REMOTE,
11455
11865
  `refs/heads/${branch}`
11456
11866
  ]);
11457
- remoteTip = resolveCommit(appDir, "FETCH_HEAD");
11867
+ remoteTip2 = resolveCommit(appDir, "FETCH_HEAD");
11458
11868
  } catch {
11459
11869
  const ls = runGitRemote(
11460
11870
  appDir,
@@ -11465,14 +11875,25 @@ var push_default = defineDeepspaceCommand({
11465
11875
  }
11466
11876
  );
11467
11877
  if (ls.status === 0 && ls.stdout.toString("utf-8").trim() === "") {
11468
- remoteTip = null;
11878
+ remoteTip2 = null;
11469
11879
  } else {
11470
11880
  const error = `Couldn't reach the cloud repo to check ${branch} before force-pushing. Refusing --force so it can't silently drop a peer's commits \u2014 retry when connected, or run \`deepspace pull --app ${shQuote(appId)} --branch ${shQuote(branch)}\` to integrate first.`;
11471
11881
  throw new Refusal(error, "force_unverified", { extra: { appId, branch } });
11472
11882
  }
11473
11883
  }
11474
- if (remoteTip && forcePushOrphansWork(lastPushed, remoteTip, tipOid, isAncestor(appDir, remoteTip, tipOid))) {
11475
- const error = `The cloud repo's ${branch} advanced since you last synced \u2014 a peer pushed commit(s) you don't have. Force-pushing now would DROP that work. Run \`deepspace pull --app ${shQuote(appId)} --branch ${shQuote(branch)}\` and merge (or rebase onto it), then push.`;
11884
+ if (remoteTip2 && forcePushOrphansWork(lastPushed, remoteTip2, tipOid, isAncestor(appDir, remoteTip2, tipOid))) {
11885
+ if (isAncestor(appDir, tipOid, remoteTip2)) {
11886
+ throw new Refusal(
11887
+ `The cloud repo's ${branch} is at ${remoteTip2.slice(0, 10)} and already contains everything this checkout has \u2014 this checkout is strictly behind. Force-pushing would REWIND the branch and drop the newer commit(s). Run \`deepspace pull --app ${shQuote(appId)} --branch ${shQuote(branch)}\` to fast-forward, then push normally if anything is left to publish.`,
11888
+ "behind",
11889
+ {
11890
+ actionRequired: true,
11891
+ action: pullRecoveryAction(pullRecoveryCwd, appId, branch),
11892
+ extra: { appId, branch }
11893
+ }
11894
+ );
11895
+ }
11896
+ const error = `The cloud repo's ${branch} is at ${remoteTip2.slice(0, 10)}, and this checkout has no record of publishing it (\`deepspace push\` records its own pushes; a plain \`git push ${SPACE_REMOTE}\` does not), so a peer's work on top can't be ruled out. Force-pushing now would DROP that line. Run \`deepspace pull --app ${shQuote(appId)} --branch ${shQuote(branch)}\` and merge (or rebase onto it), then push.`;
11476
11897
  throw new Refusal(error, "diverged", {
11477
11898
  actionRequired: true,
11478
11899
  action: pullRecoveryAction(pullRecoveryCwd, appId, branch),
@@ -11484,20 +11905,35 @@ var push_default = defineDeepspaceCommand({
11484
11905
  const result = pushToSpace(appDir, token, `refs/heads/${branch}:refs/heads/${branch}`, {
11485
11906
  force
11486
11907
  });
11487
- spinner2?.stop(
11488
- result.status === "up_to_date" ? `${branch} is already up to date at ${tipOid.slice(0, 10)}.` : result.status === "committed" ? `Pushed ${branch} \u2192 ${tipOid.slice(0, 10)}.` : `Push did not complete.`
11489
- );
11490
11908
  const ok2 = result.status === "up_to_date" || result.status === "committed";
11491
- if (ok2) {
11909
+ if (result.status === "committed") {
11492
11910
  updateRef(appDir, spacePrivateRef(`pushed/${branch}`), tipOid);
11493
11911
  }
11494
11912
  const recoverable = isRecoverablePushFailure(result.status);
11495
11913
  const rejectReason = result.reason ?? result.summary;
11496
- const errorMsg = ok2 ? null : result.status === "ref_conflict" ? `The cloud repo's ${branch} moved while you worked (${rejectReason}). Run \`deepspace pull --app ${shQuote(appId)} --branch ${shQuote(branch)}\` to integrate, then push again.` : result.status === "rejected" ? pushFailureMessage(`The cloud repo rejected ${branch}`, result, appDir ?? void 0) : `The cloud repo's ${branch} has commit(s) your local ${branch} doesn't. Run \`deepspace pull --app ${shQuote(appId)} --branch ${shQuote(branch)}\` and merge (or rebase), then push. Avoid --force here \u2014 it's guarded and will refuse rather than silently drop a peer's commits.`;
11497
- if (!args.json && ok2) p18.log.info(`App: ${appId}`);
11498
- const action = recoverable ? pullRecoveryAction(pullRecoveryCwd, appId, branch) : void 0;
11914
+ const rejectedFastForward = !ok2 && result.status === "non_fast_forward" && !force;
11915
+ if (rejectedFastForward) spinner2?.message(`Checking the cloud ${branch} tip\u2026`);
11916
+ const remoteTip = rejectedFastForward ? fetchRemoteTip(appDir, token, branch) : null;
11917
+ const ownRewrite = remoteTip !== null && remoteTip === resolveCommit(appDir, spacePrivateRef(`pushed/${branch}`));
11918
+ const strictlyBehind = remoteTip !== null && !ownRewrite && isAncestor(appDir, tipOid, remoteTip);
11919
+ spinner2?.stop(
11920
+ result.status === "up_to_date" ? `${branch} is already up to date at ${tipOid.slice(0, 10)}.` : result.status === "committed" ? `Pushed ${branch} \u2192 ${tipOid.slice(0, 10)}.` : `Push did not complete.`
11921
+ );
11922
+ const pullNext = `Run \`deepspace pull --app ${shQuote(appId)} --branch ${shQuote(branch)}\` and merge (or rebase onto it), then push.`;
11923
+ const errorMsg = ok2 ? null : result.status === "ref_conflict" ? (
11924
+ // The machine token belongs in `code`, not mid-sentence: the reason
11925
+ // arrives as `<code>: <sentence>`, and printing it raw put
11926
+ // "stale_ref: stale ref, fetch first" inside human prose.
11927
+ `The cloud repo's ${branch} moved while you worked (${parseRefusalCode(rejectReason)?.sentence ?? rejectReason}). Run \`deepspace pull --app ${shQuote(appId)} --branch ${shQuote(branch)}\` to integrate, then push again.`
11928
+ ) : result.status === "rejected" ? pushFailureMessage(`The push of ${branch}`, result, appDir ?? void 0) : ownRewrite ? `The cloud repo's ${branch} is exactly the tip THIS checkout last pushed, and your local ${branch} no longer contains it \u2014 you rewrote your own published line (amend/rebase). Publish the rewrite with \`deepspace push --force --branch ${shQuote(branch)}\`: the guard allows it because it discards only your own superseded tip. (Pulling instead would merge the commit you just amended away back in.)` : strictlyBehind ? `The cloud repo's ${branch} is at ${remoteTip.slice(0, 10)} and already contains everything this checkout has \u2014 this checkout is just behind, with nothing of its own to publish. Run \`deepspace pull --app ${shQuote(appId)} --branch ${shQuote(branch)}\` to fast-forward, then push again if anything is left.` : `The cloud repo's ${branch} has commit(s) your local ${branch} doesn't. ${pullNext} Avoid --force here \u2014 it's guarded and will refuse rather than silently drop a peer's commits.`;
11929
+ if (!args.json && ok2) p19.log.info(`App: ${appId}`);
11930
+ const action = ownRewrite ? {
11931
+ cwd: pullRecoveryCwd,
11932
+ argv: ["deepspace", "push", "--force", "--app", appId, "--branch", branch]
11933
+ } : recoverable ? pullRecoveryAction(pullRecoveryCwd, appId, branch) : void 0;
11499
11934
  if (!ok2) {
11500
- throw new Refusal(errorMsg ?? `Push did not complete.`, result.status, {
11935
+ const code = result.status === "rejected" ? classifyRejection(rejectReason, appDir ?? void 0).code : strictlyBehind ? "behind" : result.status;
11936
+ throw new Refusal(errorMsg ?? `Push did not complete.`, code, {
11501
11937
  actionRequired: recoverable,
11502
11938
  action,
11503
11939
  extra: { status: result.status, appId, branch, oid: tipOid }
@@ -11508,7 +11944,9 @@ var push_default = defineDeepspaceCommand({
11508
11944
  if (err instanceof Refusal) throw err;
11509
11945
  const transportFailure = classifyPushTransportFailure(err, appDir ?? void 0);
11510
11946
  if (transportFailure) {
11511
- throw new Refusal(transportFailure.error, transportFailure.code);
11947
+ throw new Refusal(transportFailure.error, transportFailure.code, {
11948
+ extra: { gitError: err instanceof Error ? err.message : String(err) }
11949
+ });
11512
11950
  }
11513
11951
  throw err;
11514
11952
  }
@@ -11516,7 +11954,7 @@ var push_default = defineDeepspaceCommand({
11516
11954
  });
11517
11955
 
11518
11956
  // src/cli/commands/pull.ts
11519
- import * as p19 from "@clack/prompts";
11957
+ import * as p20 from "@clack/prompts";
11520
11958
  import { realpathSync as realpathSync5 } from "fs";
11521
11959
  function targetedVcAction(command, cwd, appId, branch) {
11522
11960
  return {
@@ -11610,7 +12048,7 @@ var pull_default = defineDeepspaceCommand({
11610
12048
  const appId = await resolveAppTarget(deployUrl, token, appArg);
11611
12049
  spinner2?.message(`Checking the cloud ${branch} ref\u2026`);
11612
12050
  const remote = await repoApi(deployUrl, token, appId).getRefs();
11613
- ensureSpaceRemote(appDir, appId);
12051
+ ensureSpaceRemote(appDir, appId, void 0, token);
11614
12052
  if (!remote) {
11615
12053
  spinner2?.stop("No cloud repo yet.");
11616
12054
  const pushCommand2 = targetedVcCommand("push", appId, branch);
@@ -11778,7 +12216,7 @@ var pull_default = defineDeepspaceCommand({
11778
12216
  const actionRequired = recoveryAction !== void 0;
11779
12217
  spinner2?.stop(codeForState ? `Fetched ${SPACE_REMOTE}/${branch}.` : line);
11780
12218
  if (!args.json)
11781
- p19.log.info(`App: ${appId} \xB7 ${SPACE_REMOTE}/${branch} at ${remoteOid.slice(0, 10)}`);
12219
+ p20.log.info(`App: ${appId} \xB7 ${SPACE_REMOTE}/${branch} at ${remoteOid.slice(0, 10)}`);
11782
12220
  if (codeForState) {
11783
12221
  throw new Refusal(line, codeForState, {
11784
12222
  actionRequired,
@@ -11797,15 +12235,15 @@ var pull_default = defineDeepspaceCommand({
11797
12235
  });
11798
12236
 
11799
12237
  // src/cli/commands/clone.ts
11800
- import * as p20 from "@clack/prompts";
11801
- import { existsSync as existsSync25, readdirSync as readdirSync8, rmSync as rmSync6 } from "fs";
12238
+ import * as p21 from "@clack/prompts";
12239
+ import { existsSync as existsSync26, readdirSync as readdirSync8, rmSync as rmSync6 } from "fs";
11802
12240
  import { resolve as resolve19, basename as basename5, join as join25 } from "path";
11803
12241
  function cleanFailedCloneDir(dir) {
11804
12242
  let entries;
11805
12243
  try {
11806
12244
  entries = readdirSync8(dir);
11807
12245
  } catch {
11808
- return existsSync25(dir) ? ["<unreadable>"] : [];
12246
+ return existsSync26(dir) ? ["<unreadable>"] : [];
11809
12247
  }
11810
12248
  if (entries.length === 1 && entries[0] === ".git") {
11811
12249
  try {
@@ -11818,7 +12256,7 @@ function cleanFailedCloneDir(dir) {
11818
12256
  return entries;
11819
12257
  }
11820
12258
  function validateCloneTarget(dir) {
11821
- if (!existsSync25(dir)) return false;
12259
+ if (!existsSync26(dir)) return false;
11822
12260
  let entries;
11823
12261
  try {
11824
12262
  entries = readdirSync8(dir);
@@ -11892,15 +12330,14 @@ var clone_default = defineDeepspaceCommand({
11892
12330
  }
11893
12331
  throw err;
11894
12332
  }
11895
- ensureSpaceRemote(dir, appId);
11896
- ensureGitIdentity(dir, token);
12333
+ ensureSpaceRemote(dir, appId, void 0, token);
11897
12334
  const head = resolveCommit(dir, "HEAD");
11898
12335
  const branch = currentBranch(dir);
11899
12336
  spinner2?.stop(
11900
12337
  `Cloned ${selector} (${branches.length} ${branches.length === 1 ? "branch" : "branches"}) into ${basename5(dir)}/ at ${head?.slice(0, 10) ?? "(unborn)"}.`
11901
12338
  );
11902
12339
  if (!args.json && !branch) {
11903
- p20.log.warn("HEAD is detached \u2014 check `git branch -a`.");
12340
+ p21.log.warn("HEAD is detached \u2014 check `git branch -a`.");
11904
12341
  }
11905
12342
  return {
11906
12343
  data: {
@@ -11919,7 +12356,7 @@ var clone_default = defineDeepspaceCommand({
11919
12356
  });
11920
12357
 
11921
12358
  // src/cli/commands/releases.ts
11922
- import * as p21 from "@clack/prompts";
12359
+ import * as p22 from "@clack/prompts";
11923
12360
 
11924
12361
  // src/cli/lib/actor-labels.ts
11925
12362
  var API_URL9 = process.env.DEEPSPACE_API_URL ?? PLATFORM_URLS.api;
@@ -11981,7 +12418,7 @@ var releases_default = defineDeepspaceCommand({
11981
12418
  spinner2?.stop(`Loaded ${releases.length} ${releases.length === 1 ? "release" : "releases"}.`);
11982
12419
  if (!args.json) {
11983
12420
  if (releases.length === 0) {
11984
- p21.log.info("No releases yet \u2014 the history starts with the next `deepspace deploy`.");
12421
+ p22.log.info("No releases yet \u2014 the history starts with the next `deepspace deploy`.");
11985
12422
  } else {
11986
12423
  const actors = await actorLabels(token, appId);
11987
12424
  for (const r of releases) {
@@ -11991,7 +12428,7 @@ var releases_default = defineDeepspaceCommand({
11991
12428
  `#${r.seq} ${r.id} ${r.kind.padEnd(8)} ${r.createdAt} ${actors.get(r.actor) ?? r.actor} ${source} ${rollback}`
11992
12429
  );
11993
12430
  }
11994
- p21.log.info(
12431
+ p22.log.info(
11995
12432
  `Roll back an available release with: deepspace rollback <release-id>${wranglerEnv ? ` --env ${wranglerEnv}` : ""}`
11996
12433
  );
11997
12434
  }
@@ -12001,7 +12438,7 @@ var releases_default = defineDeepspaceCommand({
12001
12438
  });
12002
12439
 
12003
12440
  // src/cli/commands/rollback.ts
12004
- import * as p22 from "@clack/prompts";
12441
+ import * as p23 from "@clack/prompts";
12005
12442
  var REL_ID_RE = /^rel_[0-9A-HJKMNP-TV-Z]{26}$/;
12006
12443
  function unavailableDoGuardRefusal(message) {
12007
12444
  return new Refusal(
@@ -12122,20 +12559,20 @@ Re-run with --allow-do-deletion to accept the data loss.`,
12122
12559
  const doClassGuard = result.doClassGuard === "unverified" ? "unverified" : void 0;
12123
12560
  if (!args.json) {
12124
12561
  if (result.url) {
12125
- if (serving === "unconfirmed") p22.log.warn(`URL: ${result.url} (accepted; serving not yet verified)`);
12126
- else p22.log.success(`Live at: ${result.url}`);
12562
+ if (serving === "unconfirmed") p23.log.warn(`URL: ${result.url} (accepted; serving not yet verified)`);
12563
+ else p23.log.success(`Live at: ${result.url}`);
12127
12564
  }
12128
12565
  if (result.bundleRetained === false) {
12129
- p22.log.warn(
12566
+ p23.log.warn(
12130
12567
  "The rollback is live, but its bundle was concurrently retired and is not available for a future rollback."
12131
12568
  );
12132
12569
  }
12133
12570
  if (doClassGuard === "unverified") {
12134
- p22.log.warn(
12571
+ p23.log.warn(
12135
12572
  "DO-class guard bypassed (--allow-do-deletion): this rollback was NOT checked against the live Durable Object classes \u2014 any class the target doesn't declare has been deleted with its data."
12136
12573
  );
12137
12574
  }
12138
- p22.log.info(
12575
+ p23.log.info(
12139
12576
  "This rollback is itself the newest release \u2014 `deepspace releases` shows the full history."
12140
12577
  );
12141
12578
  }
@@ -12160,24 +12597,24 @@ Re-run with --allow-do-deletion to accept the data loss.`,
12160
12597
  import { defineCommand as defineCommand12 } from "citty";
12161
12598
 
12162
12599
  // src/cli/commands/workspace/new.ts
12163
- import * as p25 from "@clack/prompts";
12600
+ import * as p26 from "@clack/prompts";
12164
12601
  import { isAbsolute as isAbsolute7, resolve as resolve20 } from "path";
12165
12602
 
12166
12603
  // src/cli/commands/workspace/analysis.ts
12167
- import * as p23 from "@clack/prompts";
12604
+ import * as p24 from "@clack/prompts";
12168
12605
  var MAX_OVERLAP_PATHS = 20;
12169
12606
  var PEER_REF_PREFIX = `${spacePrivateRef("peers")}/`;
12607
+ function workspaceSyncRelation(dir, headOid, publishedTip) {
12608
+ if (dir === null || headOid === null || publishedTip === null) return "unknown";
12609
+ if (headOid === publishedTip) return "in_sync";
12610
+ if (resolveCommit(dir, publishedTip) === null) return "unknown";
12611
+ if (isAncestor(dir, headOid, publishedTip)) return "behind";
12612
+ if (isAncestor(dir, publishedTip, headOid)) return "ahead";
12613
+ return "diverged";
12614
+ }
12170
12615
  function peerWorkspaceRef(workspaceId) {
12171
12616
  return `${PEER_REF_PREFIX}${workspaceId}`;
12172
12617
  }
12173
- function workspaceSyncRelation(dir, localOid, publishedOid) {
12174
- if (localOid !== null && localOid === publishedOid) return "in_sync";
12175
- if (localOid === null || publishedOid === null) return "unknown";
12176
- if (resolveCommit(dir, publishedOid) === null) return "unknown";
12177
- if (isAncestor(dir, localOid, publishedOid)) return "behind";
12178
- if (isAncestor(dir, publishedOid, localOid)) return "ahead";
12179
- return "diverged";
12180
- }
12181
12618
  function formatCount(count) {
12182
12619
  if (!count) return "?";
12183
12620
  return count.capped ? `${count.count}+` : String(count.count);
@@ -12286,16 +12723,16 @@ function printOverlaps(overlaps, json) {
12286
12723
  for (const overlap of overlaps) {
12287
12724
  const sample = overlap.paths.slice(0, 5).join(", ");
12288
12725
  const more = overlap.paths.length - 5 + overlap.morePaths;
12289
- p23.log.warn(
12726
+ p24.log.warn(
12290
12727
  `Overlap: ${overlap.workspaceId} ("${overlap.task}") also modified ${sample}${more > 0 ? ` (+${more} more)` : ""} \u2014 coordinate before landing.`
12291
12728
  );
12292
12729
  }
12293
12730
  }
12294
12731
 
12295
12732
  // src/cli/commands/workspace/local.ts
12296
- import * as p24 from "@clack/prompts";
12733
+ import * as p25 from "@clack/prompts";
12297
12734
  import {
12298
- existsSync as existsSync26,
12735
+ existsSync as existsSync27,
12299
12736
  mkdirSync as mkdirSync8,
12300
12737
  readFileSync as readFileSync24,
12301
12738
  realpathSync as realpathSync6,
@@ -12375,11 +12812,6 @@ function appDirInWorktree(appDir, worktreeRoot) {
12375
12812
  const appRelative = relative4(realpathSync6(repoToplevel(appDir)), realpathSync6(appDir));
12376
12813
  return appRelative ? join26(worktreeRoot, appRelative) : worktreeRoot;
12377
12814
  }
12378
- function primaryAppDir(appDir) {
12379
- const primary = listWorktrees(appDir)[0];
12380
- if (!primary) throw new Error("Git did not report a primary worktree");
12381
- return appDirInWorktree(appDir, primary.path);
12382
- }
12383
12815
  function materializeWorkspaceWorktree(appDir, worktreeRoot, branch, startPoint, workspaceId, options = {}) {
12384
12816
  const createdOid = resolveCommit(appDir, startPoint);
12385
12817
  if (!createdOid) throw new Error(`Could not resolve workspace start point ${startPoint}`);
@@ -12533,7 +12965,7 @@ function cleanupWorkspaceLocal(appDir, id, trunkBranch, options = {}) {
12533
12965
  }
12534
12966
  runGit(mainDir, ["worktree", "prune"], { allowFail: true });
12535
12967
  out.worktreeDir = null;
12536
- const leftover = existsSync26(linked.path);
12968
+ const leftover = existsSync27(linked.path);
12537
12969
  if (leftover) out.leftoverDir = linked.path;
12538
12970
  else out.worktreeRemoved = linked.path;
12539
12971
  deleteWsBranch(mainDir, branch, expectedBranchOid, out);
@@ -12548,6 +12980,7 @@ function cleanupWorkspaceLocal(appDir, id, trunkBranch, options = {}) {
12548
12980
  deleteWsBranch(mainDir, branch, expectedBranchOid, out);
12549
12981
  return out;
12550
12982
  }
12983
+ runGit(mainDir, ["worktree", "prune"], { allowFail: true });
12551
12984
  if (currentBranch(mainDir) === branch && !switchOffWsBranch(mainDir, trunkBranch)) {
12552
12985
  out.error = `could not switch off ${branch} to delete it`;
12553
12986
  return out;
@@ -12579,7 +13012,7 @@ function cleanupAction(outcome) {
12579
13012
  }
12580
13013
  function reportCleanupHuman(outcome) {
12581
13014
  if (outcome.error) {
12582
- p24.log.warn(
13015
+ p25.log.warn(
12583
13016
  cleanupRefusalMessage({
12584
13017
  error: outcome.error,
12585
13018
  worktreeDir: outcome.worktreeDir,
@@ -12593,12 +13026,12 @@ function reportCleanupHuman(outcome) {
12593
13026
  outcome.worktreeRemoved ? "worktree" : null,
12594
13027
  outcome.branchDeleted ? `branch ${outcome.branch}` : null
12595
13028
  ].filter(Boolean);
12596
- if (parts.length) p24.log.success(`Removed ${parts.join(" + ")}.`);
13029
+ if (parts.length) p25.log.success(`Removed ${parts.join(" + ")}.`);
12597
13030
  if (outcome.worktreeRetained) {
12598
- p24.log.info(`Retained externally managed worktree: ${outcome.worktreeRetained}`);
13031
+ p25.log.info(`Retained externally managed worktree: ${outcome.worktreeRetained}`);
12599
13032
  }
12600
13033
  if (outcome.worktreeRemoved && outcome.mainDir) {
12601
- p24.log.info(`This directory was removed \u2014 run: cd ${shQuote(outcome.mainDir)}`);
13034
+ p25.log.info(`This directory was removed \u2014 run: cd ${shQuote(outcome.mainDir)}`);
12602
13035
  }
12603
13036
  }
12604
13037
 
@@ -12634,6 +13067,15 @@ async function resolveApiOnly(appArg) {
12634
13067
  function inferWorkspaceId(appDir, explicit) {
12635
13068
  const id = explicit?.trim() || workspaceIdFromBranch(currentBranch(appDir));
12636
13069
  if (!id) {
13070
+ const interrupted = currentBranch(appDir) === null ? interruptedGitOperation(appDir) : null;
13071
+ if (interrupted) {
13072
+ const finish = interrupted === "bisect" ? "end it (`git bisect reset`)" : `finish it (\`git ${interrupted} --continue\`) or abandon it (\`git ${interrupted} --abort\`)`;
13073
+ throw new Refusal(
13074
+ `A ${interrupted} is in progress here, so HEAD is detached and the branch no longer names a workspace. ${finish[0].toUpperCase()}${finish.slice(1)}, then re-run \u2014 the workspace is unchanged either way.`,
13075
+ "git_operation_in_progress",
13076
+ { extra: { operation: interrupted } }
13077
+ );
13078
+ }
12637
13079
  throw new Refusal(
12638
13080
  'Not inside a workspace worktree (the branch is not ws/<id>). Select one with `-w ws_\u2026` (for `workspace drop`, pass the id as the argument), or create one with `deepspace workspace new -t "\u2026"`.',
12639
13081
  "not_in_workspace"
@@ -12679,17 +13121,30 @@ function assertExplicitWorkspaceId(explicit) {
12679
13121
  );
12680
13122
  }
12681
13123
  }
12682
- function pushWorkspaceRef(appDir, token, ref, headOid) {
13124
+ function pushWorkspaceRef(appDir, token, ref, headOid, publishedTip) {
12683
13125
  const secret = committedSecretRefusal(appDir, headOid, {
12684
13126
  action: "publish this workspace",
12685
- then: "re-run the command"
13127
+ then: "re-run the command",
13128
+ // What the cloud already has for this ref, so the scan covers every commit
13129
+ // this publish would upload — which is what the server scans.
13130
+ base: publishedTip ?? null
12686
13131
  });
12687
13132
  if (secret) throw new Refusal(secret.message, secret.code);
12688
13133
  const push = pushToSpace(appDir, token, `${headOid}:${ref}`);
12689
13134
  if (push.status === "committed" || push.status === "up_to_date") return;
12690
13135
  if (push.status === "ref_conflict" || push.status === "non_fast_forward") {
13136
+ const conflicted = unmergedIndexRefusal(appDir, {
13137
+ ours: "The merge this workspace's last pull started",
13138
+ resume: "the command"
13139
+ });
13140
+ if (conflicted) {
13141
+ throw new Refusal(conflicted.message, conflicted.code, {
13142
+ extra: { conflict: true, operation: conflicted.operation }
13143
+ });
13144
+ }
13145
+ const strictlyBehind = typeof publishedTip === "string" && resolveCommit(appDir, publishedTip) !== null && isAncestor(appDir, headOid, publishedTip);
12691
13146
  throw new Refusal(
12692
- `Another checkout advanced this workspace's line, so publishing yours is not a fast-forward \u2014 the push was refused rather than drop that work. Integrate its tip (\`git pull --no-rebase ${SPACE_REMOTE} ${ref}\`, or re-attach the workspace in a fresh dir), resolve any conflicts, then re-run the command. (Amended or rebased your own commits? Same refusal \u2014 merge your old tip back in.)`,
13147
+ strictlyBehind ? `Another checkout already synced ahead of this one \u2014 nothing of yours is unpublished; this checkout is just behind. Fast-forward it (\`git pull --no-rebase ${SPACE_REMOTE} ${ref}\`), then re-run the command.` : `Another checkout advanced this workspace's line, so publishing yours is not a fast-forward \u2014 the push was refused rather than drop that work. Integrate its tip (\`git pull --no-rebase ${SPACE_REMOTE} ${ref}\`, or re-attach the workspace in a fresh dir), resolve any conflicts, then re-run the command. (Amended or rebased your own commits? Same refusal \u2014 merge your old tip back in.)`,
12693
13148
  push.status,
12694
13149
  {
12695
13150
  actionRequired: true,
@@ -12697,7 +13152,8 @@ function pushWorkspaceRef(appDir, token, ref, headOid) {
12697
13152
  }
12698
13153
  );
12699
13154
  }
12700
- throw new Refusal(pushFailureMessage("Workspace upload", push, appDir), push.status);
13155
+ const code = push.status === "rejected" ? classifyRejection(push.reason ?? push.summary ?? "", appDir).code : push.status;
13156
+ throw new Refusal(pushFailureMessage("Workspace upload", push, appDir), code);
12701
13157
  }
12702
13158
 
12703
13159
  // src/cli/commands/workspace/new.ts
@@ -12748,8 +13204,7 @@ var newWorkspaceCommand = defineDeepspaceCommand({
12748
13204
  typeof args.app === "string" ? args.app : void 0
12749
13205
  );
12750
13206
  spinner2?.message("Creating workspace\u2026");
12751
- ensureSpaceRemote(appDir, appId);
12752
- ensureGitIdentity(appDir, token);
13207
+ ensureSpaceRemote(appDir, appId, void 0, token);
12753
13208
  let baseOid = null;
12754
13209
  const refs = await api4.getRefs();
12755
13210
  if (baseArg) {
@@ -12808,8 +13263,8 @@ var newWorkspaceCommand = defineDeepspaceCommand({
12808
13263
  }
12809
13264
  spinner2?.stop(`Workspace ${id} created.`);
12810
13265
  if (!args.json) {
12811
- p25.log.info(`Work in: cd ${dir}`);
12812
- p25.log.info(
13266
+ p26.log.info(`Work in: cd ${dir}`);
13267
+ p26.log.info(
12813
13268
  `Then: commit as usual, \`deepspace workspace sync\` to publish, \`deepspace workspace land\` when done.`
12814
13269
  );
12815
13270
  }
@@ -12830,15 +13285,15 @@ var newWorkspaceCommand = defineDeepspaceCommand({
12830
13285
  });
12831
13286
 
12832
13287
  // src/cli/commands/workspace/attach.ts
12833
- import * as p26 from "@clack/prompts";
12834
- import { existsSync as existsSync27, readdirSync as readdirSync9, realpathSync as realpathSync7, rmSync as rmSync7 } from "fs";
13288
+ import * as p27 from "@clack/prompts";
13289
+ import { existsSync as existsSync28, readdirSync as readdirSync9, realpathSync as realpathSync7, rmSync as rmSync7 } from "fs";
12835
13290
  import { isAbsolute as isAbsolute8, resolve as resolve21 } from "path";
12836
13291
  function samePath(a, b) {
12837
- const real = (p35) => {
13292
+ const real = (p36) => {
12838
13293
  try {
12839
- return realpathSync7(p35);
13294
+ return realpathSync7(p36);
12840
13295
  } catch {
12841
- return resolve21(p35);
13296
+ return resolve21(p36);
12842
13297
  }
12843
13298
  };
12844
13299
  return real(a) === real(b);
@@ -12851,7 +13306,7 @@ function finishedWorkspaceMessage(id, view) {
12851
13306
  return `Workspace ${id} is ${workspace.status} \u2014 its ref is retained briefly (fetch ${workspace.ref} to inspect), but attach only works on active workspaces.`;
12852
13307
  }
12853
13308
  function cleanFailedFreshAttachDir(dir) {
12854
- if (!existsSync27(dir)) return [];
13309
+ if (!existsSync28(dir)) return [];
12855
13310
  let entries;
12856
13311
  try {
12857
13312
  entries = readdirSync9(dir);
@@ -12910,8 +13365,7 @@ var attachWorkspaceCommand = defineDeepspaceCommand({
12910
13365
  extra: { status: view2.workspace.status }
12911
13366
  });
12912
13367
  }
12913
- ensureSpaceRemote(appDir, appId2);
12914
- ensureGitIdentity(appDir, token2);
13368
+ ensureSpaceRemote(appDir, appId2, void 0, token2);
12915
13369
  const localWorkspaceRef = spacePrivateRef(`workspaces/${id}`);
12916
13370
  runGitRemote(appDir, token2, [
12917
13371
  "fetch",
@@ -12933,7 +13387,7 @@ var attachWorkspaceCommand = defineDeepspaceCommand({
12933
13387
  const localTip = resolveCommit(appDir, `refs/heads/${branch}`);
12934
13388
  if (localTip) {
12935
13389
  let existing = resolveWorkspaceWorktree(listWorktrees(appDir), id);
12936
- if (existing && !existsSync27(existing)) {
13390
+ if (existing && !existsSync28(existing)) {
12937
13391
  runGit(appDir, ["worktree", "prune"], { allowFail: true });
12938
13392
  existing = null;
12939
13393
  }
@@ -12946,10 +13400,10 @@ var attachWorkspaceCommand = defineDeepspaceCommand({
12946
13400
  const existingDir = appDirInWorktree(appDir, existing);
12947
13401
  spinner2?.stop(`Already attached: ${id}.`);
12948
13402
  if (!args.json) {
12949
- p26.log.info(`Task: ${view2.workspace.task}`);
12950
- p26.log.info(`Work in: cd ${existingDir}`);
13403
+ p27.log.info(`Task: ${view2.workspace.task}`);
13404
+ p27.log.info(`Work in: cd ${existingDir}`);
12951
13405
  if (!inSync) {
12952
- p26.log.warn(
13406
+ p27.log.warn(
12953
13407
  `The checkout is at ${localTip.slice(0, 10)} but the workspace's published tip is ${tip2.slice(0, 10)} \u2014 ${syncAdvice}.`
12954
13408
  );
12955
13409
  }
@@ -12981,10 +13435,10 @@ var attachWorkspaceCommand = defineDeepspaceCommand({
12981
13435
  }
12982
13436
  spinner2?.stop(`Re-attached ${id}.`);
12983
13437
  if (!args.json) {
12984
- p26.log.info(`Task: ${view2.workspace.task}`);
12985
- p26.log.info(`Work in: cd ${dir2}`);
13438
+ p27.log.info(`Task: ${view2.workspace.task}`);
13439
+ p27.log.info(`Work in: cd ${dir2}`);
12986
13440
  if (!inSync) {
12987
- p26.log.warn(
13441
+ p27.log.warn(
12988
13442
  `The re-materialized checkout is at ${localTip.slice(0, 10)} but the workspace's published tip is ${tip2.slice(0, 10)} \u2014 after installing, ${syncAdvice}.`
12989
13443
  );
12990
13444
  }
@@ -13015,8 +13469,8 @@ var attachWorkspaceCommand = defineDeepspaceCommand({
13015
13469
  materializeWorkspaceWorktree(appDir, worktreeRoot, branch, tip2, id);
13016
13470
  spinner2?.stop(`Attached ${id}.`);
13017
13471
  if (!args.json) {
13018
- p26.log.info(`Task: ${view2.workspace.task}`);
13019
- p26.log.info(`Work in: cd ${dir2}`);
13472
+ p27.log.info(`Task: ${view2.workspace.task}`);
13473
+ p27.log.info(`Work in: cd ${dir2}`);
13020
13474
  }
13021
13475
  const action2 = { cwd: dir2, argv: [detectPackageManager(dir2), "install"] };
13022
13476
  return {
@@ -13051,7 +13505,7 @@ var attachWorkspaceCommand = defineDeepspaceCommand({
13051
13505
  process.cwd(),
13052
13506
  (typeof args.dir === "string" ? args.dir.trim() : "") || `ws-${id.slice(3).toLowerCase()}`
13053
13507
  );
13054
- if (existsSync27(dir)) {
13508
+ if (existsSync28(dir)) {
13055
13509
  spinner2?.stop("Target exists.");
13056
13510
  throw new Refusal(`${dir} already exists \u2014 pass a different directory.`, "dir_exists");
13057
13511
  }
@@ -13065,8 +13519,7 @@ var attachWorkspaceCommand = defineDeepspaceCommand({
13065
13519
  let tip;
13066
13520
  try {
13067
13521
  runGit(process.cwd(), ["init", "--quiet", dir]);
13068
- ensureSpaceRemote(dir, appId);
13069
- ensureGitIdentity(dir, token);
13522
+ ensureSpaceRemote(dir, appId, void 0, token);
13070
13523
  const localWorkspaceRef = spacePrivateRef(`workspaces/${id}`);
13071
13524
  runGitRemote(dir, token, [
13072
13525
  "fetch",
@@ -13093,8 +13546,8 @@ var attachWorkspaceCommand = defineDeepspaceCommand({
13093
13546
  spinner2?.stop(`Attached ${id}.`);
13094
13547
  const action = { cwd: dir, argv: [detectPackageManager(dir), "install"] };
13095
13548
  if (!args.json) {
13096
- p26.log.info(`Task: ${view.workspace.task}`);
13097
- p26.log.info("Pull app secrets after install if this workspace needs them.");
13549
+ p27.log.info(`Task: ${view.workspace.task}`);
13550
+ p27.log.info("Pull app secrets after install if this workspace needs them.");
13098
13551
  }
13099
13552
  return {
13100
13553
  data: {
@@ -13113,7 +13566,7 @@ var attachWorkspaceCommand = defineDeepspaceCommand({
13113
13566
  });
13114
13567
 
13115
13568
  // src/cli/commands/workspace/sync.ts
13116
- import * as p27 from "@clack/prompts";
13569
+ import * as p28 from "@clack/prompts";
13117
13570
  var syncWorkspaceCommand = defineDeepspaceCommand({
13118
13571
  meta: {
13119
13572
  name: "sync",
@@ -13161,15 +13614,13 @@ var syncWorkspaceCommand = defineDeepspaceCommand({
13161
13614
  ]);
13162
13615
  if (before.workspace.status !== "active") {
13163
13616
  spinner2?.stop("Workspace finished.");
13164
- throw new Refusal(
13165
- `Workspace ${id} is ${before.workspace.status} \u2014 create a new workspace to continue this line of work.`,
13166
- "workspace_not_active",
13167
- { extra: { status: before.workspace.status } }
13168
- );
13617
+ throw workspaceNotActiveRefusal(id, before.workspace.status, {
13618
+ cwd: appDir,
13619
+ argv: ["deepspace", "workspace", "drop", id, "--app", appId]
13620
+ });
13169
13621
  }
13170
- ensureSpaceRemote(appDir, appId);
13171
- ensureGitIdentity(appDir, token);
13172
- pushWorkspaceRef(appDir, token, before.workspace.ref, headOid);
13622
+ ensureSpaceRemote(appDir, appId, void 0, token);
13623
+ pushWorkspaceRef(appDir, token, before.workspace.ref, headOid, before.tipOid);
13173
13624
  try {
13174
13625
  fetchTrunk(appDir, token, refs?.head ?? null);
13175
13626
  } catch {
@@ -13183,16 +13634,16 @@ var syncWorkspaceCommand = defineDeepspaceCommand({
13183
13634
  const view = upToDate ? before : (await api4.syncWorkspace(id, { headOid })).view;
13184
13635
  const dirty = statusFiles(appDir);
13185
13636
  spinner2?.stop(
13186
- upToDate ? `Already synced \u2014 ${id} is up to date at ${headOid.slice(0, 10)}.` : changedPaths !== void 0 ? `Synced ${id} \u2192 ${headOid.slice(0, 10)} (${changedPaths.length} ${changedPaths.length === 1 ? "file" : "files"} vs base).` : `Synced ${id} \u2192 ${headOid.slice(0, 10)} (base not present locally \u2014 no overlap report).`
13637
+ upToDate ? `Already synced \u2014 ${id} is up to date at ${headOid.slice(0, 10)}${dirty.length > 0 ? ` (${dirty.length} uncommitted file(s) remain unpublished)` : ""}.` : changedPaths !== void 0 ? `Synced ${id} \u2192 ${headOid.slice(0, 10)} (${changedPaths.length} ${changedPaths.length === 1 ? "file" : "files"} vs base).` : `Synced ${id} \u2192 ${headOid.slice(0, 10)} (base not present locally \u2014 no overlap report).`
13187
13638
  );
13188
13639
  printOverlaps(overlaps, args.json);
13189
13640
  if (!args.json && trunkOverlap.length > 0) {
13190
- p27.log.warn(
13641
+ p28.log.warn(
13191
13642
  `Trunk already changed ${trunkOverlap.length} file(s) you also touch \u2014 landed work you haven't merged: ${trunkOverlap.slice(0, 5).join(", ")}${trunkOverlap.length > 5 ? ` (+${trunkOverlap.length - 5} more)` : ""}. Merge trunk before landing (\`deepspace workspace status\` shows the full picture).`
13192
13643
  );
13193
13644
  }
13194
13645
  if (!args.json && dirty.length > 0) {
13195
- p27.log.warn(
13646
+ p28.log.warn(
13196
13647
  `${dirty.length} uncommitted ${dirty.length === 1 ? "file" : "files"} NOT synced \u2014 commit them to this workspace branch (WIP commits are fine \u2014 they land with the merge).`
13197
13648
  );
13198
13649
  }
@@ -13212,7 +13663,7 @@ var syncWorkspaceCommand = defineDeepspaceCommand({
13212
13663
  });
13213
13664
 
13214
13665
  // src/cli/commands/workspace/list.ts
13215
- import * as p28 from "@clack/prompts";
13666
+ import * as p29 from "@clack/prompts";
13216
13667
  function withWorkspaceOverlaps(views, overlapsById) {
13217
13668
  return views.map((view) => ({
13218
13669
  ...view,
@@ -13235,21 +13686,24 @@ var listWorkspacesCommand = defineDeepspaceCommand({
13235
13686
  typeof args.app === "string" ? args.app : void 0
13236
13687
  );
13237
13688
  const all = Boolean(args.all);
13238
- const { views } = await api4.listWorkspaces({ all, limit });
13689
+ const { views, truncated } = await api4.listWorkspaces({ all, limit });
13239
13690
  if (views.length === 0) {
13240
13691
  spinner2?.stop("No workspaces found.");
13241
13692
  if (!args.json) {
13242
- p28.log.info(
13693
+ p29.log.info(
13243
13694
  all ? "No workspaces." : 'No active workspaces. Create one with `deepspace workspace new -t "\u2026"`.'
13244
13695
  );
13245
13696
  }
13246
- return { data: { workspaces: views } };
13697
+ return { data: { workspaces: views, truncated: truncated === true } };
13247
13698
  }
13248
13699
  spinner2?.message("Checking workspace overlaps\u2026");
13249
13700
  const overlapsPromise = listOverlaps(appId, token, api4, views);
13250
13701
  if (args.json) {
13251
13702
  return {
13252
- data: { workspaces: withWorkspaceOverlaps(views, await overlapsPromise) }
13703
+ data: {
13704
+ workspaces: withWorkspaceOverlaps(views, await overlapsPromise),
13705
+ truncated: truncated === true
13706
+ }
13253
13707
  };
13254
13708
  }
13255
13709
  const [actors, overlapsById] = await Promise.all([actorLabels(token, appId), overlapsPromise]);
@@ -13268,13 +13722,18 @@ var listWorkspacesCommand = defineDeepspaceCommand({
13268
13722
  );
13269
13723
  }
13270
13724
  }
13271
- p28.log.info("\u2191 commits ahead of base \xB7 \u2193 trunk commits since base (staleness)");
13272
- return { data: { workspaces } };
13725
+ if (truncated) {
13726
+ p29.log.warn(
13727
+ `More workspaces exist than shown \u2014 this is the first ${views.length}. Raise it with \`--limit\` (up to 200); past that, narrow with \`--all\` off or land/drop finished work.`
13728
+ );
13729
+ }
13730
+ p29.log.info("\u2191 commits ahead of base \xB7 \u2193 trunk commits since base (staleness)");
13731
+ return { data: { workspaces, truncated: truncated === true } };
13273
13732
  }
13274
13733
  });
13275
13734
 
13276
13735
  // src/cli/commands/workspace/status.ts
13277
- import * as p29 from "@clack/prompts";
13736
+ import * as p30 from "@clack/prompts";
13278
13737
  var workspaceStatusCommand = defineDeepspaceCommand({
13279
13738
  meta: { name: "status", description: "This workspace: base staleness, overlaps, sync state" },
13280
13739
  args: {
@@ -13307,9 +13766,16 @@ var workspaceStatusCommand = defineDeepspaceCommand({
13307
13766
  const baseLocal = resolveCommit(appDir, workspace.baseOid) !== null;
13308
13767
  const behindExact = trunk && baseLocal ? revListCount(appDir, workspace.baseOid, trunk.oid) : null;
13309
13768
  const aheadExact = headOid && baseLocal ? revListCount(appDir, workspace.baseOid, headOid) : null;
13310
- const unsynced = headOid !== null && view.tipOid !== headOid;
13769
+ const publishedTip = view.tipOid ?? workspace.landedOid;
13770
+ if (publishedTip !== null && resolveCommit(appDir, publishedTip) === null) {
13771
+ runGitRemote(appDir, token, ["fetch", "--quiet", "--refmap=", SPACE_REMOTE, workspace.ref], {
13772
+ allowFail: true
13773
+ });
13774
+ }
13775
+ const headPublished = headOid !== null && publishedTip !== null && (publishedTip === headOid || workspaceDir !== null && resolveCommit(workspaceDir, publishedTip) !== null && isAncestor(workspaceDir, headOid, publishedTip));
13776
+ const unsynced = headOid !== null && !headPublished;
13311
13777
  const dirty = workspaceDir ? statusFiles(workspaceDir) : [];
13312
- const relation = !unsynced ? "in_sync" : workspace.status !== "active" || !workspaceDir ? "unknown" : workspaceSyncRelation(workspaceDir, headOid, view.tipOid);
13778
+ const relation = workspace.status === "active" && workspaceDir ? workspaceSyncRelation(workspaceDir, headOid, view.tipOid) : "unknown";
13313
13779
  const lines = await loadWorkspaceLines(appDir, token, api4, trunkOid);
13314
13780
  const myTip = headOid ?? resolveCommit(appDir, peerWorkspaceRef(id)) ?? view.tipOid;
13315
13781
  const line = myTip ? lineChangedPaths(appDir, workspace.baseOid, myTip, trunkOid) : null;
@@ -13339,41 +13805,39 @@ var workspaceStatusCommand = defineDeepspaceCommand({
13339
13805
  overlaps
13340
13806
  };
13341
13807
  if (!args.json) {
13342
- p29.log.info(`Task: ${workspace.task}`);
13343
- p29.log.info(
13808
+ p30.log.info(`Task: ${workspace.task}`);
13809
+ p30.log.info(
13344
13810
  `Base: ${workspace.baseOid.slice(0, 10)} \xB7 ahead ${aheadExact ?? "?"} commit(s) \xB7 trunk moved ${behindExact ?? "?"} commit(s) since your base`
13345
13811
  );
13346
13812
  if (!attached) {
13347
- p29.log.info(
13348
- `Not checked out in this clone \u2014 showing the published state (local HEAD/uncommitted unavailable). Attach it with \`deepspace workspace attach ${id} <dir>\`.`
13813
+ p30.log.info(
13814
+ workspace.status === "active" ? `Not checked out in this clone \u2014 showing the published state (local HEAD/uncommitted unavailable). Attach it with \`deepspace workspace attach ${id} <dir>\`.` : `Not checked out in this clone \u2014 showing the recorded state (the workspace is ${workspace.status}).`
13349
13815
  );
13350
13816
  }
13351
- if (unsynced) {
13352
- if (workspace.status === "landed") {
13353
- p29.log.info(
13354
- `Already landed${workspace.landedOid ? ` at ${workspace.landedOid.slice(0, 10)}` : ""} \u2014 this checkout is a leftover; \`deepspace workspace drop ${id}\` cleans it up.`
13355
- );
13356
- } else if (workspace.status === "dropped") {
13357
- p29.log.info(
13358
- `Already dropped \u2014 this checkout is a leftover; \`deepspace workspace drop ${id}\` cleans it up.`
13359
- );
13360
- } else if (relation === "behind") {
13361
- p29.log.warn(
13362
- `Local HEAD ${headOid?.slice(0, 10)} is BEHIND the published tip ${view.tipOid?.slice(0, 10)} (another checkout synced ahead) \u2014 fast-forward first (\`git pull --no-rebase ${SPACE_REMOTE} ${workspace.ref}\`).`
13363
- );
13364
- } else if (relation === "diverged") {
13365
- p29.log.warn(
13366
- `Local HEAD ${headOid?.slice(0, 10)} has DIVERGED from the published tip ${view.tipOid?.slice(0, 10)} \u2014 integrate it first (\`git pull --no-rebase ${SPACE_REMOTE} ${workspace.ref}\`), then \`deepspace workspace sync\`.`
13367
- );
13368
- } else {
13369
- p29.log.warn(
13370
- `Local HEAD ${headOid?.slice(0, 10)} is not synced (server has ${view.tipOid?.slice(0, 10) ?? "nothing"}) \u2014 run \`deepspace workspace sync\`.`
13371
- );
13372
- }
13817
+ if (attached && workspace.status === "landed") {
13818
+ p30.log.info(
13819
+ `Already landed${workspace.landedOid ? ` at ${workspace.landedOid.slice(0, 10)}` : ""} \u2014 this checkout is a leftover; \`deepspace workspace drop ${id}\` cleans it up.` + (unsynced ? " It has commits beyond the landed tip \u2014 nothing can publish them now." : "")
13820
+ );
13821
+ } else if (attached && workspace.status === "dropped") {
13822
+ p30.log.info(
13823
+ `Already dropped \u2014 this checkout is a leftover; \`deepspace workspace drop ${id}\` cleans it up.`
13824
+ );
13825
+ } else if (relation === "behind") {
13826
+ p30.log.warn(
13827
+ `Local HEAD ${headOid.slice(0, 10)} is BEHIND the published tip ${view.tipOid.slice(0, 10)} (another checkout synced ahead) \u2014 fast-forward first (\`git pull --no-rebase ${SPACE_REMOTE} ${workspace.ref}\`).`
13828
+ );
13829
+ } else if (relation === "diverged") {
13830
+ p30.log.warn(
13831
+ `Local HEAD ${headOid.slice(0, 10)} has DIVERGED from the published tip ${view.tipOid.slice(0, 10)} \u2014 integrate it first (\`git pull --no-rebase ${SPACE_REMOTE} ${workspace.ref}\`), then \`deepspace workspace sync\`.`
13832
+ );
13833
+ } else if (unsynced) {
13834
+ p30.log.warn(
13835
+ `Local HEAD ${headOid?.slice(0, 10)} is not synced (server has ${view.tipOid?.slice(0, 10) ?? "nothing"}) \u2014 run \`deepspace workspace sync\`.`
13836
+ );
13373
13837
  }
13374
- if (dirty.length > 0) p29.log.warn(`${dirty.length} uncommitted file(s) in the worktree.`);
13838
+ if (dirty.length > 0) p30.log.warn(`${dirty.length} uncommitted file(s) in the worktree.`);
13375
13839
  if (trunkOverlap.length > 0) {
13376
- p29.log.warn(
13840
+ p30.log.warn(
13377
13841
  `Trunk also modified: ${trunkOverlap.slice(0, 5).join(", ")}${trunkOverlap.length > 5 ? ` (+${trunkOverlap.length - 5} more)` : ""} \u2014 expect merge attention at land time.`
13378
13842
  );
13379
13843
  }
@@ -13384,7 +13848,7 @@ var workspaceStatusCommand = defineDeepspaceCommand({
13384
13848
  });
13385
13849
 
13386
13850
  // src/cli/commands/workspace/land.ts
13387
- import * as p30 from "@clack/prompts";
13851
+ import * as p31 from "@clack/prompts";
13388
13852
 
13389
13853
  // src/cli/lib/validation.ts
13390
13854
  import { sync as spawnSync7 } from "cross-spawn";
@@ -13432,10 +13896,27 @@ function conflictMarkerFiles(diffCheckOutput) {
13432
13896
  }
13433
13897
  return [...files];
13434
13898
  }
13435
- function pullAfterLandAction(primaryDir, intoBranch, landedOid) {
13436
- if (currentBranch(primaryDir) !== intoBranch) return null;
13437
- if (resolveCommit(primaryDir, "HEAD") === landedOid) return null;
13438
- return executableAction({ cwd: primaryDir, argv: ["deepspace", "pull"] });
13899
+ function staleTrunkPullAction(trunkCheckoutDir, intoBranch, appArg) {
13900
+ return executableAction({
13901
+ cwd: trunkCheckoutDir,
13902
+ argv: [
13903
+ "deepspace",
13904
+ "pull",
13905
+ "-b",
13906
+ intoBranch,
13907
+ // The trunk checkout is a DIFFERENT directory; carry `-a` when the
13908
+ // caller named an app, or the action could resolve a different one
13909
+ // from that cwd.
13910
+ ...appArg && appArg.trim() ? ["-a", appArg.trim()] : []
13911
+ ]
13912
+ });
13913
+ }
13914
+ function staleTrunkCheckout(appDir, intoBranch, landedOid) {
13915
+ const dir = listWorktrees(appDir).find((worktree) => worktree.branch === intoBranch)?.path ?? null;
13916
+ if (dir === null) return null;
13917
+ const localTip = resolveCommit(dir, `refs/heads/${intoBranch}`);
13918
+ if (localTip === null || localTip === landedOid) return null;
13919
+ return isAncestor(dir, localTip, landedOid) ? dir : null;
13439
13920
  }
13440
13921
  function landResumeArgv(args) {
13441
13922
  const argv = ["deepspace", "workspace", "land"];
@@ -13533,11 +14014,10 @@ var landWorkspaceCommand = defineDeepspaceCommand({
13533
14014
  spinner2?.stop("Workspace finished.");
13534
14015
  throw workspaceNotActiveRefusal(id, view.workspace.status, dropAction);
13535
14016
  }
13536
- ensureSpaceRemote(appDir, appId);
13537
- ensureGitIdentity(appDir, token);
14017
+ ensureSpaceRemote(appDir, appId, void 0, token);
13538
14018
  const isResumedMerge = resolveCommit(appDir, "HEAD^2") !== null && view.tipOid !== null && resolveCommit(appDir, "HEAD^") === view.tipOid;
13539
14019
  if (!isResumedMerge) {
13540
- pushWorkspaceRef(appDir, token, view.workspace.ref, headBefore);
14020
+ pushWorkspaceRef(appDir, token, view.workspace.ref, headBefore, view.tipOid);
13541
14021
  }
13542
14022
  const intoBranch = intoArg?.trim() || (refs?.head?.startsWith("refs/heads/") ? refs.head.slice("refs/heads/".length) : null);
13543
14023
  if (!intoBranch) {
@@ -13655,7 +14135,11 @@ var landWorkspaceCommand = defineDeepspaceCommand({
13655
14135
  }
13656
14136
  const trunkSecret = committedSecretRefusal(appDir, resolveCommit(appDir, "HEAD"), {
13657
14137
  action: `land onto ${intoBranch}`,
13658
- then: "re-run the land"
14138
+ then: "re-run the land",
14139
+ // The fetched trunk tip is what this push uploads against, so the scan
14140
+ // covers exactly the commits the merge carries onto trunk — the same
14141
+ // range the server scans.
14142
+ base: remoteTip
13659
14143
  });
13660
14144
  if (trunkSecret) {
13661
14145
  spinner2?.stop("Secret in history.");
@@ -13667,7 +14151,10 @@ var landWorkspaceCommand = defineDeepspaceCommand({
13667
14151
  if (push.status === "ref_conflict" || push.status === "non_fast_forward") {
13668
14152
  spinner2?.stop("Trunk moved.");
13669
14153
  throw new Refusal(
13670
- `${intoBranch} moved while you merged (${push.reason ?? push.summary}). Re-run \`${resumeNext}\` to integrate the new tip.`,
14154
+ // The machine token belongs in `code`, not mid-sentence: the reason
14155
+ // arrives as `<code>: <sentence>`, and printing it raw put
14156
+ // "stale_ref: stale ref, fetch first" inside human prose.
14157
+ `${intoBranch} moved while you merged (${parseRefusalCode(push.reason ?? push.summary)?.sentence ?? push.reason ?? push.summary}). Re-run \`${resumeNext}\` to integrate the new tip.`,
13671
14158
  "trunk_moved",
13672
14159
  {
13673
14160
  actionRequired: true,
@@ -13679,8 +14166,8 @@ var landWorkspaceCommand = defineDeepspaceCommand({
13679
14166
  if (push.status === "rejected") {
13680
14167
  spinner2?.stop("Push rejected.");
13681
14168
  throw new Refusal(
13682
- `The cloud repo rejected the land push: ${push.reason ?? push.summary}.`,
13683
- "push_rejected"
14169
+ pushFailureMessage("The land push", push, appDir),
14170
+ classifyRejection(push.reason ?? push.summary ?? "", appDir).code
13684
14171
  );
13685
14172
  }
13686
14173
  try {
@@ -13715,8 +14202,13 @@ var landWorkspaceCommand = defineDeepspaceCommand({
13715
14202
  );
13716
14203
  }
13717
14204
  spinner2?.stop(`Landed ${id} into ${intoBranch} at ${landedOid.slice(0, 10)}.`);
13718
- const primaryDir = primaryAppDir(appDir);
13719
- const action = pullAfterLandAction(primaryDir, intoBranch, landedOid);
14205
+ const trunkCheckoutDir = staleTrunkCheckout(appDir, intoBranch, landedOid);
14206
+ const localTrunkBehind = trunkCheckoutDir !== null;
14207
+ const action = trunkCheckoutDir ? staleTrunkPullAction(
14208
+ trunkCheckoutDir,
14209
+ intoBranch,
14210
+ typeof args.app === "string" ? args.app : void 0
14211
+ ) : null;
13720
14212
  const cleanup = keepWorktree ? null : cleanupWorkspaceLocal(appDir, id, intoBranch, { expectedBranchOid: landedOid });
13721
14213
  const inOwnWorktree = !cleanup && inOwnLinkedWorktree(appDir, id);
13722
14214
  const retainedWorktree = cleanup?.worktreeRetained ?? (keepWorktree || inOwnWorktree ? appDir : null);
@@ -13724,6 +14216,7 @@ var landWorkspaceCommand = defineDeepspaceCommand({
13724
14216
  workspaceId: id,
13725
14217
  into: intoBranch,
13726
14218
  landedOid,
14219
+ ...localTrunkBehind ? { localTrunkBehind: true } : {},
13727
14220
  ...validationResult ? { validation: validationResult } : {},
13728
14221
  ...retainedWorktree ? { worktree: retainedWorktree } : {},
13729
14222
  ...cleanup ? { cleanup: cleanupJson(cleanup) } : {}
@@ -13731,7 +14224,12 @@ var landWorkspaceCommand = defineDeepspaceCommand({
13731
14224
  if (!args.json && cleanup && !cleanup.error) {
13732
14225
  reportCleanupHuman(cleanup);
13733
14226
  } else if (!args.json && retainedWorktree) {
13734
- p30.log.info(`Retained checkout: ${retainedWorktree}`);
14227
+ p31.log.info(`Retained checkout: ${retainedWorktree}`);
14228
+ }
14229
+ if (!args.json && localTrunkBehind) {
14230
+ p31.log.info(
14231
+ `Local ${intoBranch} in ${trunkCheckoutDir} is behind the landed merge \u2014 run \`deepspace pull\` there to catch up.`
14232
+ );
13735
14233
  }
13736
14234
  if (cleanup?.error) {
13737
14235
  const action2 = cleanupAction({
@@ -13755,31 +14253,23 @@ var landWorkspaceCommand = defineDeepspaceCommand({
13755
14253
  });
13756
14254
 
13757
14255
  // src/cli/commands/workspace/drop.ts
13758
- import * as p31 from "@clack/prompts";
14256
+ import * as p32 from "@clack/prompts";
13759
14257
  function isWorkspaceTipPublished(appDir, localTip, publishedOids) {
13760
14258
  return publishedOids.some(
13761
14259
  (publishedOid) => publishedOid === localTip || isAncestor(appDir, localTip, publishedOid)
13762
14260
  );
13763
14261
  }
13764
14262
  function workspaceUnsyncedRefusal(args) {
13765
- const dropKeepArgv = [
13766
- "deepspace",
13767
- "workspace",
13768
- "drop",
13769
- args.id,
13770
- "--app",
13771
- args.appId,
13772
- "--keep-worktree"
13773
- ];
14263
+ const dropArgv = ["deepspace", "workspace", "drop", args.id, "--app", args.appId];
13774
14264
  if (args.status !== "active") {
14265
+ const keepArgv = [...dropArgv, "--keep-worktree"];
13775
14266
  return new Refusal(
13776
- `${args.id} is already ${args.status} on the server, and this checkout has committed work that was never published \u2014 nothing can publish it now. Keep it with \`${humanCommand(dropKeepArgv)}\` (retains the branch/worktree for manual disposal), or delete the branch yourself. Nothing was dropped.`,
14267
+ `${args.id} is already ${args.status} on the server, and this checkout holds commits that nothing in the cloud represents any more \u2014 nothing can publish them now. \`${humanCommand(keepArgv)}\` drops the cloud workspace but retains the local branch/worktree for manual disposal, or delete the branch yourself. Nothing was dropped.`,
13777
14268
  "workspace_unsynced",
13778
14269
  { extra: { workspaceId: args.id, status: args.status } }
13779
14270
  );
13780
14271
  }
13781
14272
  const syncArgv = ["deepspace", "workspace", "sync", "--app", args.appId, "--workspace", args.id];
13782
- const dropArgv = ["deepspace", "workspace", "drop", args.id, "--app", args.appId];
13783
14273
  const publish = args.workspaceDir ? `Publish them first with \`${humanCommand(syncArgv)}\`, then re-run \`${humanCommand(dropArgv)}\`.` : `Recreate a worktree from the branch (\`git worktree add <dir> ${args.branch}\`), publish from there with \`${humanCommand(syncArgv)}\`, then re-run \`${humanCommand(dropArgv)}\`.`;
13784
14274
  return new Refusal(
13785
14275
  `${args.id} has committed work that isn't published yet. ${publish} Alternatively, pass \`--keep-worktree\` to drop the cloud workspace while retaining the local branch/worktree for manual disposal. Nothing was dropped.`,
@@ -13817,6 +14307,11 @@ var dropWorkspaceCommand = defineDeepspaceCommand({
13817
14307
  description: "Keep the local worktree and branch after dropping",
13818
14308
  default: false
13819
14309
  },
14310
+ "abandon-unseen": {
14311
+ type: "boolean",
14312
+ description: "Drop even though this checkout has not seen the published tip \u2014 an explicit decision to abandon commits you have not read (workspace_behind)",
14313
+ default: false
14314
+ },
13820
14315
  app: APP_ARG
13821
14316
  },
13822
14317
  async run({ args }) {
@@ -13824,28 +14319,63 @@ var dropWorkspaceCommand = defineDeepspaceCommand({
13824
14319
  assertExplicitWorkspaceId(explicitId);
13825
14320
  const appArg = typeof args.app === "string" ? args.app : void 0;
13826
14321
  const keepWorktree = Boolean(args["keep-worktree"]);
14322
+ const abandonUnseen = Boolean(args["abandon-unseen"]);
13827
14323
  const spinner2 = args.json ? null : createSpinner();
13828
14324
  spinner2?.start(`Preparing to drop ${explicitId ?? "workspace"}\u2026`);
13829
- if (!findAppDir() && explicitId) {
13830
- const { api: api5 } = await resolveApiOnly(appArg);
13831
- const statusBefore = (await api5.getWorkspace(explicitId)).view.workspace.status;
13832
- const { view: view2, remoteDropped: remoteDropped2 } = await dropRemoteTolerant(api5, explicitId, statusBefore);
13833
- spinner2?.stop();
13834
- if (!args.json) {
13835
- p31.log.success(
13836
- remoteDropped2 ? `Dropped ${explicitId}. Its ref sticks around briefly for undo, then is reaped.` : `Workspace ${explicitId} was already ${view2.workspace.status} on the server \u2014 nothing to drop remotely.`
14325
+ const inCheckout = findAppDir() !== null || !explicitId;
14326
+ const { appDir, appId, token, api: api4 } = inCheckout ? await resolveTarget2(appArg) : { appDir: null, ...await resolveApiOnly(appArg) };
14327
+ const id = appDir ? inferWorkspaceId(appDir, explicitId) : explicitId;
14328
+ const inspection = appDir ? inspectWorkspaceCleanup(appDir, id) : null;
14329
+ let approvedBranchOid;
14330
+ const { view: before } = await api4.getWorkspace(id);
14331
+ if (before.workspace.status === "active" && before.tipOid !== null) {
14332
+ if (appDir) {
14333
+ ensureSpaceRemote(appDir, appId, void 0, token);
14334
+ if (resolveCommit(appDir, before.tipOid) === null) {
14335
+ runGitRemote(
14336
+ appDir,
14337
+ token,
14338
+ ["fetch", "--quiet", "--refmap=", SPACE_REMOTE, before.workspace.ref],
14339
+ { allowFail: true }
14340
+ );
14341
+ }
14342
+ }
14343
+ const relation = workspaceSyncRelation(appDir, inspection?.branchOid ?? null, before.tipOid);
14344
+ if (!abandonUnseen && relation !== "in_sync" && relation !== "ahead") {
14345
+ const pullArgv = ["git", "pull", "--no-rebase", SPACE_REMOTE, before.workspace.ref];
14346
+ const attachArgv = ["deepspace", "workspace", "attach", id, "--app", appId];
14347
+ const abandonCommand = humanCommand([
14348
+ "deepspace",
14349
+ "workspace",
14350
+ "drop",
14351
+ id,
14352
+ "--app",
14353
+ appId,
14354
+ "--abandon-unseen"
14355
+ ]);
14356
+ const worktreeDir = inspection?.checkout.dir ?? null;
14357
+ throw new Refusal(
14358
+ `Dropping ${id} would discard its published tip ${before.tipOid.slice(0, 10)}, which this seat has not read (${relation}) \u2014 integrate it first, or run \`${abandonCommand}\` to abandon it unread. Nothing was dropped.`,
14359
+ "workspace_behind",
14360
+ {
14361
+ action: worktreeDir ? { cwd: worktreeDir, argv: pullArgv } : appDir ? { cwd: appDir, argv: attachArgv } : void 0,
14362
+ extra: {
14363
+ workspaceId: id,
14364
+ publishedTip: before.tipOid,
14365
+ localTip: inspection?.branchOid ?? null,
14366
+ relation
14367
+ }
14368
+ }
13837
14369
  );
13838
14370
  }
13839
- return { data: { workspaceId: explicitId, status: view2.workspace.status, remoteDropped: remoteDropped2 } };
13840
14371
  }
13841
- const { appDir, appId, token, api: api4 } = await resolveTarget2(appArg);
13842
- const id = inferWorkspaceId(appDir, explicitId);
13843
- const inspection = inspectWorkspaceCleanup(appDir, id);
13844
- let approvedBranchOid;
13845
- const { view: before } = await api4.getWorkspace(id);
13846
- if (!keepWorktree && inspection.willDeleteBranch && inspection.branchOid) {
14372
+ if (appDir && inspection && !keepWorktree && inspection.willDeleteBranch && inspection.branchOid) {
13847
14373
  const publishedOids = [
13848
- before.tipOid,
14374
+ // A FINISHED workspace's own ref is being reaped — it can prove
14375
+ // nothing. Only trunk (landed) or the pre-existing base counts, or a
14376
+ // dropped workspace's last local copy gets deleted on the strength of
14377
+ // the very ref that is going away.
14378
+ ...before.workspace.status === "active" ? [before.tipOid] : [],
13849
14379
  before.workspace.landedOid,
13850
14380
  before.workspace.baseOid
13851
14381
  ].filter((oid) => oid !== null);
@@ -13873,37 +14403,56 @@ var dropWorkspaceCommand = defineDeepspaceCommand({
13873
14403
  }
13874
14404
  approvedBranchOid = inspection.branchOid;
13875
14405
  }
13876
- const cleanupDir = inspection.willDeleteBranch ? inspection.checkout.dir : null;
14406
+ const cleanupDir = inspection?.willDeleteBranch ? inspection.checkout.dir : null;
13877
14407
  if (!keepWorktree && cleanupDir && !isWorkTreeClean(cleanupDir)) {
13878
14408
  const dropArgv = ["deepspace", "workspace", "drop", id, "--app", appId];
13879
14409
  const dropNext = humanCommand(dropArgv);
14410
+ const conflicted = unmergedIndexRefusal(cleanupDir, {
14411
+ ours: "A merge",
14412
+ resume: dropNext
14413
+ });
14414
+ if (conflicted) {
14415
+ throw new Refusal(
14416
+ `${conflicted.message} Or pass \`--keep-worktree\` to drop the workspace while retaining this checkout. Nothing was dropped.`,
14417
+ conflicted.code,
14418
+ { extra: { workspaceId: id, conflict: true, operation: conflicted.operation } }
14419
+ );
14420
+ }
13880
14421
  throw new Refusal(
13881
14422
  `The workspace checkout has uncommitted changes and cleanup would remove it or switch it off ${inspection.branch}. Commit them and re-run \`${dropNext}\`, or pass \`--keep-worktree\` to drop while retaining the checkout. Nothing was dropped.`,
13882
14423
  "dirty_worktree",
13883
14424
  { extra: { workspaceId: id } }
13884
14425
  );
13885
14426
  }
13886
- const { view, remoteDropped } = await dropRemoteTolerant(api4, id, before.workspace.status);
13887
- spinner2?.message(`Cleaning up ${id} locally\u2026`);
13888
- const cleanup = keepWorktree ? null : cleanupWorkspaceLocal(appDir, id, null, {
14427
+ const statusBefore = (await api4.getWorkspace(id)).view.workspace.status;
14428
+ const { view, remoteDropped } = await dropRemoteTolerant(api4, id, statusBefore);
14429
+ if (appDir && !keepWorktree) spinner2?.message(`Cleaning up ${id} locally\u2026`);
14430
+ const cleanup = appDir && inspection && !keepWorktree ? cleanupWorkspaceLocal(appDir, id, null, {
13889
14431
  expectedBranchOid: approvedBranchOid,
13890
14432
  retainLocal: !inspection.willDeleteBranch
13891
- });
13892
- const retainedWorktree = cleanup?.worktreeRetained ?? (keepWorktree ? inspection.checkout.dir : null);
14433
+ }) : null;
14434
+ const retainedWorktree = cleanup?.worktreeRetained ?? (keepWorktree ? inspection?.checkout.dir ?? null : null);
14435
+ const discardedTip = remoteDropped && before.tipOid && (!inspection?.branchOid || abandonUnseen) ? before.tipOid : null;
13893
14436
  const data = {
13894
14437
  workspaceId: id,
13895
14438
  status: view.workspace.status,
13896
14439
  remoteDropped,
14440
+ ...discardedTip ? { discardedTip } : {},
13897
14441
  ...retainedWorktree ? { worktree: retainedWorktree } : {},
13898
14442
  ...cleanup ? { cleanup: cleanupJson(cleanup) } : {}
13899
14443
  };
13900
14444
  spinner2?.stop();
13901
14445
  if (!args.json) {
13902
- p31.log.success(
13903
- remoteDropped ? `Dropped ${id}. Its ref sticks around briefly for undo, then is reaped.` : `Workspace ${id} was already ${view.workspace.status} on the server \u2014 nothing to drop remotely.`
14446
+ p32.log.success(
14447
+ remoteDropped ? `Dropped ${id}. Its ref sticks around briefly for undo, then is reaped.` : `Workspace ${id} was already ${statusBefore} on the server \u2014 nothing to drop remotely.`
13904
14448
  );
14449
+ if (discardedTip) {
14450
+ p32.log.warn(
14451
+ `The published tip ${discardedTip.slice(0, 10)} was abandoned unread \u2014 this seat could not prove it had read it. Its ref is retained briefly if that was a mistake.`
14452
+ );
14453
+ }
13905
14454
  if (cleanup && !cleanup.error) reportCleanupHuman(cleanup);
13906
- else if (retainedWorktree) p31.log.info(`Retained checkout: ${retainedWorktree}`);
14455
+ else if (retainedWorktree) p32.log.info(`Retained checkout: ${retainedWorktree}`);
13907
14456
  }
13908
14457
  if (cleanup?.error) {
13909
14458
  const action = cleanupAction({
@@ -13944,7 +14493,7 @@ var workspace_default = defineCommand12({
13944
14493
  });
13945
14494
 
13946
14495
  // src/cli/commands/activity.ts
13947
- import * as p32 from "@clack/prompts";
14496
+ import * as p33 from "@clack/prompts";
13948
14497
  var FOLLOW_INTERVAL_MS = 5e3;
13949
14498
  function landIndex(events) {
13950
14499
  const m = /* @__PURE__ */ new Map();
@@ -13958,12 +14507,16 @@ function landIndex(events) {
13958
14507
  function formatEvent2(e, lands, actors) {
13959
14508
  const s = e.summary ?? {};
13960
14509
  const short = (oid) => typeof oid === "string" ? oid.slice(0, 10) : "?";
14510
+ const isZeroOid = (oid) => typeof oid === "string" && /^0{40}$/.test(oid);
13961
14511
  const who = (id) => typeof id === "string" ? actors?.get(id) ?? id : String(id ?? "?");
13962
14512
  let detail = "";
13963
14513
  switch (e.kind) {
13964
14514
  case "push": {
13965
14515
  const refs = Array.isArray(s.refs) ? s.refs : [];
13966
- detail = refs.map((r) => `${String(r.ref ?? "").replace("refs/heads/", "")} \u2192 ${short(r.newOid)}`).join(", ");
14516
+ detail = refs.map((r) => {
14517
+ const name = String(r.ref ?? "").replace("refs/heads/", "");
14518
+ return isZeroOid(r.newOid) ? `deleted ${name}` : `${name} \u2192 ${short(r.newOid)}`;
14519
+ }).join(", ");
13967
14520
  const landWs = refs.map((r) => typeof r.newOid === "string" ? lands?.get(r.newOid) : void 0).find(Boolean);
13968
14521
  if (landWs) detail += ` (land of ${landWs})`;
13969
14522
  break;
@@ -13991,7 +14544,7 @@ function formatEvent2(e, lands, actors) {
13991
14544
  default:
13992
14545
  detail = "";
13993
14546
  }
13994
- return `#${e.seq} ${e.createdAt} ${e.kind} ${who(e.actor)} ${detail}`.trimEnd();
14547
+ return displayText(`#${e.seq} ${e.createdAt} ${e.kind} ${who(e.actor)} ${detail}`.trimEnd());
13995
14548
  }
13996
14549
  var activity_default = defineDeepspaceCommand({
13997
14550
  meta: {
@@ -14060,7 +14613,7 @@ var activity_default = defineDeepspaceCommand({
14060
14613
  const lands = landIndex(page.events);
14061
14614
  for (const e of page.events) console.log(formatEvent2(e, lands, actors));
14062
14615
  if (page.events.length === 0 && !args.follow) {
14063
- p32.log.info(cursor > 0 ? `No activity after #${cursor}.` : "No activity yet.");
14616
+ p33.log.info(cursor > 0 ? `No activity after #${cursor}.` : "No activity yet.");
14064
14617
  }
14065
14618
  }
14066
14619
  cursor = page.cursor;
@@ -14070,7 +14623,7 @@ var activity_default = defineDeepspaceCommand({
14070
14623
  }
14071
14624
  if (!args.follow) {
14072
14625
  if (page.events.length > 0) {
14073
- p32.log.info(
14626
+ p33.log.info(
14074
14627
  `Cursor: ${cursor}` + (page.hasMore ? ` \xB7 more available (next page: --since ${cursor})` : ` (resume later with --since ${cursor})`)
14075
14628
  );
14076
14629
  }
@@ -14092,7 +14645,7 @@ var activity_default = defineDeepspaceCommand({
14092
14645
  })
14093
14646
  );
14094
14647
  } else {
14095
- p32.log.warn(`Activity feed interrupted (${code}); retrying in 5s.`);
14648
+ p33.log.warn(`Activity feed interrupted (${code}); retrying in 5s.`);
14096
14649
  }
14097
14650
  }
14098
14651
  await new Promise((resolveSleep) => setTimeout(resolveSleep, FOLLOW_INTERVAL_MS));
@@ -14143,13 +14696,6 @@ function declineHint(op, input, token, deployUrl) {
14143
14696
  }
14144
14697
  return "";
14145
14698
  }
14146
- async function readStdin() {
14147
- if (process.stdin.isTTY) return "";
14148
- process.stdin.setEncoding("utf-8");
14149
- let data = "";
14150
- for await (const chunk of process.stdin) data += chunk;
14151
- return data;
14152
- }
14153
14699
  var git_credential_default = defineCommand13({
14154
14700
  meta: {
14155
14701
  name: "git-credential",
@@ -14164,7 +14710,7 @@ var git_credential_default = defineCommand13({
14164
14710
  },
14165
14711
  async run({ args }) {
14166
14712
  const op = String(args.op ?? "");
14167
- const input = await readStdin();
14713
+ const input = process.stdin.isTTY ? "" : await readStreamText(process.stdin, MAX_STDIN_BYTES);
14168
14714
  if (op !== "get") return;
14169
14715
  const url = deployBaseUrl();
14170
14716
  let token = null;
@@ -14185,7 +14731,7 @@ var git_credential_default = defineCommand13({
14185
14731
  });
14186
14732
 
14187
14733
  // src/cli/commands/source.ts
14188
- import * as p33 from "@clack/prompts";
14734
+ import * as p34 from "@clack/prompts";
14189
14735
  var TRANSFER_PREFIX = spacePrivateRef("source-transfer");
14190
14736
  var source_default = defineDeepspaceCommand({
14191
14737
  meta: {
@@ -14239,14 +14785,14 @@ var source_default = defineDeepspaceCommand({
14239
14785
  const reconciliation = reconcileSpaceRemote(appDir, appId, result.source);
14240
14786
  const spaceRemote = reconciliation.spaceRemote;
14241
14787
  if (!args.json) {
14242
- p33.log.success(
14788
+ p34.log.success(
14243
14789
  result.source.provider === "github" ? `Source: GitHub \xB7 ${result.source.repository} (revision ${result.revision})` : `Source: DeepSpace (revision ${result.revision})`
14244
14790
  );
14245
- p33.log.info(
14791
+ p34.log.info(
14246
14792
  spaceRemote === "removed" ? `Removed the \`${SPACE_REMOTE}\` git remote \u2014 DeepSpace no longer owns this repo.` : spaceRemote === "absent" ? `No \`${SPACE_REMOTE}\` git remote to remove \u2014 source operations go through GitHub.` : spaceRemote === "present" ? `The \`${SPACE_REMOTE}\` git remote points at this app's cloud repo.` : `The local \`${SPACE_REMOTE}\` git remote still needs reconciliation.`
14247
14793
  );
14248
14794
  if (reconciliation.localReconciliation) {
14249
- p33.log.warn(reconciliation.localReconciliation.message);
14795
+ p34.log.warn(reconciliation.localReconciliation.message);
14250
14796
  }
14251
14797
  }
14252
14798
  return {
@@ -14482,14 +15028,14 @@ function sameRefs(left, right) {
14482
15028
  }
14483
15029
 
14484
15030
  // src/cli/commands/update.ts
14485
- import * as p34 from "@clack/prompts";
14486
- import { existsSync as existsSync28, readFileSync as readFileSync26 } from "fs";
15031
+ import * as p35 from "@clack/prompts";
15032
+ import { existsSync as existsSync29, readFileSync as readFileSync26 } from "fs";
14487
15033
  import { join as join28, resolve as resolve22 } from "path";
14488
15034
 
14489
15035
  // package.json
14490
15036
  var package_default = {
14491
15037
  name: "deepspace",
14492
- version: "0.24.1",
15038
+ version: "0.25.0",
14493
15039
  engines: {
14494
15040
  node: ">=22.15.0 <23 || >=24 <25 || >=26 <27"
14495
15041
  },
@@ -14565,7 +15111,7 @@ var package_default = {
14565
15111
  "cross-spawn": "^7.0.6",
14566
15112
  esbuild: "^0.27.0",
14567
15113
  "highlight.js": "^11.11.1",
14568
- hono: "^4.6.0",
15114
+ hono: "^4.12.34",
14569
15115
  jose: "^6.1.0",
14570
15116
  marked: "^18.0.7",
14571
15117
  "mdast-util-gfm": "^3.1.0",
@@ -14667,7 +15213,7 @@ function gapIsTooWide(from, to) {
14667
15213
  }
14668
15214
  function readManifest(appDir) {
14669
15215
  const path = join28(appDir, "package.json");
14670
- if (!existsSync28(path)) {
15216
+ if (!existsSync29(path)) {
14671
15217
  throw new Refusal("This app has no package.json \u2014 nothing to update.", "not_in_app_repo");
14672
15218
  }
14673
15219
  try {
@@ -14706,7 +15252,7 @@ function planDependencyGuidance(appDir) {
14706
15252
  function usersSchemaVisibilityUpgradeInstruction(appDir) {
14707
15253
  const schemaName = "src/schemas/users-schema.ts";
14708
15254
  const path = join28(appDir, schemaName);
14709
- if (!existsSync28(path)) return null;
15255
+ if (!existsSync29(path)) return null;
14710
15256
  const source = readFileSync26(path, "utf8");
14711
15257
  if (!/\bmember\s*:\s*\{[^}]*\bread\s*:\s*true\b/s.test(source)) return null;
14712
15258
  return `${schemaName}: review member user-row visibility at ${USERS_SCHEMA_VISIBILITY_GUIDE}; leave the app-owned schema unchanged until you choose its policy.`;
@@ -14744,7 +15290,7 @@ var update_default = defineDeepspaceCommand({
14744
15290
  } : null;
14745
15291
  if (versionGuidance) {
14746
15292
  const steps2 = [versionGuidance.step];
14747
- if (!args.json) p34.log.warn(versionGuidance.step);
15293
+ if (!args.json) p35.log.warn(versionGuidance.step);
14748
15294
  return {
14749
15295
  data: {
14750
15296
  ready: false,
@@ -14780,9 +15326,9 @@ var update_default = defineDeepspaceCommand({
14780
15326
  ];
14781
15327
  const status3 = unverifiedSdk ? "dependency_unverified" : steps.length === 0 ? "aligned" : "guidance_available";
14782
15328
  if (!args.json) {
14783
- p34.log.info(`App dependency: ${currentSpec}; guidance owner: deepspace CLI ${targetVersion}`);
14784
- for (const step of steps) p34.log.message(`\u2022 ${step}`);
14785
- if (steps.length === 0) p34.log.success("This app is already aligned with this CLI version.");
15329
+ p35.log.info(`App dependency: ${currentSpec}; guidance owner: deepspace CLI ${targetVersion}`);
15330
+ for (const step of steps) p35.log.message(`\u2022 ${step}`);
15331
+ if (steps.length === 0) p35.log.success("This app is already aligned with this CLI version.");
14786
15332
  }
14787
15333
  const data = {
14788
15334
  ready: status3 === "aligned",
@@ -14802,9 +15348,13 @@ var update_default = defineDeepspaceCommand({
14802
15348
  });
14803
15349
 
14804
15350
  // src/cli/cli.ts
15351
+ process.stdout.on("error", (error) => {
15352
+ if (error.code === "EPIPE") return;
15353
+ throw error;
15354
+ });
14805
15355
  var __dirname = dirname13(fileURLToPath3(import.meta.url));
14806
15356
  var pkg = JSON.parse(readFileSync27(join29(__dirname, "..", "package.json"), "utf-8"));
14807
- if (process.platform === "linux" && !process.env.SSL_CERT_FILE && existsSync29("/etc/ssl/certs/ca-certificates.crt")) {
15357
+ if (process.platform === "linux" && !process.env.SSL_CERT_FILE && existsSync30("/etc/ssl/certs/ca-certificates.crt")) {
14808
15358
  process.env.SSL_CERT_FILE = "/etc/ssl/certs/ca-certificates.crt";
14809
15359
  }
14810
15360
  var create2 = defineCommand14({
@@ -14832,6 +15382,16 @@ var create2 = defineCommand14({
14832
15382
  }
14833
15383
  },
14834
15384
  run({ rawArgs: rawArgs2 }) {
15385
+ if (rawArgs2.includes("--json")) {
15386
+ console.log(
15387
+ JSON.stringify({
15388
+ ok: false,
15389
+ code: "invalid_flags",
15390
+ error: "`app create` runs the interactive scaffolder, which has no JSON mode \u2014 its output is a directory, not a document. Run it without --json; every later command in the new app supports it."
15391
+ })
15392
+ );
15393
+ process.exit(1);
15394
+ }
14835
15395
  const scaffolder = `create-deepspace@${pkg.version}`;
14836
15396
  console.log(`Running: npx ${scaffolder} ${rawArgs2.join(" ")}`.trimEnd() + "\n");
14837
15397
  const res = spawnSync8("npx", ["-y", scaffolder, ...rawArgs2], { stdio: "inherit" });
@@ -14931,7 +15491,12 @@ function assertKnownCommandPath(argv) {
14931
15491
  Run \`${unknown.helpPath.join(" ")} --help\` for the command list.`
14932
15492
  );
14933
15493
  if (unknown.executable) {
14934
- printAction(executableAction({ cwd: process.cwd(), argv: unknown.suggestion }));
15494
+ printAction(
15495
+ executableAction({
15496
+ cwd: process.cwd(),
15497
+ argv: [...unknown.suggestion, ...unknown.remainder]
15498
+ })
15499
+ );
14935
15500
  }
14936
15501
  process.exit(1);
14937
15502
  }
@@ -14997,7 +15562,18 @@ if (rawArgs.includes("--json") && !wantsHelp) {
14997
15562
  ok: false,
14998
15563
  code: "unknown_command",
14999
15564
  error: unknownCommandMessage(unknown),
15000
- ...unknown.executable ? { action: executableAction({ cwd: process.cwd(), argv: unknown.suggestion }) } : {}
15565
+ // The corrected verb PLUS what the caller wrote after it: an action
15566
+ // that drops the positionals refuses `missing_argument` when run
15567
+ // verbatim, which is the one thing the action contract promises it
15568
+ // will not do. Attached ONLY when the correction is executable
15569
+ // (certain and non-destructive); a guess would hand an agent a
15570
+ // different verb to execute against its own arguments.
15571
+ ...unknown.executable ? {
15572
+ action: executableAction({
15573
+ cwd: process.cwd(),
15574
+ argv: [...unknown.suggestion, ...unknown.remainder]
15575
+ })
15576
+ } : {}
15001
15577
  })
15002
15578
  );
15003
15579
  process.exit(1);
@@ -15006,7 +15582,7 @@ if (rawArgs.includes("--json") && !wantsHelp) {
15006
15582
  stopActiveSpinner();
15007
15583
  const cittyCode = err instanceof Error && err.name === "CLIError" ? err.code : void 0;
15008
15584
  const code = cittyCode === "EARG" ? "missing_argument" : cittyCode === "E_UNKNOWN_COMMAND" ? "unknown_command" : cittyCode === "E_NO_COMMAND" ? "no_command" : errorCode(err);
15009
- const message = stripAnsi(err instanceof Error ? err.message : String(err));
15585
+ const message = stripAnsi(formatCliError(err));
15010
15586
  console.log(JSON.stringify({ ok: false, error: message, ...code ? { code } : {} }));
15011
15587
  process.exit(1);
15012
15588
  });