uniner-cli 1.0.5 → 1.0.6

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
@@ -21,9 +21,9 @@ npm install -g uniner-cli
21
21
  安装过程:
22
22
 
23
23
  1. npm 装本壳包(约 50KB)
24
- 2. `postinstall` 钩子从 `static.uniner.com` 拉取约 250MB 便携包
25
- 3. 解压到 `%USERPROFILE%\.uniner-cli\<version>\`
26
- 4. 写入 `%USERPROFILE%\.claude\skills\uniner-cli\SKILL.md`
24
+ 2. `postinstall` 钩子从 `static.uniner.com` 拉取对应平台的便携包(Windows ~280MB / macOS ~75MB / Linux ~185MB)
25
+ 3. 解压到 `~/.uniner-cli/<version>/`
26
+ 4. 写入 `~/.claude/skills/uniner-cli/SKILL.md`
27
27
 
28
28
  完成后任意终端:
29
29
 
@@ -35,12 +35,12 @@ uniner-cli --help
35
35
 
36
36
  | 项 | 要求 |
37
37
  |---|---|
38
- | 操作系统 | 64 位 Windows 10 / 11 |
38
+ | 操作系统 | Windows 10/11 x64 · macOS 12+ ARM64 · Linux x64 |
39
39
  | Node.js | >= 14 |
40
- | 磁盘空间 | ≥ 1.5 GB |
40
+ | 磁盘空间 | ≥ 500 MB(单平台) |
41
41
  | 网络 | 能访问 `static.uniner.com` |
42
42
 
43
- > Linux / macOS 支持在路上。
43
+ > 安装脚本会自动检测当前系统并下载对应版本。
44
44
 
45
45
  ## 使用示例
46
46
 
@@ -61,7 +61,7 @@ uniner-cli excel data write --file out.xlsx --cell B2 --value "hello"
61
61
  uniner-cli tool ocr ddddocr --img captcha.png
62
62
  ```
63
63
 
64
- 完整命令树(一级 + 二级 + 三级参数)见安装后 `%USERPROFILE%\.uniner-cli\<version>\skills\cli\SKILL.md`。
64
+ 完整命令树(一级 + 二级 + 三级参数)见安装后 `~/.uniner-cli/<version>/skills/cli/SKILL.md`。
65
65
 
66
66
  ## 升级
67
67
 
@@ -80,19 +80,21 @@ npm install -g uniner-cli@latest
80
80
  ## 卸载
81
81
 
82
82
  ```bash
83
- # 1. 先清理运行时数据(删 ~/.uniner-cli/ 和 Claude Code 技能)
84
- uniner-cli-uninstall
85
-
86
- # 2. 再卸载 npm 包本身
87
83
  npm uninstall -g uniner-cli
88
84
  ```
89
85
 
90
- > **为什么是两步?** npm 7+ 出于安全考虑不再在卸载时运行 lifecycle 脚本,所以我们提供 `uniner-cli-uninstall` 命令让你显式触发清理。
86
+ 卸载时 `preuninstall` 钩子会自动清理:
87
+
88
+ - `~/.uniner-cli/` 整个运行时目录(含所有版本)
89
+ - `~/.claude/skills/uniner-cli/SKILL.md` 技能文件
91
90
 
92
- 清理会移除:
91
+ > `~/.claude/skills/uniner-cli/scenarios/` 下用户积累的场景数据不会被自动删除,需手动清理。
93
92
 
94
- - `%USERPROFILE%\.uniner-cli\<version>\` 安装目录
95
- - `%USERPROFILE%\.claude\skills\uniner-cli\SKILL.md` 技能
93
+ 如需手动触发清理(如 npm 包已不在但残留仍在):
94
+
95
+ ```bash
96
+ uniner-cli-uninstall
97
+ ```
96
98
 
97
99
  ## 环境变量
98
100
 
@@ -104,13 +106,19 @@ npm uninstall -g uniner-cli
104
106
 
105
107
  确认 npm 全局 bin 在 PATH 上:
106
108
 
107
- ```powershell
109
+ ```bash
110
+ # macOS / Linux
111
+ npm config get prefix
112
+ echo $PATH | tr ':' '\n' | grep npm
113
+
114
+ # Windows (PowerShell)
108
115
  npm config get prefix
109
- # 应输出类似 C:\Users\xxx\AppData\Roaming\npm
110
116
  $env:Path -split ';' | Where-Object { $_ -like '*npm*' }
111
117
  ```
