| name | scaffold-flatten |
| description | Collapse this monorepo into a single-package layout. The chosen app's contents move to the root (src/, test/, docs/); each lib becomes a deep module at src/<lib>/. Workspace-name imports are rewritten to relative paths, dependencies are merged into the root package.json, pnpm-workspace.yaml is deleted, the root tsconfig.json switches from a references solution-config to a standard single-package config, and fallow.toml / cspell.json are reshaped to match. Run optionally on a fresh project (right after /scaffold-init) when a single-app layout fits better than the apps/libs split. The deterministic work happens in scripts/scaffold-flatten.mjs; this skill collects inputs, runs the script, and summarizes the result. |
| argument-hint | [--app <short-name>] [--dry-run] |
| disable-model-invocation | true |
| allowed-tools | Read, Edit, Write, Glob, Bash(node scripts/scaffold-flatten.mjs*), Bash(pnpm install*), Bash(pnpm check*), Bash(git status*), Bash(git diff*) |
scaffold-flatten
Inverts the scaffold's apps/* + libs/* shape into a flat single-package layout. The deterministic file operations live in scripts/scaffold-flatten.mjs; this skill handles input collection, invocation, and reporting.
What it changes
apps/<chosen>/src/** → src/** (the chosen app becomes the root package)
apps/<chosen>/test/** → test/**
apps/<chosen>/docs/** → docs/**
libs/<lib>/src/** → src/<lib>/** (each lib becomes a deep module)
libs/<lib>/test/** → test/<lib>/**
libs/<lib>/docs/** → docs/<lib>/**
libs/<lib>/{README,CHANGELOG}.md → docs/<lib>/{README,CHANGELOG}.md
- Workspace-name imports (
@scope/<name>) → relative imports in every moved .ts / .tsx / .mts / .cts file
package.json → all per-package dependencies merged into the root's; workspace:* entries dropped
tsconfig.json → rewritten from references solution-config to a standard single-package config extending tsconfig.base.json
fallow.toml → entry reshaped to the flat layout; apps/libs boundary zones removed
cspell.json → files globs reshaped to the flat layout
vitest.config.ts and stryker.config.mjs → apps/*/... and libs/*/... glob patterns reshaped to the flat layout, with adjacent duplicate entries collapsed
pnpm-workspace.yaml → packages: block stripped; allowBuilds: is preserved (pnpm 11 reads it outside a workspace too). File is deleted only if nothing else remains.
apps/ and libs/ → deleted once empty
What stays the same: AGENTS.md, CLAUDE.md, docs/getting-started.md, docs/deep-modules.md, rules/, scripts/, tsconfig.base.json, the pnpm check pipeline, and the /version skill. The deep-modules pattern still applies — each lib that lands at src/<lib>/ is exactly the kind of module the rules already enforce. /version continues to work for the root (which is now the only target); per-package versioning is moot in a single-package project.
Arguments
$ARGUMENTS is forwarded verbatim to the script. Supported flags:
--app <short-name> — short name of the app to use as the root. Auto-detected when exactly one app is present. Required when there are multiple apps; ignored when there are zero apps (the libs alone flatten into src/<lib>/).
--dry-run — print the plan, change nothing.
When the user invokes you with no arguments
If exactly one app exists, run with no flags — the script auto-detects. Otherwise ask which app should become the root, in one short message.
Preflight
- Result: !
node scripts/scaffold-flatten.mjs $ARGUMENTS
By the time you read this, one of three things is true on disk:
mode: "flatten" with dry_run: false — the changes have been applied,
mode: "flatten" with dry_run: true — nothing changed, the JSON describes what would happen,
mode: "error" — the script refused; nothing changed.
Steps
-
Parse the JSON. Branch on mode:
"error" → relay message. See "Steps — error" for error_type handling. Stop.
"flatten" → continue.
-
Render a summary in this shape (omit empty sections; truncate moves at ~15 with a count):
Flattened: <app.workspace_name> → root
Libs: <lib1>, <lib2>, ...
Moves: N files
apps/<app>/src/greet.ts → src/greet.ts
libs/tools/src/example.ts → src/tools/example.ts
...
Import rewrites: <count> files
Deps merged: <list of package names>, or "(none)"
Removed: apps/, libs/, pnpm-workspace.yaml, ...
Rewrote: package.json (dependencies), tsconfig.json, fallow.toml, cspell.json
Use the script's arrays. Don't re-read the disk to confirm; the JSON is authoritative.
-
Relay warnings if present. If warnings is non-empty, print each verbatim under a Warnings: heading. These are unrecognized files in former package roots that the script left in place — the user should review.
-
Tell the user what to do next. The script's next_steps array is the canonical answer — render it verbatim:
Next:
1. pnpm install
2. pnpm check
-
If the run was a dry run, end by asking whether to re-run for real (same arguments without --dry-run). Do not auto-promote.
Steps — error
Branch on error_type:
usage → relay the script's message; if --app was needed, ask the user for it and offer to re-invoke. Stop.
already_flat → tell the user nothing to do — the repo is already a single-package layout. Stop.
ambiguous_app → relay the list of apps; ask which one should become the root; offer to re-invoke with --app <name>. Stop.
unknown_app → relay the available app names; ask the user to pick one. Stop.
dirty_tree → tell the user the working tree must be clean before flattening (so the conversion is one reviewable commit); show the dirty files; suggest committing or stashing first. Stop.
name_conflict → relay the script's message verbatim. The user has to rename one side before re-running. Stop.
dep_conflict → relay the script's message verbatim. The user has to align the conflicting versions before re-running. Stop.
move_conflict → relay the script's message. This shouldn't happen on a clean tree; suggest the user file an issue if it does. Stop.
unsupported_import → relay the script's message. Subpath imports (@scope/lib/sub) aren't supported — the user has to re-export through the package's index.ts and import that instead. Stop.
runtime → relay the script's message and stop. Don't speculate or retry.
In every error case: no edits, no git operations, no retry. The user decides what to do next.
When to use this skill
- The user wants this scaffold to back a single-app project, not a multi-package one. Typically right after
/scaffold-init, before any project-specific code lands.
- The user says "flatten this scaffold", "collapse to single package", "I don't need apps/libs", or similar.
When NOT to use this skill
- The user has already added meaningful code across multiple apps. Flattening is reversible only through git; flag that the diff will be large and ask them to checkpoint first (the script refuses on a dirty tree anyway).
- The user wants to add a single new package alongside the existing ones — that's
mkdir apps/<name> + the per-package shape, not a flatten.
- The repo already has a flat layout (no
apps/, no libs/). The script returns already_flat; relay that and stop.
Edge cases
- Zero apps, libs only. The script flattens the libs into
src/<lib>/ and leaves the root src/index.ts (if any) untouched. The user is responsible for creating a root src/index.ts if they need a top-level entry. Mention this in the summary.
- App has its own
src/version.ts. It moves to src/version.ts at the root, but /version for the root never participates in src/version.ts — the file becomes dead. The script will not delete it; mention it in the summary so the user can clean up.
- Lib has its own
src/version.ts. It moves to src/<lib>/version.ts. After flatten there are no per-package releases, so the file is unused. Same advice — surface in the summary.
- A package directory contains files the script doesn't know about. The script records them in
warnings[] and leaves them in place. Relay verbatim; the user decides what to do with each.
- First
/version <type> after flatten. The root is now the only target. The flatten commit becomes the "last release" anchor for the next root bump.