@elvine-sk/cli
v0.2.2
Published
Scaffold and update projects from the ELV template registry
Readme
@elvine-sk/cli
Scaffold and update projects from the ELV template registry. Works like the ui-thing CLI but for full project templates — clone a starter, then pull upstream updates interactively without losing your local changes.
Table of Contents
- Quick Start
- Installation
- Templates
- Package Manager Selection
- CI/CD Workflows
- Dotenv Migration
- Commands
- Configuration (.elvrc.json)
- Using with Private Repos
- Local Development & Testing
- Publishing to npm
- Adding New Templates
- How the Update Flow Works
- Troubleshooting
Quick Start
# Scaffold a full Nuxt app (ui-template + core module)
npx @elvine-sk/cli init app my-project
# Scaffold a Hono backend
npx @elvine-sk/cli init backend my-api
# Scaffold a full-stack project (Nuxt + Hono)
npx @elvine-sk/cli init fullstack my-platform
# Check for upstream updates
npx @elvine-sk/cli diff
# Apply updates interactively (never replaces without asking)
npx @elvine-sk/cli updateInstallation
Run directly (recommended):
npx @elvine-sk/cli <command>Install globally:
npm install -g @elvine-sk/cli
# then use:
elv <command>Install as a dev dependency:
pnpm add -D @elvine-sk/cli
# then use via npx or package.json scriptsTemplates
| Template | Description | Packages |
| ------------- | ---------------------------------------------- | ---------------------------------- |
| app | Full Nuxt app with core module | ui-template + core |
| backend | Hono backend API (auth, RBAC, Kysely, Redis) | bn-template |
| fullstack | Full-stack (Nuxt frontend + Hono backend) | ui-template + core + bn-template |
| ui-only | UI component library template only | ui-template |
| core-only | Core Nuxt module only | core + authorization |
Template Details
app — Merges ui-template and core into a single Nuxt project. The core package is placed into modules/core/ as a local Nuxt module. Dependencies are merged into one package.json, and catalog: versions are resolved to real versions. The shared authorization engine is copied into vendor/authorization/ and referenced with a file: dependency, because it is not published to npm.
backend — Standalone Hono backend with authentication, RBAC/ABAC, Kysely ORM (MySQL), Redis, and Pino logging. Includes migration and seed tooling via kysely-ctl.
fullstack — Combines app and backend into a monorepo-like structure:
my-project/
├── frontend/ # Nuxt app (same as "app" template)
│ ├── app/
│ ├── modules/core/
│ ├── vendor/authorization/
│ ├── nuxt.config.ts
│ └── package.json
├── backend/ # Hono API (same as "backend" template)
│ ├── src/
│ ├── vendor/authorization/
│ └── package.json
└── package.json # Root with workspace scriptscore-only — The core Nuxt module on its own, at the project root. core depends on the shared authorization engine, so that package is copied into vendor/authorization/ and referenced with a file: dependency, exactly as in app and backend.
Note: All scaffolded projects start at version
0.1.0regardless of the template's current version. The package name is derived from the project directory name.
Package Manager Selection
During elv init, you're prompted to choose a package manager (pnpm, npm, or bun). This choice affects:
- Post-setup install commands
- CI/CD workflow files (
.github/workflows/) — install, run, and cache commands are patched - Dockerfiles — lockfile copy, vendored-directory copy, and install commands are patched
- Fullstack root
package.jsonscripts (dev, build commands)
npm: installing a scaffolded frontend currently fails with
ERESOLVE, because@vee-validate/zoddeclares a peer dependency onzod@^3while the template useszod@^4. Use pnpm (the default) or runnpm install --legacy-peer-deps.
bun on Windows:
file:dependencies are copied intonode_modulesrather than symlinked. After editing files undervendor/authorization— or after anelv updatethat changes them — runbun installagain for the changes to take effect.
CI/CD Workflows
Scaffolded projects include GitHub Actions workflows:
- ci.yml — Runs typecheck and build on push to
main/developand on PRs - deploy-ssh.yml — Manual dispatch to deploy via SSH + PM2
- deploy-docker.yml — Manual dispatch to build Docker image, push to GHCR, and deploy
All workflows are automatically patched with the correct package manager commands during scaffold.
Required GitHub Secrets for deployment:
| Secret | Description |
|---|---|
| SSH_HOST | Server hostname/IP |
| SSH_USER | SSH username |
| SSH_PRIVATE_KEY | SSH private key |
| SSH_PORT | SSH port (optional, default 22) |
Dotenv Migration
Previously, backend templates used --env-file=.env.dev (bun/tsx-specific). Scaffolded projects now use the dotenv package for universal compatibility. The entry point (src/server.ts) imports dotenv/config, which automatically loads .env. For different environments, set DOTENV_CONFIG_PATH=.env.staging before running.
Commands
elv init
Scaffold a new project from a template.
elv init [template] [directory]Arguments:
| Argument | Description | Required |
| ----------- | ---------------------------------------- | -------- |
| template | Template name (app, backend, fullstack, ui-only, core-only) | No (interactive prompt) |
| directory | Target directory for the new project | No (interactive prompt) |
Options:
| Flag | Alias | Description | Default |
| --------------- | ----- | ------------------------------------- | ------- |
| --source | -s | Source repository (github:owner/repo) | Built-in default |
| --force | -f | Overwrite existing directory | false |
| --token | | GitHub token for private repos | |
Examples:
# Interactive mode — prompts for everything
elv init
# Full Nuxt app
elv init app my-hrm-app
# Backend API only
elv init backend my-api
# Full-stack (Nuxt + Hono)
elv init fullstack my-platform
# UI template only
elv init ui-only my-design-system
# Core module only
elv init core-only shared-logic
# From a custom source repo
elv init app my-app --source github:my-org/my-templates
# Overwrite existing directory
elv init app my-app --forceWhat happens:
- Fetches the template registry from the source repo (falls back to built-in)
- Prompts for template + project name if not provided
- Downloads the full repo, extracts the relevant
packages/directories - For
app/fullstack: merges packages, resolvescatalog:versions, patchesnuxt.config.tsto loadcorefrommodules/core/ - For
backend: copiesbn-template, resolvescatalog:versions - Copies the
authorizationpackage intovendor/authorization/(once per sub-project infullstack) and pointspackage.jsonat it - Cleans up monorepo-specific files (
pnpm-lock.yaml,.npmrc, etc.) - Writes
.elvrc.jsonwith the current commit SHA for future updates - Asks to run post-setup commands (
installin each project;nuxt preparefor the frontend)
elv update
Interactively update your project from upstream changes. Never replaces files without asking.
elv update [options]Options:
| Flag | Alias | Description | Default |
| ------------ | ----- | ---------------------------------------- | ------- |
| --target | -t | Target branch or commit to update to | main |
| --cwd | | Project directory (if not current) | . |
| --token | | GitHub personal access token | |
Examples:
# Update from current directory
elv update
# Update a project in another directory
elv update --cwd ./my-project
# Update to a specific branch
elv update --target develop
# Update a private repo
elv update --token ghp_xxxxxxxxxxxxInteractive review for each file:
For every changed file, you get seven choices:
| Choice | What it does |
| -------------------- | --------------------------------------------------------- |
| Accept | Apply this change to your local file |
| Skip | Ignore this file, keep your local version |
| Skip always | Skip and add a glob pattern to .elvrc.json exclude list |
| Merge hunks | Review and accept/skip individual sections of the diff |
| Open in editor | Inline conflict markers — Accept / Reject per change |
| Merge editor | 3-way split view with checkboxes (full control) |
| View diff | Show the full colored diff, then ask again |
| Accept all | Apply all remaining changes without more prompts |
Merge Hunks
When you choose Merge hunks, the diff is split into individual sections (hunks). For each hunk you can:
- Accept — apply this specific change
- Skip — keep your local version for this section
- Accept remaining — apply all remaining hunks without prompting
Only the hunks you accept are applied. This gives you git add -p-style granularity over each update.
Open in Editor (primary)
When you choose Open in editor, the CLI:
- Auto-detects your editor (Cursor, Windsurf, VS Code, VSCodium)
- Writes the file with inline conflict markers (
<<<<<<< local (yours)/>>>>>>> upstream (incoming)) - Opens the file — your editor shows Accept Current Change / Accept Incoming Change / Accept Both / Resolve with AI buttons per change
- Click to accept or reject each change, then save and close the tab
- The CLI verifies all conflicts were resolved; if markers remain you can keep or restore
This is the fastest way to resolve changes — one click per conflict block directly in your code.
Merge Editor (optional, full control)
When you choose Merge editor, the CLI opens the editor's built-in 3-way merge editor (--merge flag):
┌──────────────────┬──────────────────┐
│ Current (yours) │ Incoming (upstream) │
│ │ │
│ Your local code │ Upstream changes │
│ [✓] Accept │ [✓] Accept │
├──────────────────┴─────────────────────┤
│ Result │
│ Live preview of merged output │
└─────────────────────────────────────────┘- Check/uncheck individual changes from each side
- The Result pane updates in real-time
- Full control over every line in the final output
- Best for complex files with many interleaved changes
Editor detection:
| Method | Detects |
| ------ | ------- |
| VSCODE_GIT_ASKPASS_NODE env var | Cursor, Windsurf, VS Code (by binary path) |
| CURSOR_CHANNEL / WINDSURF_CHANNEL env vars | Cursor, Windsurf specifically |
| TERM_PROGRAM env var | VS Code |
| CLI availability (where / which) | Any editor with a CLI command |
Supported editors: Cursor, Windsurf, VS Code, VSCodium.
Unsupported editors: If your editor doesn't support inline diffs, the CLI suggests using Merge hunks in the terminal instead.
Skip Always (Auto-Exclude)
When you choose Skip always, you're prompted to select an exclude pattern:
- The exact file path (e.g.
app/pages/login.vue) - The entire directory (e.g.
app/pages/**)
The selected pattern is immediately added to your .elvrc.json exclude list, so future updates automatically skip matching files.
After review, .elvrc.json is updated with the new commit hash.
elv diff
Preview what upstream changes exist without applying anything. Read-only.
elv diff [options]Options:
| Flag | Alias | Description | Default |
| ------------ | ----- | ---------------------------------------- | ------- |
| --target | -t | Target branch or commit to diff against | main |
| --cwd | | Project directory | . |
| --full | -F | Show full file diffs (not just summary) | false |
| --token | | GitHub personal access token | |
Examples:
# Summary only — lists changed files
elv diff
# Full diffs with colored output
elv diff --full
# Diff against a specific branch
elv diff --target release/v2elv info
Show project configuration and check for available updates.
elv info [options]Options:
| Flag | Description | Default |
| --------- | ---------------------------------- | ------- |
| --cwd | Project directory | . |
| --token | GitHub personal access token | |
Example output:
@elvine-sk/cli - Project Info
Template: app
Version: 0.1.0
Source: github:my-org/my-templates
Commit: a1b2c3d4
Updated at: 2026-07-13T03:15:00.000Z
Excludes:
· app/pages/**
· app/composables/stores/**
✔ Up to date!elv list
List all available templates from the registry.
elv list [options]Options:
| Flag | Alias | Description | Default |
| ----------- | ----- | ------------------------------------- | ------- |
| --source | -s | Source repository | Built-in default |
| --token | | GitHub personal access token | |
Example output:
Available Templates
app
Full app (ui-template + core module)
Packages: ui-template, core
backend
Hono backend API (auth, RBAC, Kysely, Redis)
Packages: bn-template
fullstack
Full-stack app (Nuxt frontend + Hono backend)
Packages: ui-template, core, bn-template
ui-only
UI component library template only
Packages: ui-template
core-only
Core Nuxt module only
Packages: core
Source: github:Elvine-SK/hrm-mono-app
Usage: npx @elvine-sk/cli init <template> <directory>Configuration (.elvrc.json)
When you run elv init, a .elvrc.json file is created in your project root:
{
"$schema": "https://github.com/Elvine-SK/hrm-mono-app/blob/main/schema.json",
"version": "0.1.0",
"sourceCommit": "a1b2c3d4e5f6...",
"template": "app",
"source": "github:Elvine-SK/hrm-mono-app",
"updatedAt": "2026-07-13T03:00:00.000Z",
"exclude": [
"app/pages/**",
"app/composables/stores/**"
]
}| Field | Description |
| --------------- | ------------------------------------------------------------------------ |
| version | Your project's local version string |
| sourceCommit | The exact commit SHA your project was scaffolded/last updated from |
| template | Which template was used (app, backend, fullstack, ui-only, core-only) |
| source | The GitHub repo the template came from |
| updatedAt | Timestamp of last init or update |
| exclude | Glob patterns for files that should never be updated by elv update |
Customizing Excludes
Edit the exclude array to protect files you've customized:
{
"exclude": [
"app/pages/**",
"app/composables/stores/**",
"app/assets/css/main.css",
"nuxt.config.ts",
"app/app.config.ts"
]
}These files will be silently skipped during elv update and elv diff.
You can also add excludes interactively during elv update by choosing Skip always when prompted for a file action.
Using with Private Repos
The CLI uses the GitHub REST API. For private repositories, you need a personal access token (classic or fine-grained) with repo / contents:read permissions.
Option 1: Pass via --token flag
elv update --token ghp_xxxxxxxxxxxxxxxxxxxx
elv diff --token ghp_xxxxxxxxxxxxxxxxxxxx
elv init app my-app --source github:my-org/my-templates --token ghp_xxxOption 2: Set the GITHUB_TOKEN environment variable
# Linux / macOS
export GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx
elv update
# Windows (PowerShell)
$env:GITHUB_TOKEN = "ghp_xxxxxxxxxxxxxxxxxxxx"
elv update
# Windows (CMD)
set GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx
elv updateOption 3: Save token interactively
When you run elv init without a token, the CLI will prompt you to enter one and optionally save it to ~/.elv/credentials.json for future use.
Creating a GitHub Token
- Go to github.com/settings/tokens
- Click Generate new token (classic) or Fine-grained token
- For classic: check the
reposcope - For fine-grained: select the target repository, grant
Contents: Read-only - Copy the token and use it with
--tokenorGITHUB_TOKEN
Rate Limits
- Without token: 60 requests/hour (shared across your IP)
- With token: 5,000 requests/hour
Each elv update or elv diff uses approximately 2 + N API calls (where N is the number of changed files), so a token is recommended for frequent use.
Local Development & Testing
Prerequisites
- Node.js >= 18
- pnpm (the monorepo uses pnpm workspaces)
Building the CLI
From the monorepo root:
# Build the CLI
pnpm elv:build
# Or from the CLI package directly
cd packages/cli
pnpm buildRunning Locally (without publishing)
After building, run directly from the dist output:
# From monorepo root
node packages/cli/dist/index.mjs --help
node packages/cli/dist/index.mjs list
node packages/cli/dist/index.mjs init app test-project
# Or use the stub mode for development (auto-rebuilds on change)
cd packages/cli
pnpm dev
node dist/index.mjs listUsing pnpm link for Global Testing
To test as if it were installed globally:
cd packages/cli
pnpm build
pnpm link --global
# Now you can run from anywhere:
elv --help
elv list
elv init app ~/Desktop/test-project
# When done, unlink:
pnpm unlink --globalTesting the Full Workflow
# 1. Build the CLI
cd packages/cli && pnpm build && cd ../..
# 2. Test "list" (no network needed — falls back to built-in)
node packages/cli/dist/index.mjs list
# 3. Test "init" — scaffold each template type
node packages/cli/dist/index.mjs init app /tmp/elv-test/my-app
node packages/cli/dist/index.mjs init backend /tmp/elv-test/my-api
node packages/cli/dist/index.mjs init fullstack /tmp/elv-test/my-platform
# 4. Verify the scaffolded projects
ls /tmp/elv-test/my-app/
ls /tmp/elv-test/my-api/
ls /tmp/elv-test/my-platform/
# 5. Test "info" inside a scaffolded project
node packages/cli/dist/index.mjs info --cwd /tmp/elv-test/my-app
# 6. Test "diff" — check for upstream changes
node packages/cli/dist/index.mjs diff --cwd /tmp/elv-test/my-app
# 7. Test "update" — interactive merge with new options
node packages/cli/dist/index.mjs update --cwd /tmp/elv-test/my-app
# 8. Clean up
rm -rf /tmp/elv-testTesting with a Private Repo
# Set your token once
export GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx
# All commands now authenticate automatically
node packages/cli/dist/index.mjs init app /tmp/test-private \
--source github:my-org/private-repo \
--token $GITHUB_TOKENDevelopment Mode (stub)
For rapid iteration, use unbuild's stub mode:
cd packages/cli
pnpm dev # Creates stub dist that auto-resolves to src/
# Now changes to src/ are reflected immediately:
node dist/index.mjs list
# Edit src/commands/list.ts...
node dist/index.mjs list # Changes reflected without rebuildPublishing to npm
First-time Setup
- Make sure you have an npm account and are logged in:
npm login- If using an
@elvine-skscope, either create the org on npmjs.com or publish as unscoped.
Publishing
cd packages/cli
# Build first
pnpm build
# Publish (dry run to verify)
npm publish --access public --dry-run
# Publish for real
npm publish --access publicVersioning
Update the version in packages/cli/package.json and src/index.ts (the citty meta version) before publishing:
cd packages/cli
npm version patch # 0.1.0 → 0.1.1
# or
npm version minor # 0.1.0 → 0.2.0Adding New Templates
1. Add the package to the monorepo
mkdir packages/my-new-template
# Set up your template files...2. Register it in registry.json
Edit the root registry.json:
{
"source": "github:Elvine-SK/hrm-mono-app",
"templates": {
"app": { "..." : "..." },
"backend": { "..." : "..." },
"fullstack": { "..." : "..." },
"my-new-template": {
"description": "Description of what this template provides",
"packages": ["my-new-template"],
"postSetup": ["pnpm install"]
}
}
}3. Map its files in LAYOUTS
Add the template's package-to-directory mapping to LAYOUTS in packages/cli/src/utils/layout.ts — it tells elv update / elv diff where each upstream packages/<name>/… file lands inside a generated project:
export const LAYOUTS: Record<string, TemplateLayout> = {
// ...existing templates...
"my-new-template": {
"my-new-template": [""], // package → project root
authorization: [VENDOR_DIR], // only if the template vendors it
},
};Without an entry here elv init still scaffolds the template, but elv update and elv diff refuse to run for projects created from it, because they cannot map upstream paths to local ones.
4. (Optional) Add to built-in fallback
If you want the template available even when the registry can't be fetched, add it to packages/cli/src/utils/template.ts:
export const BUILT_IN_TEMPLATES: RegistryManifest = {
source: DEFAULT_SOURCE,
templates: {
// ...existing templates...
"my-new-template": {
description: "Description of what this template provides",
packages: ["my-new-template"],
postSetup: ["pnpm install"],
},
},
};5. Rebuild and test
pnpm elv:build
node packages/cli/dist/index.mjs listExample: bn-template
The bn-template package is registered as both a standalone backend template and part of the fullstack combined template. When used standalone, it scaffolds directly to the project root. In the fullstack template, it goes into a backend/ subdirectory alongside the Nuxt frontend/.
How the Update Flow Works
elv update
│
├─ Read .elvrc.json → get sourceCommit (e.g. a1b2c3d4)
│
├─ Fetch latest commit from GitHub (e.g. e5f6g7h8)
│
├─ Compare a1b2c3d4...e5f6g7h8 via GitHub API
│ └─ Returns list of changed files in the repo
│
├─ Filter to only files inside template packages
│ └─ e.g. only packages/ui-template/** and packages/core/**
│
├─ Remove files matching .elvrc.json exclude patterns
│
├─ For each remaining file:
│ ├─ Show status badge (ADDED / MODIFIED / DELETED)
│ ├─ Prompt: Accept | Skip | Skip always | Merge hunks
│ │ | Open in editor | View diff | Accept all
│ │
│ ├─ If "View diff" → show colored diff → ask again
│ ├─ If "Accept" → write remote content to local file
│ ├─ If "Skip always" → prompt for pattern → add to exclude
│ ├─ If "Merge hunks" → show each hunk → accept/skip per hunk
│ ├─ If "Open in editor" → conflict markers → accept/reject inline
│ └─ If "Merge editor" → 3-way split view → checkboxes
│
└─ Update .elvrc.json with new commit SHAKey behaviors:
- Files in
excludepatterns are always skipped — your custom pages, stores, and configs stay untouched - Modified files show a line count before you decide — you know the scope of the change
- Merge hunks gives you section-level control — accept only the parts you want, like
git add -p - Inline conflict resolution — open in your editor (Cursor, Windsurf, VS Code) and accept/reject each change with one click
- Skip always auto-manages your exclude list — no need to manually edit
.elvrc.json - "Accept all remaining" is available to batch-accept when you trust the rest
- Deleted files prompt too — upstream removals don't auto-delete your files
- The commit hash is only updated after the review — if you cancel, nothing changes
Troubleshooting
"No .elvrc.json found"
You're running elv update, elv diff, or elv info in a directory that wasn't scaffolded with elv init. Either:
cdinto your elv-managed project, or- Use
--cwd /path/to/project
"Could not fetch remote registry, using built-in templates"
The CLI couldn't reach the GitHub API. Possible causes:
- No internet connection
- The source repo doesn't exist or is private (use
--token) - GitHub API rate limit exceeded (use a token for 5,000 req/hr)
The CLI falls back to built-in templates, so elv init still works.
"Failed to compare commits. The base commit may be too old."
GitHub's compare API has a limit of ~250 commits. If your project is very far behind:
- Note the current files you've customized
- Re-scaffold with
elv init --force - Manually re-apply your customizations
"Could not fetch commit SHA"
During elv init, the CLI couldn't fetch the latest commit to pin in .elvrc.json. The project is still scaffolded, but you'll need to manually set sourceCommit in .elvrc.json before elv update will work.
Rate limiting (403 errors)
Add a GitHub token:
elv update --token ghp_xxxxxxxxxxxxxxxxxxxx"Could not open editor" during merge
The CLI auto-detects your editor (Cursor, Windsurf, VS Code), but the CLI command needs to be in your system PATH. To install it:
- Open the Command Palette in your editor (
Ctrl+Shift+P/Cmd+Shift+P) - Search for "Shell Command: Install" and run the matching command
- Cursor: "Install 'cursor' command in PATH"
- Windsurf: "Install 'windsurf' command in PATH"
- VS Code: "Install 'code' command in PATH"
- Restart your terminal
Binary files or large files fail to fetch
The GitHub Contents API has a 1MB file size limit. Files larger than 1MB are skipped during elv update. These are typically assets (images, fonts) that rarely change.
