@guozhi-fe/skills 0.1.0 → 0.2.0

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/README.md CHANGED
@@ -9,14 +9,36 @@
9
9
 
10
10
  ## 首次安装
11
11
 
12
- 在 npm 发布前,可在本仓库运行 `npm pack`,通过 `npm i -g /绝对路径/guozhi-fe-skills-0.1.0.tgz` 安装 CLI。npm 发布后可用 `npm i -g @guozhi-fe/skills`,或通过 `npx --package=@guozhi-fe/skills gzfe-skills init` 直接接入。
12
+ 可用 `npm i -g @guozhi-fe/skills` 安装已发布的 CLI,或通过 `npx --package=@guozhi-fe/skills gzfe-skills init` 直接接入。试用本地修改时,在本仓库运行 `npm ci` 和 `npm pack`,再运行 `npm i -g /绝对路径/guozhi-fe-skills-0.2.0.tgz`。
13
13
 
14
14
  ```sh
15
- gzfe-skills init microfrontend-memory-assessment
15
+ gzfe-skills init
16
16
  gzfe-skills list
17
17
  ```
18
18
 
19
- 省略技能名会安装全部技能。文件和版本记录位于当前项目的 `.agents/skills/<技能名>/`,建议一起提交到项目 Git 仓库。重复 init 会保留已安装版本。
19
+ 从 0.2.0 开始,省略技能名会打开交互列表,逐项显示技能名及 `SKILL.md` 中的简介。使用上下键移动、空格勾选、回车确认;勾选一项安装一个技能,勾选多项批量安装。按 `a` 全选或取消全选,按 `Ctrl+C` 取消。未勾选任何技能时按回车会取消安装。
20
+
21
+ 直接指定技能名可以跳过交互。脚本中可使用 `--all` 安装全部技能,无终端时需要显式指定技能名或 `--all`。
22
+
23
+ ```sh
24
+ gzfe-skills init microfrontend-memory-assessment
25
+ gzfe-skills init microfrontend-memory-assessment microfrontend-memory-lifecycle
26
+ gzfe-skills init --all
27
+ ```
28
+
29
+ 文件和版本记录位于当前项目的 `.agents/skills/<技能名>/`,建议一起提交到项目 Git 仓库。重复 init 会保留已安装版本,未勾选的已安装技能也会保留。
30
+
31
+ ## 卸载
32
+
33
+ 不带参数运行 `uninstall` 会列出本工具管理的已安装技能,使用与 init 相同的勾选方式。也可直接指定技能名,或使用 `--all` 批量卸载。
34
+
35
+ ```sh
36
+ gzfe-skills uninstall
37
+ gzfe-skills uninstall microfrontend-memory-assessment
38
+ gzfe-skills uninstall --all
39
+ ```
40
+
41
+ 卸载会移除所选技能目录及其安装记录。如果检测到本地文件有改动,会列出文件并停止。全量卸载只处理本工具管理的技能。卸载后可再次运行 `gzfe-skills init` 安装。
20
42
 
21
43
  ## 更新
22
44
 
@@ -3,6 +3,10 @@ import fs from 'node:fs'
3
3
  import path from 'node:path'
4
4
  import { fileURLToPath } from 'node:url'
5
5
  import { createHash } from 'node:crypto'
6
+ import checkbox from '@inquirer/checkbox'
7
+ import { parse } from 'yaml'
8
+ import colors from 'yoctocolors-cjs'
9
+ import wrapAnsi from 'wrap-ansi'
6
10
 
7
11
  const root = fileURLToPath(new URL('../', import.meta.url))
8
12
  const source = path.join(root, 'skills')
@@ -51,15 +55,62 @@ function stage(name, dir) {
51
55
  fs.writeFileSync(path.join(dir, marker), JSON.stringify({ name, version, files: manifest(dir) }, null, 2) + '\n')
52
56
  }
53
57
 
