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.
Files changed (4) hide show
  1. package/README.md +5 -3
  2. package/cmd.js +6 -2
  3. package/index.js +63 -16
  4. 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 and bins installed as MSIX packages
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 ? '.msix' : ''
106
- const dest = isWindows
107
- ? null
108
- : to
109
- ? path.join(to, binName + ext)
110
- : isMac
111
- ? path.join('/', 'usr', 'local', 'bin', binName)
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 (isWindows) {
156
+ if (target.ext === '.msix') {
157
+ const escName = name.replace(/'/g, "''")
155
158
  const ps = spawnSync('powershell', [
156
159
  '-NoProfile',
157
160
  '-Command',
158
- `(Get-AppxPackage '${target.filename}') -ne $null`
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 message = isWindows
173
- ? `Already installed:\n${exists.map(({ filename }) => ' ' + filename).join('\n')}\n Manually uninstall first to reinstall`
174
- : `Refusing to overwrite existing:\n${exists.map(({ dest }) => ' ' + dest).join('\n')}\n Manually remove first to reinstall`
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
- this.emit('app', { app: filename, name, version, upgrade, key, tmp, dest })
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 (isWindows) {
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')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pear-install",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "main": "index.js",
5
5
  "type": "commonjs",
6
6
  "description": "Install Pear and Pear Applications",