Files
market/scripts/vendor/guizang-ppt.mjs
Yige b15fce19bf feat: add guizang-ppt market skill (vendored from op7418, AGPL-3.0) (#20)
## 概述 / Summary

新增市场技能 **`guizang-ppt`**(归藏网页 PPT),vendored 自上游开源项目
[op7418/guizang-ppt-skill](https://github.com/op7418/guizang-ppt-skill)。

Add a new **market skill** `guizang-ppt` — generates single-file HTML
horizontal-swipe slide decks (web PPT) in two visual systems (editorial
"magazine × e-ink" / "Swiss International"). Vendored from the upstream
open-source project.

## 变更内容 / Changes

- `skills/guizang-ppt/`:SKILL.md(DesireCore frontmatter 覆盖层 +
上游正文)、`references/`(10)、`assets/`(2 模板 + motion.min.js + 9 张
webp)、`scripts/validate-swiss-deck.mjs`、`LICENSE`(AGPL-3.0)、`NOTICE.md`(署名与合规)
- `skills/guizang-ppt/_desirecore/`:DesireCore 维护态(不随上游覆盖)
- `frontmatter.yaml`:市场 frontmatter 覆盖层(i18n: zh-CN 源 + en-US 显示串,body
暂回退中文,留给 CI 翻译)
  - `upstream.json`:上游溯源(commit `014c572`、AGPL-3.0、作者 歸藏/op7418)
- `scripts/vendor/guizang-ppt.mjs`:可复用的 vendor 更新脚本(`--src <本地路径>` 或
`--ref <tag>`)
- `manifest.json`:`version` → `1.2.3`,`stats.totalSkills` → 25

## 定位 / Positioning

- **仅市场可选安装**:未加入 `builtin-skills.json`,不随客户端开机自动安装;用户在市场按需安装。
- 分类 `creative`;与已有 `pptx`(生成 .pptx 文件)区分:本技能生成 **HTML deck**。

## 许可与署名 / License & Attribution

上游为 **AGPL-3.0**。本技能保留原 `LICENSE` 与作者署名(`NOTICE.md` + `metadata.author`
+ `market.maintainer`),作为聚合内容分发。

## 校验 / Validation

- `python3 scripts/i18n/validate-i18n.py skills/guizang-ppt` → `OK: no
i18n issues found.`

## 手动更新流程 / Manual update

上游发版时:`node scripts/vendor/guizang-ppt.mjs --ref <tag>` → 核对 diff → 必要时
bump `_desirecore/frontmatter.yaml#version` 与 `manifest.json` → 提交。

---
🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-06-04 11:04:05 +08:00

147 lines
5.3 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* Vendor 更新器:把上游 op7418/guizang-ppt-skill 同步进官方市场的 skills/guizang-ppt/。
*
* 设计目标(“手动可更新”):
* - 上游内容references/ assets/ scripts/ LICENSE + SKILL.md 正文)是“被 vendored”的可重复覆盖
* - DesireCore 自己维护的元数据放在 skills/guizang-ppt/_desirecore/frontmatter.yaml 覆盖层 + NOTICE.md
* vendor 时不被上游覆盖;
* - 每次同步把上游 commit/版本写进 _desirecore/upstream.json保证可追溯。
*
* 用法:
* node scripts/vendor/guizang-ppt.mjs --src /path/to/local/guizang-ppt-skill # 复用本地 clone
* node scripts/vendor/guizang-ppt.mjs --ref v1.2.0 # 联网 clone 指定 ref
* node scripts/vendor/guizang-ppt.mjs # 联网 clone 默认分支
*
* 同步后仍需人工:核对 diff、必要时 bump _desirecore/frontmatter.yaml#version 与 metadata.updated_at、
* 更新根 manifest.json再 commit / push。
*/
import { execSync } from 'node:child_process'
import {
existsSync,
readFileSync,
writeFileSync,
rmSync,
cpSync,
mkdtempSync,
} from 'node:fs'
import { join, dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { tmpdir } from 'node:os'
const SKILL_ID = 'guizang-ppt'
const UPSTREAM_URL = 'https://github.com/op7418/guizang-ppt-skill.git'
const DEFAULT_BRANCH = 'main'
// 从上游仓库 vendor 进来的顶层条目(会被每次同步覆盖刷新)
const VENDORED_ENTRIES = ['references', 'assets', 'scripts', 'LICENSE']
const __dir = dirname(fileURLToPath(import.meta.url))
const REPO_ROOT = resolve(__dir, '..', '..') // scripts/vendor -> 仓库根
const SKILL_DIR = join(REPO_ROOT, 'skills', SKILL_ID)
const DESIRE_DIR = join(SKILL_DIR, '_desirecore')
const FRONTMATTER_FILE = join(DESIRE_DIR, 'frontmatter.yaml')
function parseArgs(argv) {
const out = { src: null, ref: DEFAULT_BRANCH }
for (let i = 0; i < argv.length; i++) {
if (argv[i] === '--src') out.src = argv[++i]
else if (argv[i] === '--ref') out.ref = argv[++i]
}
return out
}
function git(cmd, cwd) {
return execSync(`git ${cmd}`, { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim()
}
/** 取得上游源目录与 commit/ref本地 --src 优先,否则浅克隆。返回 { srcDir, commit, ref, cleanup } */
function resolveSource(args) {
if (args.src) {
const srcDir = resolve(args.src)
if (!existsSync(join(srcDir, 'SKILL.md'))) {
throw new Error(`--src 指向的目录缺少 SKILL.md${srcDir}`)
}
let commit = 'unknown'
try {
commit = git('rev-parse HEAD', srcDir)
} catch {
/* 非 git 目录则忽略 */
}
return { srcDir, commit, ref: args.ref, cleanup: () => {} }
}
const tmp = mkdtempSync(join(tmpdir(), 'guizang-vendor-'))
console.log(`→ 克隆上游 ${UPSTREAM_URL} (ref=${args.ref}) ...`)
execSync(`git clone --depth 1 --branch ${args.ref} ${UPSTREAM_URL} ${tmp}`, {
stdio: ['pipe', 'pipe', 'pipe'],
})
const commit = git('rev-parse HEAD', tmp)
return { srcDir: tmp, commit, ref: args.ref, cleanup: () => rmSync(tmp, { recursive: true, force: true }) }
}
/** 去掉上游 SKILL.md 的 frontmatter返回正文 */
function stripFrontmatter(text) {
const lines = text.split('\n')
if (lines[0].trim() !== '---') return text.trim() + '\n'
for (let i = 1; i < lines.length; i++) {
if (lines[i].trim() === '---') {
return lines.slice(i + 1).join('\n').replace(/^\n+/, '')
}
}
return text.trim() + '\n'
}
function main() {
const args = parseArgs(process.argv.slice(2))
if (!existsSync(FRONTMATTER_FILE)) {
throw new Error(`缺少覆盖层 frontmatter${FRONTMATTER_FILE}`)
}
const frontmatter = readFileSync(FRONTMATTER_FILE, 'utf-8').trimEnd()
const { srcDir, commit, ref, cleanup } = resolveSource(args)
try {
// 1. 清理旧的 vendored 内容(保留 _desirecore/ 与 NOTICE.md 等维护态)
for (const entry of [...VENDORED_ENTRIES, 'SKILL.md']) {
rmSync(join(SKILL_DIR, entry), { recursive: true, force: true })
}
// 2. 复制上游 vendored 条目
for (const entry of VENDORED_ENTRIES) {
const from = join(srcDir, entry)
if (existsSync(from)) {
cpSync(from, join(SKILL_DIR, entry), { recursive: true })
} else {
console.warn(` ⚠ 上游缺少 ${entry},跳过`)
}
}
// 3. 生成 SKILL.md = 覆盖层 frontmatter + 上游正文
const upstreamSkill = readFileSync(join(srcDir, 'SKILL.md'), 'utf-8')
const body = stripFrontmatter(upstreamSkill)
writeFileSync(join(SKILL_DIR, 'SKILL.md'), `${frontmatter}\n\n${body}`)
// 4. 写溯源信息
const upstream = {
skillId: SKILL_ID,
repoUrl: UPSTREAM_URL,
branch: DEFAULT_BRANCH,
ref,
commit,
license: 'AGPL-3.0',
author: '歸藏 (op7418)',
vendoredAt: new Date().toISOString(),
}
writeFileSync(join(DESIRE_DIR, 'upstream.json'), `${JSON.stringify(upstream, null, 2)}\n`)
console.log('✓ vendor 完成')
console.log(` upstream commit: ${commit}`)
console.log(` skill dir: skills/${SKILL_ID}/`)
console.log(' 后续:核对 diff → 必要时 bump _desirecore/frontmatter.yaml#version 与根 manifest.json → commit')
} finally {
cleanup()
}
}
main()