diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cf02802 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 AnxForever + +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. diff --git a/README.md b/README.md index 2438c70..a498189 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,69 @@ -# stylekit-style-prompts +# @anxforever/stylekit-style-prompts-skill -Standalone skill for generating high-quality frontend prompts from StyleKit styles. +StyleKit 风格提示词 Skill(独立仓库版)。 +目标:把 StyleKit 的风格能力直接安装到 Codex / Claude 的 skills 目录中。 -## Quick start +## 1) 一键安装(面向使用者) + +### 安装到 Codex + +```bash +npx @anxforever/stylekit-style-prompts-skill install --tool codex +``` + +### 安装到 Claude + +```bash +npx @anxforever/stylekit-style-prompts-skill install --tool claude +``` + +### 自动检测本机工具并安装 + +```bash +npx @anxforever/stylekit-style-prompts-skill install --tool auto +``` + +### 覆盖安装(已存在时) + +```bash +npx @anxforever/stylekit-style-prompts-skill install --tool codex --force +``` + +### 卸载 + +```bash +npx @anxforever/stylekit-style-prompts-skill uninstall --tool codex +npx @anxforever/stylekit-style-prompts-skill uninstall --tool claude +``` + +### 环境自检 ```bash +npx @anxforever/stylekit-style-prompts-skill doctor +``` + +## 2) 本地开发验证(维护者) + +```bash +node bin/stylekit-style-prompts-skill.js doctor +node bin/stylekit-style-prompts-skill.js install --tool codex --target /tmp/stylekit-skill-test --force +node bin/stylekit-style-prompts-skill.js uninstall --target /tmp/stylekit-skill-test python3 scripts/smoke_test.py python3 scripts/run_pipeline.py --query "高端科技SaaS财务后台,玻璃质感,强调可读性" --stack nextjs --format json ``` -## Regression gate +## 3) 回归门禁 ```bash bash scripts/ci_regression_gate.sh --baseline references/benchmark-baseline.json --snapshot-out tmp/benchmark-ci-latest.json ``` + +## 4) 发布到 npm(让所有人可用) + +```bash +npm login +npm whoami +npm publish --access public +``` + +发布后,任何人都可以通过 `npx @anxforever/stylekit-style-prompts-skill ...` 直接安装。 diff --git a/bin/stylekit-style-prompts-skill.js b/bin/stylekit-style-prompts-skill.js new file mode 100644 index 0000000..f8e8301 --- /dev/null +++ b/bin/stylekit-style-prompts-skill.js @@ -0,0 +1,271 @@ +#!/usr/bin/env node + +"use strict"; + +const fs = require("fs"); +const os = require("os"); +const path = require("path"); + +const PACKAGE_NAME = "@anxforever/stylekit-style-prompts-skill"; +const SKILL_SLUG = "stylekit-style-prompts"; +const ROOT_DIR = path.resolve(__dirname, ".."); +const PAYLOAD_ITEMS = [ + "SKILL.md", + "LICENSE", + "agents", + "references", + "scripts", + "README.md", + "RELEASE.md", +]; + +const DEFAULT_TARGETS = { + codex: path.join(os.homedir(), ".codex", "skills", SKILL_SLUG), + claude: path.join(os.homedir(), ".claude", "skills", SKILL_SLUG), +}; + +function printHelp() { + const text = ` +${PACKAGE_NAME} + +Usage: + stylekit-style-prompts-skill [options] + +Commands: + install Install the skill payload + uninstall Remove installed skill directory + doctor Check local environment and installation status + help Show this message + +Options: + --tool Target tool. Default: auto + --target Custom install path + --force Overwrite existing target directory + --dry-run Print planned actions without file changes + -h, --help Show help + +Examples: + npx ${PACKAGE_NAME} install --tool codex + npx ${PACKAGE_NAME} install --tool claude + npx ${PACKAGE_NAME} install --tool auto + npx ${PACKAGE_NAME} install --target ~/.codex/skills/${SKILL_SLUG} --force + npx ${PACKAGE_NAME} doctor +`; + process.stdout.write(text.trimStart() + "\n"); +} + +function expandHome(inputPath) { + if (!inputPath) return inputPath; + if (inputPath === "~") return os.homedir(); + if (inputPath.startsWith("~/") || inputPath.startsWith("~\\")) { + return path.join(os.homedir(), inputPath.slice(2)); + } + return inputPath; +} + +function parseArgs(argv) { + let command = "help"; + let index = 0; + if (argv.length > 0 && !argv[0].startsWith("-")) { + command = argv[0]; + index = 1; + } + + const options = { + tool: "auto", + target: null, + force: false, + dryRun: false, + }; + + for (let i = index; i < argv.length; i += 1) { + const arg = argv[i]; + if (arg === "-h" || arg === "--help") { + command = "help"; + continue; + } + if (arg === "--force") { + options.force = true; + continue; + } + if (arg === "--dry-run") { + options.dryRun = true; + continue; + } + if (arg === "--tool") { + const value = argv[i + 1]; + if (!value) throw new Error("Missing value for --tool."); + options.tool = value; + i += 1; + continue; + } + if (arg.startsWith("--tool=")) { + options.tool = arg.slice("--tool=".length); + continue; + } + if (arg === "--target") { + const value = argv[i + 1]; + if (!value) throw new Error("Missing value for --target."); + options.target = value; + i += 1; + continue; + } + if (arg.startsWith("--target=")) { + options.target = arg.slice("--target=".length); + continue; + } + throw new Error(`Unknown argument: ${arg}`); + } + + return { command, options }; +} + +function detectAutoTools() { + const tools = []; + if (fs.existsSync(path.join(os.homedir(), ".codex"))) tools.push("codex"); + if (fs.existsSync(path.join(os.homedir(), ".claude"))) tools.push("claude"); + if (tools.length === 0) tools.push("codex"); + return tools; +} + +function resolveTargets(tool, customTarget) { + const normalizedTool = (tool || "auto").toLowerCase(); + if (customTarget) { + return [ + { + tool: normalizedTool === "auto" ? "custom" : normalizedTool, + targetPath: path.resolve(expandHome(customTarget)), + }, + ]; + } + if (normalizedTool === "auto") { + return detectAutoTools().map((t) => ({ tool: t, targetPath: DEFAULT_TARGETS[t] })); + } + if (!Object.prototype.hasOwnProperty.call(DEFAULT_TARGETS, normalizedTool)) { + throw new Error(`Invalid --tool value: ${tool}. Use codex, claude, or auto.`); + } + return [{ tool: normalizedTool, targetPath: DEFAULT_TARGETS[normalizedTool] }]; +} + +function ensurePayloadAvailable() { + const missing = []; + for (const item of PAYLOAD_ITEMS) { + const source = path.join(ROOT_DIR, item); + if (!fs.existsSync(source)) { + missing.push(item); + } + } + if (missing.length > 0) { + throw new Error(`Payload is incomplete. Missing: ${missing.join(", ")}`); + } +} + +function copyPayload(targetPath, dryRun) { + if (dryRun) { + process.stdout.write(`[dry-run] mkdir -p ${targetPath}\n`); + } else { + fs.mkdirSync(targetPath, { recursive: true }); + } + + for (const item of PAYLOAD_ITEMS) { + const source = path.join(ROOT_DIR, item); + const destination = path.join(targetPath, item); + if (dryRun) { + process.stdout.write(`[dry-run] copy ${source} -> ${destination}\n`); + } else { + fs.cpSync(source, destination, { recursive: true }); + } + } +} + +function install(options) { + ensurePayloadAvailable(); + const targets = resolveTargets(options.tool, options.target); + for (const entry of targets) { + const { tool, targetPath } = entry; + const exists = fs.existsSync(targetPath); + if (exists && !options.force) { + throw new Error( + `Target already exists (${targetPath}). Re-run with --force to overwrite.` + ); + } + if (exists) { + if (options.dryRun) { + process.stdout.write(`[dry-run] rm -rf ${targetPath}\n`); + } else { + fs.rmSync(targetPath, { recursive: true, force: true }); + } + } + copyPayload(targetPath, options.dryRun); + process.stdout.write( + `${options.dryRun ? "[dry-run] " : ""}Installed for ${tool} -> ${targetPath}\n` + ); + } +} + +function uninstall(options) { + const targets = resolveTargets(options.tool, options.target); + for (const entry of targets) { + const { tool, targetPath } = entry; + if (!fs.existsSync(targetPath)) { + process.stdout.write(`Skip ${tool}: not found at ${targetPath}\n`); + continue; + } + if (options.dryRun) { + process.stdout.write(`[dry-run] rm -rf ${targetPath}\n`); + continue; + } + fs.rmSync(targetPath, { recursive: true, force: true }); + process.stdout.write(`Removed ${tool} installation -> ${targetPath}\n`); + } +} + +function doctor() { + const nodeMajor = Number.parseInt(process.versions.node.split(".")[0], 10); + const nodeOk = Number.isFinite(nodeMajor) && nodeMajor >= 18; + process.stdout.write(`Node version: ${process.versions.node} (${nodeOk ? "ok" : "fail"})\n`); + if (!nodeOk) { + process.stdout.write("Requirement: Node >= 18\n"); + } + + const missingPayload = PAYLOAD_ITEMS.filter( + (item) => !fs.existsSync(path.join(ROOT_DIR, item)) + ); + process.stdout.write( + `Payload check: ${missingPayload.length === 0 ? "ok" : `missing ${missingPayload.join(", ")}`}\n` + ); + + for (const [tool, targetPath] of Object.entries(DEFAULT_TARGETS)) { + const installed = fs.existsSync(path.join(targetPath, "SKILL.md")); + process.stdout.write(`${tool} target: ${targetPath} (${installed ? "installed" : "not installed"})\n`); + } + + return nodeOk && missingPayload.length === 0 ? 0 : 1; +} + +function main() { + const { command, options } = parseArgs(process.argv.slice(2)); + switch (command) { + case "install": + install(options); + return 0; + case "uninstall": + uninstall(options); + return 0; + case "doctor": + return doctor(); + case "help": + printHelp(); + return 0; + default: + throw new Error(`Unknown command: ${command}`); + } +} + +try { + const code = main(); + process.exitCode = code; +} catch (error) { + process.stderr.write(`[${PACKAGE_NAME}] ${error.message}\n`); + process.exitCode = 1; +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..1db07ca --- /dev/null +++ b/package.json @@ -0,0 +1,45 @@ +{ + "name": "@anxforever/stylekit-style-prompts-skill", + "version": "0.1.0", + "description": "Standalone StyleKit style-prompts skill installer for Codex and Claude.", + "license": "MIT", + "type": "commonjs", + "bin": { + "stylekit-style-prompts-skill": "bin/stylekit-style-prompts-skill.js" + }, + "files": [ + "SKILL.md", + "README.md", + "RELEASE.md", + "LICENSE", + "agents/*.yaml", + "references/*.md", + "references/*.json", + "references/*.yml", + "scripts/*.py", + "scripts/*.sh", + "bin/stylekit-style-prompts-skill.js" + ], + "keywords": [ + "skill", + "frontend", + "stylekit", + "codex", + "claude", + "prompt-engineering" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/AnxForever/stylekit-style-prompts-skill.git" + }, + "bugs": { + "url": "https://github.com/AnxForever/stylekit-style-prompts-skill/issues" + }, + "homepage": "https://github.com/AnxForever/stylekit-style-prompts-skill#readme", + "engines": { + "node": ">=18" + }, + "publishConfig": { + "access": "public" + } +}