toyon 0.16.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/README.md +1 -1
  2. package/dist/CHANGELOG.md +71 -0
  3. package/dist/cli.js +114 -47
  4. package/dist/daemon.js +1835 -759
  5. package/dist/shell/assets/{Editor-BATywBIx.js → Editor-CU91PIeX.js} +175 -175
  6. package/dist/shell/assets/{XTerm-So8-Vafj.js → XTerm-y7vQoOEu.js} +1 -1
  7. package/dist/shell/assets/{cssMode-DlHcrkXj.js → cssMode-B7sFzM6K.js} +1 -1
  8. package/dist/shell/assets/{freemarker2-D3xOnVxc.js → freemarker2-ChTLQoDC.js} +1 -1
  9. package/dist/shell/assets/{grammar-RZJ5oG33.js → grammar-xXcxTc3V.js} +1 -1
  10. package/dist/shell/assets/{handlebars-UbIG7080.js → handlebars-DPJSozQx.js} +1 -1
  11. package/dist/shell/assets/{html-D92eLK6U.js → html-Cog2rLcD.js} +1 -1
  12. package/dist/shell/assets/{htmlMode-DsZIJzoj.js → htmlMode-Xm5_ujKb.js} +1 -1
  13. package/dist/shell/assets/index-BpW1Kufz.css +1 -0
  14. package/dist/shell/assets/index-CdY8WF4P.js +270 -0
  15. package/dist/shell/assets/{javascript-DUkgFtV2.js → javascript-Dcv6lw7b.js} +1 -1
  16. package/dist/shell/assets/{jsonMode-BiIooPg9.js → jsonMode-D5_V0TW5.js} +1 -1
  17. package/dist/shell/assets/{liquid-DFFjZ5TH.js → liquid-C_zXIF5F.js} +1 -1
  18. package/dist/shell/assets/{lspLanguageFeatures-C7Ug4qkg.js → lspLanguageFeatures-CkiTdLmp.js} +1 -1
  19. package/dist/shell/assets/{mdx-DdjSgAuG.js → mdx-gvbILQkr.js} +1 -1
  20. package/dist/shell/assets/{python-DPUF4OK9.js → python-CZTrOkh_.js} +1 -1
  21. package/dist/shell/assets/{razor-IRYJ0pOA.js → razor-DowlBj1i.js} +1 -1
  22. package/dist/shell/assets/{tsMode-CljE7MPO.js → tsMode-BNl2cV_K.js} +1 -1
  23. package/dist/shell/assets/{xml-D8Zv3Gd0.js → xml-DIiuHFHw.js} +1 -1
  24. package/dist/shell/assets/{yaml-Dxx9PL0B.js → yaml-CBnMyn4j.js} +1 -1
  25. package/dist/shell/icon-192.png +0 -0
  26. package/dist/shell/icon-512.png +0 -0
  27. package/dist/shell/icon-maskable-512.png +0 -0
  28. package/dist/shell/index.html +94 -13
  29. package/dist/shell/manifest.json +19 -1
  30. package/package.json +1 -1
  31. package/dist/shell/assets/index-BvYzBRq_.css +0 -1
  32. package/dist/shell/assets/index-CHQwm2uN.js +0 -268
package/README.md CHANGED
@@ -28,7 +28,7 @@ Point at anything on the page to talk about it or open the line that draws it, a
28
28
 
29
29
  One Toyon holds every project you open, and the list of chats shows which one needs you.
30
30
 
31
- When your check passes, land merges into main on your machine by default, or pushes, or opens a pull request.
31
+ When your check passes, land merges into main on your machine by default, or pushes, or creates a pull request.
32
32
 
33
33
  Built for web apps. A project without a page still works; the chat takes the middle instead of the preview.
34
34
 
package/dist/CHANGELOG.md CHANGED
@@ -2,6 +2,77 @@
2
2
 
3
3
  What changed in each release of Toyon, newest first.
4
4
 
