Skip to content
🚧 Early alpha — building the foundation. See the roadmap →

Development setup

Created Updated
  • 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:

git clone https://github.com/cybersader/crosswalker.git
cd crosswalker
bun 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.

bun run serve
━━━ Crosswalker local serve ━━━

  1) Docs dev server          http://localhost:14321   HMR, for KB editing
  2) Docs preview (built)     http://localhost:14321   built site
  3) Docs build only          → docs/dist
  4) Plugin dev (watch)       → test-vault
  5) Docs + plugin dev        both in parallel
  6) Share docs (Tailscale)   tailnet only
  7) Share docs (Cloudflare)  public URL
  8) Docs e2e tests           playwright test:local

  Choose [1-8]:
bun run serve:docs       # Option 1 — docs dev server
bun run serve:plugin     # Option 4 — plugin watch
bun run serve:both       # Option 5 — docs + plugin in parallel
bun run serve:share      # Option 6 — docs + Tailscale

Four .bat files at the repo root, for when you’d rather double-click than open a terminal:

FileRunsUse
serve.batbun run serveInteractive menu (docs / plugin / both / tunnel / tests)
serve-docs.batbun run serve:docsDocs dev server only → http://localhost:14321/
dev.batbun run serve:pluginPlugin watch build only, skipping the menu
reset.batnode scripts/reset-test-vault.mjsClears 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).

# Watch mode — rebuilds plugin on file changes, outputs to test-vault
bun run serve:plugin
# or the raw command:
bun run dev

Then:

  1. Open test-vault/ in Obsidian (File → Open vault → select test-vault)
  2. Enable Crosswalker in Settings → Community plugins
  3. Trigger the plugin via command palette: Crosswalker: Start here: import structured data
  4. Edits to src/ auto-rebuild. Reload the plugin in Obsidian (toggle in settings or Ctrl+R in the dev console) to pick up changes
  5. Install the Hot Reload community plugin in test-vault/ for automatic plugin reloads on rebuild

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.

bun run serve:docs

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.

Common when working on a feature that touches both plugin code and docs:

bun run serve:both

Starts the docs dev server and plugin watch build in the same terminal. Single Ctrl+C kills both cleanly.

These all work standalone if you prefer not to use the orchestrator:

CommandDescription
Plugin
bun run devPlugin watch mode, outputs to test-vault
bun run buildPlugin 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 testPlugin Jest unit tests
bun run test:watchPlugin unit tests in watch mode
bun run lintESLint with obsidian-plugin rules (required for community plugin submission)
bun run lint:fixAuto-fix lint issues
bun run hygiene:branchesFetch/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 e2ePlugin E2E tests (WebdriverIO, requires Obsidian)
bun run e2e:xvfbSame E2E suite on a private virtual display (Xvfb). Use for screenshot specs; it keeps working when the monitors sleep
bun run e2e:openclastBrowser-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 serveInteractive 8-mode menu — docs dev, plugin watch, parallel both, tunnel sharing, tests, MDX check
bun run serve:docsShortcut: docs Astro dev server (mode 1)
bun run serve:pluginShortcut: plugin watch build (mode 4)
bun run serve:bothShortcut: docs dev + plugin watch in parallel (mode 5)
bun run serve:shareShortcut: docs dev + Tailscale tunnel (mode 6)
Fixtures + spec
bun run fixturesRegenerate 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 resetClear 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 devDocs Astro dev server
cd docs && bun run buildDocs production build → docs/dist
cd docs && bun run previewServe the built dist locally
cd docs && bun run test:localDocs Playwright E2E tests
bun run check:parityCheck UI/CLI command and verb coverage, tracked gaps, and the generated parity reference
bun run parity:writeRegenerate the parity table and counts from tools/parity/capabilities.ts
bun run check:mdxLightweight MDX syntax pre-check (catches bare braces, unclosed tags, frontmatter YAML errors) — fast (~1–2s)
Release
bun run versionBumps 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.

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.

  1. bun run version — bumps manifest.json + versions.json to the version already set in package.json.
  2. Add a ## [x.y.z] heading to CHANGELOG.md with 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.
  3. Merge to main. .github/workflows/release.yml builds, tags, and publishes the three plugin assets.
  4. 0.x versions and any version with a suffix publish as prereleases, not as the latest release.
  5. 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.

crosswalker/
├── src/                      # Plugin source (TypeScript)
│   ├── main.ts               # Plugin entry point
│   ├── settings/             # Settings management
│   ├── import/               # Import wizard + parsers
│   ├── generation/           # Generation engine
│   ├── config/               # Config manager + browser
│   ├── types/                # TypeScript interfaces
│   └── utils/                # Debug logging, etc.
├── tests/                    # Plugin Jest unit tests
├── test-vault/               # Embedded Obsidian vault
│   └── .obsidian/plugins/crosswalker/    # Build output lives here
├── spec/                     # Machine-readable contracts (load-bearing)
│   ├── tier1.schema.json     # Canonical Tier 1 vault shape — what producers emit
│   ├── recipe.schema.json    # Recipe DSL — Ch 22 closed grammar
│   ├── primitives/           # (Stub) one schema per transformation primitive
│   └── README.md
├── tools/                    # Dev / test infrastructure (not shipped)
│   ├── generate-fixtures.ts  # CSV → Tier 1 markdown fixture generator
│   ├── fixtures/synthetic/   # Hand-authored sample CSVs (committed)
│   └── README.md
├── docs/                     # Astro Starlight docs site
│   ├── src/
│   │   ├── content/docs/     # .mdx content files
│   │   ├── components/       # Starlight component overrides (PageTitle, MobileMenuFooter)
│   │   └── styles/           # brand.css, global.css
│   ├── tests/                # Playwright E2E specs
│   ├── astro.config.mjs      # Starlight config
│   ├── playwright.config.ts  # Playwright config
│   ├── package.json          # Docs dependencies
│   └── tags.yml              # Controlled tag vocabulary
├── scripts/
│   └── serve.mjs             # The bun run serve orchestrator
├── frameworks_to_obsidian.py # Python CLI (external producer example)
├── .claude/                  # AI agent historical context (CLAUDE.md, numbered knowledge base)
├── .github/workflows/        # CI/CD — docs deploy, plugin release
├── CLAUDE.md                 # Root-level agent instructions
├── CONTRIBUTING.md           # Contributing quick-start (also see docs/development/contributing/)
├── README.md                 # Repo landing page
└── ROADMAP.md                # Plain-markdown mirror of the docs roadmap

After cloning and bun install, verify the setup works:

# Plugin builds cleanly
bun run build

# Plugin tests pass
bun run test

# Plugin lints cleanly
bun run lint

# Docs dev server starts
bun run serve:docs
# Open http://localhost:14321/ and check the homepage loads

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.