uniner-cli 1.0.2 → 1.0.5

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/lib/paths.js CHANGED
@@ -1,78 +1,78 @@
1
- // Path utilities shared by bin/, scripts/postinstall.js, scripts/preuninstall.js.
2
- // Single source of truth so we do not drift between launcher and installer.
3
-
4
- const os = require('os');
5
- const path = require('path');
6
- const fs = require('fs');
7
-
8
- const pkg = require('../package.json');
9
-
10
- // ----------------------------------------------------------------------
11
- // Layout we install into:
12
- //
13
- // Windows: %USERPROFILE%\.uniner-cli\<version>\
14
- // bin\uniner-cli\uniner-cli.exe ← real PyInstaller exe
15
- // skills\
16
- // docs\
17
- // workdir\
18
- // Linux: ~/.uniner-cli/<version>/ (not yet shipped)
19
- //
20
- // We keep <version> in the path so two npm-installed versions can coexist
21
- // without colliding (`npm install uniner-cli@1.0.0` then `npm install uniner-cli@1.0.1`
22
- // won't clobber each other), and so postinstall is idempotent: if the
23
- // version-specific dir already has a working binary, we skip the download.
24
- // ----------------------------------------------------------------------
25
-
26
- function homeDir() {
27
- return os.homedir();
28
- }
29
-
30
- function rootDir() {
31
- return path.join(homeDir(), '.uniner-cli');
32
- }
33
-
34
- function versionDir(version) {
35
- return path.join(rootDir(), version || pkg.version);
36
- }
37
-
38
- function realBinaryPath(version) {
39
- const ver = version || pkg.version;
40
- if (process.platform === 'win32') {
41
- return path.join(versionDir(ver), 'bin', 'uniner-cli', 'uniner-cli.exe');
42
- }
43
- // Linux/Mac path will be filled in later when those builds exist.
44
- return path.join(versionDir(ver), 'bin', 'uniner-cli', 'uniner-cli');
45
- }
46
-
47
- function skillDir() {
48
- // Same place Claude Code looks for user-global skills.
49
- return path.join(homeDir(), '.claude', 'skills', 'uniner-cli');
50
- }
51
-
52
- function skillFile() {
53
- return path.join(skillDir(), 'SKILL.md');
54
- }
55
-
56
- function templatePath() {
57
- return path.join(__dirname, 'SKILL.md.template');
58
- }
59
-
60
- function isInstalled(version) {
61
- try {
62
- return fs.statSync(realBinaryPath(version)).isFile();
63
- } catch (_) {
64
- return false;
65
- }
66
- }
67
-
68
- module.exports = {
69
- pkg,
70
- homeDir,
71
- rootDir,
72
- versionDir,
73
- realBinaryPath,
74
- skillDir,
75
- skillFile,
76
- templatePath,
77
- isInstalled,
78
- };
1
+ // Path utilities shared by bin/, scripts/postinstall.js, scripts/preuninstall.js.
2
+ // Single source of truth so we do not drift between launcher and installer.
3
+
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const fs = require('fs');
7
+
8
+ const pkg = require('../package.json');
9
+
10
+ // ----------------------------------------------------------------------
11
+ // Layout we install into:
12
+ //
13
+ // Windows: %USERPROFILE%\.uniner-cli\<version>\
14
+ // bin\uniner-cli\uniner-cli.exe ← real PyInstaller exe
15
+ // skills\
16
+ // docs\
17
+ // workdir\
18
+ // Linux: ~/.uniner-cli/<version>/ (not yet shipped)
19
+ //
20
+ // We keep <version> in the path so two npm-installed versions can coexist
21
+ // without colliding (`npm install uniner-cli@1.0.0` then `npm install uniner-cli@1.0.1`
22
+ // won't clobber each other), and so postinstall is idempotent: if the
23
+ // version-specific dir already has a working binary, we skip the download.
24
+ // ----------------------------------------------------------------------
25
+
26
+ function homeDir() {
27
+ return os.homedir();
28
+ }
29
+
30
+ function rootDir() {
31
+ return path.join(homeDir(), '.uniner-cli');
32
+ }
33
+
34
+ function versionDir(version) {
35
+ return path.join(rootDir(), version || pkg.version);
36
+ }
37
+
38
+ function realBinaryPath(version) {
39
+ const ver = version || pkg.version;
40
+ if (process.platform === 'win32') {
41
+ return path.join(versionDir(ver), 'bin', 'uniner-cli', 'uniner-cli.exe');
42
+ }
43
+ // Linux/Mac path will be filled in later when those builds exist.
44
+ return path.join(versionDir(ver), 'bin', 'uniner-cli', 'uniner-cli');
45
+ }
46
+
47
+ function skillDir() {
48
+ // Same place Claude Code looks for user-global skills.
49
+ return path.join(homeDir(), '.claude', 'skills', 'uniner-cli');
50
+ }
51
+
52
+ function skillFile() {
53
+ return path.join(skillDir(), 'SKILL.md');
54
+ }
55
+
56
+ function templatePath() {
57
+ return path.join(__dirname, 'SKILL.md.template');
58
+ }
59
+
60
+ function isInstalled(version) {
61
+ try {
62
+ return fs.statSync(realBinaryPath(version)).isFile();
63
+ } catch (_) {
64
+ return false;
65
+ }
66
+ }
67
+
68
+ module.exports = {
69
+ pkg,
70
+ homeDir,
71
+ rootDir,
72
+ versionDir,
73
+ realBinaryPath,
74
+ skillDir,
75
+ skillFile,
76
+ templatePath,
77
+ isInstalled,
78
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uniner-cli",
3
- "version": "1.0.2",
3
+ "version": "1.0.5",
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",
@@ -31,10 +31,13 @@
31
31
  "node": "\u003e=14"
32
32
  },
