Skip to content

feat: Linux desktop app (.deb, x64) and absolute connect paths - #181

Merged
siracusa5 merged 57 commits into
mainfrom
c/linux-deb
Sep 17, 2026
Merged

siracusa5 merged 57 commits into
mainfrom
c/linux-deb

Conversation

@siracusa5

@siracusa5 siracusa5 commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #177 (release table), which is on #175 and the Wave A CLI stack #176#178#180, all on #173. Targets main so CI runs. Merge those first.

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".

  • New platform row linux-x64-deb: installer ContextCake-<v>-amd64.deb, no update file, feed latest-linux.yml, updates by notification only.
  • The installed .deb boots with Chromium's sandbox on under Ubuntu 24.04 (kernel.apparmor_restrict_unprivileged_userns = 1). CI evidence is under Test Plan.
  • On Linux the app pins userData to $XDG_CONFIG_HOME/contextcake, the folder the CLI reads. It installs the CLI link in ~/.local/bin with no sudo, keeps credentials in memory when there is no keyring (and says so), and links to new releases without installing them.
  • Connect Agent now always uses the app's absolute shim path, never bare contextcake.

Implements specs/contextcake-distribution/design.md §11.2 to §11.5 and the Linux "Platforms" criteria in specs/contextcake-distribution/spec.md.

Changes

Release table and tools

  • scripts/release-platforms.mjs has the linux-x64-deb row. 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 real latest-linux.yml with sha512, size, and version.
  • apps/site/src/data/app-release.json lists the row as unavailable. 0.9.x has no .deb.

Build (apps/desktop)

  • electron-builder.yml has a linux block: target deb, arch x64, category Development, artifactName from the table, syncDesktopName, and icons from build/icons. protocols becomes the .desktop MimeType (x-scheme-handler/contextcake).
  • package.json adds homepage and desktopName (contextcake-desktop.desktop). The executable keeps its default name contextcake-desktop, so the postinst's /usr/bin link never shadows the CLI.
  • npm run dist:linux (scripts/dist-linux.mjs) reads the maintainer from CC_DEB_MAINTAINER and fails with a clear message when it is unset. The repo names no maintainer. The script also checks that resources/supabase-config.json in the built app matches the accounts-disabled marker. predist:linux generates that marker.
  • scripts/generate-icon.mjs also writes build/icons/{16..1024}x{16..1024}.png.
  • A local dist:linux on macOS produced a .deb. Its control file lists Homepage, Depends, and the maintainer from the environment. The .desktop entry has StartupWMClass=contextcake-desktop and the MimeType, and electron-builder wrote latest-linux.yml and resources/package-type (deb).

App runtime

  • main.mjs: on Linux, app.setPath('userData', resolvePaths().config) runs right after setName, unless --user-data-dir was passed (src/main/user-data.mjs). The engine module loads synchronously (require of ESM) so the pin lands before the single-instance lock reads it.
  • The smoke check compares userData against the switch, then the XDG dir on Linux, then the ContextCake folder name on macOS. The SMOKE OK line now reports sandbox=on|off.
  • A lost single-instance lock gates whenReady and ensureMainWindow. A smoke run that loses the lock prints SMOKE FAIL and exits 1. Before this, it exited 0 with no output.
  • encrypted-storage.mjs:
    • Treats safeStorage's basic_text backend as unavailable. So is unknown once the app is ready. Both are checked at each use.
    • In memory-only mode, clear() and removeItem touch only memory, never an encrypted file an earlier keyring session wrote.
    • Moves an unreadable file aside to <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.
    • Reports mode(). The new integrations:storage IPC exposes it.
  • A relative CONTEXTCAKE_CONFIG_DIR is ignored by the userData pin. It made app.setPath throw before ready. The engine's platform-paths.mjs is unchanged.
  • 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 (__CC_DESKTOP.paths), and Settings shows those.
  • CLI:
    • The shim finds contextcake-desktop beside resources/ as well as the macOS bundle binary.
    • cli-status.mjs uses ~/.local/bin/contextcake on Linux and reports linkPath.
    • A link to anything other than a ContextCake app shim is a conflict, never replaced. replaceCliLink re-reads the link immediately before unlinking. blocked is macOS only.
    • cli-install.mjs never asks for sudo on Linux and warns when ~/.local/bin is not on PATH.
  • updater.mjs: self-update is allowed on macOS only. On every other packaged build, each check first sets autoDownload and autoInstallOnAppQuit to false. The app never calls quitAndInstall and reports {state: 'available', version, url}. The menu check offers the download page. Gating on the platform rather than on package-type keeps 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.
  • Local Grafana is created on macOS only. preload.cjs exposes platform. The install metric passes packageType.