112
118
 
113
- 如果没有,手动加 `%APPDATA%\npm` 到 PATH。
119
+ 如果没有,手动将 npm 全局 bin 目录加入 PATH:
120
+ - macOS / Linux: `$(npm config get prefix)/bin`
121
+ - Windows: `%APPDATA%\npm`
114
122
 
115
123
  ### 安装时下载失败 / 超时
116
124
 
@@ -140,7 +148,7 @@ npm uninstall -g uniner-cli
140
148
  npm install -g uniner-cli
141
149
  ```
142
150
 
143
- ## 备选安装方式(不走 npm)
151
+ ## 备选安装方式(仅 Windows,不走 npm)
144
152
 
145
153
  如果你不想要 Node.js 依赖,可以用 PowerShell 一行安装(基于 Inno Setup 安装器):
146
154
 
@@ -148,6 +156,8 @@ npm install -g uniner-cli
148
156
  iwr https://static.uniner.com/soft/editor/uninercli/install.ps1 -UseBasicParsing | iex
149
157
  ```
150
158
 
159
+ > macOS / Linux 用户请使用 npm 安装。
160
+
151
161
  两种方式互不干扰,但**不要**同时启用——会有两份 SKILL.md / 两份二进制。
152
162
 
153
163
  ## 链接
@@ -156,6 +166,16 @@ iwr https://static.uniner.com/soft/editor/uninercli/install.ps1 -UseBasicParsing
156
166
  - 技术支持:support@uniner.com
