uniner-cli 1.0.6 → 1.0.7

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
@@ -2,10 +2,10 @@
2
2
 
3
3
  `uniner-cli` 是 [Uniner](https://www.uniner.com/) 的 RPA 自动化命令行工具,提供:
4
4
 
5
- - **Web 自动化**(Playwright + CDP)—— 浏览器抓取、表单填写、元素交互
5
+ - **Web 自动化** —— 浏览器抓取、表单填写、元素交互
6
6
  - **Excel 操作** —— 读写、批量处理、样式
7
- - **OCR 识别** —— ddddocr(验证码)+ PaddleOCR(通用)
8
- - **桌面 / SAP 自动化**(仅 Windows)
7
+ - **OCR 识别** —— 支持验证码和通用 OCR
8
+ - **桌面 / SAP 自动化** —— 支持桌面软件的自动化(仅 Windows)
9
9
  - **通用工具** —— 加密、HTTP、邮件、文件、PDF、压缩
10
10
 
11
11
  安装时会自动注册一个 [Claude Code](https://claude.com/claude-code) 技能(`~/.claude/skills/uniner-cli/SKILL.md`),让 Claude 一启动就能识别并调用本 CLI。
@@ -14,18 +14,27 @@
14
14
 
15
15
  ## 安装
16
16
 
17
+ 全局安装:
18
+
17
19
  ```bash
18
20
  npm install -g uniner-cli
19
21
  ```
20
22
 
21
- 安装过程:
23
+ 当前目录安装:
24
+
25
+ ```bash
26
+ npm install uniner-cli
27
+ ```
22
28
 
23
- 1. npm 装本壳包(约 50KB)
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`
29
+ > 本地安装后通常通过 `npx uniner-cli --help` 或项目里的 npm scripts 调用;只有全局安装才适合在任意终端直接运行 `uniner-cli`。
27
30
 
28
- 完成后任意终端:
31
+ 安装过程中会自动完成:
32
+
33
+ 1. 下载并配置当前平台所需的 uniner-cli 运行时
34
+ 2. 注册 Claude Code 技能
35
+ 3. 准备 `uniner-cli` 命令入口
36
+
37
+ 全局安装完成后任意终端:
29
38
 
30
39
  ```bash
31
40
  uniner-cli --help
@@ -61,7 +70,7 @@ uniner-cli excel data write --file out.xlsx --cell B2 --value "hello"
61
70
  uniner-cli tool ocr ddddocr --img captcha.png
62
71
  ```
63
72
 
64
- 完整命令树(一级 + 二级 + 三级参数)见安装后 `~/.uniner-cli/<version>/skills/cli/SKILL.md`。
73
+ 完整命令树(一级 + 二级 + 三级参数)可通过 `uniner-cli --help` 查看。
65
74
 
66
75
  ## 升级
67
76
 
@@ -79,26 +88,45 @@ npm install -g uniner-cli@latest
79
88
 
80
89
  ## 卸载
81
90
 
91
+ > **重要提示**:直接使用 `npm uninstall` 可能无法清理运行时文件。
92
+ > 请使用 CLI 自带的一键卸载命令以确保完全清理。
93
+
94
+ **推荐卸载流程**:
95
+
96
+ ```bash
97
+ uniner-cli uninstall
98
+ ```
99
+
100
+ 该命令会先清理运行时残留,再自动判断安装范围并移除 npm 包本体:
101
+
102
+ - 全局安装:执行 `npm uninstall -g uniner-cli`
103
+ - 本地安装:在当前项目执行 `npm uninstall uniner-cli`
104
+
105
+ 如果自动判断不符合预期,可显式指定:
106
+
82
107
  ```bash
83
- npm uninstall -g uniner-cli
108
+ uniner-cli uninstall --global
109
+ uniner-cli uninstall --local
84
110
  ```
85
111
 
86
- 卸载时 `preuninstall` 钩子会自动清理:
112
+ 清理内容:
87
113
 
88
114
  - `~/.uniner-cli/` 整个运行时目录(含所有版本)
89
115
  - `~/.claude/skills/uniner-cli/SKILL.md` 技能文件
90
116
 
91
- > `~/.claude/skills/uniner-cli/scenarios/` 下用户积累的场景数据不会被自动删除,需手动清理。
117
+ > `~/.claude/skills/uniner-cli/scenarios/` 下用户积累的场景数据不会被删除,需手动清理。
92
118
 
93
- 如需手动触发清理(如 npm 包已不在但残留仍在):
119
+ **备选方式**(如果 npm 包已经先被卸载,`uniner-cli` 不在 PATH 上):
94
120
 
95
121
  ```bash
96
- uniner-cli-uninstall
122
+ node ~/.uniner-cli/cleanup.js
97
123
  ```
98
124
 
125
+ > 该脚本只清理运行时残留,不再卸载 npm 包本体。
126
+
99
127
  ## 环境变量
100
128
 
101
- 无需手动配置。npm 全局 bin 目录已自动加入 PATH。
129
+ 无需手动配置。全局安装时,npm 会把命令链接到全局 bin 目录;如果终端识别不到 `uniner-cli`,按下方排查 PATH。
102
130
 
103
131
  ## 故障排查
104
132
 
@@ -131,35 +159,17 @@ npm config get https-proxy
131
159
  npm config set https-proxy http://your-proxy:port
132
160
  ```
133
161
 
134
- 或者直接走原生安装方式(不走 npm):
135
-
136
- ```powershell
137
- iwr https://static.uniner.com/soft/editor/uninercli/install.ps1 -UseBasicParsing | iex
138
- ```
162
+ ### 安装卡住 / 解压慢
139
163
 
140
- ### postinstall 卡在解压
141
-
142
- 杀软(360 / 火绒)会扫描 1279 个新文件,慢则 5–10 分钟。让它跑完即可。
164
+ 杀软(360 / 火绒)可能会扫描大量新文件,慢则 5–10 分钟。让它跑完即可。
143
165
 
144
166
  ### 想重装
145
167
 
146
168
  ```bash
147
- npm uninstall -g uniner-cli
169
+ uniner-cli uninstall
148
170
  npm install -g uniner-cli
149
171
  ```
150
172
 
151
- ## 备选安装方式(仅 Windows,不走 npm)
152
-
153
- 如果你不想要 Node.js 依赖,可以用 PowerShell 一行安装(基于 Inno Setup 安装器):
154
-
155
- ```powershell
156
- iwr https://static.uniner.com/soft/editor/uninercli/install.ps1 -UseBasicParsing | iex
157
- ```
158
-
159
- > macOS / Linux 用户请使用 npm 安装。
160
-
161
- 两种方式互不干扰,但**不要**同时启用——会有两份 SKILL.md / 两份二进制。
162
-
163
173
  ## 链接
164
174
 
165
175
  - 官网:[https://www.uniner.com/](https://www.uniner.com/)
@@ -168,9 +178,13 @@ iwr https://static.uniner.com/soft/editor/uninercli/install.ps1 -UseBasicParsing
168
178
 
169
179
  ## Changelog
170
180
 
181
+ ### 1.0.7 (2026-06-29)
182
+
183
+ - **fix**: 新增 `uniner-cli uninstall`,自动判断全局 / 本地安装范围,完整清理运行时文件并移除 npm 包。
184
+
171
185
  ### 1.0.6 (2026-06-28)
172
186
 
173
- - **fix**: 修复 `npm uninstall -g uniner-cli` 未清理 `~/.uniner-cli/` 和 `~/.claude/skills/uniner-cli/SKILL.md` 的问题(新增 `preuninstall` 钩子)
187
+ - **fix**: 修复 `npm uninstall -g uniner-cli` 未清理 `~/.uniner-cli/` 和 `~/.claude/skills/uniner-cli/SKILL.md` 的问题。
174
188
 
175
189
  ### 1.0.5 (2026-06-27)
176
190
 
@@ -1,48 +1,84 @@
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('');
1
+ #!/usr/bin/env node
2
+ // uniner-cli-uninstall — manual cleanup command.
3
+ //
4
+ // Background:
5
+ // npm 7+ (including npm 11) does NOT reliably run the `preuninstall`
6
+ // lifecycle hook for globally installed packages. This means
7
+ // `npm uninstall -g uniner-cli` removes the npm package but leaves
8
+ // residual files behind:
9
+ // - ~/.uniner-cli/ (runtime binaries, skills, docs)
10
+ // - ~/.claude/skills/uniner-cli/SKILL.md (Claude Code skill file)
11
+ //
12
+ // To solve this, postinstall writes a self-contained cleanup script to
13
+ // ~/.uniner-cli/cleanup.js. That file survives `npm uninstall` and can
14
+ // always be run manually.
15
+ //
16
+ // Recommended workflow (ensures a clean uninstall):
17
+ // npm uninstall -g uniner-cli
18
+ // node ~/.uniner-cli/cleanup.js # always works, even after npm removes the package
19
+ //
20
+ // Alternative (only works BEFORE you run `npm uninstall -g`):
21
+ // uniner-cli-uninstall # this command; removed along with the package
22
+ // npm uninstall -g uniner-cli
23
+ //
24
+ // Idempotent: if files are already gone, just no-ops.
25
+
26
+ 'use strict';
27
+
28
+ const path = require('path');
29
+ const os = require('os');
30
+ const fs = require('fs');
31
+
32
+ // Prefer the standalone cleanup.js if it exists (written by postinstall,
33
+ // survives npm uninstall). Fall back to the in-package cleanup module
34
+ // (only works while the npm package is still installed).
35
+ const standalonePath = path.join(os.homedir(), '.uniner-cli', 'cleanup.js');
36
+
37
+ if (fs.existsSync(standalonePath)) {
38
+ // Delegate to the standalone script. It handles everything: removing
39
+ // ~/.uniner-cli/, removing SKILL.md, preserving scenarios/.
40
+ // Use spawnSync to avoid any require() path issues.
41
+ const { spawnSync } = require('child_process');
42
+ const result = spawnSync(process.execPath, [standalonePath], {
43
+ stdio: 'inherit',
44
+ });
45
+ process.exit(result.status || 0);
46
+ }
47
+
48
+ // Fallback: the standalone script is gone (old install before this fix).
49
+ // Use the in-package cleanup module.
50
+ 'use strict';
51
+
52
+ const { cleanup } = require('../scripts/cleanup');
53
+
54
+ console.log('');
55
+ console.log('==========================================');
56
+ console.log(' uniner-cli uninstall cleanup (fallback)');
57
+ console.log('==========================================');
58
+ console.log('');
59
+ console.log(' Note: ~/.uniner-cli/cleanup.js not found.');
60
+ console.log(' This fallback will be removed in a future version.');
61
+ console.log(' Reinstall to get the standalone cleanup script:');
62
+ console.log(' npm install -g uniner-cli');
63
+ console.log('');
64
+
65
+ const result = cleanup({ verbose: true });
66
+
67
+ console.log('');
68
+ if (result.scenariosKept > 0) {
69
+ console.log('==========================================');
70
+ console.log(` ${result.scenariosKept} scenario(s) PRESERVED at:`);
71
+ console.log(` ${result.scenariosDir}`);
72
+ console.log(' (your accumulated knowledge — not deleted)');
73
+ console.log(' To delete them too:');
74
+ console.log(` rm -rf "${result.scenariosDir}" # bash`);
75
+ console.log(` Remove-Item "${result.scenariosDir}" -Recurse -Force # PowerShell`);
76
+ console.log('==========================================');
77
+ }
78
+
79
+ if (result.didSomething) {
80
+ console.log('Cleanup done.');
81
+ } else {
82
+ console.log('Nothing to clean — already removed.');
83
+ }
84
+ console.log('');
package/bin/uniner-cli.js CHANGED
@@ -1,15 +1,207 @@
1
1
  #!/usr/bin/env node
2
- // Tiny launcher: forward every CLI invocation to the real PyInstaller exe.
2
+ // Tiny launcher: handle npm-owned commands, then forward everything else to
3
+ // the real PyInstaller exe.
3
4
  //
4
5
  // `npm install -g uniner-cli` registers this file as the global `uniner-cli`
5
- // command (because of the `bin` entry in package.json). We just spawn the real
6
- // binary with the same argv/stdio and exit with its status code.
6
+ // command (because of the `bin` entry in package.json).
7
7
 
8
8
  'use strict';
9
9
 
10
10
  const { spawnSync } = require('child_process');
11
11
  const fs = require('fs');
12
+ const path = require('path');
12
13
  const { realBinaryPath, isInstalled, pkg } = require('../lib/paths');
14
+ const { cleanup } = require('../scripts/cleanup');
15
+
16
+ function npmCommand() {
17
+ return process.platform === 'win32' ? 'npm.cmd' : 'npm';
18
+ }
19
+
20
+ function printUninstallHelp() {
21
+ console.log('');
22
+ console.log('Usage: uniner-cli uninstall [--global|--local]');
23
+ console.log('');
24
+ console.log('Cleans ~/.uniner-cli and the installed Claude Code skill,');
25
+ console.log('then removes this npm package from the detected install scope.');
26
+ console.log('');
27
+ }
28
+
29
+ function realPath(p) {
30
+ try {
31
+ return fs.realpathSync.native(p);
32
+ } catch (_) {
33
+ return path.resolve(p);
34
+ }
35
+ }
36
+
37
+ function samePath(a, b) {
38
+ const left = realPath(a);
39
+ const right = realPath(b);
40
+ if (process.platform === 'win32') {
41
+ return left.toLowerCase() === right.toLowerCase();
42
+ }
43
+ return left === right;
44
+ }
45
+
46
+ function npmOutput(args, opts) {
47
+ const result = spawnSync(npmCommand(), args, Object.assign({
48
+ encoding: 'utf8',
49
+ shell: false,
50
+ windowsHide: false,
51
+ }, opts || {}));
52
+ if (result.error || result.status !== 0) {
53
+ return null;
54
+ }
55
+ return (result.stdout || '').trim();
56
+ }
57
+
58
+ function packageJsonHasDependency(file) {
59
+ try {
60
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
61
+ const sections = [
62
+ parsed.dependencies,
63
+ parsed.devDependencies,
64
+ parsed.optionalDependencies,
65
+ parsed.peerDependencies,
66
+ ];
67
+ return sections.some(section => section && section[pkg.name]);
68
+ } catch (_) {
69
+ return false;
70
+ }
71
+ }
72
+
73
+ function findLocalProjectRoot(startDir) {
74
+ let dir = path.resolve(startDir);
75
+ while (true) {
76
+ const packageJson = path.join(dir, 'package.json');
77
+ if (fs.existsSync(packageJson) && packageJsonHasDependency(packageJson)) {
78
+ return dir;
79
+ }
80
+ const parent = path.dirname(dir);
81
+ if (parent === dir) {
82
+ return null;
83
+ }
84
+ dir = parent;
85
+ }
86
+ }
87
+
88
+ function localProjectFromPackageDir(packageDir) {
89
+ const nodeModulesDir = path.dirname(packageDir);
90
+ if (path.basename(nodeModulesDir) !== 'node_modules') {
91
+ return null;
92
+ }
93
+ const projectRoot = path.dirname(nodeModulesDir);
94
+ const packageJson = path.join(projectRoot, 'package.json');
95
+ if (fs.existsSync(packageJson)) {
96
+ return projectRoot;
97
+ }
98
+ return null;
99
+ }
100
+
101
+ function detectUninstallPlan(args) {
102
+ if (args.includes('--global') || args.includes('-g')) {
103
+ return {
104
+ scope: 'global',
105
+ npmArgs: ['uninstall', '-g', pkg.name],
106
+ cwd: process.cwd(),
107
+ manualCommand: `npm uninstall -g ${pkg.name}`,
108
+ };
109
+ }
110
+
111
+ if (args.includes('--local')) {
112
+ const projectRoot = findLocalProjectRoot(process.cwd()) ||
113
+ localProjectFromPackageDir(path.resolve(__dirname, '..')) ||
114
+ process.cwd();
115
+ return {
116
+ scope: 'local',
117
+ npmArgs: ['uninstall', pkg.name],
118
+ cwd: projectRoot,
119
+ manualCommand: `cd "${projectRoot}" && npm uninstall ${pkg.name}`,
120
+ };
121
+ }
122
+
123
+ const packageDir = path.resolve(__dirname, '..');
124
+ const globalRoot = npmOutput(['root', '-g']);
125
+ if (globalRoot && samePath(packageDir, path.join(globalRoot, pkg.name))) {
126
+ return {
127
+ scope: 'global',
128
+ npmArgs: ['uninstall', '-g', pkg.name],
129
+ cwd: process.cwd(),
130
+ manualCommand: `npm uninstall -g ${pkg.name}`,
131
+ };
132
+ }
133
+
134
+ const projectRoot = findLocalProjectRoot(process.cwd()) ||
135
+ localProjectFromPackageDir(packageDir);
136
+ if (projectRoot) {
137
+ return {
138
+ scope: 'local',
139
+ npmArgs: ['uninstall', pkg.name],
140
+ cwd: projectRoot,
141
+ manualCommand: `cd "${projectRoot}" && npm uninstall ${pkg.name}`,
142
+ };
143
+ }
144
+
145
+ return {
146
+ scope: 'global',
147
+ npmArgs: ['uninstall', '-g', pkg.name],
148
+ cwd: process.cwd(),
149
+ manualCommand: `npm uninstall -g ${pkg.name}`,
150
+ };
151
+ }
152
+
153
+ function runFullUninstall() {
154
+ console.log('');
155
+ console.log('==========================================');
156
+ console.log(' uniner-cli uninstall');
157
+ console.log('==========================================');
158
+
159
+ const cleanupResult = cleanup({ verbose: true });
160
+
161
+ console.log('');
162
+ const plan = detectUninstallPlan(process.argv.slice(3));
163
+ console.log(`[uniner-cli] Removing ${plan.scope} npm package...`);
164
+ if (plan.scope === 'local') {
165
+ console.log(`[uniner-cli] Project: ${plan.cwd}`);
166
+ }
167
+ const result = spawnSync(npmCommand(), plan.npmArgs, {
168
+ cwd: plan.cwd,
169
+ stdio: 'inherit',
170
+ shell: false,
171
+ windowsHide: false,
172
+ });
173
+
174
+ if (result.error) {
175
+ console.error('');
176
+ console.error(`[uniner-cli] Failed to run npm uninstall: ${result.error.message}`);
177
+ console.error('[uniner-cli] Runtime cleanup has already been attempted.');
178
+ console.error(`[uniner-cli] Please run manually: ${plan.manualCommand}`);
179
+ console.error('');
180
+ process.exit(1);
181
+ }
182
+
183
+ const status = result.status === null ? 1 : result.status;
184
+ if (status === 0) {
185
+ console.log('');
186
+ if (cleanupResult.scenariosKept > 0) {
187
+ console.log(`[uniner-cli] Preserved ${cleanupResult.scenariosKept} scenario(s) at:`);
188
+ console.log(`[uniner-cli] ${cleanupResult.scenariosDir}`);
189
+ }
190
+ console.log('[uniner-cli] Uninstall complete.');
191
+ console.log('');
192
+ }
193
+ process.exit(status);
194
+ }
195
+
196
+ if (process.argv[2] === 'uninstall' &&
197
+ (process.argv[3] === '--help' || process.argv[3] === '-h')) {
198
+ printUninstallHelp();
199
+ process.exit(0);
200
+ }
201
+
202
+ if (process.argv[2] === 'uninstall') {
203
+ runFullUninstall();
204
+ }
13
205
 
14
206
  const exe = realBinaryPath();
15
207
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uniner-cli",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
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",
@@ -47,4 +47,4 @@
47
47
  "README.md",
48
48
  "LICENSE"
49
49
  ]
50
- }
50
+ }
@@ -28,6 +28,7 @@ const { spawnSync } = require('child_process');
28
28
 
