Architecture
vsuite is a pnpm monorepo. Only the CLI, @krizic/vsuite, is published; every other package is a private, source-only workspace package that is bundled into the CLI at build time.
Overview
| Directory | Package | Published | Purpose |
|---|---|---|---|
packages/json |
@vsuite/json |
No | JSON object helpers (deepMerge, isJsonObject). |
packages/migrations |
@vsuite/migrations |
No | Versioned vsuite.json migrations (migrateConfig). |
packages/catalog |
@vsuite/catalog |
No | Agent, skill, capability, and target definitions; neutral tool vocabulary; MCP server catalog; stack registry. |
packages/templates |
@vsuite/templates |
No | Restricted LiquidJS renderer, built-in agent/skill templates and vsuite:* partials (templates/), Markdown lint, job-metrics and metrics scripts. |
packages/packs |
@vsuite/packs |
No | Template packs: source fetch (path:, npm:, git:), manifest/front-matter validation, vsuite.lock.json integrity, registry merge with the catalog, pack entry rendering. |
packages/config |
@vsuite/config |
No | vsuite.json zod schema and validation, derived agent graph, reader, JSON Schema export. |
packages/detection |
@vsuite/detection |
No | Stack detection from project manifests (npm, Python, Go, Maven, Gradle, .NET). |
packages/targets |
@vsuite/targets |
No | Per-target adapters (OpenCode, Claude Code, GitHub Copilot) and OpenCode config merge. |
packages/core |
@vsuite/core |
No | loadConfig (migrate, validate, write), planGeneration and generate: rendering, the atomic write plan, .vsuite/.env handling, and legacy metrics migration. |
packages/workflows |
@vsuite/workflows |
No | Headless wizard model, stack changes, graph rendering, and project workflows shared by the CLI and the UI. |
packages/cli |
@krizic/vsuite |
Yes | The vsuite binary (init, generate, migrate, graph, stack, pack, agent, skill) and the published JSON Schema. |
packages/ui |
@vsuite/ui |
No | Electron desktop app; app at the top of the graph, never bundled into the CLI. |
packages/test-utils |
@vsuite/test-utils |
No | Dev-only test helpers; never bundled. |
Dependency graph
graph TD ui["@vsuite/ui (app)"] --> workflows & config & catalog & detection & packs cli["@krizic/vsuite (cli)"] --> workflows & core & config & catalog & detection workflows["@vsuite/workflows"] --> core & config & catalog & detection & packs & templates core["@vsuite/core"] --> catalog & templates & config & targets & json & migrations & packs packs["@vsuite/packs"] --> catalog & config & templates migrations["@vsuite/migrations"] --> json targets["@vsuite/targets"] --> catalog & json templates["@vsuite/templates"] --> catalog config["@vsuite/config"] --> catalog detection["@vsuite/detection"] --> catalog catalog["@vsuite/catalog"] json["@vsuite/json"]
@vsuite/test-utils is a dev dependency of package tests only and is not part of the runtime graph.
Layering rules
- No cycles. The graph above is the complete allowed set of edges. A new edge is a design change, not a convenience. Allowed edges:
ui → workflows, config, catalog, detection, packs(main process only);cli → workflows, core, config, catalog, detection;workflows → core, config, catalog, detection, packs, templates;core → catalog, templates, config, targets, json, migrations, packs;packs → catalog, config, templates;migrations → json;targets → catalog, json;templates → catalog;config → catalog;detection → catalog. - Renderer isolation. In
packages/ui, only the main process (src/main) may import@vsuite/*at runtime.src/renderer,src/preload, andsrc/sharedmay useimport typeor inlineimport("@vsuite/…")types only, so no workflow code reaches the sandboxed renderer bundle. - Index-only imports. Cross-package imports use the package entry (
from "@vsuite/<name>"), never deep paths such as@vsuite/catalog/src/.... Each package’ssrc/index.tsis its public API. - Where new code goes. Put a symbol in the lowest package that every consumer already depends on. Shared types used by both
templatesandcore(for exampleRenderableSkill) live incatalog. If a change seems to need a new edge, move the symbol down instead of adding the dependency.
Run the boundary checks from the repository root:
# 1. No deep or escaping imports (expect deep=1 escape=1)
grep -rnE 'from "@vsuite/[a-z-]+/' packages tests ; echo "deep=$?"
# Resolves every relative import specifier (`from "./…"`, `import("./…")`) in
# packages/*/{src,tests,e2e} and fails if it lands outside its own packages/<pkg>/.
# Non-import reads such as new URL("../../../package.json", import.meta.url)
# (root package.json version reads) are allowed and not scanned.
node -e '
const fs=require("fs"),p=require("path");let bad=0;
const walk=d=>fs.existsSync(d)?fs.readdirSync(d,{withFileTypes:true}).flatMap(e=>e.name==="node_modules"?[]:e.isDirectory()?walk(p.join(d,e.name)):/\.[cm]?tsx?$|\.[cm]?js$/.test(e.name)?[p.join(d,e.name)]:[]):[];
for(const pkg of fs.readdirSync("packages")){const root=p.resolve("packages",pkg);
for(const f of ["src","tests","e2e"].flatMap(s=>walk(p.join(root,s)))){const t=fs.readFileSync(f,"utf8");
for(const m of t.matchAll(/(?:\bfrom\s*|\bimport\s*\(\s*)["\x27](\.{1,2}\/[^"\x27]*)["\x27]/g)){
const r=p.resolve(p.dirname(f),m[1]);if(r!==root&&!r.startsWith(root+p.sep)){bad=1;console.log(f+": "+m[1])}}}}
console.log("escape="+(bad?0:1))'
# 2. Actual edges per package (must be a subset of the graph above)
for p in json migrations catalog templates config packs detection targets core workflows cli; do
echo "$p: $(grep -rhoE 'from "@vsuite/[a-z-]+"' packages/$p/src | sort -u | sed 's/from //' | tr '\n' ' ')"
done
echo "ui(main): $(grep -rhoE --exclude='*.test.ts' --exclude='test-context.ts' 'from "@vsuite/[a-z-]+"' packages/ui/src/main | sort -u | sed 's/from //' | tr '\n' ' ')"
# 3. No runtime @vsuite/* imports outside the UI main process (expect runtime-vsuite-in-renderer=1)
grep -rnE --exclude='*.test.ts' --exclude='*.test.tsx' '^import (\{|\*|[A-Za-z_$][A-Za-z0-9_$]* (,|from))[^;]* from "@vsuite/' packages/ui/src/renderer packages/ui/src/preload packages/ui/src/shared ; echo "runtime-vsuite-in-renderer=$?"Check 3 matches value, namespace, and default imports but not import type …. Test files are excluded from checks 2 (UI) and 3: they are never bundled and may import @vsuite/workflows or @vsuite/test-utils at runtime.
Generator output is guarded by golden tests: packages/templates/tests/golden/ (every catalog agent and skill) and packages/core/tests/golden.test.ts (a migrated 0.2.1 config generates equivalent output).
End-to-end data flow
flowchart LR answers["vsuite init answers"] --> json["vsuite.json"] json --> migrate["core: loadConfig → migrations: migrateConfig<br/>(atomic write if changed)"] migrate --> config["config: parseConfig / readConfig + configSchema"] config --> registry["packs: loadRegistry<br/>(fetch, verify vsuite.lock.json, merge with catalog)"] registry --> resolve["catalog: resolveSelection"] resolve --> ctx["templates: TemplateContext v1"] ctx --> skillsNode["core: loadProjectSkills + Liquid skill render<br/>(catalog or packs: renderSkillEntry)"] ctx --> bodies["Liquid agent render + auto-appended required sections<br/>(catalog or packs: renderAgentEntry)"] skillsNode --> bodies bodies --> targets["targets: adapters[target].render"] skillsNode --> skillOut["core: SKILL.md outputs"] resolve --> scripts["templates: vsuite MCP server + job-metrics tooling"] config --> native["targets: mergeOpenCodeConfig + MCP config merge"] targets --> plan["core: writePlan"] skillOut --> plan scripts --> plan native --> plan plan --> disk["files on disk"]
Before generating, the CLI calls loadConfig (packages/core/src/load-config.ts), which migrates the raw JSON to the CLI version with migrateConfig, validates it with parseConfig, and writes it atomically only if the migration changed it; migration therefore always precedes validation. generate(root) in packages/core/src/generate.ts then reads and validates the config, loads configured packs with loadRegistry (verifying vsuite.lock.json: path: mismatches warn and relock, npm:/git: mismatches fail with PACK_INTEGRITY unless --update-packs) and merges them with the catalog, resolves the selection (expanding required skills and capabilities), checks that every selected artifact supports every configured target, loads project skills from projectSkillsDirectory, renders one agent output per selected agent and target and one SKILL.md per skill and target through the restricted LiquidJS engine in @vsuite/templates (catalog and pack templates share one engine, the template context, and the auto-append of required sections), writes the vsuite MCP server and job-metrics tooling under .vsuite/, and merged native config (.opencode/opencode.json, .mcp.json, .vscode/mcp.json). writePlan then rejects unsafe or duplicate paths and paths beneath symbolic links, stages every file to a temporary path, and renames them into place. Afterwards generate ensures .vsuite/ is git-ignored, removes stale catalog skills and pack outputs, and writes vsuite.lock.json when it changed.
Build and bundling
flowchart LR src["@vsuite/* packages<br/>exports → ./src/index.ts"] --> tsup["tsup in packages/cli<br/>noExternal: /^@vsuite\//"] cliSrc["packages/cli/src/main.ts"] --> tsup tsup --> dist["packages/cli/dist/main.js"] thirdParty["@clack/prompts, diff, jsonc-parser, liquidjs, yaml, zod"] -. "external, CLI dependencies" .-> dist rootPkg["root package.json version"] -. "define __VSUITE_VERSION__" .-> tsup schemaScript["pnpm run schema"] --> schema["packages/cli/schema/vsuite.schema.json"]
- Internal packages have no build step; their
exportspoint at./src/index.ts, and TypeScript, Vitest, andtsxconsume source directly. tsupinlines every@vsuite/*import intodist/main.js. Third-party runtime packages stay external and are declared as CLIdependencies.@vsuite/*packages are CLIdevDependenciesandprivate, so the published manifest never references unpublished packages. Keeping them private avoids versioning and publishing packages whose only consumers are the CLI and the desktop app.- The vsuite version lives in the root
package.json.packages/cli/tsup.config.tsreads it and injects it withdefineas__VSUITE_VERSION__;packages/cli/src/version.tsuses that constant in the bundle and falls back to reading the rootpackage.jsonin source mode (tsx, Vitest).pnpm run version:sync(scripts/sync-version.mjs) copies the version intopackages/cli/package.json; a test enforces equality. - The published package contains only
distandschema; its exports are thevsuitebinary and@krizic/vsuite/schema/vsuite.schema.json. There is no programmatic API.
Desktop app
packages/ui is an Electron app built with electron-vite (see its README). Like tsup for the CLI, electron-vite inlines every @vsuite/* import into the main-process bundle in packages/ui/out/, so the packaged app has no runtime node_modules dependency on workspace packages. Preload and renderer bundles contain no @vsuite/* code (renderer isolation rule above). App data lives in <userData>/app.db (node:sqlite, versioned migrations); VSUITE_USER_DATA overrides userData for tests.
pnpm run ui:dist runs electron-builder (packages/ui/electron-builder.yml) and writes installers to packages/ui/release/: dmg and zip (macOS), nsis (Windows), AppImage and deb (Linux). The packaging path runs packages/ui/scripts/obfuscate.mjs between electron-vite build and electron-builder to obfuscate the main bundle and renderer app-owned chunks, and the app ships with Electron fuses + asar integrity enabled. Release installers are built by the Gitea deploy and release workflows (below); .github/workflows/ui.yml additionally runs typecheck, Biome, tests, Playwright e2e, and ui:dist on a macOS, Windows, and Ubuntu matrix. The macOS build is ad-hoc signed (identity: "-") and not notarized, so it is not distributable to other machines without a Gatekeeper override; Windows and Linux installers are unsigned. There is no auto-update.
Release flow
Releases run on Gitea Actions.
.gitea/workflows/deploy.yml runs on every push to main or master and on manual dispatch:
flowchart LR quality["quality<br/>typecheck"] --> test["test<br/>build + pnpm test"] --> version["version<br/>bump patch, tag vX, push"] version --> npm["npm<br/>prepublishOnly + publish @krizic/vsuite"] version --> linux & windows & macos linux & windows & macos --> dl["publish-dl<br/>dl/vsuite/vX/ then latest/"] docsBuild["docs-build<br/>scripts/build-docs.sh"] --> docsPublish["docs-publish"]
-
versionbumps the patch version in the rootpackage.json, runsscripts/sync-version.mjs, commitschore: release <version> [skip ci](root, CLI, and UIpackage.json), creates an annotated tagv<version>, and pushes both atomically. The[skip ci]marker prevents a release loop. -
linuxandwindowsbuild installers on Ubuntu withscripts/build-desktop.sh.macosneeds a self-hosted runner and runs only when the repository variableMACOS_RUNNERistrue; see the macOS runner guide. -
publish-dluploads the installers withSHA256SUMStohttps://dl.vedrankrizic.com/vsuite/<tag>/(for examplev0.7.1/) and then mirrors them tohttps://dl.vedrankrizic.com/vsuite/latest/. It runs only when every enabled build succeeded, then finisheslatest/in separate steps, in this order: the installers are copied intolatest/without deleting anything, then the permanent aliasesvsuite-mac.dmg,vsuite-win.exeandvsuite-linux.AppImageare uploaded, thenlatest/manifest.json, and last a prune that deletes the previous release’s installers. Nothing is deleted fromlatest/until the new manifest is live, so neither the old nor the new manifest ever names a missing file. If a step fails,latest/holds both old and new files, with no broken links, until a re-run.scripts/write-manifest.mjspicks one installer per platform from the downloaded artifacts and uses that same selection for the aliases and the manifest. An alias keeps the selected installer’s extension when it differs (for examplevsuite-mac.zip). The prune never deletesmanifest.jsonorvsuite-{mac,win,linux}.*. The alias step replaces the aliases of every built platform and removes their other-extension variants. The manifest has the shape{ "latestVersion": "0.7.3", "mac": "vsuite-0.7.3.dmg", "win": "vsuite Setup 0.7.3.exe", "lin": "vsuite-0.7.3.AppImage" }, with values that are file names insidelatest/. It is published only tolatest/.Stale macOS: when the
macosjob is skipped, the previouslatest/vsuite-mac.*alias is kept, so it is an older build thanlatestVersion. The write step checkslatest/over HTTP for that alias, and the manifest’smacnames it ("vsuite-mac.dmg"), or isnullwhen there is none or the check fails. The next release with a macOS build replaces it. After the prune, the job ends with a smoke check that requires HTTP 200 forSHA256SUMS(versioned andlatest/),latest/manifest.json, every alias written in this run, and every file the manifest names. rsync cannot set cache headers, so a short cache formanifest.jsonand the aliases has to be configured on the web server. -
The docs site is built and published independently of the release chain.
.gitea/workflows/release.yml rebuilds and republishes an existing tag without bumping the version. It runs when a tag v* is pushed by a person (tags pushed by the Actions token do not trigger workflows) or on manual dispatch with a tag; npm publishing is optional on dispatch. Dispatching it with the previous good tag is the rollback path for latest/.
The secrets and variables each workflow needs are listed in the header comment of deploy.yml.
Development
pnpm install
pnpm test # all Vitest projects
pnpm vitest run --project <name> # one project, e.g. @vsuite/core, @krizic/vsuite, root
pnpm run typecheck
pnpm run check # Biome lint and format check
pnpm run build # bundle packages/cli/dist/main.js
pnpm vsuite <command> # run the CLI from source
pnpm run schema # regenerate the published JSON Schema and the template context schema
pnpm run version:sync # copy the root version into packages/cli/package.json
pnpm run ui:dev # run the desktop app in development
pnpm run ui:dist # package installers into packages/ui/release/To try the CLI from source in another project, run pnpm vsuite <command> from this repository or install a packed tarball (see the CLI README).
The root package.json version is the single source of the vsuite version. pnpm run prepublishOnly runs schema generation, tests, type check, and build.
Documentation site
The docs site is built from the repository’s Markdown with @krizic/static-docs (static-docs.config.json; docs/superpowers/ is excluded).
pnpm docs:build # write ./docs-build
pnpm docs:serve # serve it locally
pnpm docs:screenshots # rebuild the UI and recapture docs/images/*.pngpnpm docs:screenshots runs scripts/capture-screenshots.mjs: it launches the built Electron app on a throwaway profile with sample projects and captures the screenshots used in the root README at 1440x900.
Historical design records
docs/superpowers/ holds dated specs, plans, research, and reviews written while features were designed. They record intent at the time and are not maintained; where they disagree with the code or the live docs, the code and the live docs win.
Extending
Most additions are data in @vsuite/catalog plus a Liquid template in packages/templates/templates/ (see @vsuite/templates). @vsuite/config derives its enums from the catalog id lists, so the schema follows automatically; regenerate the published schema with pnpm run schema.
Add a target
packages/catalog/src/types.ts: add the id totargetIds.packages/catalog/src/artifacts.ts: add its directory totargetDirectories, and declare support on the artifacts that support it.packages/targets/src/<target>.ts: implementTargetAdapter(id,render(input)returning{ files, warnings }from anAgentProjection), then register it inadaptersinpackages/targets/src/adapters.tsand export it fromsrc/index.ts.packages/core/src/generate.ts: add target-specific skill metadata inskillOutputand native/MCP config handling if the target needs them.packages/templates/src/scripts.ts: support the target inrenderMetricsScriptif it gets the metrics script.- Tests: a new
packages/targets/tests/<target>.test.ts, pluspackages/core/tests/generate.test.tsandpackages/config/tests/*for the new id.
Add an agent
packages/catalog/src/types.ts: add the id toagentIds.packages/catalog/src/artifacts.ts: add itsAgentDefinitiontoagents(identity, visibility, handoffs, and the defaults the wizard writes intovsuite.json).packages/templates/templates/agents/<id>.md.liquid: add its body template. Required sections (telemetry,stack,skills,metrics,memory) are appended automatically unless the template places them with{% render "vsuite:<name>" %}; the variables available are listed in the context reference.- Recapture goldens with
pnpm --filter @vsuite/templates golden:capture(the golden matrix picks the new id up from the catalog) and review the new files underpackages/templates/tests/golden/. - Tests:
packages/catalog/tests/resolve.test.ts,packages/templates/tests/render.test.ts, andpackages/core/tests/generate.test.ts.
Add a skill
packages/catalog/src/types.ts: add the id toskillIds.packages/catalog/src/artifacts.ts: add itsSkillDefinitiontoskillsand reference it from agents’requiredSkillswhere needed.packages/templates/templates/skills/<id>.md.liquid: add its body template (skills get no appended sections).- Recapture goldens with
pnpm --filter @vsuite/templates golden:captureand review the diff. - Tests:
packages/catalog/tests/resolve.test.ts,packages/templates/tests/render.test.ts, andpackages/core/tests/generate.test.ts.
Project-specific skills need no code change; see project skills in the CLI reference.
Add an MCP server
packages/catalog/src/mcp-servers.ts: add the id tomcpServerIdsand aMcpServerDefinition(stdiolaunchor remoteurl, short tool names without dots,configKey, Copilot prefix,requiredEnv, optionaloptInandprerequisite). Check the package name and command against the vendor docs first.- Targets translate it automatically. Add a named warning in
packages/targets/src/warnings.tsonly if a target cannot express it. - Tests:
packages/catalog/tests/vocabulary.test.ts,packages/config/tests/schema.test.ts, andpackages/core/tests/graph-projection.test.ts.
Add a stack technology
packages/catalog/src/stack-registry.ts: add aStackTechnologywithid,category,label,detect(dependency, module, artifact, or SDK names; order within a category is detection precedence),skill(<id>-conventions), andmcpsbindings (ownerTools,readerToolsornull, optionalreaderServerfor a read-only launch variant).- Add the skill id to
packages/catalog/src/convention-skills.ts, and its body with the eight required sections aspackages/templates/templates/skills/conventions/<id>-conventions.md.liquid; then recapture goldens withpnpm --filter @vsuite/templates golden:capture. - If the technology needs a new manifest format, extend
packages/detection/src/ecosystems.ts. - Ownership lives in
stackOwnership(packages/catalog/src/resolve-stack.ts). The wizard andvsuite stack setboth applyresolveStackthroughpackages/workflows/src/stack.ts. - Tests:
packages/catalog/tests/stack-registry.test.ts,packages/templates/tests/convention-skills.test.ts, andpackages/detection/tests/stack.test.ts.
Add a config migration
Needed when a release changes the vsuite.json format; see @vsuite/migrations.
- Root
package.json: the migration version is the vsuite release from here. Bump it if needed and runpnpm run version:sync. packages/migrations/src/migrations/<version>.ts: export aMigrationwhoseversionis that release.packages/migrations/src/registry.ts: add it tomigrationsin version order.packages/config/src/schema.ts: update the schema for the new format and regenerate withpnpm run schema.- Tests: a per-migration test and a legacy-fixture test in
packages/migrations/tests/; the registry guard test fails if a migration is newer than the root version. - Docs: the migrations README and any
vsuite.jsonexamples.