54
- function run([command, ...args]) {
58
+ async function selectSkills(command, names) {
59
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
60
+ throw Error(`交互选择需要终端,请指定技能名,或使用 gzfe-skills ${command} --all。`)
61
+ }
62
+ const choices = names.map(name => {
63
+ const file = path.join(source, name, 'SKILL.md')
64
+ const frontmatter = fs.readFileSync(file, 'utf8').match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/)
65
+ const metadata = frontmatter && parse(frontmatter[1])
66
+ if (typeof metadata?.description !== 'string' || !metadata.description.trim()) {
67
+ throw Error('技能缺少有效简介:' + file)
68
+ }
69
+ const description = metadata.description.replace(/\s+/g, ' ').trim()
70
+ const summary = description.match(/^[^。!?]+[。!?]?/u)?.[0] || description
71
+ const width = Math.max(16, Math.min(88, (process.stdout.columns || 80) - 4))
72
+ const lines = wrapAnsi(summary, width, { hard: true }).split('\n')
73
+ const detail = lines.map(line => ' ' + colors.dim(line)).join('\n')
74
+ return { name: `${colors.bold(name)}\n${detail}\n`, short: name, value: name }
75
+ })
76
+ return checkbox({
77
+ message: `选择要${command === 'init' ? '安装' : '卸载'}的技能(可单选或多选)`,
78
+ choices,
79
+ pageSize: 12,
80
+ theme: {
81
+ style: {
82
+ highlight: text => {
83
+ const [title, ...detail] = text.split('\n')
84
+ return [colors.cyan(title), ...detail].join('\n')
85
+ },
86
+ keysHelpTip: () => '↑↓ 移动 · 空格 勾选 · a 全选/取消全选 · 回车 确认 · Ctrl+C 取消',
87
+ },
88
+ },
89
+ })
90
+ }
91
+
92
+ async function run([command, ...args]) {
55
93
  if (!command || ['--help', '-h'].includes(command)) {
56
- console.log('gzfe-skills init [技能名 ...] | list | update [技能名 ...] [--dry-run]\n' + catalog.join('\n'))
94
+ console.log([
95
+ 'gzfe-skills init 交互选择技能,支持单选和多选',
96
+ 'gzfe-skills init <技能名 ...> 直接安装指定技能',
97
+ 'gzfe-skills init --all 安装全部技能,可用于脚本',
98
+ 'gzfe-skills list 查看项目安装状态',
99
+ 'gzfe-skills update [技能名 ...] [--dry-run]',
100
+ 'gzfe-skills uninstall 交互选择要卸载的已安装技能',
101
+ 'gzfe-skills uninstall <技能名 ...> 卸载指定技能',
102
+ 'gzfe-skills uninstall --all 卸载本工具管理的全部技能',
103
+ '',
104
+ ...catalog,
105
+ ].join('\n'))
57
106
  return
58
107
  }
59
108
  if (command === '--version') return console.log(version)
60
- if (!['init', 'update', 'list'].includes(command)) throw Error('未知命令:' + command)
109
+ if (!['init', 'update', 'list', 'uninstall'].includes(command)) throw Error('未知命令:' + command)
61
110
  const dry = command === 'update' && args.includes('--dry-run')
62
- const names = [...new Set(dry ? args.filter(x => x !== '--dry-run') : args)]
111
+ const all = ['init', 'uninstall'].includes(command) && args.includes('--all')
112
+ let names = [...new Set(args.filter(x => !(dry && x === '--dry-run') && !(all && x === '--all')))]
113
+ if (all && names.length) throw Error('--all 不能与技能名同时使用')
63
114
  if (names.some(x => !catalog.includes(x)) || (command === 'list' && names.length)) throw Error('参数无效,请查看 --help')
64
115
  for (const dir of [path.dirname(dest), dest]) {
65
116
  const entry = fs.lstatSync(dir, { throwIfNoEntry: false })
@@ -69,12 +120,18 @@ function run([command, ...args]) {
69
120
  for (const name of catalog) console.log(name, status(name).label)
70
121
  return
71
122
  }
123
+ if (['init', 'uninstall'].includes(command) && !names.length) {
124
+ const available = command === 'uninstall' ? catalog.filter(name => status(name).saved) : catalog
125
+ if (!available.length) return console.log('当前项目没有由 gzfe-skills 管理的已安装技能')
126
+ names = all ? available : await selectSkills(command, available)
127
+ if (!names.length) return console.log('未选择技能,操作已取消')
128
+ }
72
129
  const plan = () => (names.length ? names : catalog).filter(name => {
73
130
  const state = status(name)
74
131
  if (command === 'update' && !names.length && !state.saved) return false
75
- if (!state.saved && (command === 'update' || state.label !== '未安装')) throw Error(name + ':' + state.label)
132
+ if (!state.saved && (command !== 'init' || state.label !== '未安装')) throw Error(name + ':' + state.label)
76
133
  if (state.changes?.length) throw Error(name + ' 有本地修改:\n' + state.changes.join('\n'))
77
- return !state.saved || (command === 'update' && state.saved.version !== version)
134
+ return command === 'uninstall' || !state.saved || (command === 'update' && state.saved.version !== version)
78
135
  })
79
136
  if (dry) {
80
137
  const selected = plan()
@@ -87,17 +144,21 @@ function run([command, ...args]) {
87
144
  let cleanup = true
88
145
  try {
89
146
  const selected = plan()
90
- for (const name of selected) stage(name, path.join(work, name))
147
+ if (command !== 'uninstall') {
148
+ for (const name of selected) stage(name, path.join(work, name))
149
+ }
91
150
  for (const name of selected) {
92
151
  const target = path.join(dest, name)
93
152
  const backup = path.join(work, name + '.old')
94
153
  const existed = fs.existsSync(target)
95
154
  if (existed) fs.renameSync(target, backup)
96
155
  applied.push({ target, backup, existed, installed: false })
156
+ if (command === 'uninstall') continue
97
157
  fs.renameSync(path.join(work, name), target)
98
158
  applied.at(-1).installed = true
99
159
  }
100
- console.log(selected.length ? selected.join(', ') + ' → ' + version : '无需变更')
160
+ const result = command === 'uninstall' ? '已卸载:' + selected.join(', ') : selected.join(', ') + ' → ' + version
161
+ console.log(selected.length ? result : '无需变更')
101
162
  } catch (error) {
102
163
  cleanup = false
103
164
  for (const item of applied.reverse()) {
@@ -110,7 +171,12 @@ function run([command, ...args]) {
110
171
  if (cleanup) fs.rmSync(work, { recursive: true })
111
172
  }
112
173
  }
113
- try { run(process.argv.slice(2)) } catch (error) {
114
- console.error(error.message)
115
- process.exitCode = 1
174
+ try { await run(process.argv.slice(2)) } catch (error) {
175
+ if (error.name === 'ExitPromptError') {
176
+ console.log('操作已取消')
177
+ process.exitCode = 130
178
+ } else {
179
+ console.error(error.message)
180
+ process.exitCode = 1
181
+ }
116
182
  }
package/package.json CHANGED
@@ -1,10 +1,26 @@
1
1
  {
2
2
  "name": "@guozhi-fe/skills",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "国智前端项目级 Codex 技能集",
5
5
  "type": "module",
6
- "bin": { "gzfe-skills": "bin/gzfe-skills.js" },
7
- "files": ["bin", "skills"],
8
- "engines": { "node": ">=18" },
9
- "repository": "github:soleilLulu/gz-fe-skills"
6
+ "bin": {
7
+ "gzfe-skills": "bin/gzfe-skills.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "skills"
12
+ ],
13
+ "engines": {
14
+ "node": ">=18"
15
+ },
16
+ "repository": "github:soleilLulu/gz-fe-skills",
17
+ "scripts": {
18
+ "test": "node --test"
19
+ },
20
+ "dependencies": {
21
+ "@inquirer/checkbox": "^4.3.2",
22
+ "wrap-ansi": "^6.2.0",
23
+ "yaml": "^2.9.1",
24
+ "yoctocolors-cjs": "^2.1.3"
25
+ }
10
26
  }