157
167
  - Claude Code:[https://claude.com/claude-code](https://claude.com/claude-code)
158
168
 
169
+ ## Changelog
170
+
171
+ ### 1.0.6 (2026-06-28)
172
+
173
+ - **fix**: 修复 `npm uninstall -g uniner-cli` 未清理 `~/.uniner-cli/` 和 `~/.claude/skills/uniner-cli/SKILL.md` 的问题(新增 `preuninstall` 钩子)
174
+
175
+ ### 1.0.5 (2026-06-27)
176
+
177
+ - 首个公开版本,支持 Windows 10/11 (x64)、macOS (Apple Silicon)、Linux (x64)
178
+
159
179
  ## License
160
180
 
161
181
  Proprietary. (C) 2026 Uniner Tech.
@@ -1,145 +1,48 @@
1
- #!/usr/bin/env node
2
- // uniner-cli-uninstall - removes the runtime tree and Claude Code skill.
3
- //
4
- // npm 7+ no longer runs preuninstall/postuninstall lifecycle scripts on
5
- // uninstall (security tightening). So we ship this command for users to
6
- // run BEFORE `npm uninstall -g uniner-cli` to clean their machine.
7
- //
8
- // Recommended workflow:
9
- // uniner-cli-uninstall # this script: clean ~/.uniner-cli/ + skill
10
- // npm uninstall -g uniner-cli # npm: remove the npm package itself
11
- //
12
- // Idempotent: if files are already gone, just no-ops.
13
-
14
- 'use strict';
15
-
16
- const fs = require('fs');
17
- const path = require('path');
18
-
19
- const {
20
- pkg,
21
- rootDir,
22
- versionDir,
23
- skillDir,
24
- skillFile,
25
- } = require('../lib/paths');
26
-
27
- function log(msg) { console.log(`[uniner-cli] ${msg}`); }
28
- function warn(msg) { console.warn(`[uniner-cli] [WARN] ${msg}`); }
29
-
30
- function rmRf(p) {
31
- try {
32
- if (fs.existsSync(p)) {
33
- fs.rmSync(p, { recursive: true, force: true });
34
- log(`Removed ${p}`);
35
- return true;
36
- }
37
- return false;
38
- } catch (e) {
39
- warn(`Could not remove ${p}: ${e.message}`);
40
- return false;
41
- }
42
- }
43
-
44
- function rmFile(p) {
45
- try {
46
- if (fs.existsSync(p)) {
47
- fs.unlinkSync(p);
48
- log(`Removed ${p}`);
49
- return true;
50
- }
51
- return false;
52
- } catch (e) {
53
- warn(`Could not remove ${p}: ${e.message}`);
54
- return false;
55
- }
56
- }
57
-
58
- function main() {
59
- console.log('');
60
- console.log('==========================================');
61
- console.log(' uniner-cli uninstall cleanup');
62
- console.log('==========================================');
63
-
64
- const myVersion = pkg.version;
65
- const myVersionDir = versionDir(myVersion);
66
- let didSomething = false;
67
-
68
- // 1. Remove our version's runtime tree.
69
- if (rmRf(myVersionDir)) didSomething = true;
70
-
71
- // 2. If ~/.uniner-cli/ has no other versions, drop the whole directory.
72
- try {
73
- const root = rootDir();
74
- if (fs.existsSync(root)) {
75
- const remaining = fs.readdirSync(root);
76
- if (remaining.length === 0) {
77
- fs.rmdirSync(root);
78
- log(`Removed empty ${root}`);
79
- didSomething = true;
80
- } else {
81
- log(`Other uniner-cli versions still installed: ${remaining.join(', ')}`);
82
- log(`Keeping ${root}`);
83
- }
84
- }
85
- } catch (e) {
86
- warn(`Could not inspect ${rootDir()}: ${e.message}`);
87
- }
88
-
89
- // 3. Remove ONLY the SKILL.md file. NEVER rm -rf the whole skill dir,
90
- // because the user's accumulated scenarios/<name>/ subdirectories
91
- // live there and represent real work they've sedimented over time.
92
- // If they really want everything gone they can do it manually; we
93
- // surface the path below so they know where to look.
94
- const dir = skillDir();
95
- if (rmFile(skillFile())) didSomething = true;
96
-
97
- // 4. Look for scenarios/ — if there are any, keep them and tell the user.
98
- let scenariosKept = 0;
99
- const scenariosDir = path.join(dir, 'scenarios');
100
- if (fs.existsSync(scenariosDir)) {
101
- try {
102
- scenariosKept = fs.readdirSync(scenariosDir).filter(n => {
103
- const meta = path.join(scenariosDir, n, 'meta.json');
104
- return fs.existsSync(meta);
105
- }).length;
106
- } catch (_) {
107
- // best-effort
108
- }
109
- }
110
-
111
- // 5. If skill dir is now genuinely empty, remove it. Otherwise keep it.
112
- try {
113
- if (fs.existsSync(dir)) {
114
- const remaining = fs.readdirSync(dir);
115
- if (remaining.length === 0) {
116
- fs.rmdirSync(dir);
117
- log(`Removed empty ${dir}`);
118
- didSomething = true;
119
- }
120
- }
121
- } catch (_) {
122
- // best-effort
123
- }
124
-
125
- if (!didSomething) {
126
- log('Nothing to clean — already removed.');
127
- }
128
-
129
- console.log('');
130
- if (scenariosKept > 0) {
131
- console.log('==========================================');
132
- console.log(` ${scenariosKept} scenario(s) PRESERVED at:`);
133
- console.log(` ${scenariosDir}`);
134
- console.log(' (your accumulated knowledge — not deleted)');
135
- console.log(' To delete them too:');
136
- console.log(` rm -rf "${scenariosDir}" # bash`);
137
- console.log(` Remove-Item "${scenariosDir}" -Recurse -Force # PowerShell`);
138
- console.log('==========================================');
139
- }
140
- console.log('Cleanup done. To complete uninstall, also run:');
141
- console.log(' npm uninstall -g uniner-cli');
142
- console.log('');
143
- }
144
-
145
- main();
1
+ #!/usr/bin/env node
2
+ // uniner-cli-uninstall — manual cleanup command.
3
+ //
4
+ // Since v1.0.6 we register a `preuninstall` lifecycle hook that does the
5
+ // same cleanup automatically when you run `npm uninstall -g uniner-cli`.
6
+ // This manual command is still available for users who want verbose
7
+ // feedback, or who want to clean up without going through npm
8
+ // (e.g. the npm package is already gone but the runtime files remain).
9
+ //
10
+ // Recommended workflow:
11
+ // npm uninstall -g uniner-cli # hook runs the cleanup automatically
12
+ //
13
+ // If something is still left behind, run this to force a verbose cleanup:
14
+ // npx uniner-cli-uninstall # before uninstalling
15
+ // # or, after npm already removed the package:
16
+ // node ~/.uniner-cli/<version>/bin/uniner-cli-uninstall.js (if the shim is gone)
17
+ //
18
+ // Idempotent: if files are already gone, just no-ops.
19
+
20
+ 'use strict';
21
+
22
+ const { cleanup } = require('../scripts/cleanup');
23
+
24
+ console.log('');
25
+ console.log('==========================================');
26
+ console.log(' uniner-cli uninstall cleanup');
27
+ console.log('==========================================');
28
+
29
+ const result = cleanup({ verbose: true });
30
+
31
+ console.log('');
32
+ if (result.scenariosKept > 0) {
33
+ console.log('==========================================');
34
+ console.log(` ${result.scenariosKept} scenario(s) PRESERVED at:`);
35
+ console.log(` ${result.scenariosDir}`);
36
+ console.log(' (your accumulated knowledge — not deleted)');
37
+ console.log(' To delete them too:');
38
+ console.log(` rm -rf "${result.scenariosDir}" # bash`);
39
+ console.log(` Remove-Item "${result.scenariosDir}" -Recurse -Force # PowerShell`);
40
+ console.log('==========================================');
41
+ }
42
+
43
+ if (result.didSomething) {
44
+ console.log('Cleanup done.');
45
+ } else {
46
+ console.log('Nothing to clean — already removed.');
47
+ }
48
+ console.log('');
@@ -5,7 +5,7 @@ description: RPA 自动化 CLI 工具,提供浏览器自动化、Excel 操作
5
5
 
6
6
  # uniner-cli Skill
7
7
 
8
- > 由 UninerCLI Installer v{{VERSION}} 自动安装到 `%USERPROFILE%\.claude\skills\uniner-cli\`
8
+ > 由 UninerCLI Installer v{{VERSION}} 自动安装到 `~/.claude/skills/uniner-cli/`
9
9
  > 安装位置(绝对路径):`{{INSTALL_PATH}}`
10
10
 
11
11
  ## 🛑 任务结束硬性规则(最重要,先读这一节)
@@ -152,7 +152,7 @@ uniner-cli [全局选项] <模块> <子命令> <命令> [命令选项]
152
152
  ## 关键注意事项(6 条 TOC,详情看链接)
153
153
 
154
154
  1. **Chrome CDP 自动管理** — 别手动 kill Chrome;客户机必须装 Chrome 或 Edge
155
- 2. **session 持久化** — `$TEMP\.uniner-cli\sessions.json`;CLI 退出后用 `web browser reconnect` 恢复
155
+ 2. **session 持久化** — `$TEMP/uniner-cli-sessions/sessions.json`(Windows: `%TEMP%\uniner-cli-sessions\sessions.json`);CLI 退出后用 `web browser reconnect` 恢复
156
156
  3. **Vue/Element UI 组件** — 日期选择器、级联下拉等,用 `execute-js` 直接操作组件模型,不要硬 input
157
157
  4. **desktop 返回 -999** = Agent 服务未启动;此时改用 `web action execute-js` 或 `tool system execute`
158
158
  5. **Linux 平台** — Playwright 默认 HTTPS-only,访问 HTTP 站点需改 wrapper 加 `--disable-features=HttpsOnlyMode`,或用 Firefox `--browser-type 1`
@@ -162,14 +162,14 @@ uniner-cli [全局选项] <模块> <子命令> <命令> [命令选项]
162
162
 
163
163
  | 主题 | 路径 |
164
164
  |---|---|
165
- | 命令树全集 + 三级参数 | [{{INSTALL_PATH}}\skills\cli\SKILL.md]({{INSTALL_PATH}}\skills\cli\SKILL.md) |
166
- | SDK ↔ CLI 命令映射 | [{{INSTALL_PATH}}\skills\cli\sdk-cli-mapping.md]({{INSTALL_PATH}}\skills\cli\sdk-cli-mapping.md) |
167
- | Windows 配置指南 | [{{INSTALL_PATH}}\skills\windows-setup-guide.md]({{INSTALL_PATH}}\skills\windows-setup-guide.md) |
168
- | Linux 配置指南 | [{{INSTALL_PATH}}\skills\linux-setup-guide.md]({{INSTALL_PATH}}\skills\linux-setup-guide.md) |
169
- | 实战案例(TMS 抓单) | [{{INSTALL_PATH}}\skills\tms-workflow-test-report.md]({{INSTALL_PATH}}\skills\tms-workflow-test-report.md) |
170
- | Helper 脚本(Chrome 启动 / Vue 日期 / 表格提取) | [{{INSTALL_PATH}}\skills\cli\helpers\]({{INSTALL_PATH}}\skills\cli\helpers\) |
171
- | 用户手册 | [{{INSTALL_PATH}}\docs\]({{INSTALL_PATH}}\docs\) |
172
- | 工作目录(保存截图/Excel/下载) | [{{INSTALL_PATH}}\workdir\]({{INSTALL_PATH}}\workdir\) |
165
+ | 命令树全集 + 三级参数 | [{{INSTALL_PATH}}/skills/cli/SKILL.md]({{INSTALL_PATH}}/skills/cli/SKILL.md) |
166
+ | SDK ↔ CLI 命令映射 | [{{INSTALL_PATH}}/skills/cli/sdk-cli-mapping.md]({{INSTALL_PATH}}/skills/cli/sdk-cli-mapping.md) |
167
+ | Windows 配置指南 | [{{INSTALL_PATH}}/skills/windows-setup-guide.md]({{INSTALL_PATH}}/skills/windows-setup-guide.md) |
168
+ | Linux 配置指南 | [{{INSTALL_PATH}}/skills/linux-setup-guide.md]({{INSTALL_PATH}}/skills/linux-setup-guide.md) |
169
+ | 实战案例(TMS 抓单) | [{{INSTALL_PATH}}/skills/tms-workflow-test-report.md]({{INSTALL_PATH}}/skills/tms-workflow-test-report.md) |
170
+ | Helper 脚本(Chrome 启动 / Vue 日期 / 表格提取) | [{{INSTALL_PATH}}/skills/cli/helpers/]({{INSTALL_PATH}}/skills/cli/helpers/) |
171
+ | 用户手册 | [{{INSTALL_PATH}}/docs/]({{INSTALL_PATH}}/docs/) |
172
+ | 工作目录(保存截图/Excel/下载) | [{{INSTALL_PATH}}/workdir/]({{INSTALL_PATH}}/workdir/) |
173
173
 
174
174
  ## 典型完整流程(参考)
175
175
 
@@ -182,8 +182,8 @@ uniner-cli web browser open --url "https://example.com/login"
182
182
  # 2. 输账号密码 + 截验证码 + OCR
183
183
  uniner-cli web element input --xpath "//input[@id='user']" --value "u01"
184
184
  uniner-cli web element input --xpath "//input[@id='pwd']" --value "p01"
185
- uniner-cli web element screenshot --xpath "//img[@id='captcha']" --output "{{INSTALL_PATH}}\workdir\cap.png"
186
- uniner-cli tool ocr ddddocr --img "{{INSTALL_PATH}}\workdir\cap.png"
185
+ uniner-cli web element screenshot --xpath "//img[@id='captcha']" --output "{{INSTALL_PATH}}/workdir/cap.png"
186
+ uniner-cli tool ocr ddddocr --img "{{INSTALL_PATH}}/workdir/cap.png"
187
187
  # 从返回 JSON 的 data.result 取识别文本
188
188
 
189
189
  # 3. 提交登录
@@ -195,8 +195,8 @@ uniner-cli web navigation wait --type element --xpath "//table"
195
195
  uniner-cli web action execute-js --code "return Array.from(document.querySelectorAll('table tr')).map(r => Array.from(r.cells).map(c => c.textContent.trim()))"
196
196
 
197
197
  # 5. 写 Excel
198
- uniner-cli excel file create --file "{{INSTALL_PATH}}\workdir\out.xlsx"
199
- uniner-cli excel data write-range --file "{{INSTALL_PATH}}\workdir\out.xlsx" --cell A1 --values "<上一步的 JSON 二维数组>"
198
+ uniner-cli excel file create --file "{{INSTALL_PATH}}/workdir/out.xlsx"
199
+ uniner-cli excel data write-range --file "{{INSTALL_PATH}}/workdir/out.xlsx" --cell A1 --values "<上一步的 JSON 二维数组>"
200
200
 
201
201
  # 6. 关浏览器
202
202
  uniner-cli web browser close
@@ -209,9 +209,11 @@ uniner-cli web browser close
209
209
  每个场景一个目录,三件套:`meta.json` + `steps.json` + `SKILL.md`,存到:
210
210
 
211
211
  ```
212
- %USERPROFILE%\.claude\skills\uniner-cli\scenarios\<kebab-case-name>\
212
+ ~/.claude/skills/uniner-cli/scenarios/<kebab-case-name>/
213
213
  ```
214
214
 
215
+ (Windows: `%USERPROFILE%\.claude\skills\uniner-cli\scenarios\<kebab-case-name>\`)
216
+
215
217
  **4 步标准流程**:
216
218
 
217
219
  ```bash
@@ -331,7 +333,7 @@ Open Claude Code, paste this:
331
333
  I ran 'uniner-cli skill run' and step 'click-search' failed.
332
334
  Error: timeout
333
335
  Please look at the scenario at:
334
- C:\Users\xxx\.claude\skills\uniner-cli\scenarios\tms-export
336
+ ~/.claude/skills/uniner-cli/scenarios/tms-export
335
337
  Especially steps.json. Help me figure out why and suggest a fix.
336
338
  ──────────────────────────────────
337
339
  ```
@@ -368,4 +370,4 @@ uniner-cli skill delete <name> --yes # 删除(默认 dry-run)
368
370
  - `uniner-cli --help` 看一级
369
371
  - `uniner-cli web --help` 看二级
370
372
  - `uniner-cli web element click --help` 看具体参数
371
- - 仍不确定 → Read [{{INSTALL_PATH}}\skills\cli\SKILL.md]({{INSTALL_PATH}}\skills\cli\SKILL.md)
373
+ - 仍不确定 → Read [{{INSTALL_PATH}}/skills/cli/SKILL.md]({{INSTALL_PATH}}/skills/cli/SKILL.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uniner-cli",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "uniner-cli — RPA automation CLI: browser automation (Playwright/CDP), Excel, OCR, desktop/SAP, crypto/HTTP/email tools. Auto-installs a Claude Code skill on install.",
5
5
  "keywords": [
6
6
  "rpa",
@@ -25,7 +25,8 @@
25
25
  "uniner-cli-uninstall": "bin/uniner-cli-uninstall.js"
26
26
  },
27
27
  "scripts": {
28
- "postinstall": "node scripts/postinstall.js"
28
+ "postinstall": "node scripts/postinstall.js",
29
+ "preuninstall": "node scripts/preuninstall.js"
29
30
  },
30
31
  "engines": {
31
32
  "node": "\u003e=14"
@@ -0,0 +1,123 @@
1
+ // Shared cleanup logic for uninstall.
2
+ // Extracted so both the npm lifecycle hook (preuninstall.js) and the
3
+ // manual command (bin/uniner-cli-uninstall.js) can share it.
4
+
5
+ 'use strict';
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ const {
11
+ rootDir,
12
+ skillDir,
13
+ skillFile,
14
+ } = require('../lib/paths');
15
+
16
+ function rmRf(p) {
17
+ try {
18
+ if (fs.existsSync(p)) {
19
+ fs.rmSync(p, { recursive: true, force: true });
20
+ return true;
21
+ }
22
+ return false;
23
+ } catch (_) {
24
+ return false;
25
+ }
26
+ }
27
+
28
+ function rmFile(p) {
29
+ try {
30
+ if (fs.existsSync(p)) {
31
+ fs.unlinkSync(p);
32
+ return true;
33
+ }
34
+ return false;
35
+ } catch (_) {
36
+ return false;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Run the full uninstall cleanup.
42
+ *
43
+ * @param {object} [opts]
44
+ * @param {boolean} [opts.verbose=false] Print progress lines to stdout.
45
+ * The preuninstall hook should pass false
46
+ * because npm swallows lifecycle output
47
+ * unless --verbose is used; the manual
48
+ * command passes true so the user sees
49
+ * what happened.
50
+ * @returns {{ didSomething: boolean, scenariosKept: number, scenariosDir: string|null }}
51
+ */
52
+ function cleanup(opts) {
53
+ const verbose = (opts && opts.verbose) || false;
54
+
55
+ function log(msg) {
56
+ if (verbose) {
57
+ console.log(`[uniner-cli] ${msg}`);
58
+ }
59
+ }
60
+
61
+ const root = rootDir();
62
+ let didSomething = false;
63
+
64
+ // 1. Remove the entire ~/.uniner-cli/ tree (all versions).
65
+ // The version-specific layout exists so the installer can support
66
+ // multi-version coexistence, but on uninstall the user wants a full
67
+ // removal — orphaned old version dirs would just waste disk space.
68
+ if (rmRf(root)) {
69
+ log(`Removed ${root}`);
70
+ didSomething = true;
71
+ }
72
+
73
+ // 2. Remove ONLY the SKILL.md file. NEVER rm -rf the whole skill dir,
74
+ // because the user's accumulated scenarios/<name>/ subdirectories
75
+ // live there and represent real work they've sedimented over time.
76
+ const dir = skillDir();
77
+ if (rmFile(skillFile())) {
78
+ log(`Removed ${skillFile()}`);
79
+ didSomething = true;
80
+ }
81
+
82
+ // 3. Count scenarios/ subdirs that contain a meta.json.
83
+ let scenariosKept = 0;
84
+ const scenariosPath = path.join(dir, 'scenarios');
85
+ if (fs.existsSync(scenariosPath)) {
86
+ try {
87
+ scenariosKept = fs.readdirSync(scenariosPath).filter(n => {
88
+ const meta = path.join(scenariosPath, n, 'meta.json');
89
+ return fs.existsSync(meta);
90
+ }).length;
91
+ } catch (_) {
92
+ // best-effort
93
+ }
94
+ }
95
+
96
+ // 4. If skill dir is now genuinely empty, remove it. Otherwise keep it.
97
+ try {
98
+ if (fs.existsSync(dir)) {
99
+ const remaining = fs.readdirSync(dir);
100
+ if (remaining.length === 0) {
101
+ fs.rmdirSync(dir);
102
+ log(`Removed empty ${dir}`);
103
+ didSomething = true;
104
+ } else if (verbose) {
105
+ log(`Keeping ${dir} (${scenariosKept} scenario(s) preserved)`);
106
+ }
107
+ }
108
+ } catch (_) {
109
+ // best-effort
110
+ }
111
+
112
+ if (!didSomething) {
113
+ log('Nothing to clean — already removed.');
114
+ }
115
+
116
+ return {
117
+ didSomething,
118
+ scenariosKept,
119
+ scenariosDir: scenariosKept > 0 ? scenariosPath : null,
120
+ };
121
+ }
122
+
123
+ module.exports = { cleanup };
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+ // preuninstall.js — npm lifecycle hook, runs automatically on
3
+ // `npm uninstall -g uniner-cli`.
4
+ //
5
+ // Cleans up the runtime tree (~/.uniner-cli/<version>/) and the skill file
6
+ // (~/.claude/skills/uniner-cli/SKILL.md). Preserves user scenarios under
7
+ // ~/.claude/skills/uniner-cli/scenarios/ — those represent accumulated
8
+ // knowledge and are never deleted automatically.
9
+ //
10
+ // IMPORTANT: This script MUST exit 0 in all cases, otherwise it would
11
+ // abort the user's `npm uninstall`. Errors are swallowed silently.
12
+ //
13
+ // The manual `uniner-cli-uninstall` command is still useful for users who
14
+ // want verbose feedback, or who want to clean up before the npm package is
15
+ // gone (e.g. if the hook somehow didn't fire).
16
+
17
+ 'use strict';
18
+
19
+ const { cleanup } = require('./cleanup');
20
+
21
+ try {
22
+ cleanup({ verbose: false });
23
+ } catch (_) {
24
+ // Never fail the uninstall — swallow all errors.
25
+ }