Developing in the monorepo
The Lifosy code lives in one Turborepo monorepo with pnpm workspaces (apps/* and packages/*, from pnpm-workspace.yaml). TypeScript apps use Vite, Biome for lint and format, and Vitest for tests; the CLI and the desktop palette backend are Rust. This page covers the prerequisites, the root commands, per-app commands, CI deployment to Cloudflare Pages, versioning, documentation conventions and how the @lifosy/docspack documentation package is built.
Prerequisites and the pnpm 12.4.1 install gotcha
You need these tools before the first install:
- Node.js LTS. The root
package.jsondeclares"engines": { "node": ">=18" }; CI uses Node 22, andapps/kb-mcpneeds Node 22 or later. - pnpm 12.4.1, the version pinned in
"packageManager": "[email protected]". - Rust (stable via rustup) for
apps/cliandapps/desktop-palette. giton$PATH; the CLI uses it for repo sync.
Install pnpm 12.4.1 yourself before pnpm install:
corepack enable # fetches the pinned binary
# or
npm install -g [email protected]
Do not let an older pnpm provision 12.4.1. Its bootstrap runs pnpm add [email protected] --allow-build=@pnpm/exe. The preinstall/postinstall scripts belong to the pnpm package, not @pnpm/exe, so a pnpm that enforces build approval refuses them and the install fails with ERR_PNPM_IGNORED_BUILDS.
Installing dependencies and pnpm workspace settings
Run pnpm install once from the repository root. It installs every workspace package.
pnpm-workspace.yaml holds two pnpm settings that affect installs:
allowBuildslists the dependencies allowed to run build scripts:@biomejs/biome,esbuildandsharp. pnpm 11+ replacedonlyBuiltDependencieswith this yes/no list. A new dependency with a build script must be added here before its script runs.minimumReleaseAgeExcludepins exact@cascivo/*versions published inside pnpm’s 24-hourminimumReleaseAgewindow. Without the exclusion, a cold-cache install fails withERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION. The pins can go once those versions are older than the cutoff.
All devDependencies belong in the root package.json (@biomejs/biome, typescript, vitest, vite, turbo, playwright, jsdom, fake-indexeddb, docspack). Runtime dependencies go in each app or package.
Root scripts: build, lint, check-types, verify
The root package.json scripts call Turborepo tasks defined in turbo.json.
| Command | Runs |
|---|---|
pnpm build | turbo run build (every package’s build; depends on ^build) |
pnpm lint | turbo run lint |
pnpm format | turbo run format |
pnpm check-types | turbo run check-types |
pnpm check | turbo run lint check-types |
pnpm fix | turbo run fix |
pnpm verify | fix, then build, lint, format, check-types |
pnpm dev | turbo run dev (all dev servers) |
pnpm dev:console | turbo run dev watch for @lifosy/app-console and @lifosy/ui |
pnpm console:build | turbo run build:prod --filter=@lifosy/app-console |
pnpm landing-nextgen:build | turbo run build --filter=@lifosy/landing-page-nextgen |
turbo.json caches build outputs (dist/**, .next/**, target/release/**). dev and watch are persistent and uncached. turbo.json defines no test task and no check task. So pnpm turbo test and pnpm turbo check fail with “Could not find task”. Use pnpm check for lint and types, and pnpm -r test (or pnpm --filter <package> test) for tests.
pnpm build also runs cargo build --release in apps/cli, so a full build needs Rust.
Running tests with Vitest and cargo
Tests run per package with pnpm --filter <name> test.
| Package | test script | Notes |
|---|---|---|
@lifosy/app-console | vitest run | jsdom, setup in vitest.setup.ts; spike tests only under vitest.spike.config.ts |
browser-extension | vitest run | jsdom |
@lifosy/palette | vitest run | jsdom |
@lifosy/formats | vitest run | |
@lifosy/kb-mcp | vitest run | |
@lifosy/cli | cargo test | Rust |
desktop-palette | test:desktop | vite build && cargo test --manifest-path src-tauri/Cargo.toml |
@lifosy/docspack | docspack eval | retrieval hit rate, see below |
Example:
pnpm --filter @lifosy/formats test
pnpm --filter @lifosy/app-console test
cd apps/cli && cargo test
fake-indexeddb in the root devDependencies stands in for IndexedDB in console and core tests.
Biome lint and format configuration
Biome is the linter and formatter for all TypeScript packages. The root biome.json extends packages/config/biome.json and uses the git ignore file.
Settings in packages/config/biome.json:
- 2-space indent, line width 100, single quotes in JavaScript.
- Recommended lint rules, with
suspicious.noExplicitAnyturned off. organizeImportson.
Each package exposes the same three scripts:
pnpm --filter @lifosy/core lint # biome check .
pnpm --filter @lifosy/core format # biome check --write .
pnpm --filter @lifosy/core fix # biome check --write --unsafe .
@lifosy/ui limits Biome to src/, vite.config.ts and .storybook/. The Rust app apps/cli maps lint to cargo fmt --check && cargo clippy and format/fix to cargo fmt.
Running each app in development
| App | Command | Result |
|---|---|---|
| Web console | pnpm --filter @lifosy/app-console dev | Vite on http://localhost:4300 |
| Browser extension | cd apps/browser-extension && pnpm dev | then Load unpacked apps/browser-extension/dist in chrome://extensions |
| Terminal CLI | cd apps/cli && cargo run | TUI; cargo run -- capture "…", cargo run -- server |
| Desktop palette | cd apps/desktop-palette && pnpm run build:desktop | UI and both Rust binaries (lifosy-palette, lifosy-palette-toggle) |
| kb-mcp | pnpm --filter @lifosy/kb-mcp build then start | node dist/index.js |
| Landing page | pnpm --filter @lifosy/landing-page-nextgen dev | Astro on localhost:4321 |
| Storybook | pnpm --filter @lifosy/ui storybook | port 6006 |
The console imports @lifosy/ui from its build, so run pnpm --filter @lifosy/ui build once, or use pnpm dev:console to watch both. The console dev server runs on port 4300 (server.port in apps/console/vite.config.ts).
Rust toolchain for the CLI and desktop palette
apps/cli/rust-toolchain.toml and apps/desktop-palette/src-tauri/rust-toolchain.toml both pin the toolchain:
[toolchain]
channel = "1.94.1"
components = ["rustfmt", "clippy"]
The pin keeps cargo fmt output reproducible, because rustfmt output changes between releases. rustup installs the pinned version on first use.
The desktop palette also needs system libraries for Tauri and layer-shell. On Fedora:
sudo dnf install webkit2gtk4.1-devel gtk3-devel gtk-layer-shell-devel libsoup3-devel \
librsvg2-devel openssl-devel dbus-devel
sudo dnf group install c-development
pnpm run build in apps/desktop-palette builds only the UI; that is what the monorepo build runs. build:desktop builds the Rust binaries too.
Building the experimental Zig capture app
apps/kh-capture-native is an experimental quick-capture window built with native-sdk, which has a Zig core. It has no package.json, so it is not part of the pnpm workspace or the Turborepo build.
Its README says the code was written from the native-sdk docs and has not been compiled yet. Symbols to check are marked VERIFY(api). To build it you need the native CLI:
npm install -g @native-sdk/cli
cd apps/kh-capture-native
native dev # hot-reloading dev window
native build # release binary -> zig-out/bin/kh-capture
native check # typecheck + validate .native / app.zon
CI workflows and Cloudflare Pages deployment
.github/workflows/ has three workflows. Both deploy workflows call the reusable template-deploy-to-cloudflare.yaml.
| Workflow | Name | Trigger | Builds | Cloudflare Pages project |
|---|---|---|---|---|
deploy-magic-life-os.yaml | [PROD] Deploy Life OS | push to main touching apps/console/**, or manual | pnpm run console:build | lifosy-console |
deploy-landing.yml | [PROD] Deploy Landing Page | push to main touching apps/landing-page-nextgen/**, or manual | pnpm run landing-nextgen:build | lifosy-landing |
template-deploy-to-cloudflare.yaml | Deploy To CloudFlare | workflow_call only | — | input cfPagesName |
The template runs on ubuntu-latest with Node 22 and pnpm (pnpm/action-setup@v4). Steps: pnpm install, pnpm run build, pnpm run <buildTarget>, an optional commit of generated files, then wrangler pages deploy <distDir> --project-name=<cfPagesName> --branch=main. It needs the repository secrets CF_ACCOUNT_ID and CF_API_TOKEN.
The console workflow sets commitGeneratedFile: true. It commits apps/console/src/version.ts and apps/console/version.json back to main as github-actions[bot] with [skip ci]. That is why it needs contents: write.
No workflow runs tests or lint on pull requests.
How the console version number is bumped
The console version lives in apps/console/version.json as { "major", "minor", "patch" }. The app shows it in the status bar after login.
scripts/generate-version.jswritessrc/version.tswith the version, an ISO build timestamp and the short git hash.dev,prodandbuildrun it first.scripts/bump-version.js [patch|minor|major]incrementsversion.json(defaultpatch) and then regeneratessrc/version.ts.build:prodruns the bump, thenvite build --mode prod. Every production deploy therefore bumps the patch version, and CI commits the result.
pnpm --filter @lifosy/app-console version:bump # patch
pnpm --filter @lifosy/app-console version:bump:minor
pnpm --filter @lifosy/app-console version:bump:major
apps/console/VERSION.md describes the system. Production builds read .env.prod, which sets VITE_GH_LOGIN.
Documentation conventions: progress, features, roadmap
Project documentation lives in doc/ at the repository root. CLAUDE.md sets the update rules.
| File | Purpose | When to update |
|---|---|---|
README.md | Intro, prerequisites, quick start | On significant changes |
doc/progress.md | Changelog, newest entry first | Every change |
doc/features.md | Feature tables per app with an “Added” date | When a feature is added |
doc/documentation.md | Detailed CLI usage | When CLI features change |
doc/roadmap.md | Checklist of planned work | Mark done items [x] |
A doc/progress.md entry has the heading ## YYYY-MM-DD — <title>, bullets describing the change, and a closing Checks: line listing what was run (Biome, tsc, Vitest counts, cargo test, clippy). Format specs also live in doc/: doc/commands-format.md, doc/prompts-format.md, doc/braindump.md, doc/native-command-palette.md. App READMEs (apps/*/README.md) hold the per-app user documentation.
Building and checking the docspack documentation package
packages/docspack publishes this documentation as @lifosy/docspack, an npm package that AI agents query offline. Sources are the Markdown files in packages/docspack/docs/; packages/docspack/cmdspec.yaml describes the cli binary, one chunk per command. package.json sets "docspack": { "from": "./docs", "cmdspec": "./cmdspec.yaml" }.
pnpm --filter @lifosy/docspack build # docspack build -> .llms/ and llms.txt
pnpm --filter @lifosy/docspack lint # docspack build && docspack doctor --strict
pnpm --filter @lifosy/docspack test # docspack build && docspack eval ./eval/queries.json --min-hit-rate 90
buildwrites the.llms/payload andllms.txt; both are git-ignored and published viafiles.doctor --strictchecks the package as the indexer would and treats warnings as failures. It is the package’slintscript, so rootpnpm lintruns it.evalruns the questions ineval/queries.jsonand fails below a 90% hit rate.prepublishOnlyruns build and doctor beforenpm publish.docspack preview "<question>"inpackages/docspackshows which chunks answer a question.
Each doc file has one # title and ## sections phrased as questions. A chunk id is slug(H1)-slug(H2).
Using the Lifosy docs from another project
A project that wants an AI agent to answer Lifosy questions installs the docs package and indexes it locally:
pnpm add -D docspack @lifosy/docspack
npx docspack sync # index installed docs packages, no network
npx docspack ask "how do I sign in with GitHub"
docspack syncreadsnode_modulesand indexes every@vendor/docspackpackage into a local SQLite FTS5 store.--forcere-indexes.docspack askanswers from that index.--package lifosylimits it to this package,--limitsets the number of chunks (default 3).- One line in the consumer’s
CLAUDE.mdorAGENTS.mdis enough: “Rundocspack ask "<question>"for documentation on this project’s dependencies.”