29
29
  const {
30
30
  pkg,
31
+ rootDir,
31
32
  versionDir,
32
33
  realBinaryPath,
33
34
  isInstalled,
@@ -237,6 +238,159 @@ function writeSkillMd(installPath, version) {
237
238
  }
238
239
  }
239
240
 
241
+ // ----------------------------------------------------------------------
242
+ // Write standalone cleanup script to ~/.uniner-cli/cleanup.js
243
+ //
244
+ // npm 7+ no longer reliably runs `preuninstall` hooks for global packages,
245
+ // so `npm uninstall -g uniner-cli` leaves ~/.uniner-cli/ and the SKILL.md
246
+ // behind. To give users a reliable cleanup path, we write a self-contained
247
+ // cleanup script to ~/.uniner-cli/cleanup.js during install. This file lives
248
+ // OUTSIDE the npm package directory, so it survives `npm uninstall`.
249
+ //
250
+ // Usage after `npm uninstall -g uniner-cli`:
251
+ // node ~/.uniner-cli/cleanup.js
252
+ // ----------------------------------------------------------------------
253
+
254
+ function writeCleanupScript() {
255
+ const root = path.join(os.homedir(), '.uniner-cli');
256
+ const cleanupPath = path.join(root, 'cleanup.js');
257
+
258
+ // NOTE: this string is written verbatim to disk — no require(), no
259
+ // imports, no template tokens. It must be 100% self-contained Node.js.
260
+ const script = `#!/usr/bin/env node
261
+ // cleanup.js — uninstall cleanup for uniner-cli.
262
+ //
263
+ // WHY THIS FILE EXISTS
264
+ // --------------------
265
+ // npm 7+ (including npm 11) does NOT reliably execute the \`preuninstall\`
266
+ // lifecycle hook for globally installed packages. Running
267
+ // \`npm uninstall -g uniner-cli\` removes the npm package but leaves:
268
+ // - ~/.uniner-cli/ (runtime binaries, skills, docs)
269
+ // - ~/.claude/skills/uniner-cli/SKILL.md (Claude Code skill file)
270
+ //
271
+ // This script was written to this location by the postinstall hook. Because
272
+ // it lives OUTSIDE the npm package directory, it survives \`npm uninstall\`
273
+ // and can always be invoked manually:
274
+ //
275
+ // node ~/.uniner-cli/cleanup.js
276
+ //
277
+ // Idempotent: running it twice is harmless; the second run just reports
278
+ // "nothing to clean".
279
+ //
280
+ // WHAT IT PRESERVES
281
+ // -----------------
282
+ // The \`scenarios/\` subdirectory under
283
+ // ~/.claude/skills/uniner-cli/scenarios/
284
+ // contains user-accumulated knowledge (saved workflows). Those are NEVER
285
+ // deleted by this script — only the single SKILL.md file is removed.
286
+
287
+ 'use strict';
288
+
289
+ const fs = require('fs');
290
+ const path = require('path');
291
+ const os = require('os');
292
+
293
+ const rootDir = path.join(os.homedir(), '.uniner-cli');
294
+ const claudeSkillRoot = path.join(os.homedir(), '.claude', 'skills', 'uniner-cli');
295
+ const skillFilePath = path.join(claudeSkillRoot, 'SKILL.md');
296
+ const scenariosDir = path.join(claudeSkillRoot, 'scenarios');
297
+
298
+ function rmRf(p) {
299
+ try {
300
+ if (fs.existsSync(p)) {
301
+ fs.rmSync(p, { recursive: true, force: true });
302
+ return true;
303
+ }
304
+ return false;
305
+ } catch (_) { return false; }
306
+ }
307
+
308
+ function rmFile(p) {
309
+ try {
310
+ if (fs.existsSync(p)) {
311
+ fs.unlinkSync(p);
312
+ return true;
313
+ }
314
+ return false;
315
+ } catch (_) { return false; }
316
+ }
317
+
318
+ console.log('');
319
+ console.log('==========================================');
320
+ console.log(' uniner-cli uninstall cleanup');
321
+ console.log('==========================================');
322
+
323
+ let didSomething = false;
324
+
325
+ // 1. Remove entire ~/.uniner-cli/ tree (all versions + this script itself).
326
+ if (rmRf(rootDir)) {
327
+ console.log('[uniner-cli] Removed ' + rootDir);
328
+ didSomething = true;
329
+ }
330
+
331
+ // 2. Remove ONLY the SKILL.md file. NEVER rm -rf the whole skill dir —
332
+ // scenarios/ lives there and is the user's accumulated knowledge.
333
+ if (rmFile(skillFilePath)) {
334
+ console.log('[uniner-cli] Removed ' + skillFilePath);
335
+ didSomething = true;
336
+ }
337
+
338
+ // 3. Count preserved scenarios.
339
+ let scenariosKept = 0;
340
+ if (fs.existsSync(scenariosDir)) {
341
+ try {
342
+ scenariosKept = fs.readdirSync(scenariosDir).filter(function(n) {
343
+ return fs.existsSync(path.join(scenariosDir, n, 'meta.json'));
344
+ }).length;
345
+ } catch (_) {}
346
+ }
347
+
348
+ // 4. If the skill dir is now empty, remove it. Otherwise keep it.
349
+ try {
350
+ if (fs.existsSync(claudeSkillRoot)) {
351
+ var remaining = fs.readdirSync(claudeSkillRoot);
352
+ if (remaining.length === 0) {
353
+ fs.rmdirSync(claudeSkillRoot);
354
+ console.log('[uniner-cli] Removed empty ' + claudeSkillRoot);
355
+ didSomething = true;
356
+ } else if (scenariosKept > 0) {
357
+ console.log('[uniner-cli] Keeping ' + claudeSkillRoot +
358
+ ' (' + scenariosKept + ' scenario(s) preserved)');
359
+ }
360
+ }
361
+ } catch (_) {}
362
+
363
+ console.log('');
364
+ if (scenariosKept > 0) {
365
+ console.log(' ' + scenariosKept + ' scenario(s) PRESERVED at:');
366
+ console.log(' ' + scenariosDir);
367
+ console.log(' (your accumulated knowledge — not deleted)');
368
+ console.log(' To delete them too:');
369
+ console.log(' rm -rf "' + scenariosDir + '" # bash');
370
+ console.log(' Remove-Item "' + scenariosDir + '" -Recurse -Force # PowerShell');
371
+ console.log('');
372
+ }
373
+
374
+ if (didSomething) {
375
+ console.log('Cleanup done.');
376
+ } else {
377
+ console.log('Nothing to clean — already removed.');
378
+ }
379
+ console.log('');
380
+ `;
381
+
382
+ try {
383
+ fs.mkdirSync(root, { recursive: true });
384
+ fs.writeFileSync(cleanupPath, script, 'utf8');
385
+ log(`Uninstall cleanup helper written to ${cleanupPath}`);
386
+ log(` After \`npm uninstall -g uniner-cli\`, run:`);
387
+ log(` node ${cleanupPath}`);
388
+ } catch (e) {
389
+ // Non-fatal: cleanup.js is a convenience, not required for install to succeed.
390
+ log(`[WARN] Could not write cleanup helper: ${e.message}`);
391
+ }
392
+ }
393
+
240
394
  // ----------------------------------------------------------------------