5
+ ## 0.18.0 (2026-10-23)
6
+
7
+ Stop and the ask cards say what they are doing while they do it, and the phone rail's swipe reads as one gesture with the rungs the row actually offers.
8
+
9
+ ### New
10
+ - A plan card can take a note with its answer. Open the field under a row, write the note, and Enter answers with that row and sends the words after it, so a plan is accepted with a condition in one step. The draft stays with the card when you switch worktrees.
11
+ - Up in a blank composer walks onto the card answers you wrote a note for, and puts that note back in the box, in with your messages and bang commands.
12
+ - When Claude runs on Amazon Bedrock and the AWS sign-in has lapsed, the turn ends on a card instead of an error row. Its first button sets Claude Code to refresh the sign-in itself from then on; the others run a one-off login or send the message again.
13
+ - On a phone, tapping the usage ring opens the meters with the menu beneath them.
14
+ - An installed app and its splash screen get a full icon on one colour in place of the monogram.
15
+
16
+ ### Changed
17
+ - Pressing stop shows at once: the button breathes and the rail dot pulses red until the turn has really ended. Before, nothing moved for as long as the cancel took, up to half a minute, so the press read as ignored.
18
+ - An option's preview on an ask card sits under its own row and scrolls past twelve lines, instead of in a band that followed the cursor and resized the card.
19
+ - The editor pane head leads with the file name and dims the directory after it. The directory shortens and then drops when the pane is narrow, and copy path and reveal moved into the head's right-click menu.
20
+ - The phone rail's swipe follows one ladder, land, then archive, then more, and shows only the rungs the row's menu offers. A landed and clean row has no land rung, so archive takes the short drag. The well behind the row darkens with the slide and fades as the row settles.
21
+ - Opening a file under Toyon's home works for transcripts, logs, themes and launcher sources. Only the daemon's own state and credentials are refused, and the refusal says so.
22
+ - A slow first connection no longer flips the window to a reason: the shell tries twice before naming one, so the list stays on screen, and a wrong token is still named at once. The boot page draws the four-square mark after a short delay.
23
+ - A queued message shows its tag on a line above the text, and the commit row's tip sits on the chat's side of the row as a rail row's does.
24
+
25
+ ### Fixed
26
+ - With Fable at its weekly cap, the usage ring drew the next fullest window and the out-until warning matched no row. The Fable week is now a row of its own in the usage view, the ring and the warning.
27
+ - A message sent mid-turn that the agent went on to answer past a stop was queued and run a second time.
28
+ - A reply that ended in an error or on a sign-in card left its row marked as running, in the moment and in the stored chat.
29
+ - An image attached to a card answer was lost; it now reaches the agent with the words.
30
+ - A chat that paused to ask you something was listed twice on the updated card, as asking and as finished. It shows once, as asking.
31
+ - Typing a full @ worktree name and a space now closes the mention menu, and a trailing ?, ! or . no longer keeps it open or gets swallowed by Enter.
32
+ - Pressing go on after a stop or error now moves the row up the rail like any send.
33
+ - On a phone, the status bar matches the screen behind it once the app has booted, instead of a different shade above the body.
34
+ - A mostly vertical scroll in a diff no longer drifts it a pixel sideways.
35
+ - A code chip inside a table cell no longer wraps at its spaces; a table of chips scrolls sideways on a phone instead.
36
+ - The offer line under the composer clipped the focus ring on a long offer, and the sync note sat beside the behind-main count while a sync ran instead of at the row's end.
37
+
38
+ ### Not yet
39
+ - The app icon is a stand-in until the final one is drawn.
40
+
41
+ ## 0.17.0 (2026-10-21)
42
+
43
+ The composer's @ menu reaches the other worktrees of the project, the editor pane can wrap long lines, and a git command that hangs no longer holds up every later landing.
44
+
45
+ ### New
46
+ - The composer's @ menu lists the project's other worktrees. A bare @ offers the three you sent to most recently, each with its status dot, title and the time since the last send, and typing part of a title searches them all. The pick puts the worktree's name in the message so the agent can read that chat.
47
+ - The editor pane has a wrap button that folds long lines at the pane's edge or goes back to sideways scrolling. The choice holds for the window and applies to the diff view too.
48
+ - Up and down walk the archived and discovered rows of the rail as they already walked your own. Stepping onto an archived row opens its page and keeps the focus on the row.
49
+ - `toyon <file>` with a window already open shows the file there and brings the window forward instead of opening a second one, and the project picker takes a file as well as a folder.
50
+
51
+ ### Changed
52
+ - On the PR route the button reads create PR until a pull request exists, since open read as opening its page.
53
+ - The plan row in the transcript shows the plan's first heading, as the row above the composer does, and keeps the path in its tooltip and menu.
54
+ - The chats palette leads each group with the chat's name and the age of its newest hit, and shows the chat's ask beneath it on up to two lines.
55
+ - Project search stops at a page of results so a lockfile cannot fill it, and a new query or a closed palette cancels the running one. The footer says when not every match is shown. The symbol lookup on cmd+I stops reading once it has enough rows, so a common name no longer drains the whole project.
56
+ - A plain git command inside a landing or sync has a ten-minute ceiling. Before, one hung process could hold the project's lock and silently queue every later land or sync; now it is killed and the row says so.
57
+ - The daemon's log rotates at 10 MB, is readable only by you, opens each run with a line naming the version, and no longer prints the signed-in link with its token. Refusals, git failures and closed connections take one line each instead of a stack trace.
58
+ - Agent settings, hooks and plugin code are denied to the agent file by file instead of by folder, so its skills, commands and prompts are ordinary edits again. The refusal row names what it guards.
59
+ - Archiving the row you are on moves you to the nearest row above it that is waiting, done, unseen, working or has its app up, and only looks below when none qualifies, so old worktrees parked at the bottom of the rail are not pulled into view.
60
+ - While a commit or land runs, the commit box shows the step's line where the message was and keeps its height, so the foot does not jump. The typed message is held and clears only once the tree is clean, so a refused hook gives the words back to edit.
61
+ - Waiting lines in the files tree, history, design pane, terminal, boot and import views and the pickers show as live text instead of a spinner beside the words.
62
+ - The archived row's menu item reads delete instead of delete for good; the confirm dialog still spells out what goes with it.
63
+ - When a sync finds that the changes carried on the row no longer fit over main, it asks the agent to fix them and names the clashing files, instead of leaving a bare line on the row.
64
+
65
+ ### Fixed
66
+ - A reply containing a raw HTML tag such as `<title>` or `<script>` drew nothing after the tag; the tags now show as text.
67
+ - A table wider than the chat column scrolls inside itself, with a fade at the edge that has more behind it, instead of pushing the whole transcript sideways.
68
+ - Switching worktrees quickly could paint the previous chat's text in the new one for a moment.
69
+ - The caret in an empty composer was drawn behind the ghost text; it shows over it now.
70
+ - Sideways scrolling in the editor that ran off a line's end no longer triggers the browser's back or forward swipe.
71
+ - Clicking a tool row left a lone accent line across the top of the transcript on the next keypress; the focus ring now draws whole inside the row.
72
+ - The time tip on a daemon row followed the pointer across the next message; it sits above the word it belongs to.
73
+ - The Dock helper wrote the signed-in link, token included, to its launcher log. The link is printed only to a terminal now; a file gets the bare address and the token file's path.
74
+ - A landing mark could stay on a row after its record had moved on, and a failed Tailscale status check raised an unhandled error instead of reading as not running.
75
+
5
76
  ## 0.16.0 (2026-10-18)