Console

  • New src/platform.ts drives the following. In a browser it reads the navigator, so the Web Demo on Linux or Windows now shows Ctrl+ shortcuts instead of . That change is intentional: it matches ShortcutsReference and the real key bindings. A desktop bridge without platform counts as a Mac app, and the Web Demo keeps its current copy.
    • Shortcut glyphs: palette, sidebar, shortcuts reference.
    • "Show in Files" and "Reveal in Files".
    • XDG config and log paths.
    • "this computer" in place of "this Mac".
    • Hiding the Local Grafana tab and Docker controls in the Linux app.
  • Settings: an available update renders a "Download update" link, and the CLI row names the reported link folder.
  • Connections shows one status line when tokens are memory-only.
  • Connect Agent uses shimPath whenever 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:
    • New desktop-linux job (ubuntu-24.04, Node 22, xvfb-run -a). It runs desktop npm test, test:navigation, test:cli-status, and smoke, then checks that the smoke created ~/.config/contextcake.
    • The job is part of required, and the outer name is unchanged.
    • The unpackaged dev binary runs with ELECTRON_DISABLE_SANDBOX=1 because its chrome-sandbox is not setuid.
    • The job installs the Electron binary before the tests (see Notes).
  • app-release.yml:
    • New build-linux (ubuntu-24.04) fails closed without vars.DEB_MAINTAINER, dry runs included.
    • It builds the .deb, runs the table check, and records digests right after the build (job output checksums).
    • It installs the package with apt, requires the AppArmor profile to be loaded, and launches /opt/ContextCake/contextcake-desktop under xvfb with no sandbox override. It requires SMOKE OK and sandbox=on, then runs the CLI through the shim.
    • It uploads the desktop-linux artifact with one-day retention.
    • publish needs build-linux, downloads its files into release-dist, and appends its digests to build.sha256.
    • The .deb, latest-linux.yml, and install-ping-linux-x64-deb.txt flow through release-assets.mjs.
    • New remove-dry-run-deb deletes the .deb artifact after publish on every outcome, pushes included. The unsigned inspection upload excludes *.deb.
  • release-workflow.test.mjs parses the job graph for build-linux (needs, step order, digests, cleanup) instead of pinning strings.

Site and docs

  • /install adds a "Linux app · .deb package" group once a release carries the row. AppDownloadTable takes os.
  • The installation guide has a Linux section: apt install, notify-only updates, the ~/.local/bin link, and the keyring note. verify-install-page.mjs checks every available row.
  • The privacy page names the Linux ping asset and notify-only updates.
  • apps/desktop/CLAUDE.md and the root CLAUDE.md gotchas 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

  • Installed .deb, sandbox on (temporary probe workflow, ubuntu-24.04). The probe ran npm run dist:linux with a placeholder maintainer and passed the table check, then apt-get install ./ContextCake-0.9.3-amd64.deb. The postinst registered /usr/bin/contextcake-desktop via update-alternatives and installed /etc/apparmor.d/contextcake-desktop, and aa-status lists the profile. kernel.apparmor_restrict_unprivileged_userns = 1. With no sandbox switch and ELECTRON_DISABLE_SANDBOX unset, the app printed SMOKE 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 --version printed 0.9.3. The probe is removed in this PR. sandbox=on in the smoke line is inferred from the absence of --no-sandbox and ELECTRON_DISABLE_SANDBOX. The real evidence is that the app booted under the Ubuntu 24.04 namespace restriction with the AppArmor profile loaded, which aa-status confirms.
  • Probe desktop-linux: all tests passed except 3, which failed with spawn ETXTBSY (see Notes). Fixed in ci.yml; the PR's own desktop (linux) job is the check.
  • Full root npm test under 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/desktop on Node 22: node --test test/*.test.mjs (178 pass before the merge; the merge changed no desktop files), test:navigation, and test:cli-status.
  • macOS npm run smoke with an isolated --user-data-dir: SMOKE OK … userData=ContextCake.
  • apps/console: tsc --noEmit clean, vitest 786 pass.
  • apps/site: npm run build and both verifiers pass with the committed record. They also pass with a local copy that marks the .deb available (not committed). A visual check at 1280px and 375px showed the Linux group and no horizontal scroll.
  • New tests: 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 in cli-status-test, platform.test.ts, IntegrationsPanel.test.tsx, Diagnostics and Settings Linux cases, and the Connect Agent absolute path.
  • Not done: an app-release.yml dry run (John's to dispatch, after setting DEB_MAINTAINER).
  • Not done: a manual pass on an Ubuntu 24.04 desktop, as the plan asks. That pass would add a folder, install the CLI and connect Claude Code, restart with and without a keyring, and see a newer release's download notice.

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 onPath is removed; Settings shows the main process's real paths; the .deb artifact is deleted after every run; and App.test.tsx checks 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 --test runs test files in parallel, so node-floor.test.mjs unpacked the binary while engine-lifecycle and quit-persistence spawned it. Linux refuses to exec a file open for writing. desktop-linux now runs node node_modules/electron/install.js first. The macOS job has the same latent race and has not hit it.

  • window-lifecycle.test.mjs already 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.mjs never creates the stack elsewhere, so observability IPC answers unavailable on 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's logs path under the pinned userData). The Settings copy says so.

Open questions for John

  1. Set the DEB_MAINTAINER repository variable before the next app-v* tag. publish needs build-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.
  2. Check keyring detection on a real Linux desktop. CI's Linux runner has no keyring daemon, so only the basic_text (memory-only) path is exercised. On GNOME or KDE, confirm safeStorage.getSelectedStorageBackend() reports a real backend (such as gnome_libsecret or kwallet5) once the app is ready. Otherwise every Linux user silently gets memory-only credentials.
  3. The .deb build 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?
  4. Hero and footer copy. The site renders the Linux row wherever it renders rows. The home hero still says "Download for Mac", and Linux shows only on /install, the footer, and the docs. Should the hero mention Linux once a release carries it?
  5. Other distributions and arm64. v1 is x64 .deb only, and the guide sends everyone else to the source route. Do you want an arm64 .deb or an .rpm row later?
  6. The CLI link on a conflict. When an npm-installed contextcake owns ~/.local/bin/contextcake, the app reports conflict and 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

siracusa5 and others added 30 commits September 16, 2026 23:20
…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>
siracusa5 and others added 16 commits September 17, 2026 00:39
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>
siracusa5 and others added 8 commits September 17, 2026 02:57
…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
siracusa5 merged commit b8e8005 into main Sep 17, 2026
10 checks passed
@siracusa5
siracusa5 deleted the c/linux-deb branch September 17, 2026 20:41
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>
@siracusa5 siracusa5 mentioned this pull request Sep 17, 2026
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant