| name | lattice-scaffold |
| description | Bootstrap a new TypeScript project with the lattice feature-folder structure and the ESLint `boundaries` plugin enforcing one-way imports — minimal dependencies only. Formatter, git hooks, test runner, and parallel runner are all opt-in. Does NOT pull in Next.js, Tailwind, React, or any UI dependencies. Use when a user wants the lattice structure on a clean slate for a framework-agnostic project (CLI, library, server, etc.). |
lattice-scaffold
You are bootstrapping a minimal new TypeScript project that uses the lattice feature-folder architecture. The reference repo (lattice) ships with Next.js, Tailwind, shadcn, Playwright, Prettier, Lefthook, Vitest, etc. Strip all of that out. This skill produces only the structural guardrails — nothing framework-specific.
The output is a project where:
- The folder structure is in place but empty (placeholders only).
eslint-plugin-boundaries enforces the one-way import flow at lint time.
- TypeScript is configured strict.
pnpm lint (and any opted-in checks) run green on the empty scaffold.
- The user can immediately add their first feature without setup friction.
Inspect the reference first
Before scaffolding, build a clear picture of what the reference looks like:
- Read the lattice repo so you understand the canonical config. Either:
- Clone it locally:
git clone https://github.com/leviFrosty/lattice.git /tmp/lattice-ref && cd /tmp/lattice-ref && pnpm install
- Or read the relevant files via the GitHub web UI /
gh repo view leviFrosty/lattice.
- Confirm the user wants the minimal variant (this skill) and not a full clone of the reference. If they want the full Next.js setup, they should
git clone leviFrosty/lattice directly — that's not this skill's job.
Ask before installing anything optional
The only required core is ESLint + the boundaries plugin + TypeScript. Everything else is opt-in. Do not assume the user wants Prettier, Lefthook, Vitest, or concurrently — ask each one explicitly and respect the answer.
Ask these questions one at a time and wait for answers before writing any files. Do not present any of the optional tools as "defaulted on" — they are not.
- Project name?
- Package manager? (
pnpm, npm, yarn, or bun)
- Node version? (default
>=18 — confirm)
- Project kind? Library / CLI / server / generic — this affects whether
src/app/ gets a real entrypoint or just a placeholder.
- Formatter? Options:
- Prettier (the lattice default — adds
prettier and a .prettierrc)
- Biome (adds
@biomejs/biome instead; covers formatting AND replaces some lint rules)
- None (rely on editor formatting)
- Git hooks? Options:
- Lefthook (the lattice default)
- Husky
- Native
.git/hooks/ script (no extra dep)
- None
- Test runner? Options:
- Vitest (the lattice default)
- Node's built-in
node --test
- None for now (the user can add one later)
- Parallel
check:all runner? Only ask this if the user opted into ≥2 of {formatter, tests} on top of lint/typecheck. Options:
concurrently (the lattice default — extra dep but pretty output)
npm-run-all2 (extra dep, no shell-quoting headaches)
- Sequential with
&& (no extra dep)
- Skip — just expose
lint, typecheck, etc. individually
- Developer-experience extras? Ask only if relevant:
eslint-plugin-only-warn — turns lint errors into warnings locally (CI can still fail with --max-warnings 0). Default: skip unless asked.
eslint-config-prettier — only meaningful if the user picked Prettier. If yes, recommend including it.
Show the resulting dependency list back to the user and wait for confirmation before installing.
What gets included
Always:
typescript (strict, ES2022, noUncheckedIndexedAccess)
@types/node
eslint + @eslint/js + typescript-eslint
eslint-plugin-boundaries + eslint-import-resolver-typescript
Only if the user opted in (in the questions above):
prettier (+ pretty-quick only if both Prettier and a pre-commit hook are wanted) — formatter
@biomejs/biome — alternative formatter
lefthook — git hooks
husky (+ lint-staged) — alternative git hooks
vitest (+ vite-tsconfig-paths) — test runner
concurrently or npm-run-all2 — parallel runner
eslint-plugin-only-warn — DX
eslint-config-prettier — DX (only with Prettier)
Never:
- React, Next.js, Vite app config, Remix, Astro, anything UI-framework
- Tailwind, PostCSS, CSS-in-JS, shadcn, Radix
- Playwright, Cypress, Storybook
- Database drivers, ORMs
- State libraries, auth libraries
If the user later wants any of these, they install them themselves — this scaffold doesn't pre-decide framework choices for them.
Scaffold steps
Run these in order. Show the user the diff after each major step, and wait for go-ahead before continuing if anything is non-trivial.
1. Init the package
mkdir <project-name> && cd <project-name>
pnpm init
Write the minimum package.json. Add scripts only for the tools the user opted into:
{
"name": "<project-name>",
"version": "0.1.0",
"private": true,
"type": "module",
"license": "MIT",
"packageManager": "pnpm@10.19.0",
"scripts": {
"lint": "eslint . --cache --cache-location .cache/eslint",
"lint:fix": "eslint . --fix --cache --cache-location .cache/eslint",
"typecheck": "tsc --noEmit"
},
"engines": { "node": ">=18" }
}
Then append scripts based on the user's answers:
- Opted into Prettier: add
"fmt": "prettier --check ." and "fmt:fix": "prettier --write ."
- Opted into Biome: add
"fmt": "biome format ." and "fmt:fix": "biome format --write ."
- Opted into Vitest: add
"test": "vitest run" and "test:watch": "vitest"
- Opted into Node's built-in test runner: add
"test": "node --test"
- Opted into a
check:all script: see step "Stitch check:all" below.
Do not add a script for a tool the user didn't pick.
2. Install dev dependencies
Always install (the required core):
pnpm add -D \
typescript \
@types/node \
eslint @eslint/js typescript-eslint \
eslint-plugin-boundaries eslint-import-resolver-typescript
Then install only what the user opted into. Show this list back to the user before running it.
pnpm add -D prettier
pnpm add -D @biomejs/biome
pnpm add -D lefthook
pnpm add -D husky lint-staged
pnpm add -D vitest vite-tsconfig-paths
pnpm add -D concurrently
pnpm add -D npm-run-all2
pnpm add -D eslint-plugin-only-warn
pnpm add -D eslint-config-prettier
pnpm add -D pretty-quick
If the user picked "None" for any category, install nothing for it.
3. TypeScript config
Write tsconfig.json:
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"target": "ES2022",
"lib": ["es2022"],
"module": "ESNext",
"moduleResolution": "Bundler",
"moduleDetection": "force",
"allowJs": true,
"esModuleInterop": true,
"isolatedModules": true,
"noEmit": true,
"noUncheckedIndexedAccess": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true,
"incremental": false,
"paths": { "@/*": ["./src/*"] }
},
"include": ["**/*.ts"],
"exclude": ["node_modules", "dist", "build"]
}
Add "DOM" and "DOM.Iterable" to lib only if the user explicitly said they need them. Don't add JSX unless asked.
4. Prettier config — only if Prettier was opted into
Skip this entire step if the user picked Biome or "none" for formatter.
Write .prettierrc:
{ "singleQuote": true }
Write .prettierignore:
node_modules
dist
build
.cache
coverage
pnpm-lock.yaml
If the user picked Biome instead, write biome.json (pnpm exec biome init is the easiest start). If the user picked "none", write nothing for formatter and skip the fmt script entirely.
5. ESLint config (with boundaries)
Write eslint.config.js. Start with the required core below, then conditionally add imports/blocks for the optional plugins the user accepted. Lines marked // optional should only appear when the corresponding tool is opted in:
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import pluginBoundaries from 'eslint-plugin-boundaries';
export default [
js.configs.recommended,
...tseslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
{
languageOptions: { parserOptions: { projectService: true } },
},
{
files: ['**/*.config.{js,mjs,cjs,ts}', '**/eslint.config.js'],
...tseslint.configs.disableTypeChecked,
},
{
plugins: { boundaries: pluginBoundaries },
settings: {
'import/resolver': { typescript: { alwaysTryTypes: true } },
'boundaries/include': ['src/**/*'],
'boundaries/elements': [
{
mode: 'full',
type: 'shared',
pattern: ['src/lib/**/*', 'src/components/**/*'],
},
{
mode: 'full',
type: 'feature',
capture: ['featureName'],
pattern: ['src/features/*/**/*'],
},
{
mode: 'full',
type: 'app',
capture: ['_', 'fileName'],
pattern: ['src/app/**/*'],
},
{
mode: 'full',
type: 'neverImport',
pattern: ['src/*'],
},
],
},
rules: {
'boundaries/no-unknown': ['error'],
'boundaries/no-unknown-files': ['error'],
'boundaries/element-types': [
'error',
{
default: 'disallow',
rules: [
{ from: ['shared'], allow: ['shared'] },
{
from: ['feature'],
allow: [
'shared',
['feature', { featureName: '${from.featureName}' }],
],
},
{ from: ['app', 'neverImport'], allow: ['shared', 'feature'] },
],
},
],
},
},
{
ignores: [
'dist/**',
'build/**',
'node_modules/**',
'.cache/**',
'coverage/**',
],
},
];
Drop src/components/**/* from the shared pattern if the user said this is a non-UI project (CLI / library / server). Keep src/lib/**/* always.
6. Test runner config — only if a test runner was opted into
Skip this entire step if the user picked "None" for tests.
If Vitest: write vitest.config.ts:
import { defineConfig } from 'vitest/config';
import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
plugins: [tsconfigPaths()],
test: {
environment: 'node',
exclude: ['**/node_modules/**', '**/dist/**', '**/build/**'],
coverage: {
provider: 'v8',
include: ['src/**/*.ts'],
exclude: ['src/**/*.test.ts'],
reporter: ['text', 'html', 'json'],
},
},
});
If Node's built-in node --test: no config file needed. The test script in package.json is the entire setup.
7. Git hooks — only if git hooks were opted into
Skip this entire step if the user picked "None" for git hooks.
If Lefthook: write lefthook.yml. The pretty-quick line below should only appear if the user opted into BOTH Prettier AND pretty-quick:
pre-commit:
parallel: true
jobs:
- run: pnpm exec pretty-quick --staged
stage_fixed: true
- run: pnpm lint
glob: ['*.ts', '*.js']
stage_fixed: true
Then run pnpm exec lefthook install.
If Husky: pnpm exec husky init, then write .husky/pre-commit:
pnpm lint
If native .git/hooks/: write .git/hooks/pre-commit (executable):
#!/bin/sh
pnpm lint
chmod +x .git/hooks/pre-commit. Note that native hooks are not version-controlled — flag this trade-off to the user.
7b. Stitch check:all — only if user opted into a parallel runner or a sequential chain
Skip this step if the user picked "Skip" for check:all.
Build the check:all script using only the tools they have:
- All four (fmt + lint + typecheck + test) with
concurrently:
"check:all": "concurrently -n fmt,lint,typecheck,test -c blue,magenta,cyan,green \"pnpm fmt\" \"pnpm lint\" \"pnpm typecheck\" \"pnpm test\""
- Lint + typecheck only, no extra dep:
"check:all": "pnpm lint && pnpm typecheck"
- Lint + typecheck + test, with
npm-run-all2:
"check:all": "run-p lint typecheck test"
8. Project structure
Create the empty skeleton:
mkdir -p src/app src/features src/lib
Add src/components only if this is a UI project.
Add placeholder files so the directories aren't empty (and so the boundaries plugin sees real folders):
src/lib/.gitkeep — empty file.
src/features/.gitkeep — empty file.
src/app/index.ts (only for library/CLI/server projects — adjust the body to fit the project type):
export {};
9. Document the structure
Write docs/project-structure.md:
# Project structure
\`\`\`
src/
app/ # entrypoint (pages / routes / CLI wiring) — consumers only
features/ # isolated feature modules
<name>/
api/
components/ # optional
db/
lib/
lib/ # global utilities
components/ # global UI (UI projects only)
\`\`\`
## Import rules (enforced by ESLint)
| From | May import from |
| ------------------ | ------------------------------------------- |
| `lib`/`components` | `lib`, `components` only |
| `features/<X>` | `lib`, `components`, same-feature internals |
| `app` | `lib`, `components`, any `features/*` |
A feature never imports another feature. Global never imports feature.
If two features need the same code, promote it to `lib/`.
## Why
- Features stay deletable: `rm -rf src/features/<name>` and the lint stays green.
- No accidental coupling: the boundaries plugin rejects cross-feature imports at build time.
- Predictable placement: one answer per file. Reviewers stop arguing.
10. Smoke test
Run whatever check the user wired up:
pnpm install
pnpm check:all
pnpm lint
pnpm typecheck
Everything should be green. If lint complains about boundaries/no-unknown-files on src/.gitkeep or similar, that's expected — adjust ignores rather than weakening rules.
The non-negotiable smoke test — required even if every optional tool was declined, because this proves the boundaries plugin is actually enforcing:
- Create
src/features/demo/lib/hello.ts with export const hello = () => 'hi'.
- Create
src/features/other/lib/bad.ts importing from src/features/demo/lib/hello.ts.
- Run
pnpm lint — must fail with a boundaries violation.
- Delete the toy files. Commit the clean scaffold.
If step 3 doesn't fail, the boundaries config is wrong. Fix it before declaring done.
If the user opted into a test runner, add one extra step in between: write a trivial hello.test.ts and confirm pnpm test passes. Skip if no test runner was picked.
11. Initial commit
git init
git add -A
git commit -m "chore: scaffold lattice project structure"
What to refuse
- Pulling in framework dependencies ("can you also add Next.js while you're here?") — no. Direct the user to
git clone leviFrosty/lattice for the full stack, or to install the framework themselves after this scaffold is done.
- Weakening boundaries rules to silence violations on toy code. Boundaries violations are the entire point of the scaffold.
- Skipping the smoke test in step 10. A scaffold that doesn't prove the lint rules actually fire is not done.
- Adding example features the user didn't ask for. Empty placeholders only — the user fills in their own first feature.
Communication with the user
- After the inspection step, summarize what you saw in the reference and confirm the minimal variant is what they want.
- After collecting the answers in step 1, show the dependency list and wait for go-ahead before installing.
- After step 10's smoke test, show the proof: the toy test passing, the bad import failing lint. That's the hand-off.
The deliverable is a project where the user's next move is "write a feature," not "configure ESLint."