pear-install 1.0.7 → 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 +3 -1
  2. package/cmd.js +4 -0
  3. package/index.js +52 -15
  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
 
@@ -70,6 +70,7 @@ await corestore.close()
70
70
  - `installing` → `{ link, host }`
71
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`
@@ -88,6 +89,7 @@ await InstallCmd.output(json, stream)
88
89
  - `installing` → `{ link, host }`
89
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
@@ -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,6 +193,7 @@ 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 })
@@ -210,11 +214,13 @@ class Install extends ReadyResource {
210
214
  throw ERR_NOT_FOUND(plink.serialize({ ...parsed, pathname: key }))
211
215
  }
212
216
 
213
- if (isWindows) {
217
+ if (ext === '.msix') {
214
218
  const MSIXManager = require('msix-manager')
215
219
  await new MSIXManager().addPackage(from)
216
220
  installed++
217
221
  continue
222
+ } else if (ext === '.exe') {
223
+ exes.add(dest)
218
224
  }
219
225
 
220
226
  if (isBin) {
@@ -252,6 +258,9 @@ class Install extends ReadyResource {
252
258
  if (installed === 0) {
253
259
  throw ERR_UNKNOWN('Failed to install')
254
260
  }
261
+
262
+ for (const dest of exes) this._exe(path.dirname(dest))
263
+
255
264
  this.emit('final', { success: true, installed, exists })
256
265
  }
257
266
 
@@ -276,6 +285,34 @@ class Install extends ReadyResource {
276
285
  )
277
286
  }
278
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
+
279
316
  _extract(appImage, extracted, cwd, file) {
280
317
  const { status } = spawnSync(appImage, ['--appimage-extract', file], { cwd })
281
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.7",
3
+ "version": "1.0.8",
4
4
  "main": "index.js",
5
5
  "type": "commonjs",
6
6
  "description": "Install Pear and Pear Applications",