docs: tell the whole daemon story, and stop denying it exists
An audit of the README and the site against the implementation found 38 places the docs were missing, stale, or wrong about daemon mode. Two were outright false: the community page still answered "want a queue that outlives the command?" with "slskd has that", and the home page promised "no daemon, no API to stand up" fifty lines before the section that describes both. The download-box walkthrough now tells the truth about its sharp edges: a pushed folder or share list is live-only on a remote daemon, whose own config.toml wins at restart; `shares add` validates the path on the machine you type it on; and a `get` that reaches its `--timeout` cancels the daemon-side transfer. Its shell example also runs as written now — the old block put a second command after a foreground daemon that never returns. The reference material catches up with the feature: `daemon` and its subcommands join the command list, the config table gains the daemon/daemon_token/--no-daemon rows, nine settings become eleven, `daemon status` and `daemon token` get documented record shapes, exit 3 covers daemons, `queued` joins the serve statuses, the credential-free count goes 13 to 19, and the action-command count 4 to 6. And the surprises are written down rather than discovered: the daemon does not sweep the wishlist on its own; Windows has no socket and so no auto-attach; the attached TUI's settings form edits the daemon's folders and rewrites this machine's config.toml; `serve` monitors a daemon's uploads instead of starting a second server; `whoami` reports the daemon's world, with --no-daemon as the way out. The OpenRPC section names the six event methods and the session-wide download.set_dir rule. Site typechecks, lints, and builds through prerender; every claim was verified against the cited code before it was written.
This commit is contained in:
@@ -36,8 +36,9 @@ const ALTERNATIVES: readonly Alternative[] = [
|
||||
question: 'Want a queue that outlives the command?',
|
||||
answer: (
|
||||
<>
|
||||
Every command runs one-shot and exits: JSON on stdout, an exit code, nothing left
|
||||
running. No resident queue to pause, resume, or drive remotely. slskd has that.
|
||||
<Code>soulseek-rs daemon</Code> holds one: pause, resume and remove act on it from
|
||||
the TUI or over JSON-RPC, and <Code>--bind</Code> lets another machine drive it.
|
||||
slskd still has the web interface and accounts for several people.
|
||||
</>
|
||||
),
|
||||
links: [{ label: 'slskd', href: LINKS.slskd }],
|
||||
|
||||
+63
-12
@@ -89,7 +89,9 @@ const FirstRun = () => (
|
||||
<Prose>
|
||||
<p>
|
||||
On first run you get a login screen: sign in with an existing Soulseek account or
|
||||
register a new one. Your password goes to the OS keychain, not a plain-text file.
|
||||
register a new one, unless a daemon is already running, in which case the TUI
|
||||
attaches to it and asks for nothing. Your password goes to the OS keychain, not a
|
||||
plain-text file.
|
||||
</p>
|
||||
<p>
|
||||
Downloads land in a conventional folder, and TUI state (searches, downloads,
|
||||
@@ -158,6 +160,8 @@ const FromTheShell = () => (
|
||||
{ t: 'cm', text: '# search, pick the best, download it' },
|
||||
{ t: 'cmd', text: 'soulseek-rs search "netlabel 2004" --min-bitrate 320' },
|
||||
{ t: 'cmd', text: 'soulseek-rs browse <username> --json' },
|
||||
{ t: 'cmd', text: 'soulseek-rs wish add "rare pressing"' },
|
||||
{ t: 'cm', text: '# a standing search, re-run by serve --wishlist' },
|
||||
{ t: 'cmd', text: 'soulseek-rs room list' },
|
||||
{ t: 'cm', text: '# public rooms, busiest first' },
|
||||
{ t: 'cmd', text: 'soulseek-rs room say <room> "hello room"' },
|
||||
@@ -171,16 +175,17 @@ const FromTheShell = () => (
|
||||
<Prose>
|
||||
<p>
|
||||
The command groups: <Code>search</Code>, <Code>get</Code>, <Code>download</Code>,{' '}
|
||||
<Code>browse</Code>, <Code>room</Code>, <Code>message</Code>, <Code>serve</Code>,{' '}
|
||||
<Code>user</Code>, <Code>whoami</Code>, <Code>shares</Code>, <Code>daemon</Code>,{' '}
|
||||
<Code>config</Code>, <Code>portmap</Code>, <Code>skills</Code>,{' '}
|
||||
<Code>completions</Code>. Records are
|
||||
tab-separated fields, one per line, or NDJSON with <Code>--json</Code>.
|
||||
<Code>browse</Code>, <Code>wish</Code>, <Code>room</Code>, <Code>message</Code>,{' '}
|
||||
<Code>serve</Code>, <Code>user</Code>, <Code>whoami</Code>, <Code>shares</Code>,{' '}
|
||||
<Code>daemon</Code>, <Code>config</Code>, <Code>portmap</Code>,{' '}
|
||||
<Code>skills</Code>, <Code>completions</Code>. Records are tab-separated fields,
|
||||
one per line, or NDJSON with <Code>--json</Code>.
|
||||
</p>
|
||||
<p>
|
||||
No TTY required. <Code>search --json</Code> pipes through <Code>jq</Code> into{' '}
|
||||
<Code>download --stdin</Code>. Every ending has its own exit code: 4 found
|
||||
nothing, 3 could not reach the server, 6 the transfer died.
|
||||
nothing, 3 could not reach the server, 6 the transfer died, 7 another login took
|
||||
the session away, which is the one a daemon spares you.
|
||||
</p>
|
||||
<p>
|
||||
<Code>completions install</Code> gives bash, zsh and fish tab completion for all
|
||||
@@ -238,6 +243,8 @@ const Daemon = () => (
|
||||
{ t: 'cm', text: '# once; it holds the session' },
|
||||
{ t: 'cmd', text: 'soulseek-rs search "netlabel 2004" --json' },
|
||||
{ t: 'cm', text: '# no login, no flags; it finds the daemon' },
|
||||
{ t: 'cmd', text: 'soulseek-rs search "netlabel 2004" --no-daemon' },
|
||||
{ t: 'cm', text: "# opt out; open this run's own session" },
|
||||
{ t: 'cmd', text: 'soulseek-rs' },
|
||||
{ t: 'cm', text: '# the TUI attaches to the same session' },
|
||||
{ t: 'cmd', text: 'soulseek-rs daemon status' },
|
||||
@@ -248,13 +255,29 @@ const Daemon = () => (
|
||||
<p>
|
||||
Commands find it themselves: it listens on a Unix socket only your user can
|
||||
open, so there is no address to pass and no token to handle. Without one
|
||||
running, everything behaves exactly as it did before.
|
||||
running, everything behaves exactly as it did before, and{' '}
|
||||
<Code>--no-daemon</Code> gets that back for a single command.{' '}
|
||||
<Code>daemon status</Code> exits 3 when none is running, which is how a script
|
||||
decides whether to start one. That socket is a Unix one: on Windows, start the
|
||||
daemon with <Code>--bind 127.0.0.1:5030</Code> and point commands at it with{' '}
|
||||
<Code>config set daemon</Code> and <Code>config set daemon_token</Code>.
|
||||
</p>
|
||||
<p>
|
||||
Every window is a view of the same session. Open a second TUI and it shows the
|
||||
searches and transfers already running; queue something in either and both see
|
||||
it. The status bar names the daemon, and closing a window leaves its downloads
|
||||
going, because they belong to the daemon rather than the window.
|
||||
it. There is no login screen, because the daemon is already signed in, and the
|
||||
inbox opens on the private messages it collected while nobody was watching. The
|
||||
settings form edits the daemon’s download folder and share list, so the
|
||||
paths you type there name its filesystem rather than this machine’s. The
|
||||
status bar names the daemon, and closing a window leaves its downloads going,
|
||||
because they belong to the daemon rather than the window.
|
||||
</p>
|
||||
<p>
|
||||
Attach one and commands report its world: <Code>whoami</Code> names its account
|
||||
and server, <Code>download</Code> prints the path on its disk, and{' '}
|
||||
<Code>serve --follow</Code> watches its uploads instead of starting a second
|
||||
server. It holds the session rather than the wishlist: standing searches re-run
|
||||
under <Code>serve --wishlist</Code>, or when you call <Code>wish run</Code>.
|
||||
</p>
|
||||
<p>
|
||||
Why bother: the server permits a single session per account. Two one-off
|
||||
@@ -305,6 +328,12 @@ const DownloadBox = () => (
|
||||
tomorrow, run <Code>daemon status</Code>, and pick up where you left off. The
|
||||
files are on the box, which is where you wanted them.
|
||||
</p>
|
||||
<p>
|
||||
The two <Code>config set</Code> lines are the one-time version;{' '}
|
||||
<Code>--daemon ADDR</Code> and <Code>SOULSEEK_DAEMON</Code> point a single
|
||||
command at a different box. On the box itself,{' '}
|
||||
<Code>daemon --upload-slots N</Code> sets how many uploads it serves at once.
|
||||
</p>
|
||||
<p>
|
||||
There is no encryption on that port. On a home network that is usually fine.
|
||||
Over the internet, reach it through SSH instead:{' '}
|
||||
@@ -312,6 +341,18 @@ const DownloadBox = () => (
|
||||
</p>
|
||||
</Prose>
|
||||
</Cols>
|
||||
<div className="mt-6">
|
||||
<Callout tone="warn" title="pushed settings are live only">
|
||||
<p>
|
||||
<Code>config set download_dir</Code> and <Code>shares add</Code> reach the
|
||||
running daemon and take effect at once, but the box reads its own{' '}
|
||||
<Code>config.toml</Code> when it restarts, so make the lasting changes there.{' '}
|
||||
<Code>shares add</Code> also checks the folder on the machine you type it on, so
|
||||
a path that exists only on the box fails from your laptop: add shares on the box,
|
||||
or in the attached TUI’s settings form, which sends paths through as typed.
|
||||
</p>
|
||||
</Callout>
|
||||
</div>
|
||||
</Section>
|
||||
)
|
||||
|
||||
@@ -327,7 +368,12 @@ const OpenSchema = () => (
|
||||
It speaks newline-delimited JSON-RPC 2.0, described by an{' '}
|
||||
<ExtLink href="https://open-rpc.org">OpenRPC</ExtLink> document. The daemon
|
||||
serves that document itself through <Code>rpc.discover</Code>, so a client can
|
||||
ask a running daemon what it can do.
|
||||
ask a running daemon what it can do. The same document is checked in at{' '}
|
||||
<ExtLink href={`${LINKS.gh}/blob/master/docs/openrpc.json`}>
|
||||
docs/openrpc.json
|
||||
</ExtLink>{' '}
|
||||
on GitHub. <Code>auth</Code> answers with the protocol version, 1 today, so a
|
||||
client written against an older shape learns it at connect time.
|
||||
</p>
|
||||
<p>
|
||||
OpenRPC is the JSON-RPC counterpart to OpenAPI: point a generator at it and you
|
||||
@@ -338,7 +384,12 @@ const OpenSchema = () => (
|
||||
<p>
|
||||
Searches, transfers, rooms, private messages and shares are all on the same
|
||||
interface this CLI uses. There is no private back channel it keeps for itself,
|
||||
and live updates are pushed to every connected client rather than polled for.
|
||||
and live updates are pushed to every connected client rather than polled for:{' '}
|
||||
<Code>event.room</Code>, <Code>event.message</Code>, <Code>event.upload</Code>,{' '}
|
||||
<Code>event.download_status</Code>, <Code>event.browse</Code> and{' '}
|
||||
<Code>event.session_loss</Code>. One rule to write against: the download folder
|
||||
belongs to the daemon, so <Code>download.set_dir</Code> moves it for every
|
||||
transfer and a per-download destination is ignored.
|
||||
</p>
|
||||
</Prose>
|
||||
<Terminal
|
||||
|
||||
@@ -232,8 +232,10 @@ const About = () => (
|
||||
<Callout title="agents and automation">
|
||||
<p>
|
||||
Every command runs as a one-off from the shell. Point cron, a script, or an
|
||||
agent at them and read JSON records back off stdout. No daemon, no API to stand
|
||||
up. <Code>skills install</Code> teaches the agent the surface for you.{' '}
|
||||
agent at them and read JSON records back off stdout. Nothing to stand up first;{' '}
|
||||
<Code>soulseek-rs daemon</Code> is one command when a batch of them should share
|
||||
a single login, and the agent skill starts one for you.{' '}
|
||||
<Code>skills install</Code> teaches the agent the surface.{' '}
|
||||
<Link to="/docs" className="text-link hover:text-link-hover">
|
||||
See the commands
|
||||
</Link>
|
||||
|
||||
@@ -125,7 +125,7 @@ const PLATFORMS: readonly PlatformNote[] = [
|
||||
config: '%APPDATA%\\soulseek-rs\\config\\config.toml',
|
||||
state: '%APPDATA%\\soulseek-rs\\data\\state',
|
||||
downloads: '%USERPROFILE%\\Downloads\\Soulseek',
|
||||
note: 'Uses the standard known-folder locations for roaming app data.',
|
||||
note: 'Uses the standard known-folder locations for roaming app data. Without Unix sockets, a daemon here needs --bind 127.0.0.1:5030, and clients reach it with config set daemon plus config set daemon_token; that token file carries no permissions of its own.',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -207,6 +207,26 @@ const SETTINGS: readonly Setting[] = [
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'daemon',
|
||||
fallback: 'the local socket, if one is running',
|
||||
body: (
|
||||
<>
|
||||
A daemon to drive instead of logging in here, as <code>host:port</code> or a Unix
|
||||
socket path.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'daemon_token',
|
||||
fallback: 'unset',
|
||||
body: (
|
||||
<>
|
||||
The secret a remote daemon asks for, printed by <code>daemon token</code> on the
|
||||
machine running it. A local socket needs none.
|
||||
</>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
const CONFIG_EXAMPLE: TermLine[] = [
|
||||
@@ -431,14 +451,17 @@ const Platforms = () => (
|
||||
<code>config.toml</code> is layered: flags beat environment variables, which
|
||||
beat the file, which beats the defaults. State (searches, downloads, rooms) is
|
||||
versioned JSON and restores on restart; soulseek-rs sets anything unreadable
|
||||
aside as <code>.bak</code> rather than overwriting it.
|
||||
aside as <code>.bak</code> rather than overwriting it. Attached to a daemon, that
|
||||
state is the daemon’s, and the state folder also holds its{' '}
|
||||
<code>daemon.sock</code> and <code>daemon.token</code>, both 0600 on macOS and
|
||||
Linux.
|
||||
</FeatureCard>
|
||||
</Cols>
|
||||
</Section>
|
||||
)
|
||||
const Config = () => (
|
||||
<Section id="config">
|
||||
<SectionHead eyebrow="config.toml" title="Nine settings, all optional.">
|
||||
<SectionHead eyebrow="config.toml" title="Eleven settings, all optional.">
|
||||
Every one has a working default, so an empty file, or no file at all, is valid.{' '}
|
||||
<Code>config list</Code> prints the effective value of each.
|
||||
</SectionHead>
|
||||
@@ -471,7 +494,9 @@ const Config = () => (
|
||||
reads one setting and <Code>config set</Code> writes one. It rejects
|
||||
unknown keys, so a typo fails instead of doing nothing.{' '}
|
||||
<Code>--no-config</Code> ignores the file entirely and{' '}
|
||||
<Code>--config FILE</Code> points at another one.
|
||||
<Code>--config FILE</Code> points at another one. <Code>wishlist</Code> is in
|
||||
the file too, written by <Code>wish add</Code> rather than{' '}
|
||||
<Code>config set</Code>, since a standing search may itself contain a comma.
|
||||
</p>
|
||||
</Callout>
|
||||
</div>
|
||||
@@ -497,7 +522,8 @@ const Uninstall = () => (
|
||||
{ t: 'cmd', text: 'cargo uninstall soulseek-rs' },
|
||||
{ t: 'cm', text: '# brew only: drop the tap it came from' },
|
||||
{ t: 'cmd', text: 'brew untap michel/tap' },
|
||||
{ t: 'cm', text: '# config and state: searches, downloads, rooms, messages' },
|
||||
{ t: 'cm', text: '# config and state: searches, downloads, rooms,' },
|
||||
{ t: 'cm', text: '# messages, the daemon token' },
|
||||
{
|
||||
t: 'cmd',
|
||||
text: 'rm -rf ~/.config/soulseek-rs ~/.local/share/soulseek-rs',
|
||||
|
||||
Reference in New Issue
Block a user