无参数调用时脚本会打印用法并 exit 1(语义是对的:缺必填参数), 但 pnpm 会再补一句 "[ELIFECYCLE] Command failed with exit code 1", 看起来像脚本崩了而不是用法提示。 改成在用法前面先输出一行: 缺少正文内容,没有创建任何文件。 正确写法:pnpm new-thought "今天把树莓派的散热换掉了。" exit 码不变(无参数 1、--help 0),只是把原因说清楚。
139 lines
3.9 KiB
JavaScript
139 lines
3.9 KiB
JavaScript
/* 创建一条新的「偶得」(thought)
|
||
*
|
||
* 用法:
|
||
* pnpm new-thought "今天把树莓派的散热换掉了,温度降了 15 度。"
|
||
* pnpm new-thought "标题" --title 换散热 --location 张掖 --weather "晴 27°C" \
|
||
* --images /gallery/GalleryFav/xxx.png --tags 生活,硬件 --pinned
|
||
*
|
||
* 只提供正文,其余字段都是可选的。
|
||
*/
|
||
|
||
import fs from "node:fs"
|
||
import path from "node:path"
|
||
|
||
const TARGET_DIR = "./src/content/thoughts"
|
||
|
||
/**
|
||
* 用带时区的 ISO 时间。若写成不带时区的 `2026-09-14 21:30`,
|
||
* YAML 会按 UTC 解析,显示时会偏移。
|
||
*/
|
||
function nowIso() {
|
||
return new Date().toISOString()
|
||
}
|
||
|
||
function fileStamp() {
|
||
const now = new Date()
|
||
const pad = (n) => String(n).padStart(2, "0")
|
||
return (
|
||
`${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}` +
|
||
`-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`
|
||
)
|
||
}
|
||
|
||
const args = process.argv.slice(2)
|
||
|
||
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
||
// 无参数进来时先说清楚原因:下面 pnpm 还会补一句
|
||
// "[ELIFECYCLE] Command failed with exit code 1",不点明的话很像脚本崩了
|
||
if (args.length === 0) {
|
||
console.log('缺少正文内容,没有创建任何文件。\n正确写法:pnpm new-thought "今天把树莓派的散热换掉了。"\n')
|
||
}
|
||
console.log(`创建一条新的「偶得」
|
||
|
||
用法:
|
||
pnpm new-thought "正文内容" [选项]
|
||
|
||
选项:
|
||
--title <标题> 可选标题
|
||
--location <地点> 地点,显示在日期旁
|
||
--weather <天气> 天气,与地点一起显示
|
||
--images <路径,路径> 配图,public 下的绝对路径,逗号分隔
|
||
--tags <标签,标签> 标签,逗号分隔
|
||
--pinned 置顶
|
||
|
||
示例:
|
||
pnpm new-thought "换了散热,温度降了 15 度。" --location 张掖 --tags 硬件`)
|
||
process.exit(args.length === 0 ? 1 : 0)
|
||
}
|
||
|
||
const content = args[0]
|
||
|
||
const options = {
|
||
title: "",
|
||
location: "",
|
||
weather: "",
|
||
images: "",
|
||
tags: "",
|
||
pinned: false,
|
||
}
|
||
|
||
for (let i = 1; i < args.length; i++) {
|
||
const arg = args[i]
|
||
|
||
if (arg === "--pinned") {
|
||
options.pinned = true
|
||
continue
|
||
}
|
||
|
||
if (!arg.startsWith("--")) {
|
||
console.error(`错误:无法识别的参数 "${arg}"`)
|
||
process.exit(1)
|
||
}
|
||
|
||
const key = arg.slice(2)
|
||
if (!(key in options)) {
|
||
console.error(`错误:未知选项 --${key}`)
|
||
process.exit(1)
|
||
}
|
||
|
||
const value = args[i + 1]
|
||
if (value === undefined || value.startsWith("--")) {
|
||
console.error(`错误:--${key} 缺少值`)
|
||
process.exit(1)
|
||
}
|
||
|
||
options[key] = value
|
||
i++
|
||
}
|
||
|
||
const list = (value) =>
|
||
value
|
||
? value
|
||
.split(",")
|
||
.map((s) => s.trim())
|
||
.filter(Boolean)
|
||
: []
|
||
|
||
const images = list(options.images)
|
||
const tags = list(options.tags)
|
||
|
||
if (!fs.existsSync(TARGET_DIR)) {
|
||
fs.mkdirSync(TARGET_DIR, { recursive: true })
|
||
}
|
||
|
||
const fullPath = path.join(TARGET_DIR, `${fileStamp()}.md`)
|
||
if (fs.existsSync(fullPath)) {
|
||
console.error(`错误:${fullPath} 已存在,请稍后重试`)
|
||
process.exit(1)
|
||
}
|
||
|
||
// 字符串统一加引号,避免标题里的冒号、井号等破坏 YAML
|
||
const quote = (value) => `"${String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`
|
||
|
||
const frontmatter = [
|
||
`published: ${nowIso()}`,
|
||
options.title ? `title: ${quote(options.title)}` : "",
|
||
options.location ? `location: ${quote(options.location)}` : "",
|
||
options.weather ? `weather: ${quote(options.weather)}` : "",
|
||
images.length > 0 ? `images:\n${images.map((img) => ` - ${quote(img)}`).join("\n")}` : "",
|
||
tags.length > 0 ? `tags:\n${tags.map((tag) => ` - ${quote(tag)}`).join("\n")}` : "",
|
||
options.pinned ? "pinned: true" : "",
|
||
]
|
||
.filter(Boolean)
|
||
.join("\n")
|
||
|
||
fs.writeFileSync(fullPath, `---\n${frontmatter}\n---\n\n${content}\n`, "utf8")
|
||
|
||
console.log(`已创建偶得: ${fullPath}`)
|
||
console.log("写入正文后,dev server 会自动刷新。")
|