ledung 1.0.0 → 1.2.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/CHANGELOG.md CHANGED
@@ -9,6 +9,15 @@ under today's date, creating the heading at the top if it is missing; the pre-pu
9
9
  that changes `src/`, `web/` (not `web/dist/`) or `scripts/` without one (a commit trailer `Changelog: none` says there
10
10
  is nothing user-facing in it).
11
11
 
12
+ ## 2026-11-02
13
+ - Started at sign-in on macOS or Linux, or from Ledung.app, Ledung now gets the same PATH as in your terminal, so Update Ledung, Ledung's GitHub features and the tools your sessions use work after a login too; a shell profile that hangs delays the start by about ten seconds instead of stopping it. Switch Start Ledung when I sign in off and on in Settings to get it (#464)
14
+ - Settings → About has an update channel, Public or Dev: on Dev an npm-installed Ledung gets dev builds before they go to everyone, and Update & restart now checks npm and updates an npm install with one click (#461)
15
+
16
+ ## 2026-11-01
17
+ - On the phone, a linked computer's tab keeps its Settings → Look choices (theme, typing, sidebar) instead of losing them (#459)
18
+ - `ledung` and the Ledung icon start Ledung in the background with no window, so no terminal has to stay open; Stop Ledung in Settings → Update & restart or `ledung stop` stops it, Start Ledung when I sign in (off unless switched on) starts it with Windows, macOS or Linux, its output goes to ledung.log in the data folder, and `ledung --foreground` runs it in the terminal as before (#456)
19
+ - Claude Code is now also found where WinGet puts it, through a PATH an installer changed while Ledung ran (Windows), and, when nothing else is installed, as the Claude desktop app's own copy, which runs when it is new enough; when that copy is too old, the banner says so and gives the command to install Claude Code. The banner and Settings → Claude Code list every place Ledung looked. Check again shows "Checking…" and then "Checked again at …: still the same" when nothing changed, and a Claude Code installed while Ledung runs clears the banner without a restart (#455)
20
+
12
21
  ## 2026-10-30
13
22
  - The npm package is ready for its first publish: no longer marked private, and npm's page for it links to the public repository and its issues, with Project Ledung <martin@projectledung.com> as its author (#340)
14
23
  - Push notifications with several computers: switch push on once, in the first tab's Settings → Phones, and the phone also gets every linked computer's new Waiting-for-you items, each titled with that computer's name; tapping one opens that computer's tab. Another computer's tab now says pushes for it come through the first tab, instead of a switch the browser blocks there (#451)
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Project Ledung
2
2
 
3
- Ledung is a local program that runs several Claude Code sessions and shows them in one place: the sessions and who started them, every question waiting for an answer, permission requests, token usage and machine load. It serves a web page on localhost, which you can install as an app window from Edge or Chrome.
3
+ Ledung runs several Claude Code sessions on your computer and shows them in one place: the sessions and who started them, every question waiting for an answer, permission requests, token usage and machine load. It serves a web page on your own computer, which you can open in a browser or install as an app window. With a relay, your phone can reach it from anywhere.
4
4
 
5
- It is in step 1: one session at a time per project, without VS Code. The plan and its steps are in [docs/brief.md](docs/brief.md); decisions made since are in [docs/decisions.md](docs/decisions.md); the current step is in [docs/steps/](docs/steps/); what changed and when is in [CHANGELOG.md](CHANGELOG.md), where every PR that changes what Ledung does adds a bullet (the pre-push hook refuses a push of program changes without one).
5
+ It runs on Windows, macOS and Linux.
6
6
 
7
7
  ## Requirements
8
8
 
@@ -11,86 +11,78 @@ It is in step 1: one session at a time per project, without VS Code. The plan an
11
11
  - Claude Code installed ([how](https://code.claude.com/docs/en/setup)) and signed in: Ledung runs your own Claude Code
12
12
  - Docker Desktop, only for projects that live in dev containers
13
13
 
14
- **Anthropic's software.** Ledung runs Anthropic's Claude Agent SDK and Claude Code. They are installed from npm under [Anthropic's own terms](https://code.claude.com/docs/en/legal-and-compliance) and are not covered by Ledung's license (see [License](#license)). The SDK comes with Ledung's `npm install`. **Sessions run your own installed Claude Code** (`claude` on the PATH, or where its installers put it: `~/.local/bin`, npm's global folder, Homebrew), with your own sign-in. It must be at least the version the SDK was built for (Settings → Claude Code shows both; `claude update` updates it). If it is missing, older, or its version can't be read, a banner says how to install or update it, and sessions don't start until **Check again** finds it. **A project in a container needs Claude Code installed in that container**: Ledung starts the container's own `claude` there and doesn't install it for you.
15
-
16
- ## Install from npm
17
-
18
- **Not published yet**: the `ledung` package is built (`npm run pack`, below) but not on the npm registry. Once it is:
14
+ ## Install and first start
19
15
 
20
16
  ```sh
21
17
  npm install -g ledung
22
- ledung # starts Ledung and opens the page, signed in; ledung --no-open doesn't open it
18
+ ledung # starts Ledung in the background and opens the page, signed in
23
19
  ```
24
20
 
25
- The first start creates the user data folder (`%APPDATA%\Ledung` on Windows, `~/Library/Application Support/Ledung` on macOS, `~/.local/share/ledung` on Linux) and a default config in it, `ledung.config.json` (`LEDUNG_CONFIG` points elsewhere). Projects and accounts are added in the page; the other settings are described in `ledung.config.example.json` next to the package's `package.json`. **Restart Ledung** works as with a release, and sessions keep running through it. To update, run `npm install -g ledung` again and press **Restart Ledung**; the page's Update panel says so instead of pulling from git. Sessions run your installed Claude Code, not a program inside the package, so they don't stand in the update's way.
21
+ Ledung runs in the background with no window: `ledung` prints the page's address and returns, and the terminal can be closed. Its output goes to `ledung.log` in the user data folder (Settings → Update & restart → Log shows where). `ledung --no-open` starts it without opening the page, `ledung --foreground` runs it in the terminal instead (for development and debugging), and `ledung --version` prints the version. Running `ledung` while Ledung already runs opens its page.
26
22
 
27
- **The package installs exactly the dependency versions Ledung was tested with.** Its `package.json` names the lockfile's versions, and an `npm-shrinkwrap.json` made from this folder's `package-lock.json` pins everything below them, so the Agent SDK, and the Claude Code version it needs, are the ones `npm run check` ran. The shrinkwrap leaves out the Agent SDK's per-platform copies of Claude Code: npm would install all eight of them on every computer. So an npm-installed Ledung has no Claude Code of its own, and `"claudeExecutable": "bundled"` says so; it runs your installed one.
23
+ The first start creates the user data folder (`%APPDATA%\Ledung` on Windows, `~/Library/Application Support/Ledung` on macOS, `~/.local/share/ledung` on Linux) and a default config in it, `ledung.config.json` (the `LEDUNG_CONFIG` environment variable points elsewhere). Then, in the page:
28
24
 
29
- To build the package from this folder: `npm run pack` builds the page, bundles the core, the session host and the `ledung` command to plain JavaScript with esbuild (Node doesn't strip types under `node_modules`), and packs `dist/package/` into `dist/ledung-<version>.tgz`, which `npm install -g ./dist/ledung-<version>.tgz` installs (the `./` matters: without it npm reads `dist/…` as a GitHub repository). The package's own `package.json` leaves out this folder's scripts and devDependencies, and pins the dependencies (above); a `package-lock.json` out of step with `package.json` stops the build. The package is ready to publish (it is no longer marked private); publishing it, `npm publish ./dist/ledung-<version>.tgz`, is the owner's. The publishing npm account's e-mail is public (`npm view ledung maintainers`), so publish from the account on martin@projectledung.com, the address the package names as its author.
25
+ 1. Open **Settings**, the ⚙ at the bottom left.
26
+ 2. Add a Claude account under **Claude & usage → Accounts**, and sign in to it with the command the page shows.
27
+ 3. Add a project with **Add project**: a folder on this computer or in a running container, or a brand-new one.
30
28
 
31
- ## Setup
29
+ **Updating.** In Settings → Update & restart, **Check for updates** asks npm for a newer version and **Update Ledung** installs it and restarts; by hand, run `npm install -g ledung` again, then press **Restart Ledung**. Sessions keep running through the restart, and since they run your installed Claude Code, not a program inside the package, they don't stand in the update's way. The package installs exactly the dependency versions Ledung was tested with, and has no Claude Code of its own: it runs the one you installed.
32
30
 
33
- ```sh
34
- npm install
35
- npm run check # type check and tests
36
- ```
31
+ **Dev builds.** Builds made between public releases are published to npm's `dev` tag: `npm install -g ledung@dev` installs the newest, and **Update channel: Dev** in Settings → About makes Check for updates and Update Ledung follow them (Public, the default, follows public releases only). `npm install -g ledung` always gets the public release. A public release is a plain number (v1, v2), a dev build after it adds one (v1.1, v1.2).
37
32
 
38
- **Every push runs `npm run check` first.** `npm install` (or `npm ci`) switches on the git hook in `.githooks/pre-push` for the clone and all its worktrees, by setting git's `core.hooksPath`; a failing check stops the push. A push that only deletes a branch, or carries no commits the remote doesn't already have, skips the check. The hook is not switched on outside a git repository, nor when `core.hooksPath` already points somewhere else. `git push --no-verify` skips the check for an emergency; that is the owner's call, never a session's.
33
+ **Stopping.** **Stop Ledung** (Settings → Update & restart, on the computer Ledung runs on) or `ledung stop` in a terminal stops Ledung and its sessions; started with `ledung --foreground`, Ctrl+C or closing its window does the same. Open a session and press **Resume** to continue it.
39
34
 
40
- Ledung reads its settings from `ledung.config.local.json` in the repository root. That file is ignored by Git, because it holds project paths, container names and project-specific rules. Copy `ledung.config.example.json` to `ledung.config.local.json` and fill it in.
35
+ **Starting with your computer.** **Start Ledung when I sign in to Windows** (Settings → Update & restart → Start at sign-in, also offered next to the icon question) starts Ledung in the background when you sign in, without opening the page. It is off until you switch it on, is only for you and needs no administrator rights: a shortcut in your Startup folder on Windows, a LaunchAgent on macOS, an autostart entry on Linux desktops.
41
36
 
42
- **Changes to the config apply without a restart**, with a few exceptions. Ledung watches the file and reads it again a moment after it is saved (also by an editor that writes a temporary file and renames it), checked as at start. The settings apply at once, to every session started or resumed after that; running sessions keep what they started with. A shorter `keepToolResultsDays` shrinks old tool output straight away. Needing **Restart Ledung**: `port`, `lan`, `lanSignIn`, `remote`, `relayJoin`, `computers`, `board` and `userName`, which the server, the relay link, the boards and the rest take at start; Settings → Config file and Restart Ledung then say "Restart Ledung to apply: …". A file that isn't valid JSON or has a wrong setting is refused whole: the settings in use stay, and a banner and Settings → Config file say what is wrong (without the file's text). Settings → Config file also shows when the file was last read. Which setting is which is kept in one place, `SETTING_RELOAD` in `src/config.ts`.
37
+ ## The page
43
38
 
44
- **Projects and Claude accounts are added in the page** (**Add project** and **Accounts**, both in Settings: the ⚙ at the bottom left), not in the config: an existing folder on this computer or in a running container, picked with Ledung's folder browser, or a brand-new project (a folder with git, and optionally a private GitHub repository). Each project has one manager and one Claude account. The folder browser shows only folders under `browseRoots` (default: your home folder); new projects go in `projectsFolder` (default: `Projects` in your home folder). Projects still listed under `"projects"` in the config are copied into Ledung once.
39
+ **An app window.** In Edge or Chrome, a notice at the top of the page offers **Install as app**, which opens the browser's own install dialog (**Not now** hides the notice in that browser for good); in Safari on macOS it says to use **File → Add to Dock**. **Settings → Look & feel → App** always has the same, or says why this browser can't install the page (plain http on the local network, or a browser that can't install pages). Neither shows once the page runs as an app.
45
40
 
46
- **Accounts can also be written in the config file**, under `"claudeAccounts"`, by name, with plain fields: `whatItIsFor` (shown in the page), `signInFolderOnThisComputer` and `signInFolderInContainers`, all optional; `ledung.config.example.json` explains each and has a worked example. Nothing secret goes there. Ledung adds the accounts it doesn't have, at its start and whenever the file changes, and gives an account it has the folders the block names; a problem (a folder another account uses, say) shows in Settings → Accounts, and a wrongly written block is refused like any other config error. Removing an account stays in the page, since it also signs the account out; one still in the block comes back at the next start. Every account's sign-in folder on this computer holds an `ABOUT THIS FOLDER.txt`: whose folder it is and the exact command to sign in to it.
41
+ **A Ledung icon.** The first time the page opens on a computer it asks once whether to add a Ledung icon to your Desktop and Start menu (on macOS, to Applications); **Settings → Update & restart → Shortcuts** adds or removes it later. The icon opens the page, and starts Ledung first if it isn't running, in the background with no window.
47
42
 
48
- **Signing in.** Ledung never takes, keeps or passes a Claude sign-in token: signing in is done by Claude Code itself (Anthropic's terms allow that, not a product collecting tokens). An account is only a sign-in: each has a sign-in folder, and its sessions get it in `CLAUDE_SECURESTORAGE_CONFIG_DIR`, a Claude Code setting (not in its documentation; the SDK sets it itself) that moves only the sign-in there. Conversations, memory, skills and settings stay in the Claude Code folder every session uses, so moving a project to another account changes only whose plan and limits it uses. You sign in to an account once with Claude Code's own `/login`, using the command the page shows. For a container project, the account's sign-in folder is inside the container (`~/.ledung-accounts/<name>` by default, or a folder you name, which may be a mounted one so the sign-in survives a rebuild) and you sign in there. Ledung asks Claude Code's `claude auth status` whether each is signed in (no tokens spent). Removing an account removes its own sign-in from Claude Code's store (its sign-in file, or its Keychain entry on macOS) here and in every container its projects ran in, skipping a folder another login shares, and leaves the folders. Ledung doesn't use `claude auth logout` for this: in the shared layout it would also clear the account details of the computer's own login. An account can't use Claude Code's default folder, Ledung's own, or another account's. Token accounts from before are moved once, with no new sign-in: the token is put into Claude Code's sign-in store for the account (a file in its sign-in folder, or the Keychain on macOS), checked, and Ledung's copy is deleted; a place added later is signed in with `/login`. That move is removed before Ledung goes public. A login-folder account made before this change keeps its sign-in, but not the conversations, skills and settings it kept in that folder.
43
+ **Sessions survive a restart.** Each session runs in a small **session host**, a separate process that owns Claude Code and keeps it running while Ledung restarts. If Ledung crashes and doesn't come back within 3 minutes, the hosts stop their sessions themselves. Host logs are in `<user data>/hosts/`.
49
44
 
50
- **A workflow by default** (optional): if you run your projects under a written process, a `"workflow"` block in the config names its folder, the file a manager reads first (`managerFile`), the file every other session reads first (`sessionFile`), and where each project keeps its own settings for it (`projectConfig`, inside the project folder). Every session started or resumed after that is told to read its file and where the project's config is; **Add project** warns when that config is missing. A project can switch the workflow off or name its own config path in its settings; projects in a container are off unless switched on, since they can't see this computer's folders. Without the block, sessions get no such instructions.
45
+ ## Claude Code and Anthropic's software
51
46
 
52
- **Several workflows** (optional): a `"workflows"` list in the config holds more, each with a `name` and the same four settings. The single `"workflow"` block, if there is one, comes first and is named `default` (give it a `"name"` to change that). **Add project** and a project's settings then have a **Workflow** list: None, or one of them by name; a new project starts on the first on this computer and on None in a container, and the project-config field and the missing-config warning follow the choice. A manager's `add_project` and `create_project` take `workflow` too (a name, or `"none"`). A project from before keeps the workflow it had; one whose workflow has been taken out of the config follows none until another is chosen. A project added from the page or by a manager stores its workflow's name, so one added on the single block stores `default`: giving that block a `"name"` later drops such projects to none until they choose again.
47
+ Ledung runs Anthropic's Claude Agent SDK and Claude Code. They are installed from npm under [Anthropic's own terms](https://code.claude.com/docs/en/legal-and-compliance) and are not covered by Ledung's license (see [License](#license)). The SDK comes with Ledung's install.
53
48
 
54
- ## Running Ledung
49
+ **Sessions run your own installed Claude Code** (`claude` on the PATH, or where its installers put it: `~/.local/bin`, npm's global folder, Homebrew), with your own sign-in. It must be at least the version the SDK was built for (**Settings → Claude & usage → Claude Code** shows both; `claude update` updates it). If it is missing, older, or its version can't be read, a banner says how to install or update it, and sessions don't start until **Check again** finds it. **A project in a container needs Claude Code installed in that container**: Ledung starts the container's own `claude` there and doesn't install it for you.
55
50
 
56
- Ledung runs from a separate copy, never from this folder, so a session can edit Ledung's source without breaking the Ledung it runs in.
51
+ **Signing in.** Ledung never takes, keeps or passes a Claude sign-in token: signing in is done by Claude Code itself (Anthropic's terms allow that, not a product collecting tokens). An account is only a sign-in: each has a sign-in folder, and its sessions get it in `CLAUDE_SECURESTORAGE_CONFIG_DIR`, a Claude Code setting (not in its documentation; the SDK sets it itself) that moves only the sign-in there. Conversations, memory, skills and settings stay in the Claude Code folder every session uses, so moving a project to another account changes only whose plan and limits it uses. You sign in to an account once with Claude Code's own `/login`, using the command the page shows. For a container project, the account's sign-in folder is inside the container (`~/.ledung-accounts/<name>` by default, or a folder you name, which may be a mounted one so the sign-in survives a rebuild) and you sign in there. Ledung asks Claude Code's `claude auth status` whether each is signed in (no tokens spent). Removing an account removes its own sign-in from Claude Code's store (its sign-in file, or its Keychain entry on macOS) here and in every container its projects ran in, skipping a folder another login shares, and leaves the folders. Ledung doesn't use `claude auth logout` for this: in the shared layout it would also clear the account details of the computer's own login. An account can't use Claude Code's default folder, Ledung's own, or another account's. Token accounts from older Ledungs are moved once, with no new sign-in: the token is put into Claude Code's sign-in store for the account (a file in its sign-in folder, or the Keychain on macOS), checked, and Ledung's copy is deleted; a place added later is signed in with `/login`. A login-folder account made before that change keeps its sign-in, but not the conversations, skills and settings it kept in that folder.
57
52
 
58
- ```sh
59
- npm run release # check, build, and copy to <user data>/app/<timestamp>
60
- ```
53
+ **Projects and Claude accounts are added in the page** (**Add project**, and **Accounts** under Claude & usage, both in Settings), not in the config: an existing folder on this computer or in a running container, picked with Ledung's folder browser, or a brand-new project (a folder with git, and optionally a private GitHub repository). Each project has one manager and one Claude account. The folder browser shows only folders under `browseRoots` (default: your home folder); new projects go in `projectsFolder` (default: `Projects` in your home folder). Projects still listed under `"projects"` in the config are copied into Ledung once.
61
54
 
62
- Then start it with `%APPDATA%\Ledung\Ledung.cmd` on Windows, or `ledung.sh` in the user data folder on macOS and Linux. It opens the page in your browser and signs it in. The page can run as an app, in a window of its own: in Edge or Chrome, a notice at the top offers **Install as app**, which opens the browser's own install dialog (**Not now** hides the notice in that browser for good); in Safari on macOS it says to use **File → Add to Dock**. **Settings → App** always has the same, or says why this browser can't install the page (plain http on the local network, or a browser that can't install pages). Neither shows once the page runs as an app.
55
+ ## From your phone
63
56
 
64
- A running Ledung keeps its release until it is restarted. **Restart Ledung** (in Settings, the ⚙ at the bottom left of the page) loads the newest release; sessions keep running through it, even mid-turn. Closing Ledung (Ctrl+C, or closing its console window) stops its sessions; open one and press **Resume** to continue it.
57
+ ### On the local network
65
58
 
66
- **Update Ledung**, also in Settings, does the same from the page: **Check for updates** fetches `origin/main` in the folder the running release was built from and lists the changelog lines since; **Update Ledung** then runs `git pull --ff-only`, `npm install` and `npm run release` there and restarts. It refuses, and leaves the running Ledung alone, when that folder has uncommitted changes, is not on `main`, can't fast-forward, or a step fails.
59
+ Set `"lan"` in the config to this computer's address on the local network (for example `"192.168.1.20"`), and Ledung also listens there, and answers to the computer's name. `"lan": true` listens on every network the computer is on, including VPNs; prefer the single address. By default other computers sign in once with a link Ledung prints; `"lanSignIn": false` drops that, so **anyone on that network can use Ledung**, and through it run commands on this computer. Only for a network you alone use. The connection is plain HTTP. Windows Firewall must allow incoming connections on the port.
67
60
 
68
- Each session runs in a small **session host** (`src/host/`), a separate process that owns Claude Code and keeps it running while Ledung restarts. If Ledung crashes and doesn't come back within 3 minutes, the hosts stop their sessions themselves. Host logs are in `<user data>/hosts/`.
61
+ ### From anywhere, through a relay
69
62
 
70
- ### From another computer on the local network
63
+ A relay ([relay/README.md](relay/README.md)) lets your phone reach Ledung from anywhere, end to end encrypted. If whoever runs the relay sends you a **join code**, paste it in **Settings → Remote & devices → Remote access → Join with a code**: Ledung makes its own secret and certificate account, joins and connects at once, with no config edit and no restart (its settings are kept in the data folder's `remote` folder). Once the relay's admin has approved it, Ledung gets its certificate within a minute; then pair your phone in **Settings → Remote & devices → Phones**, where you can also switch on push notifications. **Leave this relay** undoes it. A `"remote"` block in the config file does the same by hand, and while it is there it wins: Join and Leave are off. If you run a relay yourself, **Settings → Remote & devices → Relay** invites other people's computers to it (relay/README.md, Invite a computer).
71
64
 
72
- Set `"lan"` in the config to this computer's address on the local network (for example `"192.168.1.20"`), and Ledung also listens there, and answers to the computer's name. `"lan": true` listens on every network the computer is on, including VPNs; prefer the single address. By default other computers sign in once with a link Ledung prints; `"lanSignIn": false` drops that, so **anyone on that network can use Ledung**, and through it run commands on this computer. Only for a network you alone use. The connection is plain HTTP. Windows Firewall must allow incoming connections on the port.
65
+ ## Config
73
66
 
74
- ### From anywhere, through a relay
67
+ The config file is `ledung.config.json` in the user data folder (see [Install and first start](#install-and-first-start)); **Settings → Config file** shows where it is, when it was last read and any problem in it. Every setting is described in `ledung.config.example.json`, which comes with the package (next to its `package.json`) and is in this repository. Most people need none of it at first: projects and accounts are added in the page.
75
68
 
76
- A relay ([relay/README.md](relay/README.md)) lets your phone reach Ledung from anywhere, end to end encrypted. If whoever runs the relay sends you a **join code**, paste it in Settings → Remote → **Join with a code**: Ledung makes its own secret and certificate account, joins and connects at once, with no config edit and no restart (its settings are kept in the data folder's `remote` folder). Once the relay's admin has approved it, Ledung gets its certificate within a minute; then pair your phone in Settings → Devices. **Leave this relay** undoes it. A `"remote"` block in the config file does the same by hand, and while it is there it wins: Join and Leave are off. If you run a relay yourself, Settings → Relay invites other people's computers to it (relay/README.md, Invite a computer).
69
+ **Changes to the config apply without a restart**, with a few exceptions. Ledung watches the file and reads it again a moment after it is saved (also by an editor that writes a temporary file and renames it), checked as at start. The settings apply at once, to every session started or resumed after that; running sessions keep what they started with. A shorter `keepToolResultsDays` shrinks old tool output straight away. Needing **Restart Ledung**: `port`, `lan`, `lanSignIn`, `remote`, `relayJoin`, `computers`, `board` and `userName`, which the server, the relay link, the boards and the rest take at start; Settings → Config file and Restart Ledung then say "Restart Ledung to apply: …". A file that isn't valid JSON or has a wrong setting is refused whole: the settings in use stay, and a banner and Settings → Config file say what is wrong (without the file's text).
77
70
 
78
71
  `"maxWorkingSessions"` (default 10) is how many sessions may work at the same time; above it, work waits in line before its turn starts. Managers don't count. It can be changed live in the Machine panel, and a waiting session can be started at once with **Start now**.
79
72
 
80
73
  `"remoteControl"` (default true): each manager turns on Claude Code's Remote Control after its first reply, so it shows in the Claude app on a phone as "<project> lead", and its page shows the link. It comes back when the manager is resumed or Ledung restarts, and goes off when the manager stops. Other sessions never get it. `false` turns it off for every manager.
81
74
 
82
- `"claudeExecutable"` (default `"installed"`): the Claude Code sessions on this computer run. `"installed"` is your own `claude`, found on the PATH or in the usual install folders (the native installer's `~/.local/bin`, npm's global folder, Homebrew's); a full path names another one. Either must be at least the version the Agent SDK was built for; one that is missing, older, or whose `claude --version` can't be read is never run: a banner says how to install or update it, and a session start is refused with the same words. `"bundled"` runs the copy of Claude Code the Agent SDK installs with Ledung instead, only when set here, for testing or a computer where Claude Code can't be installed; not in a Ledung installed from npm, whose package leaves that copy out. **Settings → Claude Code** shows what new sessions run (path and version) and what each running session was started with; a running session keeps its program until it stops, and a resume looks again. Container projects always run the container's own `claude`.
75
+ `"claudeExecutable"` (default `"installed"`): the Claude Code sessions on this computer run. `"installed"` is your own `claude`, found on the PATH or in the usual install folders (the native installer's `~/.local/bin`, npm's global folder, Homebrew's); a full path names another one. Either must be at least the version the Agent SDK was built for; one that is missing, older, or whose `claude --version` can't be read is never run: a banner says how to install or update it, and a session start is refused with the same words. `"bundled"` runs the copy of Claude Code the Agent SDK installs with Ledung instead, only when set here, for testing or a computer where Claude Code can't be installed; not in a Ledung installed from npm, whose package leaves that copy out. **Settings → Claude & usage → Claude Code** shows what new sessions run (path and version) and what each running session was started with; a running session keeps its program until it stops, and a resume looks again. Container projects always run the container's own `claude`.
83
76
 
84
77
  `"windowsShell"` (default `"bash"`): the shell a `!` line in the message box runs in on Windows. `"bash"` is Git Bash, as Claude's Bash tool uses (found next to `git`, or at `CLAUDE_CODE_GIT_BASH_PATH`); `"powershell"` is Windows PowerShell. On macOS and Linux it is always bash, and in a container project the container's own shell.
85
78
 
86
- For development, `npm start` runs Ledung straight from this folder (use another `port` in a separate config via `LEDUNG_CONFIG`, and `LEDUNG_DATA_DIR` for separate data, if a released Ledung is running too). `npm run build` rebuilds the page.
79
+ **Accounts can also be written in the config file**, under `"claudeAccounts"`, by name, with plain fields: `whatItIsFor` (shown in the page), `signInFolderOnThisComputer` and `signInFolderInContainers`, all optional; `ledung.config.example.json` explains each and has a worked example. Nothing secret goes there. Ledung adds the accounts it doesn't have, at its start and whenever the file changes, and gives an account it has the folders the block names; a problem (a folder another account uses, say) shows in Settings → Accounts, and a wrongly written block is refused like any other config error. Removing an account stays in the page, since it also signs the account out; one still in the block comes back at the next start. Every account's sign-in folder on this computer holds an `ABOUT THIS FOLDER.txt`: whose folder it is and the exact command to sign in to it.
87
80
 
88
- ### Elevated commands on Windows
81
+ ### Workflows
89
82
 
90
- A session that needs administrator rights runs the command through an elevation tool: [gsudo](https://github.com/gerardog/gsudo) (`gsudo <command>`, the output comes back to the session), Windows 11's own `sudo`, or `Start-Process <program> -Verb RunAs`. Each raises Windows' UAC prompt on the PC's desktop, so it works only when you are at the PC; from a phone the command waits for a click until its time limit. Such a call passes two checks: Ledung's permission prompt first (gsudo, sudo, `-Verb RunAs` and `runas` are among the suggested sensitive patterns, so they are asked in every permission mode, also through the PowerShell tool), then UAC. The prompt says it raises UAC and offers **Deny: not at the PC**, and while the command runs the status line says it may be waiting for UAC on the PC. **Stop** may not end what runs as administrator: a process that is not elevated is not allowed to end one that is, so close it on the PC.
83
+ **A workflow by default** (optional): if you run your projects under a written process, a `"workflow"` block in the config names its folder, the file a manager reads first (`managerFile`), the file every other session reads first (`sessionFile`), and where each project keeps its own settings for it (`projectConfig`, inside the project folder). Every session started or resumed after that is told to read its file and where the project's config is; **Add project** warns when that config is missing. A project can switch the workflow off or name its own config path in its settings; projects in a container are off unless switched on, since they can't see this computer's folders. Without the block, sessions get no such instructions.
91
84
 
92
- - **Never run Ledung or a session host as administrator.** Every session, MCP server and hook would then be administrator without any UAC prompt; Ledung's permission checks are Claude Code rules, not Windows security.
93
- - **Never turn gsudo's credential cache on** (`gsudo cache on`, or a `CacheMode` of `Auto`). Later elevations would skip UAC, so any session could elevate without anyone at the PC. Keep `CacheMode` at `Explicit` (the default) or `Disabled`.
85
+ **Several workflows** (optional): a `"workflows"` list in the config holds more, each with a `name` and the same four settings. The single `"workflow"` block, if there is one, comes first and is named `default` (give it a `"name"` to change that). **Add project** and a project's settings then have a **Workflow** list: None, or one of them by name; a new project starts on the first on this computer and on None in a container, and the project-config field and the missing-config warning follow the choice. A manager's `add_project` and `create_project` take `workflow` too (a name, or `"none"`). A project from before keeps the workflow it had; one whose workflow has been taken out of the config follows none until another is chosen. A project added from the page or by a manager stores its workflow's name, so one added on the single block stores `default`: giving that block a `"name"` later drops such projects to none until they choose again.
94
86
 
95
87
  ### Session profiles
96
88
 
@@ -117,40 +109,12 @@ A workflow can remind its sessions of a rule at the moment it applies, such as c
117
109
 
118
110
  Managers can post to managers in other people's Ledungs on a shared board, a GitHub repository (see [docs/steps/message-board.md](docs/steps/message-board.md)). It is set **per project**, in the project's settings in the page: on or off (off by default), the board repository (`owner/name`), who may post to that project's manager (picked from the people with access to the repository, or typed), the channels it follows, and whether it is the **front desk** that gets posts addressed to just `@<your login>`. Several projects may share a repository, or each have its own; each repository is polled once. Everyone with access to a repository can read every post on it, so give a sensitive project a repository of its own. All boards post as this computer's `gh` login (`gh auth login`). The config's `"board"` block only holds the limits (`pollSeconds`, `postsPerHour`, `wakesPerThreadPerHour`, `burstSeconds`); a block from before with `repo`, `allow` and `frontDesk` is moved into the projects that have the board on (and the front-desk project) once, on the next start.
119
111
 
120
- ## Layout
121
-
122
- | Folder | What it holds |
123
- |---|---|
124
- | `src/host/` | The session host, which keeps a session running while Ledung restarts, and Ledung's connection to it |
125
- | `src/runners/` | How a session's process is started and stopped: locally, or in a container through `docker exec` |
126
- | `src/sessions/` | Driving sessions through the Agent SDK; permission prompts |
127
- | `src/store/` | The SQLite database: sessions, every message, permission decisions |
128
- | `src/server/` | The page's API and live connection, and who may use them |
129
- | `src/platform/` | Everything that differs per operating system |
130
- | `src/shared/` | Types shared by the core and the page |
131
- | `web/` | The page (React, built with Vite) |
132
- | `scripts/` | The release script, the npm package build, the one-time data move, the public export's dry run, and the setup that switches on the git hooks |
133
- | `site/` | The one-page site for projectledung.com: static, no build step (see its README) |
134
- | `.githooks/` | The pre-push hook: a push runs `npm run check` first |
135
-
136
- ## Cross-platform rules
112
+ ## Elevated commands on Windows
137
113
 
138
- Ledung runs on Windows first, and also on macOS and Linux. Every change follows these rules.
139
-
140
- - **Paths.** Build paths with Node's `path` utilities. Never write `\`, `/` or drive letters by hand.
141
- - **User data.** Keep user data in the platform's usual place (`%APPDATA%` on Windows, `~/Library/Application Support` on macOS, the XDG folders on Linux), behind one helper.
142
- - **Processes.** Everything about starting and stopping processes stays inside the runners: binary names such as `claude.cmd` versus `claude`, argument quoting, and stopping a whole process tree.
143
- - **Machine measurement.** CPU and memory measurement sits behind one module, with a version per platform.
144
- - **Line endings.** All files use LF; `.gitattributes` enforces this.
145
- - **File names.** Never rely on file names that differ only by case.
146
- - **CI.** Off for now (2026-10-29): GitHub Actions won't start jobs while the org's payments or spending limit are the problem, so the workflow runs only when started by hand. The pre-push hook running `npm run check` is the gate; run it before you push.
147
- - **No machine details in code.** Hard-coded machine paths and project details belong in config, not in code.
148
-
149
- ## Keeping it generic
150
-
151
- This repository holds nothing specific to a work project: no people, ticket systems or compliance rules. Those go in `ledung.config.local.json`.
114
+ A session that needs administrator rights runs the command through an elevation tool: [gsudo](https://github.com/gerardog/gsudo) (`gsudo <command>`, the output comes back to the session), Windows 11's own `sudo`, or `Start-Process <program> -Verb RunAs`. Each raises Windows' UAC prompt on the PC's desktop, so it works only when you are at the PC; from a phone the command waits for a click until its time limit. Such a call passes two checks: Ledung's permission prompt first (gsudo, sudo, `-Verb RunAs` and `runas` are among the suggested sensitive patterns, so they are asked in every permission mode, also through the PowerShell tool), then UAC. The prompt says it raises UAC and offers **Deny: not at the PC**, and while the command runs the status line says it may be waiting for UAC on the PC. **Stop** may not end what runs as administrator: a process that is not elevated is not allowed to end one that is, so close it on the PC.
152
115
 
153
- `npm run export-public -- --author "Name <email>"` is a dry run of a public copy: it puts the files of one commit (`--ref`, default the current one) into a new folder as a single commit by that author, without local folders such as `.claude/` and `.test/` or local configs, and scans the result for keys, tokens, e-mail and IP addresses and home folders. `--terms <file>` adds words that must not appear, such as names, from a local file that is not committed. It creates no repository online and pushes nothing. A real public export is made with `--author "Fluffy Fluffy and Bouncy <hello@ffbcoop.com>"`.
116
+ - **Never run Ledung or a session host as administrator.** Every session, MCP server and hook would then be administrator without any UAC prompt; Ledung's permission checks are Claude Code rules, not Windows security.
117
+ - **Never turn gsudo's credential cache on** (`gsudo cache on`, or a `CacheMode` of `Auto`). Later elevations would skip UAC, so any session could elevate without anyone at the PC. Keep `CacheMode` at `Explicit` (the default) or `Disabled`.
154
118
 
155
119
  ## License
156
120
 
@@ -158,4 +122,6 @@ Ledung is dual-licensed. It is free software under the [GNU Affero General Publi
158
122
 
159
123
  The AGPL asks that people who use Ledung over a network can get its source: **Settings → About** shows the running version and a **Source** link to it.
160
124
 
161
- Contributions need a contributor license agreement: see [CONTRIBUTING.md](CONTRIBUTING.md).
125
+ ## Developing Ledung
126
+
127
+ To work on Ledung from a clone (setup, the git hook, running a release, the npm package, the layout and the cross-platform rules), see [docs/development.md](docs/development.md). What changed and when is in [CHANGELOG.md](CHANGELOG.md). Contributions need a contributor license agreement: see [CONTRIBUTING.md](CONTRIBUTING.md).
package/dist/cli.js CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { spawn as spawn2 } from "node:child_process";
4
+ import { spawn as spawn3 } from "node:child_process";
5
5
  import { fileURLToPath as fileURLToPath2 } from "node:url";
6
6
 
7
7
  // src/config.ts
8
- import path3 from "node:path";
8
+ import path4 from "node:path";
9
9
  import { z as z6 } from "zod";
10
10
 
11
11
  // src/platform/paths.ts
@@ -267,11 +267,36 @@ var USAGE_REPO = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
267
267
 
268
268
  // src/accounts/store.ts
269
269
  import os2 from "node:os";
270
+ import path3 from "node:path";
271
+
272
+ // src/platform/exercised.ts
273
+ import fs2 from "node:fs";
270
274
  import path2 from "node:path";
275
+ var EXERCISED_FILE = "exercised.log";
276
+ var MAX_BYTES = 256 * 1024;
277
+ var DEFAULT_TARGET = {
278
+ console: !PACKAGED,
279
+ file: () => path2.join(userDataDir(), EXERCISED_FILE)
280
+ };
281
+ function exercisedLine(line, target = DEFAULT_TARGET) {
282
+ if (target.console) console.log(line);
283
+ try {
284
+ const file = target.file();
285
+ if (!file) return;
286
+ const size = fs2.statSync(file, { throwIfNoEntry: false })?.size ?? 0;
287
+ const text = `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
288
+ `;
289
+ if (size > MAX_BYTES) fs2.writeFileSync(file, text);
290
+ else fs2.appendFileSync(file, text);
291
+ } catch {
292
+ }
293
+ }
294
+
295
+ // src/accounts/store.ts
271
296
  var ACCOUNT_NAME = /^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$/;
272
297
  var DEFAULT_ACCOUNT = "default";
273
298
  function canonicalFolder(folder) {
274
- return path2.resolve(folder);
299
+ return path3.resolve(folder);
275
300
  }
276
301
  function canonicalContainerFolder(folder) {
277
302
  const f = folder.trim().replace(/\/+$/, "");
@@ -284,7 +309,7 @@ function sameFolder(a, b, platform = process.platform) {
284
309
  return platform === "win32" || platform === "darwin" ? x.toLowerCase() === y.toLowerCase() : x === y;
285
310
  }
286
311
  function sharedLoginFolders() {
287
- return [path2.join(os2.homedir(), ".claude"), ...process.env.CLAUDE_CONFIG_DIR ? [process.env.CLAUDE_CONFIG_DIR] : []];
312
+ return [path3.join(os2.homedir(), ".claude"), ...process.env.CLAUDE_CONFIG_DIR ? [process.env.CLAUDE_CONFIG_DIR] : []];
288
313
  }
289
314
 
290
315
  // src/config.ts
@@ -388,7 +413,7 @@ function withoutNotes(raw) {
388
413
  }
389
414
  var ClaudeAccountSetting = z6.preprocess(withoutNotes, z6.object({
390
415
  whatItIsFor: z6.string().trim().max(200).optional(),
391
- signInFolderOnThisComputer: z6.string().trim().min(1, "give a folder, or leave the field out for Ledung's own").refine((f) => path3.isAbsolute(f), "give the whole path, such as D:\\Claude sign-ins\\work or /Users/you/claude-signins/work").refine((f) => !sharedLoginFolders().some((s) => sameFolder(s, f)), "that is this computer's own Claude Code folder; an account needs a folder of its own").optional(),
416
+ signInFolderOnThisComputer: z6.string().trim().min(1, "give a folder, or leave the field out for Ledung's own").refine((f) => path4.isAbsolute(f), "give the whole path, such as D:\\Claude sign-ins\\work or /Users/you/claude-signins/work").refine((f) => !sharedLoginFolders().some((s) => sameFolder(s, f)), "that is this computer's own Claude Code folder; an account needs a folder of its own").optional(),
392
417
  signInFolderInContainers: z6.string().trim().min(1, "give a folder, or leave the field out for ~/.ledung-accounts/<name>").refine((f) => canonicalContainerFolder(f) !== null, "starts with ~/ or /, of letters, digits and . _ / + - only").refine((f) => canonicalContainerFolder(f) !== "~/.claude", "that is the container user's own Claude Code folder; an account needs a folder of its own").optional()
393
418
  }).strict());
394
419
  var ClaudeAccountsSetting = z6.preprocess(withoutNotes, z6.record(z6.string(), ClaudeAccountSetting)).default({}).superRefine((accounts, ctx) => {
@@ -437,7 +462,7 @@ var LocalProject = z6.object({
437
462
  * Folders outside the project folder where its sessions may also work, such as where its git worktrees
438
463
  * go (start_session's cwd, decision of 2026-10-14). None: only the project folder and folders inside it.
439
464
  */
440
- extraFolders: z6.array(z6.string().min(1).refine((f) => path3.isAbsolute(f), "Give each extra folder as a full path.")).max(20).optional(),
465
+ extraFolders: z6.array(z6.string().min(1).refine((f) => path4.isAbsolute(f), "Give each extra folder as a full path.")).max(20).optional(),
441
466
  model: z6.string().optional(),
442
467
  /** Its manager's model; none: the same as its sessions (decision of 2026-10-13). */
443
468
  managerModel: z6.string().optional(),
@@ -554,7 +579,7 @@ var Config = z6.object({
554
579
  * One that is missing or older than the SDK needs is never run: sessions are refused and the page says how to fix it
555
580
  * (Martin, 2026-10-27). "bundled" only when set here. Containers run their own.
556
581
  */
557
- claudeExecutable: z6.string().min(1).refine((v) => v === "installed" || v === "bundled" || path3.isAbsolute(v), 'claudeExecutable is "installed", "bundled" or a full path to Claude Code.').default("installed"),
582
+ claudeExecutable: z6.string().min(1).refine((v) => v === "installed" || v === "bundled" || path4.isAbsolute(v), 'claudeExecutable is "installed", "bundled" or a full path to Claude Code.').default("installed"),
558
583
  /** The shell a "!" command from the message box runs in on Windows (issue #212): Git Bash, as Claude's Bash tool, or PowerShell. */
559
584
  windowsShell: z6.enum(["bash", "powershell"]).default("bash"),
560
585
  board: BoardSetting,
@@ -584,18 +609,13 @@ function workflowsOf(config) {
584
609
  }
585
610
  function configPath(packaged = PACKAGED) {
586
611
  const set = envSetting("CONFIG");
587
- if (set) return path3.resolve(set);
588
- if (packaged) return path3.join(userDataDir(), "ledung.config.json");
589
- return path3.join(appDir(), "ledung.config.local.json");
612
+ if (set) return path4.resolve(set);
613
+ if (packaged) return path4.join(userDataDir(), "ledung.config.json");
614
+ return path4.join(appDir(), "ledung.config.local.json");
590
615
  }
591
616
 
592
617
  // src/platform/node-warnings.ts
593
618
  import { pathToFileURL } from "node:url";
594
-
595
- // src/platform/exercised.ts
596
- var MAX_BYTES = 256 * 1024;
597
-
598
- // src/platform/node-warnings.ts
599
619
  function coreArgs(entry2, passthrough2, restarted2, packaged = PACKAGED) {
600
620
  const preload = new URL(packaged ? "./preload.js" : "./preload.ts", pathToFileURL(entry2)).href;
601
621
  const args = ["--import", preload, entry2, ...passthrough2];
@@ -605,9 +625,9 @@ function coreArgs(entry2, passthrough2, restarted2, packaged = PACKAGED) {
605
625
 
606
626
  // src/platform/running.ts
607
627
  import crypto from "node:crypto";
608
- import fs2 from "node:fs";
628
+ import fs3 from "node:fs";
609
629
  import http from "node:http";
610
- import path4 from "node:path";
630
+ import path5 from "node:path";
611
631
 
612
632
  // src/platform/open-browser.ts
613
633
  import { spawn } from "node:child_process";
@@ -623,8 +643,8 @@ function openBrowser(url) {
623
643
  var NOT_RUNNING_CODE = 3;
624
644
  function configuredPort(configFile) {
625
645
  try {
626
- if (!fs2.existsSync(configFile)) return DEFAULT_PORT;
627
- const port = JSON.parse(fs2.readFileSync(configFile, "utf8")).port;
646
+ if (!fs3.existsSync(configFile)) return DEFAULT_PORT;
647
+ const port = JSON.parse(fs3.readFileSync(configFile, "utf8")).port;
628
648
  return port === void 0 ? DEFAULT_PORT : typeof port === "number" && Number.isInteger(port) ? port : null;
629
649
  } catch {
630
650
  return null;
@@ -632,7 +652,7 @@ function configuredPort(configFile) {
632
652
  }
633
653
  function savedToken(dataDir = userDataDir()) {
634
654
  try {
635
- const token = fs2.readFileSync(path4.join(dataDir, "auth-token"), "utf8").trim();
655
+ const token = fs3.readFileSync(path5.join(dataDir, "auth-token"), "utf8").trim();
636
656
  return /^[0-9a-f]{64}$/.test(token) ? token : null;
637
657
  } catch {
638
658
  return null;
@@ -676,15 +696,136 @@ async function openIfRunning(opts) {
676
696
  return true;
677
697
  }
678
698
 
699
+ // src/platform/background.ts
700
+ import { spawn as spawn2 } from "node:child_process";
701
+ import fs4 from "node:fs";
702
+ import path6 from "node:path";
703
+
704
+ // src/server/devices.ts
705
+ var PAIR_CODE_MS = 10 * 60 * 1e3;
706
+ var PASSKEY_WINDOW_MS = 10 * 60 * 1e3;
707
+ var TOUCH_MS = 60 * 1e3;
708
+ var COMPUTER_IDLE_MS = 14 * 24 * 60 * 60 * 1e3;
709
+ var COOKIE_RENEW_MS = 24 * 60 * 60 * 1e3;
710
+ var OLD_COOKIE_GRACE_MS = 60 * 1e3;
711
+
712
+ // src/server/security.ts
713
+ var COOKIE_NAME = "ledung_auth";
714
+
715
+ // src/platform/background.ts
716
+ var LOG_FILE = "ledung.log";
717
+ var MAX_LOG_BYTES = 10 * 1024 * 1024;
718
+ var READY_MS = 6e4;
719
+ var STOPPED_MS = 6e4;
720
+ function logFile(dataDir = userDataDir()) {
721
+ return path6.join(dataDir, LOG_FILE);
722
+ }
723
+ function rotateLog(file, maxBytes = MAX_LOG_BYTES) {
724
+ const size = fs4.statSync(file, { throwIfNoEntry: false })?.size ?? 0;
725
+ if (size <= maxBytes) return;
726
+ const old = path6.join(path6.dirname(file), "ledung.old.log");
727
+ fs4.rmSync(old, { force: true });
728
+ fs4.renameSync(file, old);
729
+ }
730
+ function logTail(file, lines = 20) {
731
+ try {
732
+ return fs4.readFileSync(file, "utf8").trimEnd().split(/\r?\n/).slice(-lines).join("\n");
733
+ } catch {
734
+ return "";
735
+ }
736
+ }
737
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
738
+ async function startInBackground(opts) {
739
+ const print = opts.print ?? ((line) => console.log(line));
740
+ const dataDir = opts.dataDir ?? userDataDir();
741
+ const file = logFile(dataDir);
742
+ rotateLog(file);
743
+ const out = fs4.openSync(file, "a", 384);
744
+ if (process.platform !== "win32") fs4.fchmodSync(out, 384);
745
+ fs4.writeSync(out, `
746
+ --- ${(/* @__PURE__ */ new Date()).toISOString()} Ledung starting in the background
747
+ `);
748
+ const child = spawn2(process.execPath, opts.args, {
749
+ // Its own process group and session, and no console on Windows: closing the terminal or Ctrl+C there leaves it be.
750
+ detached: true,
751
+ windowsHide: true,
752
+ stdio: ["ignore", out, out],
753
+ // Shown in Settings next to the log's path: this Ledung's output goes there.
754
+ env: { ...process.env, LEDUNG_LOG_FILE: file }
755
+ });
756
+ fs4.closeSync(out);
757
+ let exited = null;
758
+ child.on("exit", (code) => {
759
+ exited = code ?? 1;
760
+ });
761
+ child.on("error", (e) => {
762
+ print(e.message);
763
+ exited = 1;
764
+ });
765
+ child.unref();
766
+ const deadline = Date.now() + (opts.readyMs ?? READY_MS);
767
+ while (Date.now() < deadline) {
768
+ await sleep(500);
769
+ if (exited !== null) {
770
+ print(`Ledung stopped while starting (exit code ${exited}). The end of its log, ${file}:`);
771
+ print(logTail(file));
772
+ return exited || 1;
773
+ }
774
+ const port = configuredPort(opts.configFile);
775
+ const token = savedToken(dataDir);
776
+ if (port !== null && token !== null && await answersSignIn(port, token, 2e3)) {
777
+ exercisedLine("EXERCISED #456 Ledung started in the background");
778
+ print(`Ledung is running in the background at http://127.0.0.1:${port}`);
779
+ print(`Log: ${file}`);
780
+ print('Stop it with "ledung stop", or Stop Ledung in Settings \u2192 Update & restart.');
781
+ return 0;
782
+ }
783
+ }
784
+ print(`Ledung is still starting in the background; it opens its page when it is ready. Log: ${file}`);
785
+ return 0;
786
+ }
787
+ async function stopRunning(opts) {
788
+ const print = opts.print ?? ((line) => console.log(line));
789
+ const port = configuredPort(opts.configFile);
790
+ const token = savedToken(opts.dataDir);
791
+ if (port === null || token === null || !await answersSignIn(port, token)) {
792
+ print("Ledung is not running.");
793
+ return NOT_RUNNING_CODE;
794
+ }
795
+ const origin = `http://127.0.0.1:${port}`;
796
+ const res = await fetch(`${origin}/api/stop`, { method: "POST", headers: { Cookie: `${COOKIE_NAME}=${token}`, Origin: origin, "Content-Type": "application/json" }, body: "{}" });
797
+ if (!res.ok) {
798
+ const error = (await res.json().catch(() => ({}))).error;
799
+ print(`Ledung refused to stop: ${error ?? res.statusText}`);
800
+ return 1;
801
+ }
802
+ print("Stopping Ledung and its sessions...");
803
+ const deadline = Date.now() + (opts.stoppedMs ?? STOPPED_MS);
804
+ while (Date.now() < deadline) {
805
+ await sleep(500);
806
+ if (!await answersSignIn(port, token, 2e3)) {
807
+ print("Ledung stopped.");
808
+ return 0;
809
+ }
810
+ }
811
+ print("Ledung has not stopped yet; it is still stopping its sessions.");
812
+ return 1;
813
+ }
814
+
679
815
  // src/cli.ts
680
816
  var RESTART_CODE = 75;
681
- var passthrough = process.argv.slice(2).filter((a) => a !== "--open-only");
817
+ var OWN = ["--open-only", "--foreground"];
818
+ var passthrough = process.argv.slice(2).filter((a) => !OWN.includes(a));
682
819
  if (passthrough.includes("--version") || passthrough.includes("-v")) {
683
820
  console.log(appVersion());
684
821
  process.exit(0);
685
822
  }
823
+ if (passthrough[0] === "stop") process.exit(await stopRunning({ configFile: configPath() }));
686
824
  if (await openIfRunning({ configFile: configPath(), open: !passthrough.includes("--no-open") })) process.exit(0);
687
825
  if (process.argv.includes("--open-only")) process.exit(NOT_RUNNING_CODE);
826
+ if (!process.argv.includes("--foreground")) {
827
+ process.exit(await startInBackground({ args: [fileURLToPath2(import.meta.url), "--foreground", ...passthrough], configFile: configPath() }));
828
+ }
688
829
  var entry = fileURLToPath2(new URL(PACKAGED ? "./main.js" : "./main.ts", import.meta.url));
689
830
  for (const signal of ["SIGINT", "SIGTERM", "SIGBREAK"]) process.on(signal, () => {
690
831
  });
@@ -692,8 +833,10 @@ var restarted = false;
692
833
  for (; ; ) {
693
834
  const args = coreArgs(entry, passthrough, restarted);
694
835
  const code = await new Promise((resolve) => {
695
- const child = spawn2(process.execPath, args, {
836
+ const child = spawn3(process.execPath, args, {
696
837
  stdio: "inherit",
838
+ // In the background this command has no console, and Windows would give Ledung a window of its own (#456).
839
+ windowsHide: true,
697
840
  // LEDUNG_LAUNCHER tells Ledung that exiting with RESTART_CODE will start it again.
698
841
  env: { ...process.env, LEDUNG_LAUNCHER: "1" }
699
842
  });