6
77
 
7
78
  Streamed replies are written to the transcript a few characters at a time instead of a line at a time, the plan card follows the file Claude Code keeps itself, and the checks and preview in the PR line are links.
package/dist/cli.js CHANGED
@@ -2,12 +2,12 @@
2
2
  // @bun
3
3
 
4
4
  // src/cli.ts
5
- import { existsSync as existsSync11, statSync as statSync4 } from "fs";
5
+ import { existsSync as existsSync12, statSync as statSync5 } from "fs";
6
6
  import { join as join7, resolve } from "path";
7
7
  // package.json
8
8
  var package_default = {
9
9
  name: "toyon",
10
- version: "0.16.0",
10
+ version: "0.18.0",
11
11
  description: "Toyon is where coding agents work on your web app while you check each change by using it. Every chat gets its own running copy with a live preview.",
12
12
  license: "MIT",
13
13
  type: "module",
@@ -57,13 +57,13 @@ var package_default = {
57
57
 
58
58
  // src/app.ts
59
59
  import { spawn as spawn3, spawnSync } from "child_process";
60
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
60
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
61
61
  import { homedir as homedir2 } from "os";
62
62
  import { join as join3 } from "path";
63
63
 
64
64
  // src/daemon.ts
65
65
  import { spawn } from "child_process";
66
- import { existsSync as existsSync2, mkdirSync, openSync, readFileSync } from "fs";
66
+ import { existsSync as existsSync3, mkdirSync, readFileSync } from "fs";
67
67
  import { homedir } from "os";
68
68
  import { join as join2 } from "path";
69
69
 
@@ -76,6 +76,8 @@ var isTailnetName = (host) => host.endsWith(".ts.net");
76
76
  var APP_SCHEME = "toyon";
77
77
  var START_LINK = `${APP_SCHEME}://start`;
78
78
  var LSREGISTER = "/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister";
79
+ var FD_LIMIT_LOW = 4096;
80
+ var fdLimitLine = (n) => `open-file limit is ${n}; raise it with ulimit -n 8192 before toyon starts`;
79
81
  var DAEMON_FILES = {
80
82
  token: "token",
81
83
  helper: "Toyon.app",
@@ -5249,19 +5251,31 @@ function parseVersion(v) {
5249
5251
  return { nums: [Number(m[1]), Number(m[2]), Number(m[3])], pre: m[4] ?? null };
5250
5252
  }
5251
5253
 
5254
+ // ../shared/src/logfile.ts
5255
+ import { chmodSync, existsSync, openSync, renameSync, statSync } from "fs";
5256
+ var LOG_ROTATE_BYTES = 10 * 1024 * 1024;
5257
+ var previousLog = (path) => `${path}.1`;
5258
+ function openLog(path, maxBytes = LOG_ROTATE_BYTES) {
5259
+ if (existsSync(path) && statSync(path).size > maxBytes)
5260
+ renameSync(path, previousLog(path));
5261
+ const fd = openSync(path, "a", 384);
5262
+ chmodSync(path, 384);
5263
+ return fd;
5264
+ }
5265
+
5252
5266
  // src/layout.ts
5253
- import { existsSync, realpathSync } from "fs";
5267
+ import { existsSync as existsSync2, realpathSync } from "fs";
5254
5268
  import { delimiter, dirname, join } from "path";
5255
5269
  import { fileURLToPath } from "url";
5256
5270
  var here = dirname(fileURLToPath(import.meta.url));
5257
- var packaged = existsSync(join(here, "daemon.js"));
5271
+ var packaged = existsSync2(join(here, "daemon.js"));
5258
5272
  var method = installMethod(packaged ? join(here, "..", "package.json") : null);
5259
5273
  function installOnPath() {
5260
5274
  for (const dir of (process.env.PATH ?? "").split(delimiter)) {
5261
5275
  if (dir === "" || dir.includes("/_npx/"))
5262
5276
  continue;
5263
5277
  const bin = join(dir, "toyon");
5264
- if (!existsSync(bin))
5278
+ if (!existsSync2(bin))
5265
5279
  continue;
5266
5280
  try {
5267
5281
  return installMethod(realpathSync(bin));
@@ -5308,13 +5322,13 @@ async function daemonFetch(path, init = {}) {
5308
5322
  }
5309
5323
  }
5310
5324
  function readToken() {
5311
- if (!existsSync2(tokenFile))
5325
+ if (!existsSync3(tokenFile))
5312
5326
  return null;
5313
5327
  const t = readFileSync(tokenFile, "utf8").trim();
5314
5328
  return t === "" ? null : t;
5315
5329
  }
5316
5330
  function readPid() {
5317
- if (!existsSync2(pidFile))
5331
+ if (!existsSync3(pidFile))
5318
5332
  return null;
5319
5333
  const n = Number(readFileSync(pidFile, "utf8").trim());
5320
5334
  return Number.isInteger(n) && n > 0 ? n : null;
@@ -5329,7 +5343,7 @@ function alive(pid) {
5329
5343
  }
5330
5344
  async function startDaemon() {
5331
5345
  mkdirSync(home, { recursive: true });
5332
- const logFd = openSync(logFile, "a");
5346
+ const logFd = openLog(logFile);
5333
5347
  const child = spawn(process.execPath, ["run", daemonEntry], {
5334
5348
  detached: true,
5335
5349
  stdio: ["ignore", logFd, logFd],
@@ -5377,6 +5391,9 @@ var shellOrigins = ["http://toyon.localhost", `http://toyon.localhost:${port}`];
5377
5391
  function shellUrl(token, branded) {
5378
5392
  return `${branded ? shellOrigins[0] : shellOrigins[1]}/#token=${token}`;
5379
5393
  }
5394
+ function shellLine(url, tty = process.stdout.isTTY === true) {
5395
+ return tty ? `toyon: ${url}` : `toyon: ${url.replace(/#token=.*$/, "")}; token read from ${tokenFile}`;
5396
+ }
5380
5397
 
5381
5398
  // src/openUrl.ts
5382
5399
  import { spawn as spawn2 } from "child_process";
@@ -5397,16 +5414,16 @@ var CHROMIUMS = [
5397
5414
  var pwaCacheDir = join3(home, "pwa");
5398
5415
  function profilesOf(browser) {
5399
5416
  const root = join3(homedir2(), "Library", "Application Support", browser.dataDir);
5400
- if (!existsSync3(root))
5417
+ if (!existsSync4(root))
5401
5418
  return [];
5402
5419
  return readdirSync(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => join3(root, d.name));
5403
5420
  }
5404
5421
  function pwaStillInstalled(browser, id) {
5405
- return profilesOf(browser).some((p) => existsSync3(join3(p, "Web Applications", "Manifest Resources", id)));
5422
+ return profilesOf(browser).some((p) => existsSync4(join3(p, "Web Applications", "Manifest Resources", id)));
5406
5423
  }
5407
5424
  function findPwaId(browser) {
5408
5425
  const cache = join3(pwaCacheDir, `${browser.name.replace(/\W+/g, "-")}.id`);
5409
- if (existsSync3(cache)) {
5426
+ if (existsSync4(cache)) {
5410
5427
  const id = readFileSync2(cache, "utf8").trim();
5411
5428
  if (/^[a-p]{32}$/.test(id) && pwaStillInstalled(browser, id))
5412
5429
  return id;
@@ -5415,7 +5432,7 @@ function findPwaId(browser) {
5415
5432
  const re = new RegExp(`web_apps-dt-([a-p]{32})[\\s\\S]{0,16}?(?:${ids})`);
5416
5433
  for (const p of profilesOf(browser)) {
5417
5434
  const dir = join3(p, "Sync Data", "LevelDB");
5418
- if (!existsSync3(dir))
5435
+ if (!existsSync4(dir))
5419
5436
  continue;
5420
5437
  for (const f of readdirSync(dir)) {
5421
5438
  if (!/\.(log|ldb)$/.test(f))
@@ -5431,7 +5448,7 @@ function findPwaId(browser) {
5431
5448
  return null;
5432
5449
  }
5433
5450
  function openShell(daemon) {
5434
- console.log(`toyon: ${daemon.url}`);
5451
+ console.log(shellLine(daemon.url));
5435
5452
  const found = process.platform === "darwin" ? launchInstalledApp() : { kind: "none" };
5436
5453
  if (found.kind === "launched")
5437
5454
  return;
@@ -5441,6 +5458,41 @@ function openShell(daemon) {
5441
5458
  const where = defaultBrowserInstalls(readLinkHandlers()) ? "" : `open it in ${found.browser} to `;
5442
5459
  console.log(`tip: ${where}install Toyon as an app from the top bar; toyon then opens in its own window`);
5443
5460
  }
5461
+ function focusShell() {
5462
+ if (process.platform !== "darwin")
5463
+ return;
5464
+ const bundle = installedAppBundle();
5465
+ if (!bundle)
5466
+ return;
5467
+ const child = spawn3("open", [bundle], { stdio: "ignore" });
5468
+ child.on("error", () => {});
5469
+ child.unref();
5470
+ }
5471
+ function installedAppBundle() {
5472
+ const apps = join3(homedir2(), "Applications");
5473
+ const shelves = existsSync4(apps) ? readdirSync(apps, { withFileTypes: true }).filter((d) => d.isDirectory() && d.name.endsWith(".localized")).map((d) => join3(apps, d.name)) : [];
5474
+ for (const app of CHROMIUMS) {
5475
+ const id = findPwaId(app);
5476
+ if (!id)
5477
+ continue;
5478
+ const kept = profilesOf(app).map((p) => join3(p, "Web Applications", `_crx_${id}`));
5479
+ const hit = [...shelves, ...kept].flatMap(bundlesIn).find((b) => {
5480
+ try {
5481
+ return readFileSync2(join3(b, "Contents", "Info.plist"), "latin1").includes(id);
5482
+ } catch {
5483
+ return false;
5484
+ }
5485
+ });
5486
+ if (hit)
5487
+ return hit;
5488
+ }
5489
+ return null;
5490
+ }
5491
+ function bundlesIn(dir) {
5492
+ if (!existsSync4(dir))
5493
+ return [];
5494
+ return readdirSync(dir).filter((n) => n.endsWith(".app")).map((n) => join3(dir, n));
5495
+ }
5444
5496
  function launchInstalledApp() {
5445
5497
  let seen = null;
5446
5498
  for (const app of CHROMIUMS) {
@@ -5671,12 +5723,12 @@ policy
5671
5723
  // src/deploy/fly.ts
5672
5724
  import { randomBytes } from "crypto";
5673
5725
  import { Resolver } from "dns/promises";
5674
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, mkdtempSync, readFileSync as readFileSync4, rmSync, writeFileSync as writeFileSync3 } from "fs";
5726
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4, mkdtempSync, readFileSync as readFileSync4, rmSync, writeFileSync as writeFileSync3 } from "fs";
5675
5727
  import { tmpdir } from "os";
5676
5728
  import { join as join5 } from "path";
5677
5729
 
5678
5730
  // ../shared/src/managed-load.ts
5679
- import { readFileSync as readFileSync3, statSync } from "fs";
5731
+ import { readFileSync as readFileSync3, statSync as statSync2 } from "fs";
5680
5732
  import { userInfo } from "os";
5681
5733
  async function plutilJson(path) {
5682
5734
  const p = Bun.spawn(["plutil", "-convert", "json", "-o", "-", path], { stdout: "pipe", stderr: "pipe" });
@@ -5698,7 +5750,7 @@ async function loadManaged(opts = {}) {
5698
5750
  for (const s of sources) {
5699
5751
  let st;
5700
5752
  try {
5701
- st = statSync(s.path);
5753
+ st = statSync2(s.path);
5702
5754
  } catch {
5703
5755
  reads.push(s);
5704
5756
  continue;
@@ -5731,12 +5783,12 @@ function refusedByPolicy(verb, m) {
5731
5783
  }
5732
5784
 
5733
5785
  // src/deploy/context.ts
5734
- import { cpSync, existsSync as existsSync4, mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "fs";
5786
+ import { cpSync, existsSync as existsSync5, mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "fs";
5735
5787
  import { join as join4 } from "path";
5736
5788
  var bunVersion = package_default.dependencies.bun;
5737
5789
  function machineSources() {
5738
5790
  const dist = packaged ? here : join4(here, "..", "dist");
5739
- return existsSync4(join4(dist, "daemon.js")) ? { dist, cloud: join4(dist, "..", "cloud") } : null;
5791
+ return existsSync5(join4(dist, "daemon.js")) ? { dist, cloud: join4(dist, "..", "cloud") } : null;
5740
5792
  }
5741
5793
  function machinePackageJson() {
5742
5794
  const manifest = {
@@ -5841,7 +5893,7 @@ function secret(env, file) {
5841
5893
  if (fromEnv)
5842
5894
  return fromEnv;
5843
5895
  const path = join5(cloudDir, file);
5844
- return existsSync5(path) ? readFileSync4(path, "utf8").trim() || null : null;
5896
+ return existsSync6(path) ? readFileSync4(path, "utf8").trim() || null : null;
5845
5897
  }
5846
5898
  async function capture(bin, args, input) {
5847
5899
  const p = Bun.spawn([bin, ...args], {
@@ -5906,7 +5958,7 @@ async function up(cmd, bin) {
5906
5958
  throw new DeployError("could not tell which Fly region is closest; pass --region (`fly platform regions` lists them)");
5907
5959
  console.log(cmd.region ? `region ${region}` : `region ${region}, the closest to you; --region picks another`);
5908
5960
  mkdirSync4(cloudDir, { recursive: true, mode: 448 });
5909
- if (!existsSync5(tokenFile2(cmd.name)))
5961
+ if (!existsSync6(tokenFile2(cmd.name)))
5910
5962
  writeFileSync3(tokenFile2(cmd.name), randomBytes(32).toString("hex"), { mode: 384 });
5911
5963
  const token = readFileSync4(tokenFile2(cmd.name), "utf8").trim();
5912
5964
  const app = ["-a", cmd.name];
@@ -6014,7 +6066,7 @@ async function deploy(cmd) {
6014
6066
  await up(cmd, bin);
6015
6067
  else if (cmd.action === "destroy")
6016
6068
  return await destroy(cmd, bin);
6017
- else if (existsSync5(tokenFile2(cmd.name))) {
6069
+ else if (existsSync6(tokenFile2(cmd.name))) {
6018
6070
  await printLinks(cmd.name, readFileSync4(tokenFile2(cmd.name), "utf8").trim());
6019
6071
  } else {
6020
6072
  throw new DeployError(`${cmd.name} was not deployed from this machine; its token is not in ${cloudDir}`);
@@ -6029,7 +6081,7 @@ async function deploy(cmd) {
6029
6081
  }
6030
6082
 
6031
6083
  // src/doctor.ts
6032
- import { existsSync as existsSync6, statSync as statSync2 } from "fs";
6084
+ import { existsSync as existsSync7, statSync as statSync3 } from "fs";
6033
6085
 
6034
6086
  // src/sandboxDeps.ts
6035
6087
  import { readFileSync as readFileSync5 } from "fs";
@@ -6147,6 +6199,8 @@ async function doctor() {
6147
6199
  } else {
6148
6200
  const version = h.version ?? "?";
6149
6201
  lines.push(line(true, "daemon", `${version} at ${base}, pid ${h.pid ?? "?"}${h.branded ? ", also http://toyon.localhost" : ""}` + (h.lag ? `, event loop worst ${h.lag.max}ms${h.lag.maxCause ? ` (${h.lag.maxCause})` : ""}` : "")));
6202
+ if (h.fdLimit !== undefined && h.fdLimit < FD_LIMIT_LOW)
6203
+ lines.push(line(false, "limits", fdLimitLine(h.fdLimit)));
6150
6204
  if (h.remote) {
6151
6205
  lines.push(line(true, "remote", `https://${h.remote.host}/ through a TLS front on 127.0.0.1:${port}, previews at ${h.remote.previews}`));
6152
6206
  }
@@ -6189,9 +6243,11 @@ async function doctor() {
6189
6243
  lines.push(line(false, "shell", `could not fetch: ${e.message}`));
6190
6244
  }
6191
6245
  }
6192
- if (existsSync6(logFile)) {
6193
- const kb = Math.round(statSync2(logFile).size / 1024);
6194
- lines.push(line(true, "log", `${logFile} (${kb} KB)`));
6246
+ if (existsSync7(logFile)) {
6247
+ const kb = (p) => Math.round(statSync3(p).size / 1024);
6248
+ const before = previousLog(logFile);
6249
+ const kept = existsSync7(before) ? `, and ${kb(before)} KB kept from before` : "";
6250
+ lines.push(line(true, "log", `${logFile} (${kb(logFile)} KB${kept})`));
6195
6251
  }
6196
6252
  const width = Math.max(...lines.map((l) => l.label.length));
6197
6253
  for (const l of lines) {
@@ -6226,19 +6282,25 @@ async function helper(args) {
6226
6282
  return 1;
6227
6283
  if (plan.kind === "start")
6228
6284
  return 0;
6285
+ let shown = true;
6229
6286
  for (const path of plan.paths) {
6230
6287
  const res = await post("/open", daemon.token, { path });
6231
6288
  if (!res.ok)
6232
6289
  console.error(`could not open ${path}: ${await res.text()}`);
6290
+ else if (!(await res.json()).delivered)
6291
+ shown = false;
6233
6292
  }
6234
- openShell(daemon);
6293
+ if (shown)
6294
+ focusShell();
6295
+ else
6296
+ openShell(daemon);
6235
6297
  return 0;
6236
6298
  }
6237
6299
 
6238
6300
  // src/logs.ts
6239
- import { existsSync as existsSync7, readFileSync as readFileSync6, statSync as statSync3 } from "fs";
6301
+ import { existsSync as existsSync8, readFileSync as readFileSync6, statSync as statSync4 } from "fs";
6240
6302
  async function logs(opts) {
6241
- if (!existsSync7(logFile)) {
6303
+ if (!existsSync8(logFile)) {
6242
6304
  console.log(`no log yet at ${logFile}; it appears once \`toyon\` has started the daemon`);
6243
6305
  return opts.follow ? 0 : 1;
6244
6306
  }
@@ -6254,12 +6316,12 @@ async function logs(opts) {
6254
6316
  `);
6255
6317
  if (!opts.follow)
6256
6318
  return 0;
6257
- let offset = statSync3(logFile).size;
6319
+ let offset = statSync4(logFile).size;
6258
6320
  for (;; ) {
6259
6321
  await Bun.sleep(300);
6260
- if (!existsSync7(logFile))
6322
+ if (!existsSync8(logFile))
6261
6323
  continue;
6262
- const size = statSync3(logFile).size;
6324
+ const size = statSync4(logFile).size;
6263
6325
  if (size < offset)
6264
6326
  offset = 0;
6265
6327
  if (size === offset)
@@ -6280,17 +6342,17 @@ function confirm(question) {
6280
6342
  }
6281
6343
 
6282
6344
  // src/remote.ts
6283
- import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
6345
+ import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
6284
6346
 
6285
6347
  // ../shared/src/tailscale.ts
6286
- import { existsSync as existsSync8, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
6348
+ import { existsSync as existsSync9, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
6287
6349
  var dnsName = (raw) => (raw ?? "").replace(/\.$/, "").toLowerCase();
6288
6350
 
6289
6351
  class TailscaleError extends Error {
6290
6352
  }
6291
6353
  var MAC_APP_CLI = "/Applications/Tailscale.app/Contents/MacOS/Tailscale";
6292
6354
  function findTailscale() {
6293
- return Bun.which("tailscale") ?? (existsSync8(MAC_APP_CLI) ? MAC_APP_CLI : null);
6355
+ return Bun.which("tailscale") ?? (existsSync9(MAC_APP_CLI) ? MAC_APP_CLI : null);
6294
6356
  }
6295
6357
  var NOT_INSTALLED = "Tailscale is not installed here (no tailscale on PATH); https://tailscale.com/download";
6296
6358
  function tailscaleCli(bin = findTailscale()) {
@@ -6456,7 +6518,7 @@ async function turnRemoteOff(ts, daemonPort, file, was) {
6456
6518
 
6457
6519
  // src/remote.ts
6458
6520
  function saved() {
6459
- if (!existsSync9(remoteFile))
6521
+ if (!existsSync10(remoteFile))
6460
6522
  return null;
6461
6523
  return parseRemote(readFileSync7(remoteFile, "utf8"));
6462
6524
  }
@@ -6685,11 +6747,11 @@ async function restart() {
6685
6747
  }
6686
6748
 
6687
6749
  // src/uninstall.ts
6688
- import { existsSync as existsSync10, readFileSync as readFileSync8, rmSync as rmSync4 } from "fs";
6750
+ import { existsSync as existsSync11, readFileSync as readFileSync8, rmSync as rmSync4 } from "fs";
6689
6751
  import { join as join6 } from "path";
6690
6752
  function readState() {
6691
6753
  const file = join6(home, DAEMON_FILES.state);
6692
- if (!existsSync10(file))
6754
+ if (!existsSync11(file))
6693
6755
  return {};
6694
6756
  try {
6695
6757
  return JSON.parse(readFileSync8(file, "utf8"));
@@ -6718,7 +6780,7 @@ async function uninstall(opts) {
6718
6780
  const repo = repos.find((r) => r.id === w.repoId);
6719
6781
  console.log(` ${w.path} (worktree of ${repo?.path ?? "?"})`);
6720
6782
  }
6721
- if (existsSync10(helper))
6783
+ if (existsSync11(helper))
6722
6784
  console.log(` ${helper} (the hidden helper that starts Toyon from the Dock app)`);
6723
6785
  console.log("and stops the daemon if it is running.");
6724
6786
  console.log("it keeps: your repos, every branch Toyon made" + (branches.length ? ` (${branches.length} under toyon/)` : "") + ", and Toyon's settings in each repo (.toyon/ or toyon.json).");
@@ -6730,16 +6792,16 @@ async function uninstall(opts) {
6730
6792
  await stop();
6731
6793
  for (const w of worktrees) {
6732
6794
  const repo = repos.find((r) => r.id === w.repoId);
6733
- if (repo && existsSync10(repo.path)) {
6795
+ if (repo && existsSync11(repo.path)) {
6734
6796
  const ok = await git(repo.path, "worktree", "remove", "--force", w.path);
6735
- if (!ok && existsSync10(w.path))
6797
+ if (!ok && existsSync11(w.path))
6736
6798
  rmSync4(w.path, { recursive: true, force: true });
6737
6799
  await git(repo.path, "worktree", "prune");
6738
- } else if (existsSync10(w.path)) {
6800
+ } else if (existsSync11(w.path)) {
6739
6801
  rmSync4(w.path, { recursive: true, force: true });
6740
6802
  }
6741
6803
  }
6742
- if (existsSync10(helper)) {
6804
+ if (existsSync11(helper)) {
6743
6805
  try {
6744
6806
  await Bun.spawn([LSREGISTER, "-u", helper], { stdout: "ignore", stderr: "ignore" }).exited;
6745
6807
  } catch {}
@@ -6837,7 +6899,7 @@ async function restartOnto(version) {
6837
6899
  async function open2(cmd) {
6838
6900
  const explicit = cmd.path !== null;
6839
6901
  const target = resolve(cmd.path ?? process.cwd());
6840
- const register = explicit || existsSync11(join7(target, ".git"));
6902
+ const register = explicit || existsSync12(join7(target, ".git"));
6841
6903
  if (process.platform === "linux") {
6842
6904
  const missing = missingSandboxTools();
6843
6905
  const blocked = missing.length === 0 ? bwrapStartError() : null;
@@ -6849,7 +6911,7 @@ async function open2(cmd) {
6849
6911
  const daemon = await ensureDaemon();
6850
6912
  if (!daemon)
6851
6913
  return 1;
6852
- const file = explicit && existsSync11(target) && statSync4(target).isFile();
6914
+ const file = explicit && existsSync12(target) && statSync5(target).isFile();
6853
6915
  if (file) {
6854
6916
  const res = await post("/open", daemon.token, { path: target });
6855
6917
  if (!res.ok) {
@@ -6857,6 +6919,11 @@ async function open2(cmd) {
6857
6919
  return 1;
6858
6920
  }
6859
6921
  const opened = await res.json();
6922
+ if (opened.delivered) {
6923
+ console.log(`toyon: ${target} is open in Toyon`);
6924
+ focusShell();
6925
+ return 0;
6926
+ }
6860
6927
  console.log(opened.kind === "file" ? `toyon: opening ${target} in its project` : `toyon: opening ${target}`);
6861
6928
  } else if (register) {
6862
6929
  const res = await post("/register", daemon.token, { path: target });
@@ -6872,7 +6939,7 @@ async function start() {
6872
6939
  const daemon = await ensureDaemon();
6873
6940
  if (!daemon)
6874
6941
  return 1;
6875
- console.log(`toyon: ${daemon.url}`);
6942
+ console.log(shellLine(daemon.url));
6876
6943
  return 0;
6877
6944
  }
6878
6945
  async function run(cmd) {