feat: Linux desktop app (.deb, x64) and absolute connect paths - #181
Merged
Merged
Conversation
…bution Record the 2026-09-16 platform decisions as dated amendments beside the original text: - distribution spec: Intel Mac (x64) and Linux .deb (x64) in scope, with EARS criteria; AppImage, a Windows desktop app, and non-macOS self-update stay out. Fix the stale npm package name. - distribution design: new §11 (one release platform table, per-platform install locations and CLI channel, Linux .deb specifics, split release jobs). - control-plane: npm publishes before Homebrew; the CLI dispatcher moves into the engine; the npm package ships the policy fixtures; milestone 7 starts with the Windows read path. - channels and site specs: native Intel Mac and Linux downloads, npm CLI as the no-app route. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
CRLF: every frontmatter reader tested startsWith("---\n"), so a file saved
with Windows line endings parsed as having no frontmatter and silently lost
its type, title, and dates; past the fence, the last field kept a trailing
\r and was dropped too. okf-local, files, layer-files, and promote now go
through one splitFrontmatter helper, and frontmatter writes keep the file's
line endings.
Paths: new platform-paths.mjs answers where config, data, and cache live on
macOS, Linux (XDG), and Windows, per control-plane spec §5.12, with
CONTEXTCAKE_* overrides. The app-bundled CLI (always the macOS path before)
and the npm CLI (~/.config on Windows before) both read their default
manifest from it.
Promotion bindings move to the data directory. Their macOS home was a
"Local State" folder inside Electron's userData, where Chromium keeps a file
of that name, so staging a promotion failed with ENOTDIR on any Mac that had
run the app. Bindings staged in the old location are still read and removed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
The SQLite store reuses postings by file fingerprint (size, mtime), which a parser change never touches. Without a format bump, an unchanged CRLF note indexed before the frontmatter fix would keep postings and a `type` column from its old frontmatter-less parse until someone edited it. Bump FORMAT_VERSION to 6, test that an older-format store re-analyzes every document, and record the rule in CLAUDE.md. Also note in the manifest trust gotcha that CONTEXTCAKE_MANIFEST and CONTEXTCAKE_CONFIG_DIR select the CLI's default manifest. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
npm-publish.yml rebuilt the package on the publish runner, so the registry copy could differ from the tarball listed in the release's SHA256SUMS. It now downloads that tarball, checks its checksum, name, version, entries, and lifecycle scripts (scripts/verify-npm-tarball.mjs, unit-tested), and runs `npm publish <tgz>`. Also: - upgrade to npm 11 and assert npm >= 11.5.1 and Node >= 22.14.0, which trusted publishing requires; Node 22 bundles npm 10 - a re-run for a version npm already has succeeds only when the published dist.integrity matches the release tarball, and fails loudly otherwise - optional input to publish the context-cake pointer package, which no workflow published before - document the one-time prerequisites (placeholder 0.0.0, trusted publisher) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
…and profiles Wave A slices (a) and (b) of the control-plane spec. - packages/core/src/cli.mjs + cli/: one command table that routes argv and generates `help --json`. Table commands answer with the versioned JSON envelope, typed ControlErrors, the §5.2 exit codes, and redaction of tokenEnv values, provider token shapes, and Authorization headers. `mcp` accepts only its serving flags. `--timeout` is refused on mutating commands before they start. - The app CLI and the npm bin become thin wrappers. The app passes a wrapSpawn hook so mcp and doctor still run through its observability launchers. - `init` creates a v2 manifest and never migrates or overwrites. - `profile` family over control/profiles.mjs: the original six commands keep their text; show, rename, clone, and purge-state are new. Delete retires sidecar state; purge-state --confirm removes it. Mutations accept --expect-revision, checked under the manifest lock. - `account status` reports a typed disabled state. - The npm package ships the capture and context policy fixtures. The distribution test asserts the exact packed file list, and a new e2e test installs the packed tarball and runs help, init, and MCP. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
scripts/release-platforms.mjs names every downloadable app artifact once: one row per platform (mac-arm64, mac-x64) with its installer, update file, feed, site download path, and install-ping asset, plus a CLI for workflow file lists and a build/publish completeness check. electron-builder now builds arm64 and x64 with explicit artifactName patterns (the default drops the arch from an x64 DMG); a test expands both patterns for every table row. dist builds every configured arch and pack builds the host arch. The app's first-launch metric fetches its own row's ping asset, so Intel and Apple silicon launches count separately. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
- Homebrew cask: on_arm and on_intel blocks, each with its own DMG URL and digest. Channel artifacts refuse a build missing any platform row. - Release surface verifier: the deployed site must link and redirect every row, and /download/mac must still reach the Apple silicon DMG. - Metrics: count installer, update, and first-launch downloads per row by exact table names, reading the legacy install-ping.txt as Apple silicon for releases before the table. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
build-mac (macos-14) builds, signs, and notarizes both architectures, checks each app is native to its arch, runs Gatekeeper on both, and uploads the table's files. smoke-mac-x64 (macos-15-intel) launches the x64 update zip with CC_SMOKE=1 and requires SMOKE OK. publish (ubuntu) checks every table row, builds channel artifacts, writes SHA256SUMS with sha256sum, checks again, and creates one GitHub Release. public-surfaces runs only after that release exists. A workflow_dispatch dry run builds and verifies but never publishes or deploys; a dispatch with dry_run=false is refused. Tag-on-main, version alignment, release gates, signed-only publish, and unsigned inspection artifacts are kept. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
The release sync writes one record row per platform-table row and marks a row available only when the published release attached its installer, so 0.9.x (Apple silicon only) still syncs. One redirect per available row; /download/mac stays an alias of mac-arm64. /install lists each available Mac download with file name, size, and SHA-256. The home hero note, the footer, and the installation doc (now MDX with AppDownloadTable) read the same rows. Platforms without a download keep the source route, and the verifier derives its expected strings from the record through the shared app-downloads.mjs views. The privacy doc now shows the per-platform ping asset URL. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
MCP's list_concepts, get_links, read_file markdown, and rule merge, plus the service's /api/resolve disposition decoration, move into concept-queries.mjs so the CLI query family answers from the same code. Behavior is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
concept list|search|read|links and file list|read answer through the resolver, the retained search store, and layer-files.mjs, with coverage naming every source a read could not reach and --require-complete exiting 6. Parity tests pin the answers to /api/resolve, /api/search, /api/files, /api/file, and MCP list_concepts and get_links. doctor runs in-process: manifest and quarantined layers, the selected profile, source reachability, config/data/cache writability, and every contextcake on PATH with its version. A failed check exits 8. The app's wrapSpawn hook still adds the observability check. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
Source operations take { profileId, expectRevision }; null is default, the
only profile the HTTP service reads, so its routes behave as before.
- addSource stages a fresh clone and promotes it under the manifest lock
after the revision and name checks.
- removeSources can keep unreferenced clones (retainClones), and
pruneClones deletes one only when nothing references it and git reports
no changes, stash, or unpushed commits.
- syncSource moves out of service.mjs so the CLI runs the same code.
- listSources, configurePendingSource, withSourceSession, testSources.
- settingsView reports which tier each value came from; patchSettings
takes expectRevision.
- repairContextManifest takes profileId and a precondition;
syncPackAssignmentLevel takes a profileId.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
`contextcake source list|show|add|update|level|reorder|remove|test|sync| prune|pending-list|pending-configure|pending-dismiss` and `contextcake settings list|get|set|reset`, both experimental, over the shared control operations. Parity tests drive the CLI and the HTTP service over the same fixtures. The packed-tarball e2e now adds its folder with `source add` and checks MCP list_concepts serves it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
…and profile state - Spawned entrypoints refuse dispatcher flags they do not implement. `write --json --dry-run` used to read --dry-run as the value of --json and write for real. --timeout on a spawned write exits 2 (TIMEOUT_REFUSED); doctor keeps its own --json. - ctx.signal aborts on --timeout and interrupts. A read is abandoned and the process exits; a write always runs to the end, so an answer never says INTERRUPTED over a mutation that lands. A second SIGINT exits. - Redaction honors toJSON, keeps Error message and code, and only marks true cycles as circular. - migrateManifestToV2 allocates the profile id under its lock and throws PROFILE_EXISTS and PROJECT_MAPPED instead of plain errors. The test harness now fails any answer whose code the command does not declare. - Writes report the revision they wrote (ctx.noteManifestWrite); reads answer from the manifest the envelope read. - profile delete writes the manifest before retiring state and reports a failed retire as a warning. purge-state checks and deletes under the manifest lock. - New table trait requireComplete for commands that must exit 6 on partial coverage. The e2e test runs the Windows .cmd shim through a shell. - Site CLI reference documents the new profile commands and the envelope and exit-code change for `contextcake profile`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
The feed check was a substring match. It now parses electron-builder's feed shape (top-level scalars plus files[] of url/sha512/size), refuses anything else, and requires the feed version to equal the release version and each row's entry to carry the sha512 and size of the file on disk. verifyReleaseDirectory and the CLI accept digests recorded where the files were built (--digests FILE, SHA256SUMS format). Every checksummed file must be listed and match, so bytes that changed between jobs stop the release. Row shape and existing exports are unchanged; parseUpdateFeed and parseChecksums are new exports. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
A missing installer used to mean the row was unavailable, which could not tell a release from before the row existed from a half-uploaded one. A row is now unavailable only when the release has no trace of it: no installer, update file, install-ping asset, or SHA256SUMS line. Any trace with a missing installer fails the sync. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
The Homebrew cask uses one arch stanza, one sha256 with arm and intel
values, and one url that interpolates #{arch}. Rendering fails if Mac DMG
names ever differ by more than the arch.
releaseAssetNames(version) lists every asset a release attaches (table
rows plus channel artifacts). scripts/release-assets.mjs prints it for
gh release create and checks a draft's reported assets: exact names,
uploaded state, and local byte sizes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
- Signing: build-mac's SIGNING_ALLOWED is true only for an app-v tag push or a dispatch from main. Only those runs enter environment: release, detect secrets, or notarize; every other dispatch builds unsigned with CSC_IDENTITY_AUTO_DISCOVERY=false. - Signed dry runs keep no signed binary: desktop-mac lives one day, the new remove-dry-run-binaries job deletes it, and the dry-run upload (contextcake-release-dry-run, one day) holds checksums and feeds only. - build-mac hashes the table's files right after Gatekeeper and passes them as the checksums output. smoke-mac-x64 and publish check the bytes against it; smoke-mac-x64 also runs codesign and spctl on the unpacked zip when signed. - publish uploads to a draft, checks the draft's assets with scripts/release-assets.mjs, and only then sets --draft=false. - site-deploy.yml also triggers on scripts/release-platforms.mjs. - release-workflow.test.mjs parses the job graph, step conditions, and step order instead of only matching strings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
…cli-wave-a-query Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
Query and doctor operations take the manifest the envelope read instead of reading the file again, stop on --timeout and interrupts through ctx.signal, and report the revision they read. Site docs describe the concept, file, and doctor commands. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
The .deb ships no update file and updates by notification only, so its feed check looks for the installer in latest-linux.yml. Consumers read the row without code changes; their tests now carry it, and the committed site record lists it as unavailable until a release attaches it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
electron-builder gains a deb target named from the platform table, with desktopName synced to the .desktop entry and the contextcake:// scheme as its MimeType. The executable keeps its default name so the postinst link never shadows the `contextcake` CLI. The package maintainer never lives in the repo: `npm run dist:linux` reads CC_DEB_MAINTAINER and refuses to build without it, then checks the packaged resources carry the accounts-disabled marker. The icon generator also writes the hicolor PNG set the .deb installs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
…ackend On Linux without a keyring, safeStorage reports encryption available but uses basic_text, a key compiled into Chromium. The store now treats that backend (and `unknown` once the app is ready) as unavailable, checked at each use, and reports its mode as persistent or memory. A file that exists but will not decrypt or parse is renamed to <file>.unreadable-<timestamp> instead of reading as empty, where the next save would have replaced it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
The shim finds the .deb binary beside resources/ as well as the macOS bundle's. The link goes in ~/.local/bin on Linux (with a note when that folder is not on PATH) and /usr/local/bin on macOS; translocation stays a macOS check. A link to something other than a ContextCake app, such as an npm-installed CLI, is now a conflict and never replaced. Status also reports the link path and the first `contextcake` on PATH. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
When resources/package-type reads deb, the updater turns off download and install-on-quit before every check, never calls quitAndInstall, and reports an `available` status with the release URL. The menu check offers the download page instead of a relaunch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
- userData is pinned to the engine's XDG config dir right after setName, unless --user-data-dir was passed, so the app and CLI read one manifest. The smoke check compares against the switch, then that dir, then the macOS folder name. - A second instance quits without starting an engine or a window; a smoke run that loses the lock fails instead of exiting 0. - Linux gets File and Help menus; Local Grafana is created on macOS only. - The preload exposes `platform`, integrations report their storage mode, and the install metric counts a .deb under its own ping asset. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
platform.ts reads the desktop bridge's platform (a bridge without one is a Mac app) and falls back to the navigator in a browser. It drives shortcut glyphs, "Show in Files", the XDG config and log paths, and "this computer" in the Linux app, which also hides Local Grafana. Settings renders a .deb's `available` update as a download link and names the ~/.local/bin link; Connections says when tokens will not survive a restart. The Web Demo keeps its copy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
Once npm ships, the first `contextcake` on PATH may be another install running a different engine against the same manifest (control-plane spec §5.11). Connect Agent now uses the app's shim path whenever the app reports one, installed shortcut or not. The bare name remains for development builds, blocked (translocated) apps, and the browser. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
build-linux (ubuntu-24.04) builds the .deb with the DEB_MAINTAINER repository variable, stopping without it on every run, installs the package with apt, and launches /opt/ContextCake/contextcake-desktop under xvfb-run with no sandbox override. The smoke line now reports sandbox=on|off and the job requires on. publish waits for build-linux and takes its .deb and latest-linux.yml into the release directory. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
Runs the desktop-linux steps and an installed-.deb sandbox smoke on pushes to this branch only. Removed before review. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
Adapts the source and settings families to the foundation fixes: - every manifest write reports the revision it wrote under the lock (MANIFEST_REVISION on the operation result, ctx.noteManifestWrite); - `source test` uses the requireComplete trait instead of throwing; - adds and pending configures check ctx.signal before the locked write, and `source test` aborts its reads on it; - the tolerant manifest read computes the file's revision itself now that currentManifestRevision is gone. Also documents the two families in the site CLI reference. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
Brings in #177's review fixes (signing gate, draft release with a checked asset list, build digests, feed sha512/size checks, strict site sync) and fits the Linux row to them: build-linux records its own digests, publish checks them with build-mac's, the dry run deletes the .deb artifact, and the tests carry a real latest-linux.yml. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
Electron downloads its binary on first use. In the probe run, one test file unpacked it while others spawned it, and three tests failed with spawn ETXTBSY. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
Its results are recorded in the PR: the installed .deb booted with the sandbox on under Ubuntu 24.04's user-namespace restriction, and the dev binary tests exposed a first-use Electron download race, fixed in ci.yml. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
`source test` and `source sync` register their session close with ctx.onClose when the dispatcher offers it, so an MCP child cannot outlive a read cut off by --timeout. Clones and pulls take ctx.signal; an aborted clone removes its partial directory and answers with the abort reason. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
/api/resolve hashed raw section text while MCP read_file and the Discrepancy Center record fingerprint() (JSON-quoted), so a section acknowledged in the Discrepancy Center read "reopened" over HTTP. The shared decorator now uses fingerprint(), and MCP calls it instead of its own copy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
…tings - ctx.onClose(fn) registers async cleanups. The dispatcher runs them newest first, 1.5 s in total, after run() settles and before it answers a timeout or interrupt; an abandoned read waits 1 s before exiting. Read sessions register their close there and also close on ctx.signal, so a timed-out query no longer orphans an MCP child that ignores SIGTERM. - Aggregate queries list each source once and read from that listing, so coverage describes the data; a read failure after listing marks the source partial. - doctor never runs what it finds on PATH. control/installs.mjs skips relative entries, counts one install per folder (PATHEXT names only on Windows), dedupes by realpath, caps the scan, and reads the version from package.json or Info.plist. - doctor lists MCP and keychain-credential sources as notProbed without failing --require-complete, uses the query coverage vocabulary, and refuses a data folder under a regular file. - file read answers INVALID_INPUT for an empty path segment. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
In memory-only mode, removing the last token called clear(), which deleted the tokens.enc an earlier keyring session wrote. clear() now touches only the in-memory map when encryption is unavailable. An unreadable file that cannot be moved aside now blocks writes instead of being overwritten, and at most three .unreadable-* copies are kept. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
The notify-only gate keyed on resources/package-type = deb, so a future rpm or pacman target would reach electron-updater's pkexec installers. Every non-macOS packaged build now only checks and links. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
replaceCliLink re-reads the link and unlinks it only when it is dangling or points at a ContextCake shim; anything else throws EEXIST. Drops onPath: the GUI process PATH is launchd's minimal one, and nothing read it. `contextcake doctor` reports PATH installs from the user's shell. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
- A relative CONTEXTCAKE_CONFIG_DIR is ignored by the userData pin; it made app.setPath throw before ready and the app died silently. - The contextcake:// scheme is registered on macOS only. Linux delivers the URL in argv, which nothing parses yet. - The main process passes the config and engine log folders to the preload, and Settings shows those instead of hardcoded XDG paths. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
An unsigned or failed tag push kept an installable .deb for a day. Only publish reads the artifact, so remove-dry-run-deb now runs after publish on every outcome. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
7 tasks
…firmed host Review findings on the source family: - prune: cloneState runs git with user config off and counts ignored files, hidden index flags, in-progress merges or rebases, and reflog-only commits as work. Prune and the app's remove rename a clone into .cache/repos/.trash under the lock and check and delete it after releasing it; a clone that changed goes back. - pending-configure refuses to attach a token to a GitHub source whose apiBase is not the default unless --api-base restates it, and the pending listing always shows apiBase. The probe override never gets a token unless a test opts in. - add re-checks a reused clone under the lock and refuses a slot holding a clone of another repository; sync refuses a non-clone folder in the slot; a failed clone only removes a directory it created. - nested scrub markers count as missing; testSources drops its abort listener. - HTTP tests for clone add, sync, remove, SYNC_FAILED, and the new 409. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
Stack the source/settings families on the query/doctor families so the Wave A PRs merge in a straight line (#176 → #178 → #180) instead of conflicting. Conflict resolution: - cli/families/index.mjs: register source, settings, concept, and file. - CLAUDE.md: take #178's dispatcher row and ctx.onClose gotcha, and #180's control-operations row and clone/token gotchas. - reference/cli.md: one description naming every family. Follow-ups the merge made possible: - source.mjs calls ctx.onClose directly now that the dispatcher provides it. - cli-doctor test: the "no source family" case builds a table without it, and the "with source family" case uses the real registry instead of a stub. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
Stack the npm publish hardening on the Wave A CLI PRs so every open PR merges in one straight line. The only conflict was scripts/test.mjs, where both sides added a release suite next to each other; both suites are kept. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
…table Stack the release platform table on the npm publish PR (itself on the Wave A CLI stack) so every open PR merges in one straight line. Conflict resolution: - CLAUDE.md: keep this branch's release-table and release-workflow gotchas, plus the clone, token, and CLI-family gotchas from below. - distribution-artifacts.test.mjs: keep both new imports (createHash, readdir). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
Keep the Linux .deb PR stacked on the updated release-table PR, which now sits on the npm publish and Wave A CLI PRs. Conflict resolution (docs only): - CLAUDE.md: this branch's release-table and release-workflow gotchas (with the Linux row and build-linux), plus the clone, token, and CLI-family gotchas from below. - apps/desktop/CLAUDE.md: this branch's harness, config-dir, credential, updater, and maintainer gotchas, updated to point at the engine CLI's spawn site and wrapper. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
The Wave A CLI PRs and the npm publish PR landed as squash commits, and #174 rewrote the homepage hero. CLAUDE.md and the distribution test conflicted by identity; this branch supersedes them. The hero merged cleanly: main's new copy with this branch's per-architecture download links. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
Pick up main's 0.9.4 version bump through the release-table branch. The only conflict was apps/desktop/package.json, where this branch adds homepage and desktopName for the .deb build and drops "for Mac" from the description. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
#177 landed as a squash commit; the release-table files this branch already carries from it conflicted by identity. This branch supersedes them, adding the Linux row, build-linux, and the deb's install-ping and feed handling. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
siracusa5
added a commit
that referenced
this pull request
Sep 17, 2026
#181 landed as a squash commit. Its Linux line in the installation doc is superseded here by the npm-gated version of the same sentence. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
siracusa5
added a commit
that referenced
this pull request
Sep 17, 2026
Pick up #181 (Linux .deb). No conflicts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Step 5 of the multi-platform downloads plan: a Linux desktop app shipped as an x64
.deb, plus the step 4 bullet "connect commands use absolute paths".linux-x64-deb: installerContextCake-<v>-amd64.deb, no update file, feedlatest-linux.yml, updates by notification only..debboots with Chromium's sandbox on under Ubuntu 24.04 (kernel.apparmor_restrict_unprivileged_userns = 1). CI evidence is under Test Plan.userDatato$XDG_CONFIG_HOME/contextcake, the folder the CLI reads. It installs the CLI link in~/.local/binwith no sudo, keeps credentials in memory when there is no keyring (and says so), and links to new releases without installing them.contextcake.Implements
specs/contextcake-distribution/design.md§11.2 to §11.5 and the Linux "Platforms" criteria inspecs/contextcake-distribution/spec.md.Changes
Release table and tools
scripts/release-platforms.mjshas thelinux-x64-debrow. The cask, site sync, surface verifier, metrics, and release asset list read it with no code changes. Their tests now carry the row, including a reallatest-linux.ymlwith sha512, size, and version.apps/site/src/data/app-release.jsonlists the row as unavailable. 0.9.x has no.deb.Build (
apps/desktop)electron-builder.ymlhas alinuxblock: targetdeb, archx64, category Development,artifactNamefrom the table,syncDesktopName, and icons frombuild/icons.protocolsbecomes the.desktopMimeType (x-scheme-handler/contextcake).package.jsonaddshomepageanddesktopName(contextcake-desktop.desktop). The executable keeps its default namecontextcake-desktop, so the postinst's/usr/binlink never shadows the CLI.npm run dist:linux(scripts/dist-linux.mjs) reads the maintainer fromCC_DEB_MAINTAINERand fails with a clear message when it is unset. The repo names no maintainer. The script also checks thatresources/supabase-config.jsonin the built app matches the accounts-disabled marker.predist:linuxgenerates that marker.scripts/generate-icon.mjsalso writesbuild/icons/{16..1024}x{16..1024}.png.dist:linuxon macOS produced a.deb. Its control file lists Homepage, Depends, and the maintainer from the environment. The.desktopentry hasStartupWMClass=contextcake-desktopand the MimeType, and electron-builder wrotelatest-linux.ymlandresources/package-type(deb).App runtime
main.mjs: on Linux,app.setPath('userData', resolvePaths().config)runs right aftersetName, unless--user-data-dirwas passed (src/main/user-data.mjs). The engine module loads synchronously (requireof ESM) so the pin lands before the single-instance lock reads it.ContextCakefolder name on macOS. TheSMOKE OKline now reportssandbox=on|off.whenReadyandensureMainWindow. A smoke run that loses the lock printsSMOKE FAILand exits 1. Before this, it exited 0 with no output.encrypted-storage.mjs:basic_textbackend as unavailable. So isunknownonce the app is ready. Both are checked at each use.clear()andremoveItemtouch only memory, never an encrypted file an earlier keyring session wrote.<file>.unreadable-<timestamp>instead of letting the next write replace it. If the move fails, writes are refused. At most three copies are kept, oldest removed first.mode(). The newintegrations:storageIPC exposes it.CONTEXTCAKE_CONFIG_DIRis ignored by the userData pin. It madeapp.setPaththrow beforeready. The engine'splatform-paths.mjsis unchanged.contextcake://scheme is registered on macOS only. Linux delivers the URL in argv, which nothing parses yet.__CC_DESKTOP.paths), and Settings shows those.contextcake-desktopbesideresources/as well as the macOS bundle binary.cli-status.mjsuses~/.local/bin/contextcakeon Linux and reportslinkPath.conflict, never replaced.replaceCliLinkre-reads the link immediately before unlinking.blockedis macOS only.cli-install.mjsnever asks for sudo on Linux and warns when~/.local/binis not on PATH.updater.mjs: self-update is allowed on macOS only. On every other packaged build, each check first setsautoDownloadandautoInstallOnAppQuitto false. The app never callsquitAndInstalland reports{state: 'available', version, url}. The menu check offers the download page. Gating on the platform rather than onpackage-typekeeps a future rpm or pacman target away from electron-updater's pkexec installers.menu.mjs: off macOS, File has Settings and Quit, and Help has About, Check for Updates, and Install Command Line Tool. Linux keeps the native window frame.preload.cjsexposesplatform. The install metric passespackageType.Console
src/platform.tsdrives the following. In a browser it reads the navigator, so the Web Demo on Linux or Windows now showsCtrl+shortcuts instead of⌘. That change is intentional: it matchesShortcutsReferenceand the real key bindings. A desktop bridge withoutplatformcounts as a Mac app, and the Web Demo keeps its current copy.availableupdate renders a "Download update" link, and the CLI row names the reported link folder.shimPathwhenever the app reports one, including when the link is installed. The bare name remains for development,blocked, and the browser.CI and release
ci.yml:desktop-linuxjob (ubuntu-24.04, Node 22,xvfb-run -a). It runs desktopnpm test,test:navigation,test:cli-status, andsmoke, then checks that the smoke created~/.config/contextcake.required, and the outer name is unchanged.ELECTRON_DISABLE_SANDBOX=1because itschrome-sandboxis not setuid.app-release.yml:build-linux(ubuntu-24.04) fails closed withoutvars.DEB_MAINTAINER, dry runs included..deb, runs the table check, and records digests right after the build (job outputchecksums)./opt/ContextCake/contextcake-desktopunder xvfb with no sandbox override. It requiresSMOKE OKandsandbox=on, then runs the CLI through the shim.desktop-linuxartifact with one-day retention.publishneedsbuild-linux, downloads its files intorelease-dist, and appends its digests tobuild.sha256..deb,latest-linux.yml, andinstall-ping-linux-x64-deb.txtflow throughrelease-assets.mjs.remove-dry-run-debdeletes the.debartifact afterpublishon every outcome, pushes included. The unsigned inspection upload excludes*.deb.release-workflow.test.mjsparses the job graph forbuild-linux(needs, step order, digests, cleanup) instead of pinning strings.Site and docs
/installadds a "Linux app · .deb package" group once a release carries the row.AppDownloadTabletakesos.~/.local/binlink, and the keyring note.verify-install-page.mjschecks every available row.apps/desktop/CLAUDE.mdand the rootCLAUDE.mdgotchas cover the userData pin and--user-data-dir, the lost-lock gate,basic_text, the deb updater, the maintainer variable, and the CLI link.Test Plan
npm run dist:linuxwith a placeholder maintainer and passed the table check, thenapt-get install ./ContextCake-0.9.3-amd64.deb. The postinst registered/usr/bin/contextcake-desktopvia update-alternatives and installed/etc/apparmor.d/contextcake-desktop, andaa-statuslists the profile.kernel.apparmor_restrict_unprivileged_userns = 1. With no sandbox switch andELECTRON_DISABLE_SANDBOXunset, the app printedSMOKE OK http://127.0.0.1:33275 api=200 unauth=401 userData=contextcake sandbox=on … engineLog=/home/runner/.config/contextcake/logs/engine.log./opt/ContextCake/resources/bin/contextcake --versionprinted0.9.3. The probe is removed in this PR.sandbox=onin the smoke line is inferred from the absence of--no-sandboxandELECTRON_DISABLE_SANDBOX. The real evidence is that the app booted under the Ubuntu 24.04 namespace restriction with the AppArmor profile loaded, whichaa-statusconfirms.desktop-linux: all tests passed except 3, which failed withspawn ETXTBSY(see Notes). Fixed inci.yml; the PR's owndesktop (linux)job is the check.npm testunder the integration lock, before merging feat(release): platform table, Intel Mac build, and split release jobs #177's fixes: 65/65 suites. After the merge,node --test scripts/tests/*.test.mjs: 70 pass.apps/desktopon Node 22:node --test test/*.test.mjs(178 pass before the merge; the merge changed no desktop files),test:navigation, andtest:cli-status.npm run smokewith an isolated--user-data-dir:SMOKE OK … userData=ContextCake.apps/console:tsc --noEmitclean, vitest 786 pass.apps/site:npm run buildand both verifiers pass with the committed record. They also pass with a local copy that marks the.debavailable (not committed). A visual check at 1280px and 375px showed the Linux group and no horizontal scroll.dist-linux,user-data,package-type,encrypted-storage,cli-shim(runs the shim through a symlink for both layouts),menu,updater(deb notify-only), Linux fixtures incli-status-test,platform.test.ts,IntegrationsPanel.test.tsx, Diagnostics and Settings Linux cases, and the Connect Agent absolute path.app-release.ymldry run (John's to dispatch, after settingDEB_MAINTAINER).Notes
Review fixes (adversarial review, no critical or high findings). The fixes: memory-only mode no longer deletes an earlier encrypted file; the self-update gate is inverted to macOS only; the scheme is registered on macOS only; a failed set-aside now blocks writes, and at most three copies are kept; the CLI link is re-checked before unlinking; the pin ignores a relative config override; the unused
onPathis removed; Settings shows the main process's real paths; the.debartifact is deleted after every run; andApp.test.tsxchecks concrete labels per platform. Each testable fix has a test that failed first.ETXTBSY on Linux CI. Electron 43 downloads its binary on first use.
node --testruns test files in parallel, sonode-floor.test.mjsunpacked the binary whileengine-lifecycleandquit-persistencespawned it. Linux refuses to exec a file open for writing.desktop-linuxnow runsnode node_modules/electron/install.jsfirst. The macOS job has the same latent race and has not hit it.window-lifecycle.test.mjsalready skips off macOS. Its reason is beside the skip: closing the last window keeps the app running only on macOS. No other desktop test needed a platform skip.Local Grafana stays macOS only:
main.mjsnever creates the stack elsewhere, so observability IPC answersunavailableon Linux.Deep-link argv parsing is not implemented on Linux. Accounts ship disabled, so nothing needs it yet.
Engine logs on Linux land in
~/.config/contextcake/logs(Electron'slogspath under the pinned userData). The Settings copy says so.Open questions for John
DEB_MAINTAINERrepository variable before the nextapp-v*tag.publishneedsbuild-linux, which fails without the variable, so the next tagged release, Mac included, fails until it is set. The value is public: it lands in every.deb's control file. Use a project or noreply address (Name <address>), never a personal one. Dry runs stop without it too.basic_text(memory-only) path is exercised. On GNOME or KDE, confirmsafeStorage.getSelectedStorageBackend()reports a real backend (such asgnome_libsecretorkwallet5) once the app is ready. Otherwise every Linux user silently gets memory-only credentials..debbuild artifact is deleted after every run, and the unsigned inspection upload leaves it out. The package is never signed, so a copy from any run would look released. Is that the right call, or do you want it kept for a day like unsigned DMGs?/install, the footer, and the docs. Should the hero mention Linux once a release carries it?.debonly, and the guide sends everyone else to the source route. Do you want an arm64.debor an.rpmrow later?contextcakeowns~/.local/bin/contextcake, the app reportsconflictand leaves it. Connect Agent still works through the absolute path.contextcake doctor(feat(cli): concept, file, and doctor families (Wave A e+f) #178) reports PATH installs from the user's shell, so the app no longer tries to.🤖 Generated with Claude Code