pear-install 1.0.6 → 1.0.8
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 +5 -3
- package/cmd.js +6 -2
- package/index.js +63 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ npx pear-install [link]
|
|
|
20
20
|
|
|
21
21
|
- **macOS** — apps to `/Applications`, bins to `/usr/local/bin`
|
|
22
22
|
- **Linux** — apps to `~/Applications`, `~/AppImages`, or `~/.local/bin`; bins to `~/.local/bin`
|
|
23
|
-
- **Windows** — apps
|
|
23
|
+
- **Windows** — apps installed as MSIX packages; bins (`.exe`) to `%LOCALAPPDATA%\Programs\<app>\` with the install directory added to the User `PATH`
|
|
24
24
|
|
|
25
25
|
## API
|
|
26
26
|
|
|
@@ -68,8 +68,9 @@ await corestore.close()
|
|
|
68
68
|
### Events
|
|
69
69
|
|
|
70
70
|
- `installing` → `{ link, host }`
|
|
71
|
-
- `app` → `{ app, name, version, upgrade, key, dest }`
|
|
71
|
+
- `app` → `{ app, name, version, upgrade, verlink, key, dest }`
|
|
72
72
|
- `stats` → `{ download, upload, peers }`
|
|
73
|
+
- `path` → `{ dir }` — Windows only; emitted when a bin directory is appended to the User `PATH`
|
|
73
74
|
- `final` → `{ success, installed, exists }`
|
|
74
75
|
|
|
75
76
|
## Command Integration: `/cmd`
|
|
@@ -86,8 +87,9 @@ await InstallCmd.output(json, stream)
|
|
|
86
87
|
### Tags
|
|
87
88
|
|
|
88
89
|
- `installing` → `{ link, host }`
|
|
89
|
-
- `app` → `{ app, name, version, upgrade, key, dest }`
|
|
90
|
+
- `app` → `{ app, name, version, upgrade, verlink, key, dest }`
|
|
90
91
|
- `stats` → `{ download, upload, peers }`
|
|
92
|
+
- `path` → `{ dir }` — Windows only
|
|
91
93
|
- `error` → `{ code, message, stack, info, success: false }`
|
|
92
94
|
- `final` → `{ success, installed, exists }`
|
|
93
95
|
|
package/cmd.js
CHANGED
|
@@ -11,8 +11,8 @@ const down = isWindows ? '↓' : '⬇'
|
|
|
11
11
|
class InstallCmd extends Opstream {
|
|
12
12
|
static outputs = {
|
|
13
13
|
installing: ({ link }) => `Installing... ${link}`,
|
|
14
|
-
app: ({ app, version, upgrade, dest, key }) =>
|
|
15
|
-
`App: ${app}\nVersion: ${version}\nLink: ${upgrade}\nPathname: ${key}\nTarget: ${dest}`,
|
|
14
|
+
app: ({ app, version, upgrade, dest, key, verlink }) =>
|
|
15
|
+
`App: ${app}\nVersion: ${version}\nLink: ${upgrade}\nVerlink: ${verlink}\nPathname: ${key}\nTarget: ${dest}`,
|
|
16
16
|
stats({ download, peers }) {
|
|
17
17
|
const dl =
|
|
18
18
|
download.bytes + download.speed === 0
|
|
@@ -20,6 +20,7 @@ class InstallCmd extends Opstream {
|
|
|
20
20
|
: ` [ ${down} ${byteSize(download.bytes)} - ${byteSize(download.speed)}/s ] `
|
|
21
21
|
return `[ Peers: ${peers} ]${dl}`
|
|
22
22
|
},
|
|
23
|
+
path: ({ dir }) => `Added to User PATH: ${dir}\n Restart shell for change to take effect.`,
|
|
23
24
|
error: ({ message }) => message,
|
|
24
25
|
final({ success, message }) {
|
|
25
26
|
if (success) return 'Installed'
|
|
@@ -78,6 +79,9 @@ class InstallCmd extends Opstream {
|
|
|
78
79
|
install.on('stats', (data) => {
|
|
79
80
|
this.push({ tag: 'stats', data })
|
|
80
81
|
})
|
|
82
|
+
install.on('path', (data) => {
|
|
83
|
+
this.push({ tag: 'path', data })
|
|
84
|
+
})
|
|
81
85
|
install.on('final', (data) => {
|
|
82
86
|
this.final = data
|
|
83
87
|
})
|
package/index.js
CHANGED
|
@@ -99,16 +99,18 @@ class Install extends ReadyResource {
|
|
|
99
99
|
const appName = productName ?? name
|
|
100
100
|
const home = os.homedir()
|
|
101
101
|
|
|
102
|
+
const localAppData = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local')
|
|
103
|
+
|
|
102
104
|
if (bin) {
|
|
103
105
|
const bins = typeof bin === 'string' ? { [name]: bin } : bin
|
|
104
106
|
for (const binName of Object.keys(bins)) {
|
|
105
|
-
const ext = isWindows ? '.
|
|
106
|
-
const dest =
|
|
107
|
-
?
|
|
108
|
-
:
|
|
109
|
-
? path.join(
|
|
110
|
-
:
|
|
111
|
-
? path.join(
|
|
107
|
+
const ext = isWindows ? '.exe' : ''
|
|
108
|
+
const dest = to
|
|
109
|
+
? path.join(to, binName + ext)
|
|
110
|
+
: isMac
|
|
111
|
+
? path.join('/', 'usr', 'local', 'bin', binName)
|
|
112
|
+
: isWindows
|
|
113
|
+
? path.join(localAppData, 'Programs', appName, binName + ext)
|
|
112
114
|
: path.join(home, '.local', 'bin', binName)
|
|
113
115
|
this.targets.push({ filename: binName, ext, dest, isBin: true })
|
|
114
116
|
}
|
|
@@ -151,27 +153,28 @@ class Install extends ReadyResource {
|
|
|
151
153
|
const exists = []
|
|
152
154
|
const installs = []
|
|
153
155
|
for (const target of this.targets) {
|
|
154
|
-
if (
|
|
156
|
+
if (target.ext === '.msix') {
|
|
157
|
+
const escName = name.replace(/'/g, "''")
|
|
155
158
|
const ps = spawnSync('powershell', [
|
|
156
159
|
'-NoProfile',
|
|
157
160
|
'-Command',
|
|
158
|
-
|
|
161
|
+
`$null -ne (Get-AppxPackage -Name '${escName}' -ErrorAction SilentlyContinue)`
|
|
159
162
|
])
|
|
160
163
|
if (ps.stdout.toString().trim() === 'True') {
|
|
161
|
-
exists.push({ filename: target.filename, dest: target.dest })
|
|
164
|
+
exists.push({ filename: target.filename, dest: target.dest, ext: target.ext })
|
|
162
165
|
continue
|
|
163
166
|
}
|
|
164
167
|
} else if (fs.existsSync(target.dest)) {
|
|
165
|
-
exists.push({ filename: target.filename, dest: target.dest })
|
|
168
|
+
exists.push({ filename: target.filename, dest: target.dest, ext: target.ext })
|
|
166
169
|
continue
|
|
167
170
|
}
|
|
168
171
|
installs.push(target)
|
|
169
172
|
}
|
|
170
173
|
|
|
171
174
|
if (installs.length === 0) {
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
+
const lines = exists.map(({ filename, dest }) => ' ' + (dest ?? filename))
|
|
176
|
+
const header = isWindows ? 'Already installed:' : 'Refusing to overwrite existing:'
|
|
177
|
+
const message = `${header}\n${lines.join('\n')}\nTo reinstall, manually remove then rerun command`
|
|
175
178
|
throw ERR_EXISTS(message)
|
|
176
179
|
}
|
|
177
180
|
|
|
@@ -190,9 +193,20 @@ class Install extends ReadyResource {
|
|
|
190
193
|
monitor.destroy()
|
|
191
194
|
|
|
192
195
|
let installed = 0
|
|
196
|
+
const exes = new Set()
|
|
193
197
|
for (const { filename, ext, dest, isBin } of installs) {
|
|
194
198
|
const key = appPath + filename + ext
|
|
195
|
-
|
|
199
|
+
const verlink = plink.serialize({ drive: this.drive.core })
|
|
200
|
+
this.emit('app', {
|
|
201
|
+
app: filename,
|
|
202
|
+
name,
|
|
203
|
+
version,
|
|
204
|
+
upgrade,
|
|
205
|
+
verlink,
|
|
206
|
+
key,
|
|
207
|
+
tmp,
|
|
208
|
+
dest
|
|
209
|
+
})
|
|
196
210
|
|
|
197
211
|
const from = path.join(tmp, 'by-arch', host, 'app', filename + ext)
|
|
198
212
|
|
|
@@ -200,11 +214,13 @@ class Install extends ReadyResource {
|
|
|
200
214
|
throw ERR_NOT_FOUND(plink.serialize({ ...parsed, pathname: key }))
|
|
201
215
|
}
|
|
202
216
|
|
|
203
|
-
if (
|
|
217
|
+
if (ext === '.msix') {
|
|
204
218
|
const MSIXManager = require('msix-manager')
|
|
205
219
|
await new MSIXManager().addPackage(from)
|
|
206
220
|
installed++
|
|
207
221
|
continue
|
|
222
|
+
} else if (ext === '.exe') {
|
|
223
|
+
exes.add(dest)
|
|
208
224
|
}
|
|
209
225
|
|
|
210
226
|
if (isBin) {
|
|
@@ -242,6 +258,9 @@ class Install extends ReadyResource {
|
|
|
242
258
|
if (installed === 0) {
|
|
243
259
|
throw ERR_UNKNOWN('Failed to install')
|
|
244
260
|
}
|
|
261
|
+
|
|
262
|
+
for (const dest of exes) this._exe(path.dirname(dest))
|
|
263
|
+
|
|
245
264
|
this.emit('final', { success: true, installed, exists })
|
|
246
265
|
}
|
|
247
266
|
|
|
@@ -266,6 +285,34 @@ class Install extends ReadyResource {
|
|
|
266
285
|
)
|
|
267
286
|
}
|
|
268
287
|
|
|
288
|
+
_exe(dir) {
|
|
289
|
+
const read = spawnSync('powershell', [
|
|
290
|
+
'-NoProfile',
|
|
291
|
+
'-Command',
|
|
292
|
+
"[Environment]::GetEnvironmentVariable('Path', 'User')"
|
|
293
|
+
])
|
|
294
|
+
if (read.status !== 0) {
|
|
295
|
+
const err = (read.stderr || '').toString().trim()
|
|
296
|
+
throw new Error('Failed to read User PATH: ' + (err || 'powershell exit ' + read.status))
|
|
297
|
+
}
|
|
298
|
+
const current = read.stdout.toString().replace(/\r?\n$/, '')
|
|
299
|
+
const entries = current ? current.split(';').filter(Boolean) : []
|
|
300
|
+
if (entries.includes(dir)) return false
|
|
301
|
+
const next = (current ? current + ';' : '') + dir
|
|
302
|
+
const escNext = next.replace(/'/g, "''")
|
|
303
|
+
const write = spawnSync('powershell', [
|
|
304
|
+
'-NoProfile',
|
|
305
|
+
'-Command',
|
|
306
|
+
`[Environment]::SetEnvironmentVariable('Path', '${escNext}', 'User')`
|
|
307
|
+
])
|
|
308
|
+
if (write.status !== 0) {
|
|
309
|
+
const err = (write.stderr || '').toString().trim()
|
|
310
|
+
throw new Error('Failed to update User PATH: ' + (err || 'powershell exit ' + write.status))
|
|
311
|
+
}
|
|
312
|
+
this.emit('path', { dir })
|
|
313
|
+
return true
|
|
314
|
+
}
|
|
315
|
+
|
|
269
316
|
_extract(appImage, extracted, cwd, file) {
|
|
270
317
|
const { status } = spawnSync(appImage, ['--appimage-extract', file], { cwd })
|
|
271
318
|
if (status !== 0) throw new Error('appimage-extract failed')
|