Development setup
Prerequisites
Section titled “Prerequisites”- Bun — primary package manager and runtime
- Node.js 18+ (used by some sub-tools like Playwright)
- Obsidian — for plugin testing
- Git
Optional for sharing your local dev server with others:
- Tailscale — for tailnet-only sharing
- Cloudflare Tunnel (via
bun x cloudflared) — for public URL sharing
Clone and install
Section titled “Clone and install”That installs the plugin’s dependencies. The docs site has its own separate node_modules in docs/ that gets installed automatically on first use of the orchestrator.
The local dev orchestrator — bun run serve
Section titled “The local dev orchestrator — bun run serve”From the repo root, bun run serve is an interactive menu wrapping every local workflow. It’s the recommended entry point for development — it handles cross-OS node_modules contamination automatically (e.g. rollup native binary mismatches when bouncing between WSL and Windows), auto-installs docs/node_modules on first run, and cleans up all spawned children on Ctrl+C.
Non-interactive shortcuts
Section titled “Non-interactive shortcuts”Windows double-click launchers
Section titled “Windows double-click launchers”Four .bat files at the repo root, for when you’d rather double-click than open a terminal:
| File | Runs | Use |
|---|---|---|
serve.bat | bun run serve | Interactive menu (docs / plugin / both / tunnel / tests) |
serve-docs.bat | bun run serve:docs | Docs dev server only → http://localhost:14321/ |
dev.bat | bun run serve:plugin | Plugin watch build only, skipping the menu |
reset.bat | node scripts/reset-test-vault.mjs | Clears ad-hoc test imports from test-vault/ — previews what would be deleted, then waits for a keypress to confirm |
All four cd to the repo root first (so they work from anywhere) and pause on exit so the window stays open if something errors. serve.bat and serve-docs.bat run through Windows bun directly; dev.bat and reset.bat shell out to WSL (matching how the underlying watch build and reset script are normally run).
Plugin development workflow
Section titled “Plugin development workflow”Then:
- Open
test-vault/in Obsidian (File → Open vault → selecttest-vault) - Enable Crosswalker in Settings → Community plugins
- Trigger the plugin via command palette: Crosswalker: Start here: import structured data
- Edits to
src/auto-rebuild. Reload the plugin in Obsidian (toggle in settings or Ctrl+R in the dev console) to pick up changes - Install the Hot Reload community plugin in
test-vault/for automatic plugin reloads on rebuild
Docs site development workflow
Section titled “Docs site development workflow”Status last verified: 2026-11-13 — website location and compatibility implementation checked against docs/astro.config.mjs, docs/site-urls.mjs, docs/website-compat.mjs and docs/playwright.config.ts. This marker covers website paths, not unrelated setup commands.
The canonical website is crosswalker.io, served at / (site and base come from docs/site-urls.mjs). New links should use root routes such as /getting-started/installation/; Astro generates canonical and Open Graph URLs on the new origin.
The render middleware normalizes old website URLs in actual link, image and SVG attributes and CSS url() values, without rewriting code examples, recipe documents, schema identities or historical source prose. After canonical search, graph and sitemap generation, the build creates compatibility pages for both /crosswalker/ and /Crosswalker/ from Astro’s published route inventory, including generated blog and tag listings. These are HTTP 200 HTML/client redirects, not HTTP 301; JavaScript retains the incoming query and fragment. With JavaScript disabled, the page offers a canonical fallback link. Public assets are copied byte-for-byte under both prefixes, rather than redirected as HTML. Current hashed bundles are referenced directly at /_astro/; obsolete hashes from earlier builds are not retained.
Search and sitemap exclude compatibility copies; the graph’s URL slots are normalized after its Markdown/HTML merge. Historical GitHub Pages origin redirects, DNS, certificate issuance and remote hosting behavior require separate post-deployment verification; a local preview cannot prove them.
Build once with cd docs && bun run build, then run cd docs && bun run test:local against that same output. The dedicated test preview uses port 14325, at most 2 workers (source: docs/playwright.config.ts), and refuses to reuse an unrelated server. Deterministic URL/alias tests run from the repository root with node --test docs/tests/site-urls.unit.mjs. See docs testing for the full harness.
Opens the Astro dev server at http://localhost:14321/. HMR is active — edits to .mdx files hot-reload in the browser (with the exception of inline <style> blocks in MDX files, which sometimes require a full page refresh).
Do not open docs/ as an Obsidian vault — content files are .mdx, not .md. Use VS Code with the MDX extension for rich editing.
Running both in parallel
Section titled “Running both in parallel”Common when working on a feature that touches both plugin code and docs:
Starts the docs dev server and plugin watch build in the same terminal. Single Ctrl+C kills both cleanly.
Build commands (raw)
Section titled “Build commands (raw)”These all work standalone if you prefer not to use the orchestrator:
| Command | Description |
|---|---|
| Plugin | |
bun run dev | Plugin watch mode, outputs to test-vault |
bun run build | Plugin production build (type-checks the plugin and the command-line tool, then bundles) to the repo root, also copied into test-vault/.obsidian/plugins/crosswalker/ |
bun run test | Plugin Jest unit tests |
bun run test:watch | Plugin unit tests in watch mode |
bun run lint | ESLint with obsidian-plugin rules (required for community plugin submission) |
bun run lint:fix | Auto-fix lint issues |
bun run hygiene:branches | Fetch/prune origin and report merged branches, stale unmerged branches and worktree state. Opt-in bun run hygiene:branches -- --apply removes only ancestry-merged local branches and clean, unlocked worktrees with known activity at least 24 hours old; recent or unknown activity is retained as recent. Repeat --keep <path-or-branch> or use the ignored, per-user .branch-hygiene-keep in the primary checkout (one entry per line; whole-line # comments only). Keep paths and .workspace/ restore manifests resolve against that primary checkout, even when invoked from a linked worktree. Main, current, kept and locked work are protected; unmerged branches are report-only. Ignored scratch files, hidden index flags and in-progress Git operations count as dirty. Manifests record tip SHAs only; deleting a branch drops its reflog. Do not run cleanup concurrently with other branch/worktree maintenance |
bun run e2e | Plugin E2E tests (WebdriverIO, requires Obsidian) |
bun run e2e:xvfb | Same E2E suite on a private virtual display (Xvfb). Use for screenshot specs; it keeps working when the monitors sleep |
bun run e2e:openclast | Browser-native host smoke: loads the root build into OpenClast (Obsidian in a headless Chromium tab) and checks the search index is stored, survives a tab reload, and that Settings says so. Run bun run build first. Needs an OpenClast checkout at ../../obsidian-in-the-browser or set OPENCLAST_REPO; output goes to test-screenshots/openclast/ (or OPENCLAST_OUT) |
| Orchestrator (interactive menu wraps the workflows below) | |
bun run serve | Interactive 8-mode menu — docs dev, plugin watch, parallel both, tunnel sharing, tests, MDX check |
bun run serve:docs | Shortcut: docs Astro dev server (mode 1) |
bun run serve:plugin | Shortcut: plugin watch build (mode 4) |
bun run serve:both | Shortcut: docs dev + plugin watch in parallel (mode 5) |
bun run serve:share | Shortcut: docs dev + Tailscale tunnel (mode 6) |
| Fixtures + spec | |
bun run fixtures | Regenerate Tier 1 test fixtures into test-vault/Frameworks/NIST-mini/ from tools/fixtures/synthetic/nist-mini.csv; also re-emits the committed reference .base views from tools/fixtures/examples/ (--examples flag) |
bun run reset | Clear ad-hoc test imports from test-vault/ (dry-run by default; -- --yes to delete). Removes only Crosswalker-generated notes outside the protected corpus (_licensed/, NIST-mini/, _crosswalker/, GRC analysis/, PROVENANCE.md). Double-click reset.bat for a preview-then-confirm one-click |
| Command line | |
bun run cw -- <command> | Run import, validate or export against a plain folder without Obsidian. Commands: import, validate, export, replay, recipes, graph configure, graph connections; add --deterministic for byte-identical output. Exit codes: 0 ok, 1 usage or runtime error, 2 validation failures. bun run cw -- --help lists the options. For a workbook, import takes --sheet <name> and --header-row <n> (counted from 1, default 1); when the sheet is wrong, the error lists the workbook’s sheets and the sheet the recipe was written for. replay --vault <dir> --set <set id> --source <file> runs a set again with the exact recipe revision it ran, read from the copy the set keeps; pass --digest sha256-<hex> when the set holds more than one revision |
bun run demo:build -- --out <dir> [--sources-dir <checkout>] | Build the public demo offline through cw: full NIST CSF 2.0, SP 800-53 r5, MITRE F3 v1.2, ATT&CK Enterprise v16.1 techniques plus a separate mitigation set, and D3FEND; publisher crosswalks, synthetic CSF-to-ATT&CK links and evidence, Bases queries, Start here.md and graph colors. Reads held Frameworks/ files from --sources-dir (default: repository root), never modifies that directory, stages derived mapping TSVs outside the repository, and refuses output inside either checkout. Obtain the pinned files using the source reference. Ends with cw validate; no restricted framework content |
bun run demo:add-licensed -- --private-room-only --vault <dir> [--only cri,cis,scf] [--sources-dir <dir>] [--source <workbook>] | Add locally held CRI Profile 2.2, CIS Controls 8.1 and SCF 2026.1.1, subject tags, available mappings and selected identifier-only referenced-framework notes to a fresh demo vault outside the repository. Fixed manifest order; missing sources are skipped with acquisition guidance. --only selects framework keys; --sources-dir resolves default relative source paths (default: this checkout); --source overrides the framework workbook only and requires one explicit --only key. Never part of demo:build; explicit private-room acknowledgement required. See source bindings and restrictions |
| Docs | |
cd docs && bun run dev | Docs Astro dev server |
cd docs && bun run build | Docs production build → docs/dist |
cd docs && bun run preview | Serve the built dist locally |
cd docs && bun run test:local | Docs Playwright E2E tests |
bun run check:parity | Check UI/CLI command and verb coverage, tracked gaps, and the generated parity reference |
bun run parity:write | Regenerate the parity table and counts from tools/parity/capabilities.ts |
bun run check:mdx | Lightweight MDX syntax pre-check (catches bare braces, unclosed tags, frontmatter YAML errors) — fast (~1–2s) |
| Release | |
bun run version | Bumps version in manifest.json + versions.json; stages git changes for commit |
Graph configuration last verified: 2026-10-30 against cli/crosswalker.ts, cli/graph.ts and synthetic CLI tests. bun run cw -- graph configure --vault ./demo emits a no-write JSON color preview. --mode overview previews the explicit display preset; --apply saves the same policy as the plugin. Close Obsidian before applying so open views cannot overwrite global defaults. --config-dir <folder> accepts a vault-relative directory and rejects traversal or symlink escapes. Other settings, notes and local graphs are preserved. See graph display behavior.
Connections last verified: 2026-10-30 against src/graph/connections-query.ts, cli/graph.ts and synthetic CLI tests. bun run cw -- graph connections --vault ./demo --source-set <id> --target-set <id> returns read-only recorded assertions, negative/unresolved totals and coverage. Optional --focus-curie, --focus-set, --search, --limit (1–100, default 40) and --offset share the plugin policy (source: cli/crosswalker.ts, commandGraph). No --apply or desktop launch exists. See selection, provenance and limitations.
Mapping release isolation. bun run cw -- import --source <file.sssom.tsv> --vault <dir> --set new-set-qualified explicitly creates a new mapping set whose assertion identities are scoped to that set. Use it to keep overlapping publisher releases separate without taking over an earlier release. It is mapping-only; framework imports reject it. The default remains a new set, never an automatic refresh: the CLI checks exact assertion identities and scopes the entire new set only if any overlap existing claims. Disjoint mapping sets keep their source identities even when their namespace matches. Refresh still requires the chosen set id. The explicit override forces isolation even without an overlap (sources: cli/crosswalker.ts, src/generation/import-set.ts, tests/cli/mapping-release-isolation.test.ts).
Mapping identity selection last verified: 2026-10-30 against cli/crosswalker.ts, src/generation/import-set.ts and the qualification and mapping-release-isolation tests. See the shared-namespace identity decision. This does not re-verify unrelated setup commands.
Private add-on options last verified: 2026-10-30 against scripts/demo-add-licensed.mjs, synthetic CLI tests and two byte-identical local licensed builds. This scope does not re-verify the unrelated development workflows above.
See the testing page for details on each test surface.
Pre-commit static gates
Section titled “Pre-commit static gates”Run all 7 static checks (source: .github/workflows/static-checks.yml): bun run check:personal-data, bun run check:mdx, bun run check:frontmatter, bun run check:not-content, bun run check:log-labels, bun run check:links, and bun run check:parity. The knowledge operations reference explains what each enforces and what still needs review.
Every UI vault action needs a cw route or a tracked gap, and every headless action needs the same accounting in reverse. See the generated UI/CLI parity table before extending the command line. cw recipes lists built-ins, not the saved recipe library; refresh already exists through import --set and replay.
Releasing
Section titled “Releasing”bun run version— bumpsmanifest.json+versions.jsonto the version already set inpackage.json.- Add a
## [x.y.z]heading toCHANGELOG.mdwith the notes for that version. The release workflow uses that section verbatim and fails if it is missing or empty; there is no fallback body. - Merge to
main..github/workflows/release.ymlbuilds, tags, and publishes the three plugin assets. 0.xversions and any version with a suffix publish as prereleases, not as the latest release.- A version whose tag or release already exists is skipped with a notice. The workflow never deletes, moves, or overwrites published state, so republishing means shipping a new version.
scripts/release-preflight.mjs makes those decisions and is covered by tests/release-preflight.test.ts, so they can be checked without running Actions.
Project structure
Section titled “Project structure”First-time sanity check
Section titled “First-time sanity check”After cloning and bun install, verify the setup works:
If the plugin build or docs dev server fails with a rollup native-binary error (e.g. Cannot find module @rollup/rollup-<platform>), just re-run bun run serve — the orchestrator auto-detects the contamination and reinstalls docs/node_modules with the correct platform binaries.