
Scaffold Cli
- 388 installs
- 74 repo stars
- Updated August 5, 2026
- mblode/agent-skills
scaffold-cli is an agent skill that generates CLI project skeletons with argument parsing, packaging, tests, and release conventions so developers and agents ship command-line tools quickly.
About
scaffold-cli is an mblode agent-skills generator for bootstrapping command-line tool repositories. It produces project skeletons that include argument parsing setup, package manifest structure, test layout, and release conventions so new CLIs are runnable and publishable without hand-rolling boilerplate. Developers reach for scaffold-cli when starting a internal dev tool, agent-invoked utility, or open-source CLI and want consistent structure across repos. The skill targets the build phase before feature logic is added—pair it with domain-specific implementation skills afterward. It suits Node, Python, or similar CLI ecosystems where packaging and test harness mistakes slow first releases. Specify language, binary name, and subcommands in the prompt for a tighter scaffold aligned with team conventions.
- CLI project templates
- Argument and subcommand layout
- Packaging and binary layout
- Test harness setup
- Release-friendly structure
Scaffold Cli by the numbers
- 388 all-time installs (skills.sh)
- +19 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #141 of 550 CLI & Terminal skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/mblode/agent-skills --skill scaffold-cliAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 388 |
|---|---|
| repo stars | ★ 74 |
| Last updated | August 5, 2026 |
| Repository | mblode/agent-skills ↗ |
How do you scaffold a CLI project with tests and packaging?
Generate CLI project skeletons with argument parsing, packaging, tests, and release conventions so agents and developers ship command-line tools quickly.
Who is it for?
Developers starting a new command-line tool who want parsing, tests, and packaging wired before writing command logic.
Skip if: Adding subcommands to an existing mature CLI or building GUI, API-only, or library projects without a binary entrypoint.
When should I use this skill?
The user asks to create, scaffold, or bootstrap a new CLI tool with tests, packaging, and release setup.
What you get
CLI project skeleton with argument parsing, package manifests, test files, and release configuration
- CLI project skeleton
- Test and packaging scaffolding
Files
Scaffold CLI
Scaffold a production-ready TypeScript CLI project (Node 22+) with ESM modules, a dual build (CLI binary plus typed library), automated changeset releases, and an agent skill definition.
- IS: bootstrapping a brand-new TypeScript CLI or npm package from the pinned templates in
references/. - IS NOT: a Next.js web app (use
scaffold-nextjs), folder structure or module contracts for an existing codebase (usedefine-architecture), or shipping a release of an existing package (useautoship).
This is a low-freedom scaffold. Generate files exactly as templated, substituting only the {{placeholder}} variables. Do not swap tools (no eslint, prettier, tsup, jest, chalk, or ora) or restructure the layout.
Reference Files
| File | Read When |
|---|---|
references/scaffold-configs.md | Step 3: templates for package.json, tsconfig, tsdown, gitignore, license, changeset config, GitHub Actions |
references/scaffold-source.md | Steps 4-5: templates for src/cli.ts, src/index.ts, src/types.ts, AGENTS.md, README.md, skills/SKILL.md |
references/post-scaffold.md | Steps 6-7: post-scaffold command sequence, validation checklist, troubleshooting |
Scaffold Workflow
Copy this checklist to track progress:
Scaffold progress:
- [ ] Step 1: Gather project info
- [ ] Step 2: Create directory structure
- [ ] Step 3: Generate config files
- [ ] Step 4: Generate source files
- [ ] Step 5: Generate docs and skill
- [ ] Step 6: Run post-scaffold commands
- [ ] Step 7: Validate scaffoldStep 1: Gather project info
Collect from the user (ask only what was not provided):
| Variable | Example | Default | Used in |
|---|---|---|---|
{{name}} | md-tools | required | package.json name, README title |
{{description}} | CLI tool to convert content to markdown | required | package.json, README, SKILL.md |
{{bin}} | md | same as {{name}} | package.json bin field, CLI examples |
{{repo}} | acme/md-tools | required | package.json repository, badges |
{{author}} | Your Name | required | package.json, LICENSE |
{{year}} | 2026 | current year | LICENSE |
Step 2: Create directory structure
{{name}}/
.changeset/
.github/
workflows/
src/
skills/{{bin}}/Step 3: Generate config files
Load references/scaffold-configs.md. Generate all config files, replacing every {{placeholder}} with actual values.
Files: package.json, tsconfig.json, tsdown.config.ts, .gitignore, LICENSE.md, .changeset/config.json, .changeset/README.md, .github/workflows/ci.yml, .github/workflows/npm-publish.yml
Step 4: Generate source files
Load references/scaffold-source.md. Generate:
src/cli.ts: Commander entry pointsrc/index.ts: Public API exportssrc/types.ts: Shared type definitions
Step 5: Generate docs and skill
From the same references/scaffold-source.md, generate:
AGENTS.md: commands, architecture, gotchasREADME.md: install, usage, API, agent skill install, licenseskills/{{bin}}/SKILL.md: agent skill definition
Do not create the CLAUDE.md symlink here; the post-scaffold sequence in Step 6 creates it exactly once.
Step 6: Run post-scaffold commands
Load references/post-scaffold.md. Run the full command sequence in order. The order matters: git init must run before ultracite init (lefthook hooks need .git/ to install into).
Step 7: Validate scaffold
Run the validation checklist in references/post-scaffold.md. Every item must pass with command output as evidence; do not report success from a visual once-over. The placeholder sweep (grep for leftover {{variable}} tokens) is part of this checklist.
Dependencies
Runtime: @clack/prompts, commander
Development (in the package.json template): @changesets/cli, @types/node, tsdown, typescript, ultracite, vitest
Added by `ultracite init` (never list by hand): oxlint, oxfmt, lefthook, plus check, fix, and prepare scripts
Replacements for common packages: use node:util styleText instead of chalk (stable since Node 22.13), and the @clack/prompts spinner instead of ora.
Anti-patterns
- Do not use CommonJS. Everything is ESM with
"type": "module"; arequire()call or missing.jsimport extension fails the NodeNext typecheck and build. - Do not put a shebang in
src/cli.ts. The tsdownbanneroption injects#!/usr/bin/env nodeat build time; a source shebang produces a doubled shebang indist/cli.js. - Do not merge the dual tsdown builds. The CLI entry (shebang, no dts) and library entry (dts, no shebang) have conflicting output needs; merging breaks one or the other.
- Do not add
oxlint/oxfmtscripts or devDependencies by hand, and do not call those binaries directly.ultracite initowns them; runnpm run check(lint) andnpm run fix(autofix) instead, or duplicate script entries and version skew result. - Do not run
ultracite initbeforegit init. Its lefthook integration installs hooks into.git/hooksduring the install it triggers and fails without a repo. - Do not write
"test": "vitest run"without--passWithNoTests. The scaffold ships zero test files, so plainvitest runexits 1 and the first CI run goes red. - Do not skip AGENTS.md or the
skills/directory; the scaffold's contract is that every generated CLI is agent-ready out of the box. - Do not create test files in the scaffold; the user adds tests for their specific features.
- Do not add chalk or ora; see the replacements above.
After Scaffolding
For the first and subsequent releases of the generated package, the autoship skill drives the changeset, CI, and Version Packages PR flow end to end.
Post-Scaffold Commands
Run these commands in order after all files are generated.
Command Sequence
cd {{name}}
git init
npx ultracite@latest init --linter oxlint --integrations lefthook --pm npm --quiet
ln -s AGENTS.md CLAUDE.md
git add .
git commit -m "Initial commit"Command Notes
git initmust come beforeultracite init. The lefthook integration adds aprepare: lefthook installscript and runs the install immediately;lefthook installwrites into.git/hooksand fails without a repository.npx ultracite initrunsnpm installitself, then writesoxlint.config.ts,oxfmt.config.ts, andlefthook.yml, and updatespackage.json(addscheck,fix, andprepare: lefthook installscripts and theoxlint/oxfmt/lefthook/ultracitedevDeps). Pass--linter oxlintto skip the interactive linter prompt;--quietsuppresses the rest.- Create the
ln -s AGENTS.md CLAUDE.mdsymlink exactly once, here. Running it a second time fails withFile exists. - The initial commit captures the clean scaffold state, including the ultracite-generated files.
Validation Checklist
Verify every item by running the command and checking its output. Do not mark an item done without the command's evidence.
Validation:
- [ ] `npm run build` succeeds (produces dist/cli.js and dist/index.js, plus dist/index.d.ts)
- [ ] `head -1 dist/cli.js` prints exactly one `#!/usr/bin/env node` shebang
- [ ] `npm run typecheck` passes with no errors
- [ ] `npm run check` passes with no errors
- [ ] `npm run test` passes (0 test files; requires --passWithNoTests in the test script)
- [ ] `node dist/cli.js --version` prints 0.0.1
- [ ] `node dist/cli.js --help` shows the description
- [ ] `ls -la CLAUDE.md` shows a symlink to AGENTS.md
- [ ] `.github/workflows/ci.yml` and `.github/workflows/npm-publish.yml` exist
- [ ] `skills/{{bin}}/SKILL.md` has frontmatter with name and description
- [ ] `grep -rn '{{[a-z]' --exclude-dir=node_modules --exclude-dir=.git .` returns nothing (no leftover template placeholders; the pattern skips the `${{ secrets... }}` syntax in workflows)Troubleshooting
ultracite initfails or hangs: re-run without--quietto see which prompt blocked it, answer interactively, then continue the sequence.ln -sfails on Windows: copy AGENTS.md to CLAUDE.md instead (cp AGENTS.md CLAUDE.md).npm installfails: verify Node >= 22 withnode --version; the engines field rejects older versions.npm run buildfails with unresolved import errors: check that every relative import uses a.jsextension (NodeNext resolution requires them even for.tssources).npm run testexits 1 with "No test files found": the test script is missing--passWithNoTests.git commitblocked by a hook: lefthook is already active fromultracite init; runnpm run fixand retry rather than bypassing with--no-verify.
Scaffold Config Templates
Contents
- package.json
- tsconfig.json
- tsdown.config.ts
- .gitignore
- LICENSE.md
- .changeset/config.json
- .changeset/README.md
- .github/workflows/ci.yml
- .github/workflows/npm-publish.yml
---
package.json
{
"name": "{{name}}",
"version": "0.0.1",
"description": "{{description}}",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"bin": {
"{{bin}}": "./dist/cli.js"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
},
"files": [
"dist",
"README.md",
"LICENSE.md"
],
"scripts": {
"build": "tsdown",
"dev": "tsdown --watch",
"start": "node dist/cli.js",
"typecheck": "tsc --noEmit",
"test": "vitest run --passWithNoTests",
"changeset": "changeset",
"changeset:version": "changeset version",
"release": "npm run build && changeset publish"
},
"keywords": [],
"author": "{{author}}",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/{{repo}}.git"
},
"homepage": "https://github.com/{{repo}}#readme",
"bugs": {
"url": "https://github.com/{{repo}}/issues"
},
"engines": {
"node": ">=22"
},
"dependencies": {
"@clack/prompts": "^1.0.0",
"commander": "^14.0.3"
},
"devDependencies": {
"@changesets/cli": "^2.29.0",
"@types/node": "^22.19.11",
"tsdown": "^0.22.2",
"typescript": "^5.8.0",
"ultracite": "^7.2.2",
"vitest": "^4.0.0"
}
}Notes:
--passWithNoTestsis required: the scaffold ships zero test files, and plainvitest runexits 1, failing the first CI run.ultracite init --linter oxlint --integrations lefthook(run in the post-scaffold sequence) addsoxlint,oxfmt, andlefthookto devDependencies pluscheck,fix, andprepare: lefthook installscripts. Do not list any of those by hand here; doing so causes duplicate script entries and version skew against what ultracite pins.
tsconfig.json
{
"compilerOptions": {
"target": "ES2023",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2024"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"isolatedModules": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}tsdown.config.ts
import { defineConfig } from "tsdown";
export default defineConfig([
{
entry: { cli: "src/cli.ts" },
format: ["esm"],
clean: true,
sourcemap: true,
target: "node22",
banner: { js: "#!/usr/bin/env node" },
},
{
entry: { index: "src/index.ts" },
format: ["esm"],
dts: true,
sourcemap: true,
target: "node22",
},
]);The banner option injects the shebang into dist/cli.js at build time. Never add #!/usr/bin/env node to src/cli.ts itself or the built file gets a doubled shebang. Keep the two configs separate: the CLI entry needs the shebang and no .d.ts, the library entry needs .d.ts and no shebang.
.gitignore
node_modules/
dist/
*.tsbuildinfo
.env
.env.local
.DS_StoreLICENSE.md
The MIT License (MIT)
Copyright (c) {{year}} {{author}}
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE..changeset/config.json
{
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}.changeset/README.md
# Changesets
Run `npm run changeset` to add a changeset when making changes to {{name}}.
This generates a changeset file that describes the change and its semver bump type (patch, minor, or major). Changesets are consumed during release to update the version and generate changelog entries..github/workflows/ci.yml
name: CI
on:
push:
branches:
- main
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: 22
cache: npm
- name: Install
run: npm ci
- name: Changeset Status
if: github.event_name == 'pull_request'
run: npx changeset status --since origin/main
- name: Lint
run: npm run check
- name: Typecheck
run: npm run typecheck
- name: Test
run: npm run test
- name: Build
run: npm run buildNotes:
npm run checkis the ultracite-added lint script. It exists by the time CI runs because the post-scaffoldultracite initruns before the initial commit.Changeset Statusmakes PRs without a changeset fail CI on purpose;fetch-depth: 0is required for the--since origin/maincomparison.
.github/workflows/npm-publish.yml
name: Release
on:
push:
branches:
- main
workflow_dispatch:
permissions:
contents: write
pull-requests: write
id-token: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Node
uses: actions/setup-node@v6
with:
node-version: 22
registry-url: https://registry.npmjs.org
cache: npm
- name: Upgrade npm for OIDC trusted publishing
run: npm install -g npm@latest
- name: Install dependencies
run: npm ci
- name: Create release PR or publish
uses: changesets/action@v1
with:
publish: npm run release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_CONFIG_PROVENANCE: "true"Scaffold Source Templates
Contents
---
src/cli.ts
No shebang here: tsdown's banner option injects it at build time, and a source shebang would double it in dist/cli.js.
import { Command } from "commander";
const program = new Command();
program
.name("{{bin}}")
.description("{{description}}")
.version("0.0.1");
// Register commands here
// import { registerExampleCommand } from "./commands/example.js";
// registerExampleCommand(program);
program.parse();src/index.ts
// Public API exports
// export { example } from "./example.js";src/types.ts
// Shared type definitionsAGENTS.md
The CLAUDE.md symlink is created later by the post-scaffold command sequence, not here.
# {{name}}
{{description}}
## Commands
\`\`\`bash
npm install # setup (requires Node >= 22)
npm run build # tsdown, outputs to dist/
npm run dev # tsdown --watch
npm run test # vitest run --passWithNoTests
npm run typecheck # tsc --noEmit
npm run fix # ultracite fix: format + lint autofix
npm run check # ultracite check: lint (CI)
\`\`\`
## Architecture
\`\`\`
src/
cli.ts # Commander entry point
index.ts # Public API exports
types.ts # Shared type definitions
\`\`\`
## Gotchas
- **ESM only**: This project uses `"type": "module"`. Use `.js` extensions in imports (e.g., `import { foo } from "./foo.js"`); extensionless imports fail the NodeNext typecheck.
- **Dual build**: `tsdown.config.ts` produces two entry points, `cli.js` (with shebang) and `index.js` (with .d.ts). Do not merge them, and do not add a shebang to `src/cli.ts`.
- **Linting via ultracite**: Run `npm run fix` (autofix) or `npm run check` (CI lint) instead of calling oxlint or oxfmt directly.
- **Git hooks via lefthook**: The `prepare` script runs `lefthook install` on every `npm install`; no manual hook setup.
- **No chalk/ora**: Use `import { styleText } from "node:util"` for colors (stable in Node 22.13+) and the `@clack/prompts` spinner for progress indicators.README.md
# {{name}}
{{description}}
## Installation
\`\`\`bash
npm install -g {{name}}
\`\`\`
Or use directly with npx:
\`\`\`bash
npx {{name}} --help
\`\`\`
## Usage
\`\`\`bash
{{bin}} --help
\`\`\`
## Programmatic API
\`\`\`typescript
import {} from "{{name}}";
\`\`\`
## Usage with AI Agents
Add the skill to your AI coding assistant:
\`\`\`bash
npx skills add {{repo}}
\`\`\`
This works with Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, Goose, OpenCode, and Windsurf.
## Requirements
- Node.js >= 22
## License
[MIT](LICENSE.md)skills/SKILL.md
Create at skills/{{bin}}/SKILL.md. The frontmatter and body go in one file:
---
name: {{bin}}
description: {{description}}. Use when the user wants to use {{bin}}, run {{bin}} commands, or asks about {{name}} features.
---
# {{name}}
{{description}}
## Commands
| Command | What it does |
|---------|-------------|
| `{{bin}} --help` | Show available commands and options |
| `{{bin}} --version` | Show version number |Related skills
FAQ
What does scaffold-cli include in a new project?
scaffold-cli generates a CLI skeleton with argument parsing, packaging configuration, test layout, and release conventions so developers can implement command logic on a runnable, publishable base.
When should developers use scaffold-cli?
scaffold-cli fits greenfield CLI projects before feature code is written. Use it when you need consistent parsing, tests, and packaging rather than patching an existing mature CLI codebase.