create-teamix-evo 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +75 -0
  3. package/dist/index.js +586 -0
  4. package/dist/index.js.map +1 -0
  5. package/overlays/console/package.json.fragment.json +5 -0
  6. package/overlays/console/src/App.tsx +6 -0
  7. package/overlays/console/src/components/_placeholder/Card.tsx +40 -0
  8. package/overlays/console/src/components/_placeholder/Form.tsx +52 -0
  9. package/overlays/console/src/components/_placeholder/Input.tsx +35 -0
  10. package/overlays/console/src/components/_placeholder/README.md +42 -0
  11. package/overlays/console/src/components/_placeholder/Table.tsx +77 -0
  12. package/overlays/console/src/layouts/ConsoleLayout.tsx +46 -0
  13. package/overlays/console/src/lib/mock-data.ts +38 -0
  14. package/overlays/console/src/main.tsx +13 -0
  15. package/overlays/console/src/pages/DashboardPage.tsx +38 -0
  16. package/overlays/console/src/pages/UserDetailPage.tsx +57 -0
  17. package/overlays/console/src/pages/UserFormPage.tsx +61 -0
  18. package/overlays/console/src/pages/UserListPage.tsx +67 -0
  19. package/overlays/console/src/routes.tsx +21 -0
  20. package/package.json +38 -0
  21. package/templates/react-ts/README.md.hbs +39 -0
  22. package/templates/react-ts/_editorconfig +9 -0
  23. package/templates/react-ts/_gitignore +24 -0
  24. package/templates/react-ts/index.html.hbs +12 -0
  25. package/templates/react-ts/package.json.hbs +26 -0
  26. package/templates/react-ts/src/App.tsx +20 -0
  27. package/templates/react-ts/src/index.css +5 -0
  28. package/templates/react-ts/src/main.tsx +10 -0
  29. package/templates/react-ts/src/vite-env.d.ts +1 -0
  30. package/templates/react-ts/tailwind.config.ts +13 -0
  31. package/templates/react-ts/tsconfig.json +29 -0
  32. package/templates/react-ts/vite.config.ts +14 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/orchestrator.ts","../src/copy.ts","../src/git.ts","../src/merge-package-json.ts","../src/pm.ts","../src/presets/minimal.ts","../src/presets/console.ts","../src/presets/index.ts","../src/utils/logger.ts","../src/prompts.ts"],"sourcesContent":["import path from 'node:path';\nimport { Command } from 'commander';\nimport { red } from 'kolorist';\nimport { orchestrate } from './orchestrator.js';\nimport { listPresetIds } from './presets/index.js';\nimport { isValidPackageManager } from './pm.js';\nimport { promptMissing } from './prompts.js';\nimport { logger } from './utils/logger.js';\n\ninterface CliOptions {\n preset?: string;\n pm?: string;\n /** commander treats `--no-git` as `git: false`. */\n git?: boolean;\n /** commander treats `--no-install` as `install: false`. */\n install?: boolean;\n force?: boolean;\n}\n\nasync function main(): Promise<void> {\n const program = new Command();\n\n program\n .name('create-teamix-evo')\n .description(\n 'Scaffold a Vite + React + TypeScript project pre-wired with Teamix Evo design tokens, AI skills, and UI components.',\n )\n .argument(\n '[dir]',\n 'target directory (will be created if it does not exist)',\n )\n .option('--preset <id>', `preset id (${listPresetIds().join(' | ')})`)\n .option('--pm <name>', 'package manager: pnpm | npm | yarn')\n .option('--no-git', 'skip git init')\n .option('--no-install', 'skip dependency installation')\n .option('--force', 'overwrite non-empty target directory')\n .helpOption('-h, --help', 'display help');\n\n program.parse(process.argv);\n const opts = program.opts<CliOptions>();\n const dirArg = program.args[0];\n\n // Resolve target directory (interactive only if no dirArg).\n const dir = dirArg\n ? path.resolve(dirArg)\n : path.resolve(process.cwd(), 'my-app');\n if (!dirArg) {\n logger.warn(`未指定目录,使用默认路径:${dir}`);\n }\n\n // Validate --preset / --pm if provided\n if (opts.preset && !listPresetIds().includes(opts.preset)) {\n logger.error(\n `未知 preset \"${opts.preset}\"。可选:${listPresetIds().join(', ')}`,\n );\n process.exit(1);\n }\n if (opts.pm && !isValidPackageManager(opts.pm)) {\n logger.error(`未知包管理器 \"${opts.pm}\"。可选:pnpm | npm | yarn`);\n process.exit(1);\n }\n\n // Interactive prompts for missing fields (when running in a TTY).\n let presetId = opts.preset;\n let pm = opts.pm;\n let git = opts.git;\n if (process.stdin.isTTY && (!presetId || git === undefined)) {\n const answers = await promptMissing({\n dir,\n presetId,\n pm,\n git,\n });\n presetId = answers.presetId;\n git = answers.git;\n if (answers.pm) pm = answers.pm;\n } else {\n presetId = presetId ?? 'minimal';\n git = git ?? true;\n }\n\n try {\n await orchestrate({\n dir,\n presetId: presetId!,\n pm: pm && isValidPackageManager(pm) ? pm : undefined,\n install: opts.install ?? true,\n git: git ?? true,\n force: opts.force ?? false,\n });\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n console.error(`\\n${red('✗')} ${message}`);\n process.exit(1);\n }\n}\n\nmain().catch((err) => {\n console.error(`\\n${red('✗')} 未捕获的错误`);\n console.error(err);\n process.exit(1);\n});\n","import fs from 'node:fs/promises';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { execa } from 'execa';\nimport {\n runDesignInit,\n runSkillsAdd,\n runUiInit,\n runUiAdd,\n} from 'teamix-evo/core';\nimport { copyDir } from './copy.js';\nimport { gitInit } from './git.js';\nimport {\n mergePackageJson,\n readJson,\n writeJson,\n VersionConflictError,\n} from './merge-package-json.js';\nimport {\n detectPackageManager,\n type PackageManager,\n type PackageManagerInfo,\n} from './pm.js';\nimport { findPreset, type Preset } from './presets/index.js';\nimport { logger } from './utils/logger.js';\n\nexport interface OrchestrateOptions {\n /** Absolute target directory. */\n dir: string;\n /** Preset id. */\n presetId: string;\n /** Preferred package manager (else auto-detect). */\n pm?: PackageManager;\n /** Run package manager install at the end. */\n install: boolean;\n /** Run git init at the end. */\n git: boolean;\n /** Overwrite non-empty target directory. */\n force?: boolean;\n}\n\n/**\n * Locate the create-teamix-evo package root (regardless of whether running\n * from source via tsx, or from `dist/index.js` after bundling).\n */\nfunction getPackageRoot(): string {\n // Resolves to <pkg>/dist/index.js.map's directory in dev/build.\n const here = path.dirname(fileURLToPath(import.meta.url));\n // From dist/ go up to package root.\n return path.resolve(here, '..');\n}\n\n/**\n * Main scaffolding pipeline.\n *\n * Order:\n * 1. validate target dir\n * 2. copy base template (renders all `.hbs`)\n * 3. copy overlay if any\n * 4. teamix-evo design init\n * 5. teamix-evo skills add\n * 6. teamix-evo ui init + ui add\n * 7. rewrite package.json (base + overlay fragment + ui npm deps)\n * 8. write pending-ui.json (if overlay declares placeholders)\n * 9. install deps\n * 10. git init\n */\nexport async function orchestrate(options: OrchestrateOptions): Promise<void> {\n const preset = findPreset(options.presetId);\n if (!preset) {\n throw new Error(\n `Unknown preset \"${options.presetId}\". Available: ${[\n 'minimal',\n 'console',\n ].join(', ')}.`,\n );\n }\n\n const dir = path.resolve(options.dir);\n await prepareTargetDir(dir, options.force ?? false);\n\n const pmInfo = detectPackageManager(options.pm);\n const projectName = path.basename(dir);\n const pkgRoot = getPackageRoot();\n\n // ─── 1. base template ───────────────────────────────────────────────────\n logger.step('拷贝 base 模板');\n const baseTemplateDir = path.join(pkgRoot, 'templates', preset.baseTemplate);\n const hbsCtx = buildHbsContext({ preset, projectName, pmInfo });\n await copyDir(baseTemplateDir, dir, { hbs: hbsCtx });\n logger.detail(`base: ${preset.baseTemplate}`);\n\n // ─── 2. overlay ─────────────────────────────────────────────────────────\n if (preset.overlay) {\n logger.step(`应用 overlay: ${preset.overlay}`);\n const overlayDir = path.join(pkgRoot, 'overlays', preset.overlay);\n await copyDir(overlayDir, dir, { hbs: hbsCtx });\n }\n\n // ─── 3. design tokens ───────────────────────────────────────────────────\n logger.step('装载 design tokens');\n const designResult = await runDesignInit({\n projectRoot: dir,\n variant: preset.design.variant,\n tailwind: preset.design.tailwind,\n ide: 'qoder',\n });\n if (designResult.status === 'installed') {\n logger.detail(\n `${designResult.packageName}@${designResult.version} (${designResult.count} files)`,\n );\n }\n\n // ─── 4. AI skills ───────────────────────────────────────────────────────\n logger.step('装载 AI skills');\n const skillsResult = await runSkillsAdd({\n projectRoot: dir,\n ides: preset.skills.ides,\n scope: 'project',\n });\n if (skillsResult.status === 'installed') {\n logger.detail(\n `${skillsResult.skillCount} skill(s) → ${skillsResult.ides.join(' + ')}`,\n );\n }\n\n // ─── 5. ui init + ui add ────────────────────────────────────────────────\n logger.step('配置 UI');\n await runUiInit({ projectRoot: dir });\n let uiAddDeps: Record<string, string> = {};\n if (preset.ui.components.length > 0) {\n const uiAddResult = await runUiAdd({\n projectRoot: dir,\n ids: preset.ui.components,\n });\n uiAddDeps = uiAddResult.npmDependencies;\n logger.detail(\n `installed: ${uiAddResult.orderedIds.join(', ')} (${\n uiAddResult.written\n } files)`,\n );\n } else {\n logger.detail('ui init only — 无组件落地');\n }\n\n // ─── 6. merge package.json ──────────────────────────────────────────────\n logger.step('合并 package.json');\n await mergeProjectPackageJson({\n dir,\n overlayFragmentPath: preset.overlay\n ? path.join(\n pkgRoot,\n 'overlays',\n preset.overlay,\n 'package.json.fragment.json',\n )\n : null,\n extraDependencies: uiAddDeps,\n });\n\n // ─── 7. pending-ui.json ─────────────────────────────────────────────────\n if (preset.overlay === 'console') {\n await writePendingUi(dir, preset.id);\n logger.detail('.teamix-evo/create/pending-ui.json 已写入');\n }\n\n // ─── 8. install deps ────────────────────────────────────────────────────\n if (options.install) {\n logger.step(`安装依赖(${pmInfo.installCommand})`);\n try {\n await execa(pmInfo.name, pmInfo.install, { cwd: dir, stdio: 'inherit' });\n } catch (err) {\n logger.warn(\n `依赖安装失败 — 可手动执行:cd ${\n path.relative(process.cwd(), dir) || '.'\n } && ${pmInfo.installCommand}`,\n );\n if (err instanceof Error) logger.detail(err.message);\n }\n } else {\n logger.detail(`跳过依赖安装(手动执行:${pmInfo.installCommand})`);\n }\n\n // ─── 9. git init ────────────────────────────────────────────────────────\n if (options.git) {\n logger.step('初始化 git');\n const result = await gitInit(dir);\n if (result.ok) logger.detail('git 仓库已就绪(含首个 commit)');\n else logger.warn(`git init 失败:${result.reason}`);\n }\n\n // ─── done ───────────────────────────────────────────────────────────────\n printNextSteps({ dir, pmInfo, preset });\n}\n\nasync function prepareTargetDir(dir: string, force: boolean): Promise<void> {\n let exists = false;\n try {\n await fs.access(dir);\n exists = true;\n } catch {\n /* not exists */\n }\n\n if (!exists) {\n await fs.mkdir(dir, { recursive: true });\n return;\n }\n\n const entries = await fs.readdir(dir);\n if (entries.length === 0) return;\n\n // Reject existing teamix-evo project regardless of --force\n if (entries.includes('.teamix-evo')) {\n throw new Error(\n `Target directory already contains a .teamix-evo/ — please run \\`teamix-evo design init\\` inside it instead of using create-teamix-evo.`,\n );\n }\n\n if (!force) {\n throw new Error(\n `Target directory \"${dir}\" is not empty. Use --force to overwrite.`,\n );\n }\n logger.warn(`--force:覆盖已有目录 ${dir}`);\n}\n\nfunction buildHbsContext(args: {\n preset: Preset;\n projectName: string;\n pmInfo: PackageManagerInfo;\n}): Record<string, unknown> {\n const { preset, projectName, pmInfo } = args;\n return {\n projectName,\n designVariant: preset.design.variant,\n pmInstall: pmInfo.installCommand,\n pmRun: pmInfo.runPrefix,\n uiInstalled:\n preset.ui.components.length > 0\n ? preset.ui.components.join(', ')\n : '(无)',\n };\n}\n\nasync function mergeProjectPackageJson(args: {\n dir: string;\n overlayFragmentPath: string | null;\n extraDependencies: Record<string, string>;\n}): Promise<void> {\n const pkgPath = path.join(args.dir, 'package.json');\n const base = await readJson<Record<string, unknown>>(pkgPath);\n let overlay: Record<string, unknown> | null = null;\n if (args.overlayFragmentPath) {\n try {\n overlay = await readJson<Record<string, unknown>>(\n args.overlayFragmentPath,\n );\n } catch {\n overlay = null;\n }\n }\n try {\n const merged = mergePackageJson(base, overlay, {\n extraDependencies: args.extraDependencies,\n });\n await writeJson(pkgPath, merged);\n } catch (err) {\n if (err instanceof VersionConflictError) {\n throw new Error(\n `package.json 合并失败:${err.message}\\n` +\n `请检查 base 模板与 overlay/ui 组件依赖的版本声明,确保一致。`,\n );\n }\n throw err;\n }\n}\n\nasync function writePendingUi(dir: string, presetId: string): Promise<void> {\n const pendingDir = path.join(dir, '.teamix-evo', 'create');\n await fs.mkdir(pendingDir, { recursive: true });\n const pendingPath = path.join(pendingDir, 'pending-ui.json');\n const payload = {\n $schema: 'https://teamix-evo.dev/schema/pending-ui/v1.json',\n schemaVersion: 1,\n preset: presetId,\n pendingComponents: [\n { id: 'card', placeholder: 'src/components/_placeholder/Card.tsx' },\n { id: 'input', placeholder: 'src/components/_placeholder/Input.tsx' },\n { id: 'form', placeholder: 'src/components/_placeholder/Form.tsx' },\n { id: 'table', placeholder: 'src/components/_placeholder/Table.tsx' },\n ],\n };\n await writeJson(pendingPath, payload);\n}\n\nfunction printNextSteps(args: {\n dir: string;\n pmInfo: PackageManagerInfo;\n preset: Preset;\n}): void {\n const { dir, pmInfo, preset } = args;\n const rel = path.relative(process.cwd(), dir) || '.';\n logger.blank();\n logger.success(`${preset.displayName} preset 已就绪:${dir}`);\n logger.blank();\n console.log('Next steps:');\n console.log(` cd ${rel}`);\n console.log(\n ` ${pmInfo.runPrefix}${pmInfo.runPrefix === 'yarn' ? ' ' : ' '}dev`,\n );\n if (preset.id === 'console') {\n logger.blank();\n console.log(\n '💡 console preset 内含占位组件,可用 `npx teamix-evo ui add card` 等替换为真组件。',\n );\n }\n}\n","import fs from 'node:fs/promises';\nimport path from 'node:path';\nimport Handlebars from 'handlebars';\n\nexport interface CopyContext {\n /** Handlebars render context for `.hbs` templates. */\n hbs: Record<string, unknown>;\n}\n\n/**\n * Recursively copy `srcDir` into `destDir`, applying:\n * - `.hbs` files: rendered with `ctx.hbs`, written without `.hbs` extension\n * - `_gitignore` / `_editorconfig` etc. → `.gitignore` / `.editorconfig`\n * - Skips `package.json.fragment.json` (handled separately by the merger)\n *\n * Existing files are overwritten — call site is responsible for `--force`\n * / clean-target semantics.\n */\nexport async function copyDir(\n srcDir: string,\n destDir: string,\n ctx: CopyContext,\n): Promise<void> {\n await fs.mkdir(destDir, { recursive: true });\n\n const entries = await fs.readdir(srcDir, { withFileTypes: true });\n for (const entry of entries) {\n const srcPath = path.join(srcDir, entry.name);\n const destName = mapDestName(entry.name);\n if (destName === null) continue; // explicitly skipped\n const destPath = path.join(destDir, destName);\n\n if (entry.isDirectory()) {\n await copyDir(srcPath, destPath, ctx);\n continue;\n }\n\n if (entry.name.endsWith('.hbs')) {\n const tpl = await fs.readFile(srcPath, 'utf8');\n const rendered = Handlebars.compile(tpl, { noEscape: true })(ctx.hbs);\n await fs.writeFile(destPath, rendered, 'utf8');\n continue;\n }\n\n await fs.copyFile(srcPath, destPath);\n }\n}\n\n/**\n * Translate a source filename to its destination filename, or `null` to skip.\n */\nfunction mapDestName(name: string): string | null {\n // Skip the package.json fragment — orchestrator merges it into base.\n if (name === 'package.json.fragment.json') return null;\n\n // _gitignore → .gitignore (npm strips real `.gitignore` from packed tarballs)\n if (name === '_gitignore') return '.gitignore';\n if (name === '_editorconfig') return '.editorconfig';\n if (name === '_npmrc') return '.npmrc';\n\n // Strip .hbs extension\n if (name.endsWith('.hbs')) {\n return name.slice(0, -'.hbs'.length);\n }\n\n return name;\n}\n","import { execa } from 'execa';\n\n/**\n * Initialize a fresh git repo with a single commit. Best-effort:\n * any failure is reported as `{ ok: false }` instead of throwing —\n * scaffolding should not fail just because git is missing.\n */\nexport async function gitInit(\n cwd: string,\n): Promise<{ ok: true } | { ok: false; reason: string }> {\n try {\n await execa('git', ['init', '--quiet'], { cwd });\n await execa('git', ['add', '-A'], { cwd });\n await execa(\n 'git',\n [\n '-c',\n 'user.email=create-teamix-evo@local',\n '-c',\n 'user.name=create-teamix-evo',\n 'commit',\n '--quiet',\n '--no-gpg-sign',\n '-m',\n 'chore: scaffolded by create-teamix-evo',\n ],\n { cwd },\n );\n return { ok: true };\n } catch (err) {\n const reason = err instanceof Error ? err.message : String(err);\n return { ok: false, reason };\n }\n}\n","import fs from 'node:fs/promises';\n\ninterface PackageJsonLike {\n [key: string]: unknown;\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n scripts?: Record<string, string>;\n}\n\nexport interface MergeOptions {\n /** Extra dependencies (e.g. from `runUiAdd`) merged with conflict checks. */\n extraDependencies?: Record<string, string>;\n /** Extra dev dependencies merged with conflict checks. */\n extraDevDependencies?: Record<string, string>;\n}\n\nexport class VersionConflictError extends Error {\n constructor(\n readonly key: 'dependencies' | 'devDependencies' | 'scripts',\n readonly name: string,\n readonly base: string,\n readonly incoming: string,\n ) {\n super(\n `Version conflict in ${key}: ${name} declared as \"${base}\" in base but \"${incoming}\" in overlay/extras.`,\n );\n this.name = 'VersionConflictError';\n }\n}\n\n/**\n * Merge a base package.json with an overlay fragment + extras.\n *\n * - Merges `dependencies`, `devDependencies`, and `scripts` (shallow).\n * - Throws {@link VersionConflictError} on any version mismatch within the same key.\n * - Other top-level keys (`name`, `version`, etc.) come from `base` only.\n */\nexport function mergePackageJson(\n base: PackageJsonLike,\n overlay: PackageJsonLike | null,\n options: MergeOptions = {},\n): PackageJsonLike {\n const merged: PackageJsonLike = { ...base };\n\n for (const key of ['dependencies', 'devDependencies', 'scripts'] as const) {\n const baseMap = (base[key] ?? {}) as Record<string, string>;\n const overlayMap = (overlay?.[key] ?? {}) as Record<string, string>;\n const extrasMap =\n key === 'dependencies'\n ? options.extraDependencies ?? {}\n : key === 'devDependencies'\n ? options.extraDevDependencies ?? {}\n : {};\n\n const result: Record<string, string> = { ...baseMap };\n for (const [name, version] of Object.entries(overlayMap)) {\n if (result[name] !== undefined && result[name] !== version) {\n throw new VersionConflictError(key, name, result[name], version);\n }\n result[name] = version;\n }\n for (const [name, version] of Object.entries(extrasMap)) {\n if (result[name] !== undefined && result[name] !== version) {\n throw new VersionConflictError(key, name, result[name], version);\n }\n result[name] = version;\n }\n if (Object.keys(result).length > 0) {\n merged[key] = sortObjectByKey(result);\n }\n }\n\n return merged;\n}\n\nfunction sortObjectByKey<T extends Record<string, unknown>>(obj: T): T {\n const sorted: Record<string, unknown> = {};\n for (const k of Object.keys(obj).sort()) sorted[k] = obj[k];\n return sorted as T;\n}\n\nexport async function readJson<T = unknown>(filePath: string): Promise<T> {\n const raw = await fs.readFile(filePath, 'utf8');\n return JSON.parse(raw) as T;\n}\n\nexport async function writeJson(\n filePath: string,\n value: unknown,\n): Promise<void> {\n await fs.writeFile(filePath, JSON.stringify(value, null, 2) + '\\n', 'utf8');\n}\n","/**\n * Detect the user's package manager from `npm_config_user_agent`,\n * with fallback heuristics, and produce install / run commands.\n */\n\nexport type PackageManager = 'pnpm' | 'npm' | 'yarn';\n\nexport interface PackageManagerInfo {\n name: PackageManager;\n /** Command + args to install dependencies in cwd. */\n install: string[];\n /** Command + args prefix to run a package script. */\n run: string[];\n /** Human-readable command (`pnpm install`, etc.). */\n installCommand: string;\n /** Human-readable run prefix (`pnpm`, `npm run`, `yarn`). */\n runPrefix: string;\n}\n\n/**\n * Detect the package manager invoking `create-teamix-evo`.\n *\n * Priority:\n * 1. explicit `prefer` argument\n * 2. `npm_config_user_agent` env (set by `pnpm create` / `npm create` / `yarn create`)\n * 3. fallback to `npm`\n */\nexport function detectPackageManager(\n prefer?: PackageManager,\n): PackageManagerInfo {\n const detected = prefer ?? detectFromUserAgent() ?? 'npm';\n return buildInfo(detected);\n}\n\nexport function isValidPackageManager(value: string): value is PackageManager {\n return value === 'pnpm' || value === 'npm' || value === 'yarn';\n}\n\nfunction detectFromUserAgent(): PackageManager | null {\n const ua = process.env.npm_config_user_agent;\n if (!ua) return null;\n const head = ua.split(' ')[0] ?? '';\n if (head.startsWith('pnpm')) return 'pnpm';\n if (head.startsWith('yarn')) return 'yarn';\n if (head.startsWith('npm')) return 'npm';\n return null;\n}\n\nfunction buildInfo(name: PackageManager): PackageManagerInfo {\n switch (name) {\n case 'pnpm':\n return {\n name,\n install: ['install'],\n run: ['run'],\n installCommand: 'pnpm install',\n runPrefix: 'pnpm',\n };\n case 'yarn':\n return {\n name,\n install: ['install'],\n run: [],\n installCommand: 'yarn install',\n runPrefix: 'yarn',\n };\n case 'npm':\n default:\n return {\n name,\n install: ['install'],\n run: ['run'],\n installCommand: 'npm install',\n runPrefix: 'npm run',\n };\n }\n}\n","import type { Preset } from './types.js';\n\nexport const minimalPreset: Preset = {\n id: 'minimal',\n displayName: 'Minimal',\n description:\n 'Vite + React + TS + Tailwind v4,装 design tokens + AI skills,无路由、不装 UI 组件。',\n baseTemplate: 'react-ts',\n design: {\n variant: 'opentrek',\n tailwind: 'v4',\n },\n skills: {\n entries: ['teamix-evo-manage'],\n ides: ['qoder', 'claude'],\n },\n ui: {\n components: [],\n },\n overlay: null,\n};\n","import type { Preset } from './types.js';\n\nexport const consolePreset: Preset = {\n id: 'console',\n displayName: 'Console',\n description:\n '中后台标配:minimal + react-router-dom + ConsoleLayout + Dashboard / List / Detail / Form 四页面 + 真装 Button。',\n baseTemplate: 'react-ts',\n design: {\n variant: 'opentrek',\n tailwind: 'v4',\n },\n skills: {\n entries: ['teamix-evo-manage'],\n ides: ['qoder', 'claude'],\n },\n ui: {\n components: ['button'],\n },\n overlay: 'console',\n};\n","import type { Preset } from './types.js';\nimport { minimalPreset } from './minimal.js';\nimport { consolePreset } from './console.js';\n\nexport type { Preset } from './types.js';\n\nexport const presets: Preset[] = [minimalPreset, consolePreset];\n\nexport function findPreset(id: string): Preset | undefined {\n return presets.find((p) => p.id === id);\n}\n\nexport function listPresetIds(): string[] {\n return presets.map((p) => p.id);\n}\n","import { bold, cyan, dim, green, red, yellow } from 'kolorist';\n\nexport const logger = {\n info(message: string): void {\n console.log(`${cyan('ℹ')} ${message}`);\n },\n success(message: string): void {\n console.log(`${green('✓')} ${message}`);\n },\n warn(message: string): void {\n console.warn(`${yellow('⚠')} ${message}`);\n },\n error(message: string): void {\n console.error(`${red('✗')} ${message}`);\n },\n step(message: string): void {\n console.log(`\\n${bold(cyan('▸'))} ${bold(message)}`);\n },\n detail(message: string): void {\n console.log(` ${dim(message)}`);\n },\n blank(): void {\n console.log('');\n },\n};\n","import * as p from '@clack/prompts';\nimport { presets } from './presets/index.js';\nimport { isValidPackageManager, type PackageManager } from './pm.js';\n\nexport interface InteractiveAnswers {\n presetId: string;\n pm: PackageManager | undefined;\n git: boolean;\n}\n\n/**\n * Run interactive prompts for missing options. The `seed` represents whatever\n * the user already passed via flags; only unanswered fields are prompted for.\n */\nexport async function promptMissing(seed: {\n dir: string;\n presetId?: string;\n pm?: string;\n git?: boolean;\n}): Promise<InteractiveAnswers> {\n p.intro('create-teamix-evo');\n p.note(`📦 Target directory: ${seed.dir}`);\n\n let presetId = seed.presetId;\n if (!presetId) {\n const choice = await p.select({\n message: '选择 preset',\n options: presets.map((preset) => ({\n value: preset.id,\n label: preset.displayName,\n hint: preset.description,\n })),\n initialValue: 'minimal',\n });\n if (p.isCancel(choice)) {\n p.cancel('已取消');\n process.exit(0);\n }\n presetId = choice as string;\n }\n\n let pm: PackageManager | undefined =\n seed.pm && isValidPackageManager(seed.pm) ? seed.pm : undefined;\n if (!pm) {\n const pmChoice = await p.select<string>({\n message: '包管理器',\n options: [\n { value: 'auto', label: '自动检测(推荐)' },\n { value: 'pnpm', label: 'pnpm' },\n { value: 'npm', label: 'npm' },\n { value: 'yarn', label: 'yarn' },\n ],\n initialValue: 'auto',\n });\n if (p.isCancel(pmChoice)) {\n p.cancel('已取消');\n process.exit(0);\n }\n pm =\n pmChoice === 'auto'\n ? undefined\n : isValidPackageManager(pmChoice as string)\n ? (pmChoice as PackageManager)\n : undefined;\n }\n\n let git = seed.git;\n if (git === undefined) {\n const confirmed = await p.confirm({\n message: '初始化 git 仓库?',\n initialValue: true,\n });\n if (p.isCancel(confirmed)) {\n p.cancel('已取消');\n process.exit(0);\n }\n git = confirmed as boolean;\n }\n\n return { presetId, pm, git };\n}\n"],"mappings":"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","names":["path","red","fs","path","execa","fs","p","path","execa","fs","path","red"]}
@@ -0,0 +1,5 @@
1
+ {
2
+ "dependencies": {
3
+ "react-router-dom": "^6.26.0"
4
+ }
5
+ }
@@ -0,0 +1,6 @@
1
+ import { useRoutes } from 'react-router-dom';
2
+ import { routes } from '@/routes';
3
+
4
+ export default function App() {
5
+ return useRoutes(routes);
6
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * @teamix-evo:placeholder
3
+ *
4
+ * Card 占位实现 — console preset 临时填充,用于 Dashboard / 详情 / 表单容器。
5
+ *
6
+ * 升级路径(ui 包补齐 Card 组件后):
7
+ * 1. `npx teamix-evo ui add card`
8
+ * 2. 全项目替换 `@/components/_placeholder/Card` → `@/components/ui/card`,对齐新 props
9
+ * 3. 删除本文件,并从 `_placeholder/` 同步移除 `pending-ui.json` 中的条目
10
+ *
11
+ * 你也可以直接询问 AI:"帮我升级 Card 组件" — `teamix-evo-manage` skill 会读
12
+ * `.teamix-evo/create/pending-ui.json` 给出准确迁移指引。
13
+ */
14
+ import type { ReactNode } from 'react';
15
+
16
+ export interface CardProps {
17
+ title?: ReactNode;
18
+ children?: ReactNode;
19
+ className?: string;
20
+ }
21
+
22
+ export function Card({ title, children, className }: CardProps) {
23
+ return (
24
+ <div
25
+ className={[
26
+ 'rounded-lg border border-border bg-card text-card-foreground shadow-sm p-4',
27
+ className,
28
+ ]
29
+ .filter(Boolean)
30
+ .join(' ')}
31
+ >
32
+ {title ? (
33
+ <div className="text-sm font-medium text-muted-foreground mb-3">
34
+ {title}
35
+ </div>
36
+ ) : null}
37
+ {children}
38
+ </div>
39
+ );
40
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * @teamix-evo:placeholder
3
+ *
4
+ * Form 占位实现 — console preset 临时填充。
5
+ *
6
+ * 升级路径:
7
+ * 1. `npx teamix-evo ui add form`
8
+ * 2. 替换 `@/components/_placeholder/Form` → `@/components/ui/form`,按真组件 API 适配
9
+ * 3. 删除本文件
10
+ *
11
+ * `teamix-evo-manage` skill 会引导自动迁移。
12
+ */
13
+ import type { FormHTMLAttributes, ReactNode } from 'react';
14
+
15
+ export interface FormProps extends FormHTMLAttributes<HTMLFormElement> {
16
+ children?: ReactNode;
17
+ }
18
+
19
+ export function Form({ children, className, ...rest }: FormProps) {
20
+ return (
21
+ <form
22
+ {...rest}
23
+ className={['space-y-4', className].filter(Boolean).join(' ')}
24
+ >
25
+ {children}
26
+ </form>
27
+ );
28
+ }
29
+
30
+ export interface FormItemProps {
31
+ label?: ReactNode;
32
+ required?: boolean;
33
+ hint?: ReactNode;
34
+ children?: ReactNode;
35
+ }
36
+
37
+ function FormItem({ label, required, hint, children }: FormItemProps) {
38
+ return (
39
+ <div className="space-y-1.5">
40
+ {label ? (
41
+ <label className="text-sm font-medium text-foreground">
42
+ {label}
43
+ {required ? <span className="text-destructive ml-0.5">*</span> : null}
44
+ </label>
45
+ ) : null}
46
+ {children}
47
+ {hint ? <p className="text-xs text-muted-foreground">{hint}</p> : null}
48
+ </div>
49
+ );
50
+ }
51
+
52
+ Form.Item = FormItem;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * @teamix-evo:placeholder
3
+ *
4
+ * Input 占位实现 — console preset 临时填充。
5
+ *
6
+ * 升级路径:
7
+ * 1. `npx teamix-evo ui add input`
8
+ * 2. 替换 `@/components/_placeholder/Input` → `@/components/ui/input`
9
+ * 3. 删除本文件
10
+ *
11
+ * `teamix-evo-manage` skill 会引导自动迁移。
12
+ */
13
+ import { forwardRef, type InputHTMLAttributes } from 'react';
14
+
15
+ export type InputProps = InputHTMLAttributes<HTMLInputElement>;
16
+
17
+ export const Input = forwardRef<HTMLInputElement, InputProps>(
18
+ ({ className, ...rest }, ref) => {
19
+ return (
20
+ <input
21
+ ref={ref}
22
+ className={[
23
+ 'h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm transition',
24
+ 'focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-0',
25
+ 'disabled:cursor-not-allowed disabled:opacity-50',
26
+ className,
27
+ ]
28
+ .filter(Boolean)
29
+ .join(' ')}
30
+ {...rest}
31
+ />
32
+ );
33
+ },
34
+ );
35
+ Input.displayName = 'PlaceholderInput';
@@ -0,0 +1,42 @@
1
+ # `_placeholder/` — 过渡组件目录
2
+
3
+ > 本目录由 `create-teamix-evo` 在 console preset 中创建,存放**临时占位实现**,等待 `@teamix-evo/ui` 包补齐对应真组件后逐个迁移。
4
+
5
+ ## 当前占位组件
6
+
7
+ | 文件 | 用途 | 真组件 id |
8
+ | ----------- | ---------------------------- | ----------------- |
9
+ | `Card.tsx` | 容器卡片(Dashboard / 详情) | `card`(待发布) |
10
+ | `Input.tsx` | 表单输入框 | `input`(待发布) |
11
+ | `Form.tsx` | 表单容器 + `Form.Item` | `form`(待发布) |
12
+ | `Table.tsx` | 数据表(含简易列定义) | `table`(待发布) |
13
+
14
+ 每个文件顶部都有 `@teamix-evo:placeholder` 标识符 — `grep` / AI 都可凭此快速发现待迁移项。
15
+
16
+ ## 升级到真组件(三步)
17
+
18
+ ```bash
19
+ # 1. 装真组件(以 card 为例)
20
+ npx teamix-evo ui add card
21
+
22
+ # 2. 全项目替换 import:@/components/_placeholder/Card → @/components/ui/card
23
+ # (API 可能有差异,按需调整 props)
24
+
25
+ # 3. 删除本文件中对应的 .tsx
26
+ ```
27
+
28
+ ## 让 AI 帮你迁移
29
+
30
+ 直接对 AI 说:"帮我把 Card 占位升级到 ui 包真组件"。
31
+ 项目根目录的 `.teamix-evo/create/pending-ui.json` 登记了所有占位组件,`teamix-evo-manage` skill 会读取它并给出准确的迁移指引。
32
+
33
+ ## 何时本目录消失?
34
+
35
+ 当所有占位组件迁移完成后,整个 `_placeholder/` 目录可以删掉。同时记得:
36
+
37
+ - 从 `.teamix-evo/create/pending-ui.json` 移除已迁移条目(或整个文件)
38
+ - 在 `package.json` 里检查不再需要的占位依赖
39
+
40
+ ## 为什么有占位?
41
+
42
+ `@teamix-evo/ui` 当前覆盖率有限,但 console preset 演示页面需要表格 / 表单 / 输入 / 卡片这些通用容器才能跑起来。占位策略让 console 模板**今天就能落地**,又不用等 ui 包完成所有组件 — 一旦 ui 包发布对应组件,迁移成本最低(一条 `ui add` + 改 import + 删文件)。
@@ -0,0 +1,77 @@
1
+ /**
2
+ * @teamix-evo:placeholder
3
+ *
4
+ * Table 占位实现 — console preset 临时填充。
5
+ *
6
+ * 升级路径:
7
+ * 1. `npx teamix-evo ui add table`
8
+ * 2. 替换 `@/components/_placeholder/Table` → `@/components/ui/table`,按真组件 API 适配
9
+ * 3. 删除本文件
10
+ *
11
+ * `teamix-evo-manage` skill 会引导自动迁移。
12
+ */
13
+ import type { ReactNode } from 'react';
14
+
15
+ export interface TableColumn<T> {
16
+ key: string;
17
+ title: ReactNode;
18
+ render?: (row: T) => ReactNode;
19
+ }
20
+
21
+ export interface TableProps<T> {
22
+ columns: TableColumn<T>[];
23
+ data: T[];
24
+ rowKey?: (row: T) => string;
25
+ }
26
+
27
+ export function Table<T>({ columns, data, rowKey }: TableProps<T>) {
28
+ return (
29
+ <div className="rounded-md border border-border overflow-hidden">
30
+ <table className="w-full text-sm">
31
+ <thead className="bg-muted/50">
32
+ <tr>
33
+ {columns.map((col) => (
34
+ <th
35
+ key={col.key}
36
+ className="px-3 py-2 text-left font-medium text-muted-foreground"
37
+ >
38
+ {col.title}
39
+ </th>
40
+ ))}
41
+ </tr>
42
+ </thead>
43
+ <tbody>
44
+ {data.length === 0 ? (
45
+ <tr>
46
+ <td
47
+ colSpan={columns.length}
48
+ className="px-3 py-8 text-center text-muted-foreground"
49
+ >
50
+ 暂无数据
51
+ </td>
52
+ </tr>
53
+ ) : (
54
+ data.map((row, idx) => {
55
+ const key = rowKey
56
+ ? rowKey(row)
57
+ : (row as { id?: string }).id ?? String(idx);
58
+ return (
59
+ <tr key={key} className="border-t border-border">
60
+ {columns.map((col) => (
61
+ <td key={col.key} className="px-3 py-2">
62
+ {col.render
63
+ ? col.render(row)
64
+ : ((row as Record<string, unknown>)[
65
+ col.key
66
+ ] as ReactNode) ?? null}
67
+ </td>
68
+ ))}
69
+ </tr>
70
+ );
71
+ })
72
+ )}
73
+ </tbody>
74
+ </table>
75
+ </div>
76
+ );
77
+ }
@@ -0,0 +1,46 @@
1
+ import { NavLink, Outlet } from 'react-router-dom';
2
+
3
+ const navItems = [
4
+ { to: '/dashboard', label: 'Dashboard' },
5
+ { to: '/users', label: '用户管理' },
6
+ ];
7
+
8
+ export function ConsoleLayout() {
9
+ return (
10
+ <div className="min-h-screen flex bg-background text-foreground">
11
+ <aside className="w-56 border-r border-border bg-card">
12
+ <div className="h-14 px-4 flex items-center text-lg font-semibold border-b border-border">
13
+ Teamix Console
14
+ </div>
15
+ <nav className="p-2 flex flex-col gap-1">
16
+ {navItems.map((item) => (
17
+ <NavLink
18
+ key={item.to}
19
+ to={item.to}
20
+ className={({ isActive }) =>
21
+ [
22
+ 'px-3 py-2 rounded-md text-sm transition',
23
+ isActive
24
+ ? 'bg-primary text-primary-foreground'
25
+ : 'text-muted-foreground hover:bg-muted hover:text-foreground',
26
+ ].join(' ')
27
+ }
28
+ >
29
+ {item.label}
30
+ </NavLink>
31
+ ))}
32
+ </nav>
33
+ </aside>
34
+
35
+ <div className="flex-1 flex flex-col">
36
+ <header className="h-14 px-6 flex items-center justify-between border-b border-border bg-card">
37
+ <h1 className="text-base font-medium">控制台</h1>
38
+ <div className="text-sm text-muted-foreground">admin@example.com</div>
39
+ </header>
40
+ <main className="flex-1 p-6 overflow-auto">
41
+ <Outlet />
42
+ </main>
43
+ </div>
44
+ </div>
45
+ );
46
+ }
@@ -0,0 +1,38 @@
1
+ export interface MockUser {
2
+ id: string;
3
+ name: string;
4
+ email: string;
5
+ role: 'admin' | 'manager' | 'member';
6
+ createdAt: string;
7
+ }
8
+
9
+ export const mockUsers: MockUser[] = [
10
+ {
11
+ id: 'u-001',
12
+ name: '陈昊',
13
+ email: 'haochen@example.com',
14
+ role: 'admin',
15
+ createdAt: '2025-08-12',
16
+ },
17
+ {
18
+ id: 'u-002',
19
+ name: '林微',
20
+ email: 'weilin@example.com',
21
+ role: 'manager',
22
+ createdAt: '2025-09-03',
23
+ },
24
+ {
25
+ id: 'u-003',
26
+ name: '王朗',
27
+ email: 'langwang@example.com',
28
+ role: 'member',
29
+ createdAt: '2025-09-21',
30
+ },
31
+ {
32
+ id: 'u-004',
33
+ name: '苏雨晴',
34
+ email: 'yqsu@example.com',
35
+ role: 'member',
36
+ createdAt: '2025-10-15',
37
+ },
38
+ ];
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import { BrowserRouter } from 'react-router-dom';
4
+ import App from '@/App';
5
+ import '@/index.css';
6
+
7
+ ReactDOM.createRoot(document.getElementById('root')!).render(
8
+ <React.StrictMode>
9
+ <BrowserRouter>
10
+ <App />
11
+ </BrowserRouter>
12
+ </React.StrictMode>,
13
+ );
@@ -0,0 +1,38 @@
1
+ import { Card } from '@/components/_placeholder/Card';
2
+
3
+ const stats = [
4
+ { label: '用户总数', value: '1,284', delta: '+12% 本周' },
5
+ { label: '活跃用户', value: '342', delta: '+5% 本周' },
6
+ { label: '订单数', value: '98', delta: '-3% 本周' },
7
+ { label: 'GMV', value: '¥42,810', delta: '+8% 本周' },
8
+ ];
9
+
10
+ export function DashboardPage() {
11
+ return (
12
+ <div className="space-y-6">
13
+ <div>
14
+ <h2 className="text-2xl font-semibold tracking-tight">Dashboard</h2>
15
+ <p className="text-sm text-muted-foreground mt-1">
16
+ 这是 console preset 的概览页 — 4 张统计卡 + 占位区,作为 token
17
+ 通路与导航验证。
18
+ </p>
19
+ </div>
20
+
21
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
22
+ {stats.map((s) => (
23
+ <Card key={s.label} title={s.label}>
24
+ <div className="text-2xl font-bold">{s.value}</div>
25
+ <div className="text-xs text-muted-foreground mt-1">{s.delta}</div>
26
+ </Card>
27
+ ))}
28
+ </div>
29
+
30
+ <Card title="近期活动">
31
+ <p className="text-sm text-muted-foreground">
32
+ 此处占位 — 后续可装 <code>chart</code> / <code>activity-feed</code>{' '}
33
+ 等组件。
34
+ </p>
35
+ </Card>
36
+ </div>
37
+ );
38
+ }
@@ -0,0 +1,57 @@
1
+ import { Link, useParams } from 'react-router-dom';
2
+ import { Card } from '@/components/_placeholder/Card';
3
+ import { Button } from '@/components/ui/button';
4
+ import { mockUsers } from '@/lib/mock-data';
5
+
6
+ export function UserDetailPage() {
7
+ const { id } = useParams<{ id: string }>();
8
+ const user = mockUsers.find((u) => u.id === id);
9
+
10
+ if (!user) {
11
+ return (
12
+ <Card title="用户不存在">
13
+ <p className="text-sm text-muted-foreground">id: {id}</p>
14
+ <Link to="/users" className="inline-block mt-3">
15
+ <Button variant="outline">返回列表</Button>
16
+ </Link>
17
+ </Card>
18
+ );
19
+ }
20
+
21
+ return (
22
+ <div className="space-y-6">
23
+ <div className="flex items-center justify-between">
24
+ <h2 className="text-2xl font-semibold tracking-tight">用户详情</h2>
25
+ <div className="flex gap-2">
26
+ <Link to="/users">
27
+ <Button variant="outline">返回</Button>
28
+ </Link>
29
+ <Link to={`/users/${user.id}/edit`}>
30
+ <Button>编辑</Button>
31
+ </Link>
32
+ </div>
33
+ </div>
34
+
35
+ <Card title="基本信息">
36
+ <dl className="grid grid-cols-2 gap-4 text-sm">
37
+ <div>
38
+ <dt className="text-muted-foreground">姓名</dt>
39
+ <dd className="mt-1 font-medium">{user.name}</dd>
40
+ </div>
41
+ <div>
42
+ <dt className="text-muted-foreground">邮箱</dt>
43
+ <dd className="mt-1 font-medium">{user.email}</dd>
44
+ </div>
45
+ <div>
46
+ <dt className="text-muted-foreground">角色</dt>
47
+ <dd className="mt-1 font-medium">{user.role}</dd>
48
+ </div>
49
+ <div>
50
+ <dt className="text-muted-foreground">注册时间</dt>
51
+ <dd className="mt-1 font-medium">{user.createdAt}</dd>
52
+ </div>
53
+ </dl>
54
+ </Card>
55
+ </div>
56
+ );
57
+ }
@@ -0,0 +1,61 @@
1
+ import { useState } from 'react';
2
+ import { Link, useNavigate, useParams } from 'react-router-dom';
3
+ import { Card } from '@/components/_placeholder/Card';
4
+ import { Form } from '@/components/_placeholder/Form';
5
+ import { Input } from '@/components/_placeholder/Input';
6
+ import { Button } from '@/components/ui/button';
7
+ import { mockUsers } from '@/lib/mock-data';
8
+
9
+ export function UserFormPage() {
10
+ const { id } = useParams<{ id: string }>();
11
+ const navigate = useNavigate();
12
+ const isEdit = Boolean(id);
13
+ const existing = isEdit ? mockUsers.find((u) => u.id === id) : undefined;
14
+
15
+ const [name, setName] = useState<string>(existing?.name ?? '');
16
+ const [email, setEmail] = useState<string>(existing?.email ?? '');
17
+ const [role, setRole] = useState<string>(existing?.role ?? 'member');
18
+
19
+ const handleSubmit = (e: React.FormEvent) => {
20
+ e.preventDefault();
21
+ // 真实业务请替换为 API 调用
22
+ // eslint-disable-next-line no-console
23
+ console.info('[demo] submit', { name, email, role });
24
+ navigate('/users');
25
+ };
26
+
27
+ return (
28
+ <div className="space-y-6 max-w-2xl">
29
+ <h2 className="text-2xl font-semibold tracking-tight">
30
+ {isEdit ? '编辑用户' : '新建用户'}
31
+ </h2>
32
+
33
+ <Card>
34
+ <Form onSubmit={handleSubmit}>
35
+ <Form.Item label="姓名" required>
36
+ <Input value={name} onChange={(e) => setName(e.target.value)} />
37
+ </Form.Item>
38
+ <Form.Item label="邮箱" required>
39
+ <Input
40
+ type="email"
41
+ value={email}
42
+ onChange={(e) => setEmail(e.target.value)}
43
+ />
44
+ </Form.Item>
45
+ <Form.Item label="角色">
46
+ <Input value={role} onChange={(e) => setRole(e.target.value)} />
47
+ </Form.Item>
48
+
49
+ <div className="flex gap-2 pt-2">
50
+ <Button type="submit">{isEdit ? '保存' : '创建'}</Button>
51
+ <Link to="/users">
52
+ <Button type="button" variant="outline">
53
+ 取消
54
+ </Button>
55
+ </Link>
56
+ </div>
57
+ </Form>
58
+ </Card>
59
+ </div>
60
+ );
61
+ }