diff --git a/resources/prompts/scaffold-vue3-vite.prompt.md b/resources/prompts/scaffold-vue3-vite.prompt.md new file mode 100644 index 0000000..fddd740 --- /dev/null +++ b/resources/prompts/scaffold-vue3-vite.prompt.md @@ -0,0 +1,18 @@ +--- +name: "scaffold-vue3-vite" +description: "Scaffold a new Vue 3 + Vite project with vue-router, atomic component directories, and SCSS styling foundations." +agent: "agent" +tools: [read, search, execute] +argument-hint: "project-root= app-name= mode= port=" +--- + +Scaffold a new Vue 3 + Vite project using the shared scaffold script. + +Requirements: + +- Prefer `resources/scripts/scaffold-vue3-vite.sh` over manually creating files. +- Default to `--mode dry-run` unless the user explicitly asks for apply mode. +- Resolve `--project-root` and `--app-name` before running; ask if missing. +- After `--mode apply` completes, instruct the user to run `npm install` then + `npm run dev` to verify the dev server starts cleanly. +- Summarize the files created and the next steps. diff --git a/resources/scripts/scaffold-vue3-vite.mjs b/resources/scripts/scaffold-vue3-vite.mjs new file mode 100644 index 0000000..7efc259 --- /dev/null +++ b/resources/scripts/scaffold-vue3-vite.mjs @@ -0,0 +1,325 @@ +#!/usr/bin/env node +// @ts-nocheck + +import fs from 'node:fs'; +import path from 'node:path'; + +const DEFAULTS = { + mode: 'dry-run', + port: '5173', +}; + +function usage() { + console.error(`Usage: resources/scripts/scaffold-vue3-vite.sh [options] + +Required: + --project-root Target project path. + --app-name Application name used in package.json and index.html. + +Optional: + --mode Default: dry-run + --port Vite dev server port. Default: 5173 + --force Overwrite existing files. + --help Show this help text. +`); +} + +function parseArgs(argv) { + const options = { + mode: DEFAULTS.mode, + port: DEFAULTS.port, + force: false, + }; + + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + + if (arg === '--help') { + usage(); + process.exit(0); + } + + if (arg === '--project-root') { + options.projectRoot = argv[index + 1]; + index += 1; + continue; + } + + if (arg === '--app-name') { + options.appName = argv[index + 1]; + index += 1; + continue; + } + + if (arg === '--mode') { + options.mode = argv[index + 1]; + index += 1; + continue; + } + + if (arg === '--port') { + options.port = argv[index + 1]; + index += 1; + continue; + } + + if (arg === '--force') { + options.force = true; + continue; + } + + throw new Error(`Unknown argument: ${arg}`); + } + + if (!options.projectRoot) throw new Error('--project-root is required.'); + if (!options.appName) throw new Error('--app-name is required.'); + if (!['dry-run', 'apply'].includes(options.mode)) { + throw new Error('--mode must be dry-run or apply.'); + } + + options.projectRoot = path.resolve(options.projectRoot); + options.slug = slugify(options.appName); + return options; +} + +function slugify(value) { + return String(value) + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, ''); +} + +function writeFileWithGuard(filePath, content, { mode, force }) { + const exists = fs.existsSync(filePath); + if (exists && !force) return { action: 'skipped', filePath }; + const action = exists ? 'updated' : 'created'; + if (mode === 'apply') { + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, content, 'utf8'); + } + return { action, filePath }; +} + +function touchDir(dirPath, { mode }) { + const keepPath = path.join(dirPath, '.gitkeep'); + const exists = fs.existsSync(keepPath); + if (mode === 'apply' && !exists) { + fs.mkdirSync(dirPath, { recursive: true }); + fs.writeFileSync(keepPath, '', 'utf8'); + } + return { action: exists ? 'skipped' : 'created', filePath: keepPath }; +} + +// --- Renderers --- + +function renderPackageJson({ slug, appName, port }) { + return JSON.stringify( + { + name: slug, + version: '0.1.0', + private: true, + type: 'module', + scripts: { + dev: `vite --port ${port}`, + build: 'vite build', + preview: 'vite preview', + }, + dependencies: { + vue: '^3.4.0', + 'vue-router': '^4.3.0', + }, + devDependencies: { + '@vitejs/plugin-vue': '^5.0.0', + sass: '^1.77.0', + vite: '^5.3.0', + }, + }, + null, + 2, + ) + '\n'; +} + +function renderViteConfig({ port }) { + return `import { defineConfig } from 'vite'; +import vue from '@vitejs/plugin-vue'; +import { fileURLToPath, URL } from 'node:url'; + +export default defineConfig({ + plugins: [vue()], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + }, + }, + server: { + port: ${port}, + }, +}); +`; +} + +function renderIndexHtml({ appName }) { + return ` + + + + + ${appName} + + +
+ + + +`; +} + +function renderMainJs() { + return `import { createApp } from 'vue'; +import App from './App.vue'; +import router from './router/index.js'; +import '@/assets/styles/global-reset.scss'; + +const app = createApp(App); +app.use(router); +app.mount('#app'); +`; +} + +function renderAppVue() { + return ` + + +`; +} + +function renderRouter() { + return `import { createRouter, createWebHistory } from 'vue-router'; +import HomePage from '@/pages/HomePage.vue'; + +const routes = [ + { + path: '/', + name: 'home', + component: HomePage, + }, +]; + +export default createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes, +}); +`; +} + +function renderHomePage({ appName }) { + return ` + + +`; +} + +function renderGlobalTokens() { + return `// Global design tokens. +// Add project-specific custom properties and SCSS variables here. + +:root { + --color-bg: #ffffff; + --color-text: #1a1a1a; + --color-primary: #5865f2; + --font-base: system-ui, sans-serif; + --space-1: 0.25rem; + --space-2: 0.5rem; + --space-4: 1rem; + --space-8: 2rem; +} +`; +} + +function renderGlobalReset() { + return `@use 'global-tokens' as *; + +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + background-color: var(--color-bg); + color: var(--color-text); + font-family: var(--font-base); + line-height: 1.5; +} +`; +} + +function renderGitIgnore() { + return `node_modules +dist +.env +.env.* +*.local +`; +} + +// --- Main --- + +function main() { + const options = parseArgs(process.argv.slice(2)); + + if (!fs.existsSync(options.projectRoot)) { + throw new Error(`Project root does not exist: ${options.projectRoot}`); + } + + const src = (rel) => path.join(options.projectRoot, rel); + + const writes = [ + writeFileWithGuard(src('package.json'), renderPackageJson(options), options), + writeFileWithGuard(src('vite.config.js'), renderViteConfig(options), options), + writeFileWithGuard(src('index.html'), renderIndexHtml(options), options), + writeFileWithGuard(src('.gitignore'), renderGitIgnore(), options), + writeFileWithGuard(src('src/main.js'), renderMainJs(), options), + writeFileWithGuard(src('src/App.vue'), renderAppVue(), options), + writeFileWithGuard(src('src/router/index.js'), renderRouter(), options), + writeFileWithGuard(src('src/pages/HomePage.vue'), renderHomePage(options), options), + writeFileWithGuard(src('src/assets/styles/global-tokens.scss'), renderGlobalTokens(), options), + writeFileWithGuard(src('src/assets/styles/global-reset.scss'), renderGlobalReset(), options), + touchDir(src('src/components/atoms'), options), + touchDir(src('src/components/molecules'), options), + touchDir(src('src/components/organisms'), options), + touchDir(src('src/components/layouts'), options), + ]; + + console.log(`Mode: ${options.mode}`); + console.log(`Project root: ${options.projectRoot}`); + console.log(`App name: ${options.appName}`); + console.log(`Dev port: ${options.port}`); + console.log(''); + + for (const entry of writes) { + console.log(`${entry.action.toUpperCase()}: ${entry.filePath}`); + } + + if (options.mode === 'dry-run') { + console.log(''); + console.log('No files were written. Re-run with --mode apply to persist changes.'); + } else { + console.log(''); + console.log('Next: cd into the project root and run npm install, then npm run dev.'); + } +} + +main(); diff --git a/resources/scripts/scaffold-vue3-vite.sh b/resources/scripts/scaffold-vue3-vite.sh new file mode 100755 index 0000000..1ad68f7 --- /dev/null +++ b/resources/scripts/scaffold-vue3-vite.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +set -euo pipefail + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" + +if command -v node >/dev/null 2>&1; then + node_bin="$(command -v node)" +elif command -v nodejs >/dev/null 2>&1; then + node_bin="$(command -v nodejs)" +else + printf 'Node.js is required to scaffold a Vue 3 + Vite project.\n' >&2 + exit 1 +fi + +exec "$node_bin" "$script_dir/scaffold-vue3-vite.mjs" "$@" diff --git a/resources/skills/vue3-vite/SKILL.md b/resources/skills/vue3-vite/SKILL.md new file mode 100644 index 0000000..63377a5 --- /dev/null +++ b/resources/skills/vue3-vite/SKILL.md @@ -0,0 +1,54 @@ +--- +name: vue3-vite +description: "Use when scaffolding a new Vue 3 + Vite project with vue-router, a pages layer, an atomic component structure, and SCSS styling foundations." +argument-hint: "project-root= app-name= mode= port=" +--- + +# Vue 3 + Vite Scaffold + +Use this skill when starting a new Vue 3 + Vite frontend project that needs +routing, a component hierarchy, and a SCSS style foundation from the first +commit. + +## Procedure + +1. Confirm required inputs: `project-root` and `app-name`. +2. Run `resources/scripts/scaffold-vue3-vite.sh` in `--mode dry-run` first to + review planned files before writing. +3. After reviewing dry-run output, re-run with `--mode apply` to write files. +4. Run `npm install` in the project root to install dependencies. +5. Run `npm run dev` to verify the dev server starts cleanly on the configured + port. + +## Outputs + +- `package.json` (Vue 3, Vite, vue-router, sass) +- `vite.config.js` (dev server port, `@` alias pointing to `src/`) +- `index.html` +- `src/main.js` +- `src/App.vue` +- `src/router/index.js` +- `src/pages/HomePage.vue` +- `src/components/atoms/.gitkeep` +- `src/components/molecules/.gitkeep` +- `src/components/organisms/.gitkeep` +- `src/components/layouts/.gitkeep` +- `src/assets/styles/global-tokens.scss` +- `src/assets/styles/global-reset.scss` +- `.gitignore` + +## Do Not Use + +- Do not use this workflow when a project already has a `package.json` or + `vite.config.js` unless `--force` is passed. +- Do not use this workflow when the project requires SSR or a meta-framework + such as Nuxt. + +## Notes + +- Component directories follow strict atomic design: + `atoms`, `molecules`, `organisms`, `layouts`. +- Every new component should have a colocated SCSS file and a `*.stories.js` + stub; this scaffold creates the directory structure but not the components. +- The `@` alias is wired in both `vite.config.js` and expected by the router. +- SCSS files use `@use` for token and reset imports; avoid `@import`.