`ccs bar stop` deletes bar.json, so a bar.json-only sticky port is lost on
every stop/start cycle: the next launch reverted to 3000 and the probe could
no longer find a server still running on the previously chosen port.
resolveBarPort now falls back to the --port recorded in launch.json (written
by launch, not deleted by stop), which restores both the sticky port and
probe discovery after a stop.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ccs bar always forced the dashboard onto port 3000 (first free of a
hardcoded candidate list), which collides with other local dev servers, and
bar.json was rewritten to 3000 on every launch.
- `ccs bar [launch] --port N` runs the server on exactly N: reuses a live
server already on N, moves a running server from another port (SIGTERM via
server.pid, wait for exit), errors clearly when N is busy or the value is
invalid.
- The chosen port is persisted into launch.json args, so the Swift app
self-starts the server on the same port.
- Without --port, launch and serve now try the port recorded in bar.json
first (sticky), so the server keeps coming back on the port the user last
chose instead of reverting to 3000.
- Bare flags (`ccs bar --port N`) route to the launch subcommand; --port is
documented in `ccs bar --help`.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Use a private safe ccs.js shim for native CCS Bar launch descriptors so symlinked Bun installs do not fall back to an app environment without node on PATH.
* test(bar): cover socket-level 401/403 auth detection and liveness-poll rogue rejection
Add two new describe blocks to bar-command.test.ts:
1. `defaultFindRunningServer: socket-level 401/403 classifies authRequired=true`
Drives `net.connect` via mock.module('net') to verify that status lines
401 and 403 off the wire set authRequired=true. A correct-token 200 must
not set authRequired. Prior tests bypassed the real status-line parser by
injecting authRequired via deps; these tests couple to the actual branch.
2. `defaultWaitForServerLive: rogue 200 without matching token is rejected`
Uses the same net mock pattern to verify that a 200 carrying a wrong
x-ccs-bar-token never causes the liveness poll to resolve (rogue reflector
cannot pass). Correct token resolves immediately.
Export `defaultWaitForServerLive` from launch-subcommand.ts so the direct
unit test can import it without forcing a full handleBarLaunch invocation.
Both test groups are behavior-coupled: removing the relevant branch in
production causes the tests to fail (verified under mutation).
* refactor(bar): drop dead ensureLoopbackNoProxy proxy shim
The probe now uses raw net.connect() which never consults proxy env vars
(undici was fully removed in an earlier PR). ensureLoopbackNoProxy()
permanently mutated process-wide NO_PROXY/no_proxy on every discovery call
with no effect on the actual network path.
Verified zero remaining references to ensureLoopbackNoProxy outside the
declaration and call site that are now removed.
Closes a spoofed-loopback-200 bar.json poisoning vector with a per-user 0600 capability token. The real bar server echoes its file-derived token on /bar/* responses; the launch probe verifies the echoed token against the locally-read file (fail-closed) before reusing a server, so a rogue loopback process that cannot read the token file is rejected.
Raw-socket probe detects auth-protected (401/403) bar servers, reuses them, and kills the orphaned detached server on auth timeout. Integrated with the non-blocking priority probe.
Preserves the Gatekeeper quarantine attribute on the installed CCS Bar app so the user makes the right-click to Open trust decision, instead of silently clearing it.
ccs bar now spawns the web-server as a detached background process and
exits immediately instead of hosting it in the foreground, so closing the
terminal no longer takes the bar offline. Adds 'bar serve' (the long-lived
host), 'bar stop', and 'bar status', and records ~/.ccs/bar/launch.json at
install so the app can start the server without a shell PATH.
Refs #1526
'open -a' only activates a running app, so suggesting 'ccs bar' as an
alternative to quitting could not load the new binary. The hint now
says to quit from the menu bar first, then run 'ccs bar' to relaunch
the updated app.
The reinstall guard deleted the existing bundle before download, so a
transient download or extraction failure left no app on disk. The
archive now extracts into a hidden staging directory inside the
Applications folder; the old bundle is removed only after the new one
is verified in staging, then renamed into place. Every failure before
the swap leaves the previous install untouched, and staging is cleaned
up on all paths.
Three review/CI corrections to the install flow:
The compat handshake now runs before the quarantine-clear and launch
block. It is a server-side check unrelated to Gatekeeper, and the
previous ordering let a failed quarantine clear (always the case where
xattr is absent, e.g. Linux CI) skip the handshake entirely.
Reinstalls remove the existing bundle before extraction so the
post-extraction existence check actually proves the fresh bundle
landed; an unremovable bundle aborts install instead of extracting
over it.
Declining the launch prompt now prints the same run-ccs-bar hint as
the non-TTY path instead of ending silently. Install tests inject the
newer deps (clearQuarantine, isBarRunning, promptLaunch) everywhere
the defaults could touch host binaries, keeping results identical on
macOS and Linux runners.
A failed quarantine clear previously fell through to the launch
handoff, so a default-yes prompt (or --launch) opened the still
quarantined app straight into the Gatekeeper block. Install now ends
after printing the manual xattr guidance, with a hint to run 'ccs bar'
once quarantine is cleared; --launch does not override a failed clear.
Pin xattr and pgrep to absolute /usr/bin paths so quarantine clearing
and process detection cannot be hijacked through a caller-controlled
PATH. Gate the launch prompt on stdin being a TTY instead of stdout, so
piping install output through tee no longer silently skips the
handoff. Detect an already-running CCS Bar via pgrep before prompting:
a running instance gets a quit-and-reopen hint instead of a redundant
launch prompt, while --launch still proceeds explicitly.
'ccs bar install' previously ended with two manual steps: clearing the
Gatekeeper quarantine by hand and running 'ccs bar' separately.
Install now detects an existing installation and says so before
reinstalling, clears the quarantine attribute itself via execFile with
a graceful fallback to the printed hint when xattr fails, and ends with
a TTY-aware 'Launch CCS Bar now?' prompt (default yes) that hands off
to the existing launch flow. --launch forces the handoff and
--no-launch suppresses it for scripted installs; non-TTY runs skip the
prompt and print the manual command instead.
Closes#1504
Sequential probing of up to 10 loopback targets at 1.5s timeout each
could stall 'ccs bar' for ~15s when a non-CCS service occupied a
candidate port without answering. All probes now run concurrently and
the first success in priority order (bar.json port first, IPv4 before
IPv6 per port) is selected, bounding detection at roughly one probe
timeout.
'ccs config' starts the web-server on host 'localhost', which macOS
resolves to ::1, so a reuse probe limited to 127.0.0.1 missed the most
common already-running server and launch started a redundant second
instance. Each candidate port is now probed on 127.0.0.1 first and then
[::1]; an IPv6 hit writes the bracketed-literal baseUrl into bar.json,
which URLSession in the Swift app resolves correctly.
'ccs bar' always tried to start its own web-server. With a CCS server
already listening on 127.0.0.1:3000, get-port (probing the unspecified
address, which macOS allows to bind alongside a specific loopback
listener) reported 3000 as free, and the subsequent 127.0.0.1 bind
failed with EADDRINUSE, aborting launch.
Launch now probes candidate ports (bar.json port first, then the
default list) with a short-timeout GET /api/bar/summary and reuses the
first live CCS server it finds; only when none responds does it start a
new server. The get-port probe also passes host 127.0.0.1 so the
availability check matches the actual bind target. Probe failures are
treated as no-server-found and never break launch.
Closes#1500
The floating ccs-bar-latest release tag carries no version, so deriving the
app version from tag_name printed 'vccs-bar-latest' and pinned that literal
string. The version now comes from CFBundleShortVersionString in the
extracted bundle's Info.plist; an unreadable plist skips pinning and clears
any stale pin.
The post-install check compared app semver major against the CCS server
major, but the app is versioned independently of the CLI, so the mismatch
warning fired on every install. It is now a capability handshake against
GET /api/bar/summary: 200 confirms the server serves the bar API, 404 warns
the server predates CCS Bar, and anything else keeps the soft warning.
Install never hard-fails on the handshake.
Closes#1497
Gate /api/bar/* behind the localhost-when-auth-disabled guard (single DRY
choke point) so native quota/tier/cost can't leak on a non-loopback bind with
auth disabled; add a guard test. Delete the dishonest maxRedirections test that
asserted the opposite of the production redirect hardening. Key per-account
today-cost on the local day (matching analytics) instead of UTC. Stop the
inner 429 retry in the Claude usage fetch so the outer cache + circuit breaker
honor Retry-After. Narrow the usage-transformer map type and fix stale
doc-comments; clarify the one-alert-per-reset-window quota rule; gitignore the
local demo scaffolding.
Resolve the app path via os.homedir() in launch to match install/uninstall;
validate the host on every redirect hop (manual follow, not blind
maxRedirections); reject zip-slip entries before extracting into ~/Applications.
Add ccs bar (install/launch/uninstall/version) and the ~/.ccs/bar.json
discovery handshake the app reads. install resolves a floating release asset,
follows redirects, validates the download host over HTTPS, checks status,
verifies the extracted app, and runs a real version-compat handshake against
/api/overview.