create-napukettoqq 0.2.7 → 0.2.9
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.
- package/dist/index.mjs +42 -22
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -241,9 +241,13 @@ async function askStartNow(yes) {
|
|
|
241
241
|
}
|
|
242
242
|
return ok;
|
|
243
243
|
}
|
|
244
|
+
/** 空白/引号判定。 */
|
|
245
|
+
const SHELL_WHITESPACE_RE = /[\s"]/;
|
|
246
|
+
/** 内部引号转义。 */
|
|
247
|
+
const QUOTE_RE = /"/g;
|
|
244
248
|
/** 拼命令行时最小转义:含空白/引号则加双引号包裹(参数均为内部字面量,仅兜底)。 */
|
|
245
249
|
function quoteShellArg(arg) {
|
|
246
|
-
return
|
|
250
|
+
return SHELL_WHITESPACE_RE.test(arg) ? `"${arg.replace(QUOTE_RE, "\\\"")}"` : arg;
|
|
247
251
|
}
|
|
248
252
|
/** 前台执行命令(stdio 继承,用户可见进度/交互),返回退出码。 */
|
|
249
253
|
function runInteractive(bin, args, cwd) {
|
|
@@ -259,6 +263,39 @@ function runInteractive(bin, args, cwd) {
|
|
|
259
263
|
});
|
|
260
264
|
});
|
|
261
265
|
}
|
|
266
|
+
/** 目标目录准备:非空需确认清空(-f 跳过确认;-y 不隐式清空)。返回 null = 已取消。 */
|
|
267
|
+
async function prepareTargetDir(dirName, opts) {
|
|
268
|
+
const targetDir = resolveTargetDir(dirName);
|
|
269
|
+
let overwrite = false;
|
|
270
|
+
if (await checkDirStatus(targetDir) === "nonempty") {
|
|
271
|
+
if (!opts.forced) {
|
|
272
|
+
log.warn(`目标目录 "${dirName}" 已存在且非空。`);
|
|
273
|
+
overwrite = await confirmRemove(dirName);
|
|
274
|
+
if (!overwrite) {
|
|
275
|
+
outro("已取消,未做任何修改。");
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
} else overwrite = true;
|
|
279
|
+
}
|
|
280
|
+
return {
|
|
281
|
+
targetDir,
|
|
282
|
+
overwrite
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
/** 安装依赖(失败提示手动安装路径)。返回是否继续。 */
|
|
286
|
+
async function installDeps(pm, result) {
|
|
287
|
+
const color = brandColor(pm);
|
|
288
|
+
log.info("前置要求:本机已安装 QQ NT(wrapper.node 来自 QQ 安装目录)。");
|
|
289
|
+
log.info(`正在安装依赖(${color(pm)} install)...`);
|
|
290
|
+
const installCode = await runInteractive(pmBin(pm), ["install"], result.dir);
|
|
291
|
+
if (installCode === 0) return true;
|
|
292
|
+
log.error(`依赖安装失败(退出码 ${installCode})。请手动执行:`);
|
|
293
|
+
log.step(`cd ${result.dir}`);
|
|
294
|
+
log.step(color(`${pm} install`));
|
|
295
|
+
outro("创建失败,请手动安装后重试。");
|
|
296
|
+
process.exitCode = 1;
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
262
299
|
async function main() {
|
|
263
300
|
const opts = parseArgs(process.argv.slice(2));
|
|
264
301
|
if (opts.help) {
|
|
@@ -269,33 +306,16 @@ async function main() {
|
|
|
269
306
|
const brand = brandColor(pm);
|
|
270
307
|
intro(brand(`create-napukettoqq v${readVersion()}`));
|
|
271
308
|
const dirName = opts.name ?? await askProjectName(opts.yes);
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
log.warn(`目标目录 "${dirName}" 已存在且非空。`);
|
|
276
|
-
overwrite = await confirmRemove(dirName);
|
|
277
|
-
if (!overwrite) {
|
|
278
|
-
outro("已取消,未做任何修改。");
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
} else overwrite = true;
|
|
309
|
+
const prepared = await prepareTargetDir(dirName, opts);
|
|
310
|
+
if (prepared === null) return;
|
|
311
|
+
const { targetDir, overwrite } = prepared;
|
|
282
312
|
const s = spinner();
|
|
283
313
|
s.start(`正在生成项目骨架 ${dirName} ...`);
|
|
284
314
|
const result = await scaffoldProject(dirName, overwrite);
|
|
285
315
|
s.stop(`项目骨架已生成:${dirName}`);
|
|
286
316
|
log.message(renderTree(dirName, brand));
|
|
287
317
|
log.success(`位置:${result.dir}`);
|
|
288
|
-
|
|
289
|
-
log.info(`正在安装依赖(${brand(pm)} install)...`);
|
|
290
|
-
const installCode = await runInteractive(pmBin(pm), ["install"], result.dir);
|
|
291
|
-
if (installCode !== 0) {
|
|
292
|
-
log.error(`依赖安装失败(退出码 ${installCode})。请手动执行:`);
|
|
293
|
-
log.step(`cd ${result.dir}`);
|
|
294
|
-
log.step(brand(`${pm} install`));
|
|
295
|
-
outro("创建失败,请手动安装后重试。");
|
|
296
|
-
process.exitCode = 1;
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
318
|
+
if (!await installDeps(pm, result)) return;
|
|
299
319
|
if (await askStartNow(opts.yes)) {
|
|
300
320
|
log.info("正在启动 NapukettoQQ(Ctrl+C 停止)...\n");
|
|
301
321
|
const startCode = await runInteractive(pmBin(pm), ["start"], result.dir);
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/scaffold.ts","../src/index.ts"],"sourcesContent":["/**\n * create-napukettoqq 脚手架核心:模板渲染 + 目标目录创建 + 文件写入。\n *\n * 模板为独立文件(templates/ 目录,运行时读取),不硬编码在源码里;\n * 需要插值的占位符用 {{key}} 标记(见 renderTemplate)。\n * 依赖极轻(node:fs/promises / node:os / node:path / node:process)。\n * 生成的用户项目依赖发布版的 @napuketto/cli,用户 install 后经 cli → loader\n * 自建宿主启动。\n */\n\nimport { readFileSync } from \"node:fs\";\nimport { mkdir, readdir, readFile, rm, writeFile } from \"node:fs/promises\";\n\nimport path from \"node:path\";\nimport process from \"node:process\";\n\n/** 模板目录(相对本文件:src 与 dist 都在包根下,../templates 均指向包根/templates)。 */\nconst TEMPLATES = new URL(\"../templates/\", import.meta.url);\n\n/**\n * 生成的用户项目所依赖的 @napuketto/cli 版本范围。\n * 运行时从脚手架自身 package.json 读取(dependencies[\"@napuketto/cli\"])——\n * 发版时 changesets 升版本 + pnpm publish 把 workspace:* 替换为实际版本,\n * 模板版本自动跟随,无需手动同步(比硬编码常量可靠)。\n */\nfunction cliVersionRange(): string {\n try {\n const ownPkg = JSON.parse(\n readFileSync(new URL(\"../package.json\", import.meta.url), \"utf8\"),\n ) as { dependencies?: Record<string, string> };\n const range = ownPkg.dependencies?.[\"@napuketto/cli\"];\n // workspace:* 仅在本地 monorepo 直跑脚手架时出现(发布后 pnpm 替换为实际版本),\n // 此时兜底默认范围,避免生成的用户项目 install 失败\n if (range !== undefined && range !== \"workspace:*\") {\n return range;\n }\n } catch {\n // package.json 缺失(异常环境)→ 兜底默认范围\n }\n return \"^0.0.1\";\n}\n\n/** 默认部署文件夹名(用户交互回车缺省值)。 */\nexport const DEFAULT_PROJECT_NAME = \"NapukettoQQ\";\n\n/** 用户可用的包管理器(npm_config_user_agent 检测)。 */\nexport type PackageManager = \"pnpm\" | \"yarn\" | \"npm\";\n\n/**\n * 由 npm_config_user_agent 检测调用方包管理器(yarn create / npm create /\n * pnpm create 都会注入该变量)。兜底 pnpm(项目生态,或直接 node 运行)。\n */\nexport function detectPackageManager(): PackageManager {\n // noPropertyAccessFromIndexSignature:process.env 是索引签名,必须括号访问\n const ua = process.env[\"npm_config_user_agent\"] ?? \"\";\n if (ua.startsWith(\"yarn/\")) {\n return \"yarn\";\n }\n if (ua.startsWith(\"npm/\")) {\n return \"npm\";\n }\n return \"pnpm\";\n}\n\n/** Windows 下包管理器 bin 需带 .cmd 后缀(CreateProcess 不认无扩展名 shim)。 */\nexport function pmBin(pm: PackageManager): string {\n return process.platform === \"win32\" ? `${pm}.cmd` : pm;\n}\n\n/** Windows 路径非法字符(项目文件夹名校验用)。 */\nconst ILLEGAL_NAME = /[\\\\/:*?\"<>|]/;\n\n/** 脚手架生成结果。 */\nexport interface ScaffoldResult {\n /** 目标目录绝对路径。 */\n dir: string;\n /** 派生出的 npm 包名(小写、空格 → -)。 */\n packageName: string;\n}\n\n/** 校验文件夹名:非空、非 . / ..、不含 Windows 非法字符。 */\nexport function validateProjectName(raw: string): string {\n const name = raw.trim();\n if (name === \"\" || name === \".\" || name === \"..\") {\n throw new Error(\"文件夹名不能为空或为 . / ..\");\n }\n if (ILLEGAL_NAME.test(name)) {\n throw new Error(`文件夹名含非法字符:${name}(不允许 \\\\ / : * ? \" < > |)`);\n }\n return name;\n}\n\n/** 由文件夹名派生 npm 包名(小写、空格 → -)。 */\nfunction derivePackageName(dirName: string): string {\n return dirName.toLowerCase().replace(/\\s+/g, \"-\");\n}\n\n/** 解析目标目录绝对路径(校验名称后,基于 cwd)。 */\nexport function resolveTargetDir(dirName: string): string {\n return path.resolve(process.cwd(), validateProjectName(dirName));\n}\n\n/** 目标目录状态:不存在 / 空 / 非空。 */\nexport type DirStatus = \"missing\" | \"empty\" | \"nonempty\";\n\n/** 检测目标目录状态(供交互层决定是否询问清空)。 */\nexport async function checkDirStatus(dir: string): Promise<DirStatus> {\n let entries: string[] = [];\n try {\n entries = await readdir(dir);\n } catch {\n return \"missing\";\n }\n return entries.length === 0 ? \"empty\" : \"nonempty\";\n}\n\n/** 清空目录内容(保留目录本身)。 */\nasync function emptyDir(dir: string): Promise<void> {\n for (const entry of await readdir(dir)) {\n await rm(path.join(dir, entry), { recursive: true, force: true });\n }\n}\n\n/** 读取模板文件(templates/ 目录,UTF-8)。 */\nfunction readTemplate(file: string): Promise<string> {\n return readFile(new URL(file, TEMPLATES), \"utf8\");\n}\n\n/**\n * 占位符渲染:把 {{key}} 替换为 vars[key]。\n * 模板中出现未提供的占位符 → 抛错(防漏插值写出半成品)。\n */\nfunction renderTemplate(content: string, vars: Record<string, string>): string {\n return content.replace(/\\{\\{(\\w+)\\}\\}/g, (match, key: string) => {\n const value = vars[key];\n if (value === undefined) {\n throw new Error(`模板占位符未提供:{{${key}}}(模板 ${match} 无法渲染)`);\n }\n return value;\n });\n}\n\n/**\n * 生成 NapukettoQQ 项目骨架到当前目录下的 <dirName>/。\n * 目标目录已存在且非空:overwrite=false → 抛错;overwrite=true → 清空后生成。\n * 模板与写入目标一一对应(同文件名)。\n */\nexport async function scaffoldProject(dirName: string, overwrite = false): Promise<ScaffoldResult> {\n const name = validateProjectName(dirName);\n const packageName = derivePackageName(name);\n const dir = resolveTargetDir(name);\n\n const status = await checkDirStatus(dir);\n if (status === \"nonempty\" && !overwrite) {\n throw new Error(`目录已存在且非空:${dir}\\n请换一个文件夹名,或先清空该目录。`);\n }\n if (status === \"nonempty\") {\n await emptyDir(dir);\n }\n\n // 渲染变量(napuketto.toml 模板不再插值 dataDir——缺省 = 项目根/.napuketto,跨平台)\n const vars: Record<string, string> = {\n packageName,\n cliVersion: cliVersionRange(),\n };\n // 用户项目是「运行壳」:不开发代码,只需 package.json(依赖 + start)+ napuketto.toml(配置)\n // 模板文件为 templates/<name>.tmpl(.tmpl 后缀避免 TOML/JSON 语言服务把占位符当非法语法)\n const files = [\"package.json\", \"napuketto.toml\"];\n\n await mkdir(dir, { recursive: true });\n for (const file of files) {\n const rendered = renderTemplate(await readTemplate(`${file}.tmpl`), vars);\n await writeFile(path.join(dir, file), rendered, \"utf8\");\n }\n\n return { dir, packageName };\n}\n","#!/usr/bin/env node\n\n/**\n * create-napukettoqq 入口(2026-08-07 美化 v3,交互层用 @clack/prompts——\n * 现代脚手架交互标准,create-vue / create-turbo 同款)。\n *\n * 用法:\n * create-napukettoqq # intro + 交互问部署文件夹名(默认 NapukettoQQ)\n * create-napukettoqq my-bot # 位置参数指定文件夹名,跳过命名交互\n * create-napukettoqq my-bot -f # 目标目录非空时强制清空覆盖\n * create-napukettoqq -y # 全默认(命名用默认值、启动询问用默认 Y)\n * create-napukettoqq -h # 打印帮助\n *\n * 流程:intro → 问文件夹名 → 目标目录准备(非空需确认清空)→ spinner 生成骨架\n * (package.json / napuketto.toml / readme.md / .gitignore)→ 用调用方包管理器\n * 自动 install → 询问是否现在启动(默认 Y)→ 前台运行 → outro 收尾。\n */\n\nimport { spawn } from \"node:child_process\";\nimport { readFileSync } from \"node:fs\";\nimport { relative } from \"node:path\";\nimport process from \"node:process\";\nimport { cancel, confirm, intro, isCancel, log, outro, spinner, text } from \"@clack/prompts\";\nimport pc from \"picocolors\";\nimport type { PackageManager } from \"./scaffold.js\";\nimport {\n checkDirStatus,\n DEFAULT_PROJECT_NAME,\n detectPackageManager,\n pmBin,\n resolveTargetDir,\n scaffoldProject,\n validateProjectName,\n} from \"./scaffold.js\";\n\n// picocolors 是 CJS 包(无 ESM 命名导出),Node ESM 下需 default import 再解构\nconst { blue, bold, cyan, dim, red, yellow } = pc;\n\n/** CLI 参数解析结果。 */\ninterface CliOptions {\n /** 位置参数:部署文件夹名(可选)。 */\n name?: string;\n /** -f/--forced:目标目录非空时强制清空覆盖。 */\n forced: boolean;\n /** -y/--yes:跳过所有交互提示(使用默认值)。 */\n yes: boolean;\n /** -h/--help:显示帮助。 */\n help: boolean;\n}\n\n/** 帮助文本(-h/--help)。 */\nconst HELP = ` 用法: create-napukettoqq [name] [选项]\n\n 选项:\n -f, --forced 目标目录非空时强制清空并覆盖\n -y, --yes 跳过所有交互提示(使用默认值)\n -h, --help 显示本帮助\n`;\n\n/** 解析命令行参数(flag 仅三个,手写保持自包含,不引 yargs-parser)。 */\nfunction parseArgs(argv: string[]): CliOptions {\n const opts: CliOptions = { forced: false, yes: false, help: false };\n for (const arg of argv) {\n if (arg === \"-f\" || arg === \"--forced\") {\n opts.forced = true;\n } else if (arg === \"-y\" || arg === \"--yes\") {\n opts.yes = true;\n } else if (arg === \"-h\" || arg === \"--help\") {\n opts.help = true;\n } else if (arg.startsWith(\"-\")) {\n throw new Error(`未知参数:${arg}`);\n } else if (opts.name === undefined) {\n opts.name = arg;\n } else {\n throw new Error(`多余的位置参数:${arg}`);\n }\n }\n return opts;\n}\n\n/** 读取脚手架自身版本号(intro 显示;dist 场景 import.meta.url 指向包内)。 */\nfunction readVersion(): string {\n try {\n const ownPkg = JSON.parse(\n readFileSync(new URL(\"../package.json\", import.meta.url), \"utf8\"),\n ) as { version?: string };\n return ownPkg.version ?? \"0.0.0\";\n } catch {\n // package.json 缺失(异常环境)→ 兜底\n return \"0.0.0\";\n }\n}\n\n/** 交互询问部署文件夹名(回车取默认值;-y 直接返回默认值)。 */\nasync function askProjectName(yes: boolean): Promise<string> {\n if (yes) {\n return DEFAULT_PROJECT_NAME;\n }\n // placeholder = 淡灰默认名:直接回车 = 用默认名,键入任意字符 = 从空自定义。\n // 不设 initialValue——否则 clack 渲染的是已输入态(蓝色、光标在末尾),\n // 淡灰 placeholder 不显示(对齐 koishi create 的观感)。\n const name = await text({\n message: \"部署文件夹名\",\n placeholder: DEFAULT_PROJECT_NAME,\n validate: (input: string | undefined) => {\n // 空输入 = 使用默认名,不报错\n if (input === undefined || input.trim() === \"\") {\n return undefined;\n }\n try {\n validateProjectName(input);\n return undefined;\n } catch (err) {\n return err instanceof Error ? err.message : String(err);\n }\n },\n });\n if (isCancel(name)) {\n cancel(\"操作已取消\");\n process.exit(0);\n }\n const trimmed = name.trim();\n return trimmed === \"\" ? DEFAULT_PROJECT_NAME : validateProjectName(trimmed);\n}\n\n/**\n * 包管理器品牌色(美化 v4,对齐 koishi create 观感)。\n * 16 色近似 + bold:兼容所有终端(不依赖 truecolor),Windows Terminal /\n * 老 conhost 都能正常显示。只做「标题 + 命令」两层,不做三套 UI。\n */\nfunction brandColor(pm: PackageManager): (text: string) => string {\n switch (pm) {\n case \"pnpm\":\n return (t) => yellow(bold(t));\n case \"yarn\":\n return (t) => blue(bold(t));\n case \"npm\":\n return (t) => red(bold(t));\n }\n}\n\n/** 生成后的项目骨架文件树(提升完成感,dim 树枝 + 品牌色勾)。 */\nfunction renderTree(dirName: string, brand: (t: string) => string): string {\n return [\n brand(\"✓\") + dim(\" 生成的文件:\"),\n dim(` ${dirName}/`),\n dim(\" ├── \") + cyan(\"package.json\"),\n dim(\" └── \") + cyan(\"napuketto.toml\"),\n ].join(\"\\n\");\n}\n\n/** 询问是否移除现有文件并继续(目标目录非空时)。 */\nasync function confirmRemove(dirName: string): Promise<boolean> {\n const ok = await confirm({\n message: `目录 \"${dirName}\" 已存在且非空,移除现有文件并继续?`,\n initialValue: false,\n });\n if (isCancel(ok)) {\n cancel(\"操作已取消\");\n process.exit(0);\n }\n return ok;\n}\n\n/** 询问是否现在启动(默认 Y;-y 直接返回 true)。 */\nasync function askStartNow(yes: boolean): Promise<boolean> {\n if (yes) {\n return true;\n }\n const ok = await confirm({\n message: \"是否现在启动?\",\n initialValue: true,\n });\n if (isCancel(ok)) {\n cancel(\"操作已取消\");\n process.exit(0);\n }\n return ok;\n}\n\n/** 拼命令行时最小转义:含空白/引号则加双引号包裹(参数均为内部字面量,仅兜底)。 */\nfunction quoteShellArg(arg: string): string {\n return /[\\s\"]/.test(arg) ? `\"${arg.replace(/\"/g, '\\\\\"')}\"` : arg;\n}\n\n/** 前台执行命令(stdio 继承,用户可见进度/交互),返回退出码。 */\nfunction runInteractive(bin: string, args: string[], cwd: string): Promise<number> {\n return new Promise((resolve) => {\n // Windows 下 .cmd shim 不能直接 CreateProcess 执行,需 shell:true 经 cmd.exe;\n // 但 Node ≥22 对「shell:true + args 数组」触发 DEP0190 告警(args 仅简单拼接、不转义),\n // 因此 shell 模式下把命令拼成单个字符串传给 spawn 以规避告警;\n // POSIX 下无 shell,保持 bin + args。\n const shell = process.platform === \"win32\";\n const child = spawn(\n shell ? [bin, ...args.map(quoteShellArg)].join(\" \") : bin,\n shell ? [] : args,\n {\n cwd,\n stdio: \"inherit\",\n windowsHide: false,\n shell,\n },\n );\n child.on(\"exit\", (code) => {\n resolve(code ?? 0);\n });\n });\n}\n\nasync function main(): Promise<void> {\n const opts = parseArgs(process.argv.slice(2));\n if (opts.help) {\n console.log(HELP);\n return;\n }\n\n // 包管理器提前检测:intro 品牌色 + install/start 命令都按它定向\n const pm = detectPackageManager();\n const brand = brandColor(pm);\n\n // intro:品牌色标题(按调用方包管理器取色,PNPM 黄 / Yarn 蓝 / npm 红)\n intro(brand(`create-napukettoqq v${readVersion()}`));\n\n const dirName = opts.name ?? (await askProjectName(opts.yes));\n const targetDir = resolveTargetDir(dirName);\n\n // 目标目录准备:非空需确认清空(-f 跳过确认;-y 不隐式清空,保持默认安全语义)\n let overwrite = false;\n if ((await checkDirStatus(targetDir)) === \"nonempty\") {\n if (!opts.forced) {\n log.warn(`目标目录 \"${dirName}\" 已存在且非空。`);\n overwrite = await confirmRemove(dirName);\n if (!overwrite) {\n outro(\"已取消,未做任何修改。\");\n return;\n }\n } else {\n overwrite = true;\n }\n }\n\n // 生成骨架(spinner 进度)\n const s = spinner();\n s.start(`正在生成项目骨架 ${dirName} ...`);\n const result = await scaffoldProject(dirName, overwrite);\n s.stop(`项目骨架已生成:${dirName}`);\n log.message(renderTree(dirName, brand));\n log.success(`位置:${result.dir}`);\n\n // 自动安装依赖(用调用方包管理器)\n log.info(\"前置要求:本机已安装 QQ NT(wrapper.node 来自 QQ 安装目录)。\");\n log.info(`正在安装依赖(${brand(pm)} install)...`);\n const installCode = await runInteractive(pmBin(pm), [\"install\"], result.dir);\n if (installCode !== 0) {\n log.error(`依赖安装失败(退出码 ${installCode})。请手动执行:`);\n log.step(`cd ${result.dir}`);\n log.step(brand(`${pm} install`));\n outro(\"创建失败,请手动安装后重试。\");\n process.exitCode = 1;\n return;\n }\n\n // 询问是否现在启动(默认 Y → 前台运行)\n if (await askStartNow(opts.yes)) {\n log.info(\"正在启动 NapukettoQQ(Ctrl+C 停止)...\\n\");\n const startCode = await runInteractive(pmBin(pm), [\"start\"], result.dir);\n if (startCode !== 0) {\n process.exitCode = startCode;\n }\n } else {\n log.message(\"你可以稍后这样启动:\");\n if (targetDir !== process.cwd()) {\n const related = relative(process.cwd(), targetDir);\n log.step(`cd ${related}`);\n }\n log.step(brand(`${pm} start -q <你的QQ号>`));\n log.message(\"配置说明见项目内 napuketto.toml。\");\n }\n\n outro(\"完成。\");\n}\n\nmain().catch((err: unknown) => {\n const message = err instanceof Error ? err.message : String(err);\n log.error(`创建失败:${message}`);\n process.exitCode = 1;\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/scaffold.ts","../src/index.ts"],"sourcesContent":["/**\n * create-napukettoqq 脚手架核心:模板渲染 + 目标目录创建 + 文件写入。\n *\n * 模板为独立文件(templates/ 目录,运行时读取),不硬编码在源码里;\n * 需要插值的占位符用 {{key}} 标记(见 renderTemplate)。\n * 依赖极轻(node:fs/promises / node:os / node:path / node:process)。\n * 生成的用户项目依赖发布版的 @napuketto/cli,用户 install 后经 cli → loader\n * 自建宿主启动。\n */\n\nimport { readFileSync } from \"node:fs\";\nimport { mkdir, readdir, readFile, rm, writeFile } from \"node:fs/promises\";\n\nimport path from \"node:path\";\nimport process from \"node:process\";\n\n/** 模板目录(相对本文件:src 与 dist 都在包根下,../templates 均指向包根/templates)。 */\nconst TEMPLATES = new URL(\"../templates/\", import.meta.url);\n\n/**\n * 生成的用户项目所依赖的 @napuketto/cli 版本范围。\n * 运行时从脚手架自身 package.json 读取(dependencies[\"@napuketto/cli\"])——\n * 发版时 changesets 升版本 + pnpm publish 把 workspace:* 替换为实际版本,\n * 模板版本自动跟随,无需手动同步(比硬编码常量可靠)。\n */\nfunction cliVersionRange(): string {\n try {\n const ownPkg = JSON.parse(\n readFileSync(new URL(\"../package.json\", import.meta.url), \"utf8\"),\n ) as { dependencies?: Record<string, string> };\n const range = ownPkg.dependencies?.[\"@napuketto/cli\"];\n // workspace:* 仅在本地 monorepo 直跑脚手架时出现(发布后 pnpm 替换为实际版本),\n // 此时兜底默认范围,避免生成的用户项目 install 失败\n if (range !== undefined && range !== \"workspace:*\") {\n return range;\n }\n } catch {\n // package.json 缺失(异常环境)→ 兜底默认范围\n }\n return \"^0.0.1\";\n}\n\n/** 默认部署文件夹名(用户交互回车缺省值)。 */\nexport const DEFAULT_PROJECT_NAME = \"NapukettoQQ\";\n\n/** 用户可用的包管理器(npm_config_user_agent 检测)。 */\nexport type PackageManager = \"pnpm\" | \"yarn\" | \"npm\";\n\n/**\n * 由 npm_config_user_agent 检测调用方包管理器(yarn create / npm create /\n * pnpm create 都会注入该变量)。兜底 pnpm(项目生态,或直接 node 运行)。\n */\nexport function detectPackageManager(): PackageManager {\n // noPropertyAccessFromIndexSignature:process.env 是索引签名,必须括号访问\n const ua = process.env[\"npm_config_user_agent\"] ?? \"\";\n if (ua.startsWith(\"yarn/\")) {\n return \"yarn\";\n }\n if (ua.startsWith(\"npm/\")) {\n return \"npm\";\n }\n return \"pnpm\";\n}\n\n/** Windows 下包管理器 bin 需带 .cmd 后缀(CreateProcess 不认无扩展名 shim)。 */\nexport function pmBin(pm: PackageManager): string {\n return process.platform === \"win32\" ? `${pm}.cmd` : pm;\n}\n\n/** Windows 路径非法字符(项目文件夹名校验用)。 */\nconst ILLEGAL_NAME = /[\\\\/:*?\"<>|]/;\n\n/** 脚手架生成结果。 */\nexport interface ScaffoldResult {\n /** 目标目录绝对路径。 */\n dir: string;\n /** 派生出的 npm 包名(小写、空格 → -)。 */\n packageName: string;\n}\n\n/** 校验文件夹名:非空、非 . / ..、不含 Windows 非法字符。 */\nexport function validateProjectName(raw: string): string {\n const name = raw.trim();\n if (name === \"\" || name === \".\" || name === \"..\") {\n throw new Error(\"文件夹名不能为空或为 . / ..\");\n }\n if (ILLEGAL_NAME.test(name)) {\n throw new Error(`文件夹名含非法字符:${name}(不允许 \\\\ / : * ? \" < > |)`);\n }\n return name;\n}\n\n/** 由文件夹名派生 npm 包名(小写、空格 → -)。 */\nfunction derivePackageName(dirName: string): string {\n return dirName.toLowerCase().replace(/\\s+/g, \"-\");\n}\n\n/** 解析目标目录绝对路径(校验名称后,基于 cwd)。 */\nexport function resolveTargetDir(dirName: string): string {\n return path.resolve(process.cwd(), validateProjectName(dirName));\n}\n\n/** 目标目录状态:不存在 / 空 / 非空。 */\nexport type DirStatus = \"missing\" | \"empty\" | \"nonempty\";\n\n/** 检测目标目录状态(供交互层决定是否询问清空)。 */\nexport async function checkDirStatus(dir: string): Promise<DirStatus> {\n let entries: string[] = [];\n try {\n entries = await readdir(dir);\n } catch {\n return \"missing\";\n }\n return entries.length === 0 ? \"empty\" : \"nonempty\";\n}\n\n/** 清空目录内容(保留目录本身)。 */\nasync function emptyDir(dir: string): Promise<void> {\n for (const entry of await readdir(dir)) {\n await rm(path.join(dir, entry), { recursive: true, force: true });\n }\n}\n\n/** 读取模板文件(templates/ 目录,UTF-8)。 */\nfunction readTemplate(file: string): Promise<string> {\n return readFile(new URL(file, TEMPLATES), \"utf8\");\n}\n\n/**\n * 占位符渲染:把 {{key}} 替换为 vars[key]。\n * 模板中出现未提供的占位符 → 抛错(防漏插值写出半成品)。\n */\nfunction renderTemplate(content: string, vars: Record<string, string>): string {\n return content.replace(/\\{\\{(\\w+)\\}\\}/g, (match, key: string) => {\n const value = vars[key];\n if (value === undefined) {\n throw new Error(`模板占位符未提供:{{${key}}}(模板 ${match} 无法渲染)`);\n }\n return value;\n });\n}\n\n/**\n * 生成 NapukettoQQ 项目骨架到当前目录下的 <dirName>/。\n * 目标目录已存在且非空:overwrite=false → 抛错;overwrite=true → 清空后生成。\n * 模板与写入目标一一对应(同文件名)。\n */\nexport async function scaffoldProject(dirName: string, overwrite = false): Promise<ScaffoldResult> {\n const name = validateProjectName(dirName);\n const packageName = derivePackageName(name);\n const dir = resolveTargetDir(name);\n\n const status = await checkDirStatus(dir);\n if (status === \"nonempty\" && !overwrite) {\n throw new Error(`目录已存在且非空:${dir}\\n请换一个文件夹名,或先清空该目录。`);\n }\n if (status === \"nonempty\") {\n await emptyDir(dir);\n }\n\n // 渲染变量(napuketto.toml 模板不再插值 dataDir——缺省 = 项目根/.napuketto,跨平台)\n const vars: Record<string, string> = {\n packageName,\n cliVersion: cliVersionRange(),\n };\n // 用户项目是「运行壳」:不开发代码,只需 package.json(依赖 + start)+ napuketto.toml(配置)\n // 模板文件为 templates/<name>.tmpl(.tmpl 后缀避免 TOML/JSON 语言服务把占位符当非法语法)\n const files = [\"package.json\", \"napuketto.toml\"];\n\n await mkdir(dir, { recursive: true });\n for (const file of files) {\n const rendered = renderTemplate(await readTemplate(`${file}.tmpl`), vars);\n await writeFile(path.join(dir, file), rendered, \"utf8\");\n }\n\n return { dir, packageName };\n}\n","#!/usr/bin/env node\n\n/**\n * create-napukettoqq 入口(2026-08-07 美化 v3,交互层用 @clack/prompts——\n * 现代脚手架交互标准,create-vue / create-turbo 同款)。\n *\n * 用法:\n * create-napukettoqq # intro + 交互问部署文件夹名(默认 NapukettoQQ)\n * create-napukettoqq my-bot # 位置参数指定文件夹名,跳过命名交互\n * create-napukettoqq my-bot -f # 目标目录非空时强制清空覆盖\n * create-napukettoqq -y # 全默认(命名用默认值、启动询问用默认 Y)\n * create-napukettoqq -h # 打印帮助\n *\n * 流程:intro → 问文件夹名 → 目标目录准备(非空需确认清空)→ spinner 生成骨架\n * (package.json / napuketto.toml / readme.md / .gitignore)→ 用调用方包管理器\n * 自动 install → 询问是否现在启动(默认 Y)→ 前台运行 → outro 收尾。\n */\n\nimport { spawn } from \"node:child_process\";\nimport { readFileSync } from \"node:fs\";\nimport { relative } from \"node:path\";\nimport process from \"node:process\";\nimport { cancel, confirm, intro, isCancel, log, outro, spinner, text } from \"@clack/prompts\";\nimport pc from \"picocolors\";\nimport type { PackageManager, ScaffoldResult } from \"./scaffold.js\";\nimport {\n checkDirStatus,\n DEFAULT_PROJECT_NAME,\n detectPackageManager,\n pmBin,\n resolveTargetDir,\n scaffoldProject,\n validateProjectName,\n} from \"./scaffold.js\";\n\n// picocolors 是 CJS 包(无 ESM 命名导出),Node ESM 下需 default import 再解构\nconst { blue, bold, cyan, dim, red, yellow } = pc;\n\n/** CLI 参数解析结果。 */\ninterface CliOptions {\n /** 位置参数:部署文件夹名(可选)。 */\n name?: string;\n /** -f/--forced:目标目录非空时强制清空覆盖。 */\n forced: boolean;\n /** -y/--yes:跳过所有交互提示(使用默认值)。 */\n yes: boolean;\n /** -h/--help:显示帮助。 */\n help: boolean;\n}\n\n/** 帮助文本(-h/--help)。 */\nconst HELP = ` 用法: create-napukettoqq [name] [选项]\n\n 选项:\n -f, --forced 目标目录非空时强制清空并覆盖\n -y, --yes 跳过所有交互提示(使用默认值)\n -h, --help 显示本帮助\n`;\n\n/** 解析命令行参数(flag 仅三个,手写保持自包含,不引 yargs-parser)。 */\nfunction parseArgs(argv: string[]): CliOptions {\n const opts: CliOptions = { forced: false, yes: false, help: false };\n for (const arg of argv) {\n if (arg === \"-f\" || arg === \"--forced\") {\n opts.forced = true;\n } else if (arg === \"-y\" || arg === \"--yes\") {\n opts.yes = true;\n } else if (arg === \"-h\" || arg === \"--help\") {\n opts.help = true;\n } else if (arg.startsWith(\"-\")) {\n throw new Error(`未知参数:${arg}`);\n } else if (opts.name === undefined) {\n opts.name = arg;\n } else {\n throw new Error(`多余的位置参数:${arg}`);\n }\n }\n return opts;\n}\n\n/** 读取脚手架自身版本号(intro 显示;dist 场景 import.meta.url 指向包内)。 */\nfunction readVersion(): string {\n try {\n const ownPkg = JSON.parse(\n readFileSync(new URL(\"../package.json\", import.meta.url), \"utf8\"),\n ) as { version?: string };\n return ownPkg.version ?? \"0.0.0\";\n } catch {\n // package.json 缺失(异常环境)→ 兜底\n return \"0.0.0\";\n }\n}\n\n/** 交互询问部署文件夹名(回车取默认值;-y 直接返回默认值)。 */\nasync function askProjectName(yes: boolean): Promise<string> {\n if (yes) {\n return DEFAULT_PROJECT_NAME;\n }\n // placeholder = 淡灰默认名:直接回车 = 用默认名,键入任意字符 = 从空自定义。\n // 不设 initialValue——否则 clack 渲染的是已输入态(蓝色、光标在末尾),\n // 淡灰 placeholder 不显示(对齐 koishi create 的观感)。\n const name = await text({\n message: \"部署文件夹名\",\n placeholder: DEFAULT_PROJECT_NAME,\n validate: (input: string | undefined) => {\n // 空输入 = 使用默认名,不报错\n if (input === undefined || input.trim() === \"\") {\n return undefined;\n }\n try {\n validateProjectName(input);\n return undefined;\n } catch (err) {\n return err instanceof Error ? err.message : String(err);\n }\n },\n });\n if (isCancel(name)) {\n cancel(\"操作已取消\");\n process.exit(0);\n }\n const trimmed = name.trim();\n return trimmed === \"\" ? DEFAULT_PROJECT_NAME : validateProjectName(trimmed);\n}\n\n/**\n * 包管理器品牌色(美化 v4,对齐 koishi create 观感)。\n * 16 色近似 + bold:兼容所有终端(不依赖 truecolor),Windows Terminal /\n * 老 conhost 都能正常显示。只做「标题 + 命令」两层,不做三套 UI。\n */\nfunction brandColor(pm: PackageManager): (text: string) => string {\n switch (pm) {\n case \"pnpm\":\n return (t) => yellow(bold(t));\n case \"yarn\":\n return (t) => blue(bold(t));\n case \"npm\":\n return (t) => red(bold(t));\n }\n}\n\n/** 生成后的项目骨架文件树(提升完成感,dim 树枝 + 品牌色勾)。 */\nfunction renderTree(dirName: string, brand: (t: string) => string): string {\n return [\n brand(\"✓\") + dim(\" 生成的文件:\"),\n dim(` ${dirName}/`),\n dim(\" ├── \") + cyan(\"package.json\"),\n dim(\" └── \") + cyan(\"napuketto.toml\"),\n ].join(\"\\n\");\n}\n\n/** 询问是否移除现有文件并继续(目标目录非空时)。 */\nasync function confirmRemove(dirName: string): Promise<boolean> {\n const ok = await confirm({\n message: `目录 \"${dirName}\" 已存在且非空,移除现有文件并继续?`,\n initialValue: false,\n });\n if (isCancel(ok)) {\n cancel(\"操作已取消\");\n process.exit(0);\n }\n return ok;\n}\n\n/** 询问是否现在启动(默认 Y;-y 直接返回 true)。 */\nasync function askStartNow(yes: boolean): Promise<boolean> {\n if (yes) {\n return true;\n }\n const ok = await confirm({\n message: \"是否现在启动?\",\n initialValue: true,\n });\n if (isCancel(ok)) {\n cancel(\"操作已取消\");\n process.exit(0);\n }\n return ok;\n}\n\n/** 空白/引号判定。 */\nconst SHELL_WHITESPACE_RE = /[\\s\"]/;\n/** 内部引号转义。 */\nconst QUOTE_RE = /\"/g;\n\n/** 拼命令行时最小转义:含空白/引号则加双引号包裹(参数均为内部字面量,仅兜底)。 */\nfunction quoteShellArg(arg: string): string {\n return SHELL_WHITESPACE_RE.test(arg) ? `\"${arg.replace(QUOTE_RE, '\\\\\"')}\"` : arg;\n}\n\n/** 前台执行命令(stdio 继承,用户可见进度/交互),返回退出码。 */\nfunction runInteractive(bin: string, args: string[], cwd: string): Promise<number> {\n return new Promise((resolve) => {\n // Windows 下 .cmd shim 不能直接 CreateProcess 执行,需 shell:true 经 cmd.exe;\n // 但 Node ≥22 对「shell:true + args 数组」触发 DEP0190 告警(args 仅简单拼接、不转义),\n // 因此 shell 模式下把命令拼成单个字符串传给 spawn 以规避告警;\n // POSIX 下无 shell,保持 bin + args。\n const shell = process.platform === \"win32\";\n const child = spawn(\n shell ? [bin, ...args.map(quoteShellArg)].join(\" \") : bin,\n shell ? [] : args,\n {\n cwd,\n stdio: \"inherit\",\n windowsHide: false,\n shell,\n },\n );\n child.on(\"exit\", (code) => {\n resolve(code ?? 0);\n });\n });\n}\n\n/** 目标目录准备:非空需确认清空(-f 跳过确认;-y 不隐式清空)。返回 null = 已取消。 */\nasync function prepareTargetDir(\n dirName: string,\n opts: CliOptions,\n): Promise<{ targetDir: string; overwrite: boolean } | null> {\n const targetDir = resolveTargetDir(dirName);\n let overwrite = false;\n if ((await checkDirStatus(targetDir)) === \"nonempty\") {\n if (!opts.forced) {\n log.warn(`目标目录 \"${dirName}\" 已存在且非空。`);\n overwrite = await confirmRemove(dirName);\n if (!overwrite) {\n outro(\"已取消,未做任何修改。\");\n return null;\n }\n } else {\n overwrite = true;\n }\n }\n return { targetDir, overwrite };\n}\n\n/** 安装依赖(失败提示手动安装路径)。返回是否继续。 */\nasync function installDeps(pm: PackageManager, result: ScaffoldResult): Promise<boolean> {\n const color = brandColor(pm);\n log.info(\"前置要求:本机已安装 QQ NT(wrapper.node 来自 QQ 安装目录)。\");\n log.info(`正在安装依赖(${color(pm)} install)...`);\n const installCode = await runInteractive(pmBin(pm), [\"install\"], result.dir);\n if (installCode === 0) {\n return true;\n }\n log.error(`依赖安装失败(退出码 ${installCode})。请手动执行:`);\n log.step(`cd ${result.dir}`);\n log.step(color(`${pm} install`));\n outro(\"创建失败,请手动安装后重试。\");\n process.exitCode = 1;\n return false;\n}\n\nasync function main(): Promise<void> {\n const opts = parseArgs(process.argv.slice(2));\n if (opts.help) {\n console.log(HELP);\n return;\n }\n\n // 包管理器提前检测:intro 品牌色 + install/start 命令都按它定向\n const pm = detectPackageManager();\n const brand = brandColor(pm);\n\n // intro:品牌色标题(按调用方包管理器取色,PNPM 黄 / Yarn 蓝 / npm 红)\n intro(brand(`create-napukettoqq v${readVersion()}`));\n\n const dirName = opts.name ?? (await askProjectName(opts.yes));\n const prepared = await prepareTargetDir(dirName, opts);\n if (prepared === null) {\n return;\n }\n const { targetDir, overwrite } = prepared;\n\n // 生成骨架(spinner 进度)\n const s = spinner();\n s.start(`正在生成项目骨架 ${dirName} ...`);\n const result = await scaffoldProject(dirName, overwrite);\n s.stop(`项目骨架已生成:${dirName}`);\n log.message(renderTree(dirName, brand));\n log.success(`位置:${result.dir}`);\n\n // 自动安装依赖(用调用方包管理器)\n if (!(await installDeps(pm, result))) {\n return;\n }\n\n // 询问是否现在启动(默认 Y → 前台运行)\n if (await askStartNow(opts.yes)) {\n log.info(\"正在启动 NapukettoQQ(Ctrl+C 停止)...\\n\");\n const startCode = await runInteractive(pmBin(pm), [\"start\"], result.dir);\n if (startCode !== 0) {\n process.exitCode = startCode;\n }\n } else {\n log.message(\"你可以稍后这样启动:\");\n if (targetDir !== process.cwd()) {\n const related = relative(process.cwd(), targetDir);\n log.step(`cd ${related}`);\n }\n log.step(brand(`${pm} start -q <你的QQ号>`));\n log.message(\"配置说明见项目内 napuketto.toml。\");\n }\n\n outro(\"完成。\");\n}\n\nmain().catch((err: unknown) => {\n const message = err instanceof Error ? err.message : String(err);\n log.error(`创建失败:${message}`);\n process.exitCode = 1;\n});\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-napukettoqq",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
4
4
|
"description": "一键生成 NapukettoQQ 机器人项目(yarn/pnpm/npm create napukettoqq)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"@clack/prompts": "^1.7.0",
|
|
16
16
|
"picocolors": "^1.0.0",
|
|
17
|
-
"@napuketto/cli": "0.0.
|
|
17
|
+
"@napuketto/cli": "0.0.10"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"tsdown": "^0.22.14"
|