33
33
  "os": [
34
- "win32"
34
+ "win32",
35
+ "darwin",
36
+ "linux"
35
37
  ],
36
38
  "cpu": [
37
- "x64"
39
+ "x64",
40
+ "arm64"
38
41
  ],
39
42
  "files": [
40
43
  "bin/",
@@ -0,0 +1,140 @@
1
+ /**
2
+ * postinstall-helpers.js — 平台检测和分发逻辑
3
+ *
4
+ * 从 postinstall.js 中提取的可测试辅助函数。
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const { spawnSync } = require('child_process');
10
+
11
+ /**
12
+ * 获取当前平台信息
13
+ * @returns {{ platform: string, arch: string } | null} 平台信息,不支持的平台返回 null
14
+ */
15
+ function getPlatformInfo() {
16
+ const platformMap = {
17
+ 'win32': 'windows',
18
+ 'darwin': 'darwin',
19
+ 'linux': 'linux'
20
+ };
21
+
22
+ const platform = platformMap[process.platform];
23
+ if (!platform) {
24
+ return null;
25
+ }
26
+
27
+ return {
28
+ platform,
29
+ arch: process.arch
30
+ };
31
+ }
32
+
33
+ /**
34
+ * 根据当前平台从元数据中选择对应的下载信息
35
+ * @param {object} meta - latest.json 的完整内容
36
+ * @returns {object} 包含 portable_url, portable_sha256, portable_size 的对象
37
+ * @throws {Error} 如果平台不受支持或元数据缺失
38
+ */
39
+ function selectMetadata(meta) {
40
+ const info = getPlatformInfo();
41
+ if (!info) {
42
+ throw new Error(`不支持的平台: ${process.platform} ${process.arch}`);
43
+ }
44
+
45
+ const { platform, arch } = info;
46
+ const platformMeta = meta[platform];
47
+
48
+ if (!platformMeta) {
49
+ throw new Error(`latest.json 缺少 ${platform} 平台的元数据`);
50
+ }
51
+
52
+ const archMeta = platformMeta[arch];
53
+ if (!archMeta || !archMeta.portable_url || !archMeta.portable_sha256) {
54
+ throw new Error(`latest.json 缺少 ${platform}.${arch} 的 portable_url / portable_sha256`);
55
+ }
56
+
57
+ return archMeta;
58
+ }
59
+
60
+ /**
61
+ * 获取当前平台的解压命令
62
+ * @param {string} archivePath - 压缩文件路径
63
+ * @param {string} destDir - 解压目标目录
64
+ * @returns {{ command: string, args: string[] }} 解压命令和参数
65
+ */
66
+ function getExtractCommand(archivePath, destDir) {
67
+ const info = getPlatformInfo();
68
+ if (!info) {
69
+ throw new Error(`不支持的平台: ${process.platform}`);
70
+ }
71
+
72
+ if (info.platform === 'windows') {
73
+ // Windows: 使用 PowerShell Expand-Archive
74
+ return {
75
+ command: 'powershell.exe',
76
+ args: [
77
+ '-NoProfile',
78
+ '-NonInteractive',
79
+ '-ExecutionPolicy', 'Bypass',
80
+ '-Command',
81
+ `Expand-Archive -LiteralPath '${archivePath}' -DestinationPath '${destDir}' -Force`
82
+ ]
83
+ };
84
+ } else if (info.platform === 'darwin' || info.platform === 'linux') {
85
+ // macOS / Linux: 使用系统 tar
86
+ return {
87
+ command: 'tar',
88
+ args: ['-xzf', archivePath, '-C', destDir]
89
+ };
90
+ }
91
+
92
+ throw new Error(`未实现的平台解压逻辑: ${info.platform}`);
93
+ }
94
+
95
+ /**
96
+ * 执行解压
97
+ * @param {string} archivePath - 压缩文件路径
98
+ * @param {string} destDir - 解压目标目录
99
+ */
100
+ function extractArchive(archivePath, destDir) {
101
+ const fs = require('fs');
102
+ fs.mkdirSync(destDir, { recursive: true });
103
+
104
+ const { command, args } = getExtractCommand(archivePath, destDir);
105
+ const result = spawnSync(command, args, { stdio: 'inherit' });
106
+
107
+ if (result.error) {
108
+ throw new Error(`无法执行解压命令: ${result.error.message}`);
109
+ }
110
+ if (result.status !== 0) {
111
+ throw new Error(`解压命令退出码: ${result.status}`);
112
+ }
113
+ }
114
+
115
+ /**
116
+ * 获取当前平台的二进制文件名
117
+ * @returns {string} 二进制文件名(带或不带扩展名)
118
+ */
119
+ function getBinaryName() {
120
+ const info = getPlatformInfo();
121
+ if (!info) {
122
+ throw new Error(`不支持的平台: ${process.platform}`);
123
+ }
124
+
125
+ if (info.platform === 'windows') {
126
+ return 'uniner-cli.exe';
127
+ } else if (info.platform === 'darwin' || info.platform === 'linux') {
128
+ return 'uniner-cli';
129
+ }
130
+
131
+ throw new Error(`未实现的平台二进制名: ${info.platform}`);
132
+ }
133
+
134
+ module.exports = {
135
+ getPlatformInfo,
136
+ selectMetadata,
137
+ getExtractCommand,
138
+ extractArchive,
139
+ getBinaryName
140
+ };