241
395
  // Main
242
396
  // ----------------------------------------------------------------------
@@ -267,6 +421,9 @@ async function main() {
267
421
  // Still re-render the skill — it's tiny and lets users edit the template
268
422
  // and re-run rebuild to refresh.
269
423
  writeSkillMd(versionDir(version), version);
424
+ // Refresh the cleanup helper too — it's idempotent and ensures the
425
+ // script is always present after any postinstall run.
426
+ writeCleanupScript();
270
427
  return;
271
428
  }
272
429
 
@@ -344,6 +501,13 @@ async function main() {
344
501
  step('Installing Claude Code skill');
345
502
  writeSkillMd(targetDir, version);
346
503
 
504
+ // Write the standalone cleanup helper to ~/.uniner-cli/cleanup.js.
505
+ // This survives `npm uninstall` (the npm package dir is gone but this
506
+ // file persists), giving users a reliable way to clean up residual
507
+ // files even when the `preuninstall` hook doesn't fire.
508
+ step('Writing uninstall cleanup helper');
509
+ writeCleanupScript();
510
+
347
511
  console.log('');
348
512
  console.log('==========================================');
349
513
  console.log(` uniner-cli ${version} installed`);
@@ -352,6 +516,10 @@ async function main() {
352
516
  console.log(` Location: ${targetDir}`);
353
517
  console.log(` Skill: ${skillFile()}`);
354
518
  console.log('');
519
+ console.log(' To uninstall:');
520
+ console.log(' uniner-cli uninstall # auto-detects global vs local install');
521
+ console.log(` node ${path.join(rootDir(), 'cleanup.js')} # fallback cleanup after npm uninstall`);
522
+ console.log('');
355
523
 
356
524
  // macOS 首次运行提示(无代码签名)
357
525
  if (platformInfo.platform === 'darwin') {