pear-install 1.0.4 → 1.0.6
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 +100 -4
- package/bin.js +27 -0
- package/cmd.js +88 -26
- package/index.js +221 -235
- package/package.json +9 -3
- package/.gitattributes +0 -1
- package/.github/workflows/publish.yml +0 -17
- package/.github/workflows/test.yml +0 -32
- package/.prettierignore +0 -1
- package/.prettierrc +0 -1
- package/NOTICE +0 -13
- package/test/all.js +0 -13
- package/test/helper.js +0 -80
- package/test/index.test.js +0 -238
package/README.md
CHANGED
|
@@ -1,14 +1,110 @@
|
|
|
1
|
-
# pear-install
|
|
2
|
-
|
|
3
1
|
> Install Pear and Pear Applications
|
|
4
2
|
|
|
5
|
-
|
|
3
|
+
## CLI
|
|
4
|
+
|
|
5
|
+
The published binary. With no `link`, installs the Pear platform. With a `pear://` link, installs that application and/or its binaries.
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
8
|
npx pear-install [link]
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
### Flags
|
|
12
|
+
|
|
13
|
+
- `--to <dir>` — target directory (overrides platform default)
|
|
14
|
+
- `--only <paths>` — comma-separated filenames to install
|
|
15
|
+
- `--timeout <seconds>` — network timeout (default `30`)
|
|
16
|
+
- `--dht-bootstrap <nodes>` — comma-separated `host:port`
|
|
17
|
+
- `--json` — newline-delimited JSON output (one `{ cmd: 'install', tag, data }` per line)
|
|
18
|
+
|
|
19
|
+
### Default install destinations
|
|
20
|
+
|
|
21
|
+
- **macOS** — apps to `/Applications`, bins to `/usr/local/bin`
|
|
22
|
+
- **Linux** — apps to `~/Applications`, `~/AppImages`, or `~/.local/bin`; bins to `~/.local/bin`
|
|
23
|
+
- **Windows** — apps and bins installed as MSIX packages
|
|
24
|
+
|
|
25
|
+
## API
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
const Install = require('pear-install')
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### `const install = new Install(opts)`
|
|
32
|
+
|
|
33
|
+
- `link` _(string)_ — `pear://` link
|
|
34
|
+
- `to` _(string, optional)_ — target directory
|
|
35
|
+
- `only` _(string, optional)_ — comma-separated filenames
|
|
36
|
+
- `bootstrap` _(array, optional)_ — `[{ host, port }, ...]` DHT nodes
|
|
37
|
+
- `timeout` _(number, optional)_ — milliseconds, default `30000`
|
|
38
|
+
- `corestore` _(Corestore, optional)_ — inject an existing corestore; not closed by `install.close()`
|
|
39
|
+
- `swarm` _(Hyperswarm, optional)_ — inject an existing swarm; not destroyed by `install.close()`
|
|
40
|
+
|
|
41
|
+
If `corestore` or `swarm` is omitted, `Install` creates and owns its own.
|
|
42
|
+
|
|
43
|
+
### `await install.ready()`
|
|
44
|
+
|
|
45
|
+
Runs the install. Throws on failure.
|
|
46
|
+
|
|
47
|
+
### `await install.close()`
|
|
48
|
+
|
|
49
|
+
Releases the drive, the owned swarm and corestore, and removes the temp dir.
|
|
50
|
+
|
|
51
|
+
### Reusing a corestore and swarm
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
const corestore = new Corestore('./store')
|
|
55
|
+
const swarm = new Hyperswarm()
|
|
56
|
+
swarm.on('connection', (c) => corestore.replicate(c))
|
|
57
|
+
|
|
58
|
+
for (const link of links) {
|
|
59
|
+
const install = new Install({ link, corestore, swarm })
|
|
60
|
+
await install.ready()
|
|
61
|
+
await install.close()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
await swarm.destroy()
|
|
65
|
+
await corestore.close()
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Events
|
|
69
|
+
|
|
70
|
+
- `installing` → `{ link, host }`
|
|
71
|
+
- `app` → `{ app, name, version, upgrade, key, dest }`
|
|
72
|
+
- `stats` → `{ download, upload, peers }`
|
|
73
|
+
- `final` → `{ success, installed, exists }`
|
|
74
|
+
|
|
75
|
+
## Command Integration: `/cmd`
|
|
76
|
+
|
|
77
|
+
For embedding in another CLI. Wraps `Install` as a `pear-opstream` of `{ tag, data }` records with stdout formatters.
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
const InstallCmd = require('pear-install/cmd')
|
|
81
|
+
|
|
82
|
+
const stream = new InstallCmd({ link, to, only, bootstrap, timeout })
|
|
83
|
+
await InstallCmd.output(json, stream)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Tags
|
|
87
|
+
|
|
88
|
+
- `installing` → `{ link, host }`
|
|
89
|
+
- `app` → `{ app, name, version, upgrade, key, dest }`
|
|
90
|
+
- `stats` → `{ download, upload, peers }`
|
|
91
|
+
- `error` → `{ code, message, stack, info, success: false }`
|
|
92
|
+
- `final` → `{ success, installed, exists }`
|
|
93
|
+
|
|
94
|
+
### `await InstallCmd.output(json, stream)`
|
|
95
|
+
|
|
96
|
+
Drains `stream` to STDOUT.
|
|
97
|
+
|
|
98
|
+
- `json` truthy — emits each record as `{ cmd: 'install', tag, data }`, newline-delimited.
|
|
99
|
+
- `json` falsy — formats per-tag; `stats` updates in place via ANSI; static tags stack above.
|
|
100
|
+
|
|
101
|
+
Returns the `final` record.
|
|
102
|
+
|
|
103
|
+
### `await InstallCmd.runner(cmd)`
|
|
104
|
+
|
|
105
|
+
Pass directly as a [paparam](https://github.com/holepunchto/paparam) command runner. Creates `InstallCmd` instance and passes it to `InstallCmd.output`
|
|
106
|
+
|
|
107
|
+
- `cmd` is a [paparam](https://github.com/holepunchto/paparam) `cmd` instance
|
|
12
108
|
|
|
13
109
|
## License
|
|
14
110
|
|
package/bin.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const pkg = require('./package.json')
|
|
3
|
+
const { isWindows } = require('which-runtime')
|
|
4
|
+
const { command, arg, bail } = require('paparam')
|
|
5
|
+
const { runner } = require('./cmd')
|
|
6
|
+
|
|
7
|
+
const program = command(
|
|
8
|
+
'install',
|
|
9
|
+
arg('[link]', 'Pear link origin to install from'),
|
|
10
|
+
runner,
|
|
11
|
+
pkg.command,
|
|
12
|
+
bail((info = {}) => {
|
|
13
|
+
process.exitCode = 1
|
|
14
|
+
let message
|
|
15
|
+
if (info.reason === 'UNKNOWN_FLAG') message = 'Unrecognized Flag: --' + info.flag.name
|
|
16
|
+
else if (info.reason === 'UNKNOWN_ARG') {
|
|
17
|
+
message = `Unrecognized Argument at index ${info.arg.index} with value ${info.arg.value}`
|
|
18
|
+
} else message = info.err?.message ?? 'Failed'
|
|
19
|
+
const cross = isWindows ? 'x' : '\x1B[31m✖\x1B[39m'
|
|
20
|
+
console.error(cross, message)
|
|
21
|
+
if (info.reason === 'UNKNOWN_FLAG' || info.reason === 'UNKNOWN_ARG') {
|
|
22
|
+
console.error('\n' + info.command.usage())
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
program.parse(process.argv.slice(2))
|
package/cmd.js
CHANGED
|
@@ -1,30 +1,92 @@
|
|
|
1
|
-
|
|
2
|
-
const
|
|
1
|
+
'use strict'
|
|
2
|
+
const process = require('process')
|
|
3
|
+
const Opstream = require('pear-opstream')
|
|
4
|
+
const byteSize = require('tiny-byte-size')
|
|
3
5
|
const { isWindows } = require('which-runtime')
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
+
const pkg = require('./package.json')
|
|
7
|
+
const Install = require('.')
|
|
8
|
+
|
|
9
|
+
const down = isWindows ? '↓' : '⬇'
|
|
10
|
+
|
|
11
|
+
class InstallCmd extends Opstream {
|
|
12
|
+
static outputs = {
|
|
13
|
+
installing: ({ link }) => `Installing... ${link}`,
|
|
14
|
+
app: ({ app, version, upgrade, dest, key }) =>
|
|
15
|
+
`App: ${app}\nVersion: ${version}\nLink: ${upgrade}\nPathname: ${key}\nTarget: ${dest}`,
|
|
16
|
+
stats({ download, peers }) {
|
|
17
|
+
const dl =
|
|
18
|
+
download.bytes + download.speed === 0
|
|
19
|
+
? ''
|
|
20
|
+
: ` [ ${down} ${byteSize(download.bytes)} - ${byteSize(download.speed)}/s ] `
|
|
21
|
+
return `[ Peers: ${peers} ]${dl}`
|
|
22
|
+
},
|
|
23
|
+
error: ({ message }) => message,
|
|
24
|
+
final({ success, message }) {
|
|
25
|
+
if (success) return 'Installed'
|
|
26
|
+
return message ?? 'Failed'
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
static async output(json, stream) {
|
|
30
|
+
let status = false
|
|
31
|
+
for await (const { tag, data } of stream) {
|
|
32
|
+
if (json) {
|
|
33
|
+
process.stdout.write(JSON.stringify({ cmd: 'install', tag, data }) + '\n')
|
|
34
|
+
continue
|
|
35
|
+
}
|
|
36
|
+
if (!this.outputs[tag]) continue
|
|
37
|
+
const line = this.outputs[tag](data)
|
|
38
|
+
const clear = status ? '\r\x1B[2K' : ''
|
|
39
|
+
if (tag === 'stats') {
|
|
40
|
+
process.stdout.write('\r\x1B[2K' + line)
|
|
41
|
+
status = true
|
|
42
|
+
continue
|
|
43
|
+
}
|
|
44
|
+
process.stdout.write(clear + line + '\n')
|
|
45
|
+
status = false
|
|
46
|
+
if (tag === 'final') return data
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
static async runner(cmd) {
|
|
51
|
+
const { json, only, to, dhtBootstrap } = cmd.flags
|
|
52
|
+
const timeout = (cmd.flags.timeout || 30) * 1000
|
|
53
|
+
const link = cmd.args.link ?? pkg.pear.platform.key
|
|
54
|
+
const bootstrap = dhtBootstrap
|
|
55
|
+
? dhtBootstrap.split(',').map((tuple) => {
|
|
56
|
+
const [host, port] = tuple.split(':')
|
|
57
|
+
const int = +port
|
|
58
|
+
if (Number.isInteger(int) === false) throw new Error(`Invalid port: ${port}`)
|
|
59
|
+
return { host, port: int }
|
|
60
|
+
})
|
|
61
|
+
: undefined
|
|
62
|
+
const stream = new InstallCmd({ link, only, to, bootstrap, timeout })
|
|
63
|
+
await InstallCmd.output(json, stream)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
constructor(params) {
|
|
67
|
+
super((...args) => this.#op(...args), params)
|
|
68
|
+
}
|
|
6
69
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
console.error('\n' + info.command.usage())
|
|
70
|
+
async #op(opts) {
|
|
71
|
+
const install = new Install(opts)
|
|
72
|
+
install.on('installing', (data) => {
|
|
73
|
+
this.push({ tag: 'installing', data })
|
|
74
|
+
})
|
|
75
|
+
install.on('app', (data) => {
|
|
76
|
+
this.push({ tag: 'app', data })
|
|
77
|
+
})
|
|
78
|
+
install.on('stats', (data) => {
|
|
79
|
+
this.push({ tag: 'stats', data })
|
|
80
|
+
})
|
|
81
|
+
install.on('final', (data) => {
|
|
82
|
+
this.final = data
|
|
83
|
+
})
|
|
84
|
+
try {
|
|
85
|
+
await install.ready()
|
|
86
|
+
} finally {
|
|
87
|
+
await install.close()
|
|
26
88
|
}
|
|
27
|
-
}
|
|
28
|
-
|
|
89
|
+
}
|
|
90
|
+
}
|
|
29
91
|
|
|
30
|
-
|
|
92
|
+
module.exports = InstallCmd
|
package/index.js
CHANGED
|
@@ -11,9 +11,8 @@ const Corestore = require('corestore')
|
|
|
11
11
|
const Hyperdrive = require('hyperdrive')
|
|
12
12
|
const Hyperswarm = require('hyperswarm')
|
|
13
13
|
const plink = require('pear-link')
|
|
14
|
-
const Opstream = require('pear-opstream')
|
|
15
14
|
const PearError = require('pear-errors')
|
|
16
|
-
const
|
|
15
|
+
const ReadyResource = require('ready-resource')
|
|
17
16
|
const { ERR_INVALID_MANIFEST, ERR_NOT_FOUND, ERR_PERMISSION_REQUIRED, ERR_UNKNOWN } = PearError
|
|
18
17
|
|
|
19
18
|
function ERR_NETWORK_TIMEOUT(msg, info = null) {
|
|
@@ -24,254 +23,226 @@ function ERR_EXISTS(msg, info = null) {
|
|
|
24
23
|
return new PearError(msg, ERR_EXISTS, info)
|
|
25
24
|
}
|
|
26
25
|
|
|
27
|
-
const down = isWindows ? '↓' : '⬇'
|
|
28
26
|
const PEAR_DIR = isMac
|
|
29
27
|
? path.join(os.homedir(), 'Library', 'Application Support', 'pear')
|
|
30
28
|
: isLinux
|
|
31
29
|
? path.join(os.homedir(), '.config', 'pear')
|
|
32
30
|
: path.join(os.homedir(), 'AppData', 'Roaming', 'pear')
|
|
33
31
|
|
|
34
|
-
class Install extends
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
: ` [ ${down} ${byteSize(download.bytes)} - ${byteSize(download.speed)}/s ] `
|
|
44
|
-
return `[ Peers: ${peers} ]${dl}`
|
|
45
|
-
},
|
|
46
|
-
error: ({ message }) => message,
|
|
47
|
-
final({ success, message }) {
|
|
48
|
-
if (success) return 'Installed'
|
|
49
|
-
return message ?? 'Failed'
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
constructor(params) {
|
|
54
|
-
super((...args) => this.#op(...args), params)
|
|
32
|
+
class Install extends ReadyResource {
|
|
33
|
+
constructor({ link, only, to, bootstrap, timeout = 30_000, corestore = null, swarm = null }) {
|
|
34
|
+
super()
|
|
35
|
+
this.doneFinding = null
|
|
36
|
+
this.link = link
|
|
37
|
+
this.only = only
|
|
38
|
+
this.to = to
|
|
39
|
+
this.bootstrap = bootstrap
|
|
40
|
+
this.timeout = timeout
|
|
55
41
|
this.targets = []
|
|
42
|
+
this.base = null
|
|
43
|
+
this.drive = null
|
|
44
|
+
this.corestore = corestore
|
|
45
|
+
this.swarm = swarm
|
|
46
|
+
this._ownCorestore = corestore === null
|
|
47
|
+
this._ownSwarm = swarm === null
|
|
56
48
|
}
|
|
57
|
-
|
|
58
|
-
|
|
49
|
+
async _open() {
|
|
50
|
+
try {
|
|
51
|
+
await this._install()
|
|
52
|
+
} catch (err) {
|
|
53
|
+
await this._teardown()
|
|
54
|
+
throw err
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async _install() {
|
|
58
|
+
const { link, only, to, bootstrap, timeout = 30_000 } = this
|
|
59
59
|
const parsed = plink.parse(link)
|
|
60
60
|
if (parsed.pathname) throw new Error('Link must not have pathname')
|
|
61
61
|
const host = process.platform + '-' + process.arch
|
|
62
|
-
this.
|
|
62
|
+
this.emit('installing', { link, host })
|
|
63
63
|
|
|
64
64
|
const rand = crypto.randomBytes(16).toString('hex')
|
|
65
|
-
|
|
66
|
-
fs.mkdirSync(base, { recursive: true })
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
swarm.dht.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
.then(() => topic.destroy())
|
|
87
|
-
}
|
|
88
|
-
})
|
|
89
|
-
const deferred = Promise.withResolvers()
|
|
90
|
-
const countdown = setTimeout(() => {
|
|
91
|
-
deferred.reject(ERR_NETWORK_TIMEOUT('Network Timeout ' + timeout / 1000 + 's'))
|
|
92
|
-
}, timeout)
|
|
93
|
-
await Promise.race([drive.core.update({ wait: true }), deferred.promise])
|
|
94
|
-
clearTimeout(countdown)
|
|
95
|
-
const pkg = await drive.get('/package.json')
|
|
96
|
-
if (pkg === null) throw ERR_INVALID_MANIFEST('Unable to read application package.json')
|
|
97
|
-
const manifest = JSON.parse(pkg.toString())
|
|
98
|
-
|
|
99
|
-
const { name, productName, version, upgrade, bin } = manifest
|
|
100
|
-
const appName = productName ?? name
|
|
101
|
-
const home = os.homedir()
|
|
102
|
-
|
|
103
|
-
if (bin) {
|
|
104
|
-
const bins = typeof bin === 'string' ? { [name]: bin } : bin
|
|
105
|
-
for (const binName of Object.keys(bins)) {
|
|
106
|
-
const ext = isWindows ? '.msix' : ''
|
|
107
|
-
const dest = isWindows
|
|
108
|
-
? null
|
|
109
|
-
: to
|
|
110
|
-
? path.join(to, binName + ext)
|
|
111
|
-
: isMac
|
|
112
|
-
? path.join('/', 'usr', 'local', 'bin', binName)
|
|
113
|
-
: path.join(home, '.local', 'bin', binName)
|
|
114
|
-
this.targets.push({ filename: binName, ext, dest, isBin: true })
|
|
115
|
-
}
|
|
65
|
+
this.base = path.join(PEAR_DIR, 'gc', rand)
|
|
66
|
+
fs.mkdirSync(this.base, { recursive: true })
|
|
67
|
+
|
|
68
|
+
if (this._ownCorestore) this.corestore = new Corestore(this.base)
|
|
69
|
+
if (this._ownSwarm) this.swarm = new Hyperswarm({ bootstrap })
|
|
70
|
+
|
|
71
|
+
this.drive = new Hyperdrive(this.corestore.session(), parsed.drive.key)
|
|
72
|
+
|
|
73
|
+
await this.drive.ready()
|
|
74
|
+
this.doneFinding = this.drive.findingPeers()
|
|
75
|
+
const topic = this.swarm.join(this.drive.discoveryKey, { server: false, client: true })
|
|
76
|
+
this.swarm.on('connection', (c) => this.corestore.replicate(c))
|
|
77
|
+
|
|
78
|
+
let serving = false
|
|
79
|
+
this.swarm.dht.on('nat-update', () => {
|
|
80
|
+
if (!this.swarm.dht.randomized && !serving) {
|
|
81
|
+
serving = true
|
|
82
|
+
this.swarm
|
|
83
|
+
.join(this.drive.discoveryKey, { server: true, client: false })
|
|
84
|
+
.flushed()
|
|
85
|
+
.then(() => topic.destroy())
|
|
116
86
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
.
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
.filter((r) => !present.has(r))
|
|
145
|
-
.map((name) => plink.serialize({ ...parsed, pathname: appPath + name }))
|
|
146
|
-
if (missing.length) {
|
|
147
|
-
throw ERR_NOT_FOUND('Not found: ' + missing.join(', '))
|
|
87
|
+
})
|
|
88
|
+
const deferred = Promise.withResolvers()
|
|
89
|
+
const countdown = setTimeout(() => {
|
|
90
|
+
deferred.reject(ERR_NETWORK_TIMEOUT('Network Timeout ' + timeout / 1000 + 's'))
|
|
91
|
+
}, timeout)
|
|
92
|
+
await Promise.race([this.drive.core.update({ wait: true }), deferred.promise])
|
|
93
|
+
clearTimeout(countdown)
|
|
94
|
+
const pkg = await this.drive.get('/package.json')
|
|
95
|
+
if (pkg === null) throw ERR_INVALID_MANIFEST('Unable to read application package.json')
|
|
96
|
+
const manifest = JSON.parse(pkg.toString())
|
|
97
|
+
|
|
98
|
+
const { name, productName, version, upgrade, bin } = manifest
|
|
99
|
+
const appName = productName ?? name
|
|
100
|
+
const home = os.homedir()
|
|
101
|
+
|
|
102
|
+
if (bin) {
|
|
103
|
+
const bins = typeof bin === 'string' ? { [name]: bin } : bin
|
|
104
|
+
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)
|
|
112
|
+
: path.join(home, '.local', 'bin', binName)
|
|
113
|
+
this.targets.push({ filename: binName, ext, dest, isBin: true })
|
|
148
114
|
}
|
|
115
|
+
}
|
|
149
116
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
'
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
117
|
+
const ext = isMac ? '.app' : isWindows ? '.msix' : '.AppImage'
|
|
118
|
+
const dest = isWindows
|
|
119
|
+
? null
|
|
120
|
+
: to
|
|
121
|
+
? path.join(to, appName + ext)
|
|
122
|
+
: isMac
|
|
123
|
+
? path.join('/', 'Applications', appName + ext)
|
|
124
|
+
: fs.existsSync(path.join(home, 'Applications'))
|
|
125
|
+
? path.join(home, 'Applications', appName + ext)
|
|
126
|
+
: fs.existsSync(path.join(home, 'AppImages'))
|
|
127
|
+
? path.join(home, 'AppImages', appName + ext)
|
|
128
|
+
: path.join(home, '.local', 'bin', appName + ext)
|
|
129
|
+
|
|
130
|
+
this.targets.push({ filename: appName, ext, dest, isBin: false })
|
|
131
|
+
|
|
132
|
+
const present = new Set()
|
|
133
|
+
const appPath = '/by-arch/' + host + '/app/'
|
|
134
|
+
for await (const name of this.drive.readdir(appPath)) present.add(name)
|
|
135
|
+
|
|
136
|
+
const required = only
|
|
137
|
+
? only
|
|
138
|
+
.split(',')
|
|
139
|
+
.map((s) => s.trim())
|
|
140
|
+
.filter(Boolean)
|
|
141
|
+
: this.targets.filter((t) => t.isBin || !bin).map((t) => t.filename + t.ext)
|
|
142
|
+
const missing = required
|
|
143
|
+
.filter((r) => !present.has(r))
|
|
144
|
+
.map((name) => plink.serialize({ ...parsed, pathname: appPath + name }))
|
|
145
|
+
if (missing.length) {
|
|
146
|
+
throw ERR_NOT_FOUND('Not found: ' + missing.join(', '))
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
this.targets = this.targets.filter(({ filename, ext }) => present.has(filename + ext))
|
|
150
|
+
|
|
151
|
+
const exists = []
|
|
152
|
+
const installs = []
|
|
153
|
+
for (const target of this.targets) {
|
|
154
|
+
if (isWindows) {
|
|
155
|
+
const ps = spawnSync('powershell', [
|
|
156
|
+
'-NoProfile',
|
|
157
|
+
'-Command',
|
|
158
|
+
`(Get-AppxPackage '${target.filename}') -ne $null`
|
|
159
|
+
])
|
|
160
|
+
if (ps.stdout.toString().trim() === 'True') {
|
|
166
161
|
exists.push({ filename: target.filename, dest: target.dest })
|
|
167
162
|
continue
|
|
168
163
|
}
|
|
169
|
-
|
|
164
|
+
} else if (fs.existsSync(target.dest)) {
|
|
165
|
+
exists.push({ filename: target.filename, dest: target.dest })
|
|
166
|
+
continue
|
|
170
167
|
}
|
|
168
|
+
installs.push(target)
|
|
169
|
+
}
|
|
171
170
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
171
|
+
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
|
+
throw ERR_EXISTS(message)
|
|
176
|
+
}
|
|
178
177
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
let installed = 0
|
|
194
|
-
for (const { filename, ext, dest, isBin } of installs) {
|
|
195
|
-
const key = appPath + filename + ext
|
|
196
|
-
this.push({ tag: 'app', data: { app: filename, name, version, upgrade, key, tmp, dest } })
|
|
197
|
-
|
|
198
|
-
const from = path.join(tmp, 'by-arch', host, 'app', filename + ext)
|
|
199
|
-
|
|
200
|
-
if (fs.existsSync(from) === false) {
|
|
201
|
-
throw ERR_NOT_FOUND(plink.serialize({ ...parsed, pathname: key }))
|
|
202
|
-
}
|
|
178
|
+
const tmp = path.join(this.base, 'targets')
|
|
179
|
+
fs.mkdirSync(tmp, { recursive: true })
|
|
180
|
+
const prefixes = installs.map(({ filename, ext }) => appPath + filename + ext)
|
|
181
|
+
const mirror = this.drive.mirror(new LocalDrive(tmp), {
|
|
182
|
+
prefix: prefixes,
|
|
183
|
+
prune: false,
|
|
184
|
+
progress: true,
|
|
185
|
+
dedup: true
|
|
186
|
+
})
|
|
187
|
+
const monitor = mirror.monitor()
|
|
188
|
+
monitor.on('update', (stats) => this.emit('stats', stats))
|
|
189
|
+
await mirror.done()
|
|
190
|
+
monitor.destroy()
|
|
203
191
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
continue
|
|
209
|
-
}
|
|
192
|
+
let installed = 0
|
|
193
|
+
for (const { filename, ext, dest, isBin } of installs) {
|
|
194
|
+
const key = appPath + filename + ext
|
|
195
|
+
this.emit('app', { app: filename, name, version, upgrade, key, tmp, dest })
|
|
210
196
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
} catch (err) {
|
|
216
|
-
if (err.code === 'EACCES' || err.code === 'EPERM') {
|
|
217
|
-
const dir = path.dirname(dest)
|
|
218
|
-
const fix = isMac
|
|
219
|
-
? `sudo chgrp admin ${dir} && sudo chmod g+w ${dir}`
|
|
220
|
-
: `sudo chown -R "$(id -un):$(id -gn)" ${dir}`
|
|
221
|
-
throw ERR_PERMISSION_REQUIRED(`Permission denied: ${dest}\n Fix: ${fix}`)
|
|
222
|
-
}
|
|
223
|
-
throw err
|
|
224
|
-
}
|
|
225
|
-
fs.chmodSync(dest, 0o755)
|
|
226
|
-
} else {
|
|
227
|
-
try {
|
|
228
|
-
await fs.promises.rename(from, dest)
|
|
229
|
-
} catch (err) {
|
|
230
|
-
if (err.code === 'EACCES' || err.code === 'EPERM') {
|
|
231
|
-
const dir = path.dirname(dest)
|
|
232
|
-
const fix = isMac
|
|
233
|
-
? `sudo chgrp admin ${dir} && sudo chmod g+w ${dir}`
|
|
234
|
-
: `sudo chown -R "$(id -un):$(id -gn)" ${dir}`
|
|
235
|
-
throw ERR_PERMISSION_REQUIRED(`Permission denied: ${dest}\n Fix: ${fix}`)
|
|
236
|
-
}
|
|
237
|
-
throw err
|
|
238
|
-
}
|
|
239
|
-
if (isLinux) await this._linux(dest, filename, tmp, home)
|
|
240
|
-
}
|
|
241
|
-
installed++
|
|
242
|
-
}
|
|
243
|
-
if (installed === 0) {
|
|
244
|
-
throw ERR_UNKNOWN('Failed to install')
|
|
197
|
+
const from = path.join(tmp, 'by-arch', host, 'app', filename + ext)
|
|
198
|
+
|
|
199
|
+
if (fs.existsSync(from) === false) {
|
|
200
|
+
throw ERR_NOT_FOUND(plink.serialize({ ...parsed, pathname: key }))
|
|
245
201
|
}
|
|
246
|
-
this.final = { success: true, installed, exists }
|
|
247
|
-
} finally {
|
|
248
|
-
if (findingDone) findingDone()
|
|
249
|
-
await drive.close()
|
|
250
|
-
await swarm.destroy()
|
|
251
|
-
await corestore.close()
|
|
252
|
-
fs.rmSync(base, { recursive: true, force: true })
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
202
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
process.stdout.write(JSON.stringify({ cmd: 'install', tag, data }) + '\n')
|
|
203
|
+
if (isWindows) {
|
|
204
|
+
const MSIXManager = require('msix-manager')
|
|
205
|
+
await new MSIXManager().addPackage(from)
|
|
206
|
+
installed++
|
|
261
207
|
continue
|
|
262
208
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
209
|
+
|
|
210
|
+
if (isBin) {
|
|
211
|
+
try {
|
|
212
|
+
if (!to) fs.mkdirSync(path.dirname(dest), { recursive: true })
|
|
213
|
+
this._move(from, dest)
|
|
214
|
+
} catch (err) {
|
|
215
|
+
if (err.code === 'EACCES' || err.code === 'EPERM') {
|
|
216
|
+
const dir = path.dirname(dest)
|
|
217
|
+
const fix = isMac
|
|
218
|
+
? `sudo chgrp admin ${dir} && sudo chmod g+w ${dir}`
|
|
219
|
+
: `sudo chown -R "$(id -un):$(id -gn)" ${dir}`
|
|
220
|
+
throw ERR_PERMISSION_REQUIRED(`Permission denied: ${dest}\n Fix: ${fix}`)
|
|
221
|
+
}
|
|
222
|
+
throw err
|
|
223
|
+
}
|
|
224
|
+
fs.chmodSync(dest, 0o755)
|
|
225
|
+
} else {
|
|
226
|
+
try {
|
|
227
|
+
await fs.promises.rename(from, dest)
|
|
228
|
+
} catch (err) {
|
|
229
|
+
if (err.code === 'EACCES' || err.code === 'EPERM') {
|
|
230
|
+
const dir = path.dirname(dest)
|
|
231
|
+
const fix = isMac
|
|
232
|
+
? `sudo chgrp admin ${dir} && sudo chmod g+w ${dir}`
|
|
233
|
+
: `sudo chown -R "$(id -un):$(id -gn)" ${dir}`
|
|
234
|
+
throw ERR_PERMISSION_REQUIRED(`Permission denied: ${dest}\n Fix: ${fix}`)
|
|
235
|
+
}
|
|
236
|
+
throw err
|
|
237
|
+
}
|
|
238
|
+
if (isLinux) await this._linux(dest, filename, tmp, home)
|
|
270
239
|
}
|
|
271
|
-
|
|
272
|
-
status = false
|
|
273
|
-
if (tag === 'final') return data
|
|
240
|
+
installed++
|
|
274
241
|
}
|
|
242
|
+
if (installed === 0) {
|
|
243
|
+
throw ERR_UNKNOWN('Failed to install')
|
|
244
|
+
}
|
|
245
|
+
this.emit('final', { success: true, installed, exists })
|
|
275
246
|
}
|
|
276
247
|
|
|
277
248
|
async _linux(dest, appName, tmp, home) {
|
|
@@ -327,20 +298,35 @@ class Install extends Opstream {
|
|
|
327
298
|
fs.rmSync(src)
|
|
328
299
|
}
|
|
329
300
|
}
|
|
330
|
-
}
|
|
331
301
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
302
|
+
async _close() {
|
|
303
|
+
await this._teardown()
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async _teardown() {
|
|
307
|
+
if (this.doneFinding) {
|
|
308
|
+
this.doneFinding()
|
|
309
|
+
this.doneFinding = null
|
|
310
|
+
}
|
|
311
|
+
if (this.drive) {
|
|
312
|
+
await this.drive.close()
|
|
313
|
+
this.drive = null
|
|
314
|
+
}
|
|
315
|
+
if (this.swarm && this._ownSwarm) {
|
|
316
|
+
await this.swarm.destroy()
|
|
317
|
+
this.swarm = null
|
|
318
|
+
}
|
|
319
|
+
if (this.corestore && this._ownCorestore) {
|
|
320
|
+
await this.corestore.close()
|
|
321
|
+
this.corestore = null
|
|
322
|
+
}
|
|
323
|
+
if (this.base) {
|
|
324
|
+
try {
|
|
325
|
+
fs.rmSync(this.base, { recursive: true, force: true })
|
|
326
|
+
} catch {}
|
|
327
|
+
this.base = null
|
|
328
|
+
}
|
|
329
|
+
}
|
|
346
330
|
}
|
|
331
|
+
|
|
332
|
+
module.exports = Install
|
package/package.json
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pear-install",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"description": "Install Pear and Pear Applications",
|
|
7
7
|
"author": "Holepunch Inc",
|
|
8
8
|
"license": "Apache-2.0",
|
|
9
|
-
"
|
|
10
|
-
"
|
|
9
|
+
"files": [
|
|
10
|
+
"cmd.js"
|
|
11
|
+
],
|
|
12
|
+
"bin": "bin.js",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": "./index.js",
|
|
15
|
+
"./cmd": "./cmd.js",
|
|
16
|
+
"./package.json": "./package.json"
|
|
11
17
|
},
|
|
12
18
|
"imports": {
|
|
13
19
|
"process": {
|
package/.gitattributes
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
* text=auto eol=lf
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
name: Publish
|
|
2
|
-
on:
|
|
3
|
-
push:
|
|
4
|
-
tags:
|
|
5
|
-
- v*
|
|
6
|
-
permissions:
|
|
7
|
-
id-token: write
|
|
8
|
-
contents: write
|
|
9
|
-
jobs:
|
|
10
|
-
publish:
|
|
11
|
-
runs-on: ubuntu-latest
|
|
12
|
-
environment:
|
|
13
|
-
name: npm
|
|
14
|
-
name: Publish
|
|
15
|
-
steps:
|
|
16
|
-
- uses: holepunchto/actions/node-base@v1
|
|
17
|
-
- uses: holepunchto/actions/publish@v1
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
name: Test
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [main]
|
|
6
|
-
pull_request:
|
|
7
|
-
branches: [main]
|
|
8
|
-
|
|
9
|
-
jobs:
|
|
10
|
-
lint:
|
|
11
|
-
runs-on: ubuntu-latest
|
|
12
|
-
name: Lint
|
|
13
|
-
steps:
|
|
14
|
-
- uses: holepunchto/actions/node-base@v1
|
|
15
|
-
- run: npm run lint
|
|
16
|
-
|
|
17
|
-
test:
|
|
18
|
-
strategy:
|
|
19
|
-
matrix:
|
|
20
|
-
include:
|
|
21
|
-
- os: ubuntu-latest
|
|
22
|
-
platform: linux
|
|
23
|
-
- os: macos-latest
|
|
24
|
-
platform: darwin
|
|
25
|
-
- os: windows-latest
|
|
26
|
-
platform: win32
|
|
27
|
-
runs-on: ${{ matrix.os }}
|
|
28
|
-
name: Test / ${{ matrix.platform }}
|
|
29
|
-
timeout-minutes: 2
|
|
30
|
-
steps:
|
|
31
|
-
- uses: holepunchto/actions/bare-base@v1
|
|
32
|
-
- run: npm test
|
package/.prettierignore
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
template/appling/package.json
|
package/.prettierrc
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"prettier-config-holepunch"
|
package/NOTICE
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
Copyright 2026 Holepunch Inc
|
|
2
|
-
|
|
3
|
-
Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
you may not use this file except in compliance with the License.
|
|
5
|
-
You may obtain a copy of the License at
|
|
6
|
-
|
|
7
|
-
http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
|
|
9
|
-
Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
See the License for the specific language governing permissions and
|
|
13
|
-
limitations under the License.
|
package/test/all.js
DELETED
package/test/helper.js
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
const Corestore = require('corestore')
|
|
3
|
-
const Hyperdrive = require('hyperdrive')
|
|
4
|
-
const Hyperswarm = require('hyperswarm')
|
|
5
|
-
const tmp = require('test-tmp')
|
|
6
|
-
const { command, arg, bail } = require('paparam')
|
|
7
|
-
const pkg = require('../package.json')
|
|
8
|
-
const install = require('..')
|
|
9
|
-
const process = require('process')
|
|
10
|
-
const arch = `${process.platform}-${process.arch}`
|
|
11
|
-
|
|
12
|
-
async function seed(t, { bootstrap, manifest, files }) {
|
|
13
|
-
const storage = await tmp(t)
|
|
14
|
-
const corestore = new Corestore(storage)
|
|
15
|
-
const drive = new Hyperdrive(corestore)
|
|
16
|
-
await drive.ready()
|
|
17
|
-
await drive.put('/package.json', Buffer.from(JSON.stringify(manifest)))
|
|
18
|
-
for (const [p, content] of Object.entries(files)) {
|
|
19
|
-
await drive.put(p, Buffer.from(content))
|
|
20
|
-
}
|
|
21
|
-
const swarm = new Hyperswarm({ bootstrap })
|
|
22
|
-
swarm.on('connection', (c) => corestore.replicate(c))
|
|
23
|
-
swarm.join(drive.discoveryKey, { server: true, client: false })
|
|
24
|
-
await swarm.flush()
|
|
25
|
-
t.teardown(async () => {
|
|
26
|
-
await swarm.destroy()
|
|
27
|
-
await drive.close()
|
|
28
|
-
await corestore.close()
|
|
29
|
-
})
|
|
30
|
-
return drive.key
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async function run(args) {
|
|
34
|
-
const lines = []
|
|
35
|
-
let buf = ''
|
|
36
|
-
const origWrite = process.stdout.write
|
|
37
|
-
const origExitCode = process.exitCode
|
|
38
|
-
process.stdout.write = function (chunk) {
|
|
39
|
-
buf += chunk.toString()
|
|
40
|
-
let i
|
|
41
|
-
while ((i = buf.indexOf('\n')) !== -1) {
|
|
42
|
-
lines.push(buf.slice(0, i))
|
|
43
|
-
buf = buf.slice(i + 1)
|
|
44
|
-
}
|
|
45
|
-
return true
|
|
46
|
-
}
|
|
47
|
-
let bailInfo = null
|
|
48
|
-
let exitCode
|
|
49
|
-
try {
|
|
50
|
-
const program = command(
|
|
51
|
-
'install',
|
|
52
|
-
arg('[link]', 'Pear link'),
|
|
53
|
-
pkg.command,
|
|
54
|
-
install,
|
|
55
|
-
bail((info) => {
|
|
56
|
-
bailInfo = info
|
|
57
|
-
})
|
|
58
|
-
)
|
|
59
|
-
const c = program.parse(args, { run: true })
|
|
60
|
-
if (c?.running) await c.running.catch(() => {})
|
|
61
|
-
} finally {
|
|
62
|
-
process.stdout.write = origWrite
|
|
63
|
-
exitCode = process.exitCode
|
|
64
|
-
process.exitCode = origExitCode
|
|
65
|
-
}
|
|
66
|
-
const events = lines.flatMap((l) => {
|
|
67
|
-
try {
|
|
68
|
-
return [JSON.parse(l)]
|
|
69
|
-
} catch {
|
|
70
|
-
return []
|
|
71
|
-
}
|
|
72
|
-
})
|
|
73
|
-
return { events, bail: bailInfo, stdout: lines.join('\n') + buf, exitCode }
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function bootstrapArg(testnet) {
|
|
77
|
-
return testnet.bootstrap.map((b) => `${b.host}:${b.port}`).join(',')
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
module.exports = { seed, run, arch, bootstrapArg }
|
package/test/index.test.js
DELETED
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
const test = require('brittle')
|
|
3
|
-
const path = require('path')
|
|
4
|
-
const fs = require('fs')
|
|
5
|
-
const createTestnet = require('hyperdht/testnet')
|
|
6
|
-
const hypercoreCrypto = require('hypercore-crypto')
|
|
7
|
-
const hid = require('hypercore-id-encoding')
|
|
8
|
-
const plink = require('pear-link')
|
|
9
|
-
const { isWindows } = require('which-runtime')
|
|
10
|
-
const tmp = require('test-tmp')
|
|
11
|
-
const { seed, run, arch, bootstrapArg } = require('./helper')
|
|
12
|
-
|
|
13
|
-
test('successful bin install via testnet', { skip: isWindows }, async function (t) {
|
|
14
|
-
t.timeout(60000)
|
|
15
|
-
const testnet = await createTestnet(3, t.teardown)
|
|
16
|
-
const key = await seed(t, {
|
|
17
|
-
bootstrap: testnet.bootstrap,
|
|
18
|
-
manifest: { name: 'tbin', version: '1.0.0', upgrade: 'pear://x', bin: 'cli.js' },
|
|
19
|
-
files: { ['/by-arch/' + arch + '/app/tbin']: 'BIN' }
|
|
20
|
-
})
|
|
21
|
-
const link = plink.serialize({ drive: { key } })
|
|
22
|
-
const target = await tmp(t)
|
|
23
|
-
const { events } = await run([
|
|
24
|
-
'--json',
|
|
25
|
-
'--to',
|
|
26
|
-
target,
|
|
27
|
-
'--dht-bootstrap',
|
|
28
|
-
bootstrapArg(testnet),
|
|
29
|
-
link
|
|
30
|
-
])
|
|
31
|
-
const final = events.find((e) => e.tag === 'final')
|
|
32
|
-
t.ok(final, 'final event emitted')
|
|
33
|
-
t.is(final.data.success, true, 'success')
|
|
34
|
-
t.is(fs.readFileSync(path.join(target, 'tbin'), 'utf8'), 'BIN', 'bin written to target')
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
test('notFound emits full pear:// link when platform binary missing', async function (t) {
|
|
38
|
-
t.timeout(60000)
|
|
39
|
-
const testnet = await createTestnet(3, t.teardown)
|
|
40
|
-
const key = await seed(t, {
|
|
41
|
-
bootstrap: testnet.bootstrap,
|
|
42
|
-
manifest: { name: 'tbin', version: '1.0.0', upgrade: 'pear://x', bin: 'cli.js' },
|
|
43
|
-
files: {}
|
|
44
|
-
})
|
|
45
|
-
const link = plink.serialize({ drive: { key } })
|
|
46
|
-
const target = await tmp(t)
|
|
47
|
-
const { events } = await run([
|
|
48
|
-
'--json',
|
|
49
|
-
'--to',
|
|
50
|
-
target,
|
|
51
|
-
'--dht-bootstrap',
|
|
52
|
-
bootstrapArg(testnet),
|
|
53
|
-
link
|
|
54
|
-
])
|
|
55
|
-
const final = events.find((e) => e.tag === 'final')
|
|
56
|
-
const error = events.find((e) => e.tag === 'error')
|
|
57
|
-
t.ok(final, 'final event emitted')
|
|
58
|
-
t.is(final.data.success, false, 'failure')
|
|
59
|
-
t.is(error?.data.code, 'ERR_NOT_FOUND', 'ERR_NOT_FOUND emitted')
|
|
60
|
-
t.ok(error?.data.message.includes('pear://'), 'error message includes pear link')
|
|
61
|
-
t.ok(
|
|
62
|
-
error?.data.message.includes('/by-arch/' + arch + '/app/tbin'),
|
|
63
|
-
'error message includes platform path'
|
|
64
|
-
)
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
test('refuses to overwrite an existing install', { skip: isWindows }, async function (t) {
|
|
68
|
-
t.timeout(60000)
|
|
69
|
-
const testnet = await createTestnet(3, t.teardown)
|
|
70
|
-
const key = await seed(t, {
|
|
71
|
-
bootstrap: testnet.bootstrap,
|
|
72
|
-
manifest: { name: 'tbin', version: '1.0.0', upgrade: 'pear://x', bin: 'cli.js' },
|
|
73
|
-
files: { ['/by-arch/' + arch + '/app/tbin']: 'NEW' }
|
|
74
|
-
})
|
|
75
|
-
const link = plink.serialize({ drive: { key } })
|
|
76
|
-
const target = await tmp(t)
|
|
77
|
-
fs.writeFileSync(path.join(target, 'tbin'), 'EXISTING')
|
|
78
|
-
const { events } = await run([
|
|
79
|
-
'--json',
|
|
80
|
-
'--to',
|
|
81
|
-
target,
|
|
82
|
-
'--dht-bootstrap',
|
|
83
|
-
bootstrapArg(testnet),
|
|
84
|
-
link
|
|
85
|
-
])
|
|
86
|
-
const final = events.find((e) => e.tag === 'final')
|
|
87
|
-
const error = events.find((e) => e.tag === 'error')
|
|
88
|
-
t.ok(final, 'final event emitted')
|
|
89
|
-
t.is(final.data.success, false, 'failure')
|
|
90
|
-
t.is(error?.data.code, 'ERR_EXISTS', 'ERR_EXISTS emitted')
|
|
91
|
-
t.ok(
|
|
92
|
-
error?.data.message.includes('Refusing to overwrite existing'),
|
|
93
|
-
'error message reports refusal'
|
|
94
|
-
)
|
|
95
|
-
t.ok(
|
|
96
|
-
error?.data.message.includes(path.join(target, 'tbin')),
|
|
97
|
-
'error message lists existing target path'
|
|
98
|
-
)
|
|
99
|
-
t.is(fs.readFileSync(path.join(target, 'tbin'), 'utf8'), 'EXISTING', 'existing file untouched')
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
test('invalid port in --dht-bootstrap reaches bail handler', async function (t) {
|
|
103
|
-
const { bail: info } = await run([
|
|
104
|
-
'--dht-bootstrap',
|
|
105
|
-
'127.0.0.1:nope',
|
|
106
|
-
'pear://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
|
|
107
|
-
])
|
|
108
|
-
t.ok(info, 'bail handler invoked')
|
|
109
|
-
t.is(info.err?.message, 'Invalid port: nope')
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
test('link with pathname emits error event', async function (t) {
|
|
113
|
-
const key = hid.encode(hypercoreCrypto.keyPair().publicKey)
|
|
114
|
-
const link = `pear://${key}/some/path`
|
|
115
|
-
const { events } = await run(['--json', link])
|
|
116
|
-
const error = events.find((e) => e.tag === 'error')
|
|
117
|
-
t.ok(error, 'error event emitted')
|
|
118
|
-
t.is(error.data.message, 'Link must not have pathname')
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
test(
|
|
122
|
-
'non-json output prints installing, app and Installed',
|
|
123
|
-
{ skip: isWindows },
|
|
124
|
-
async function (t) {
|
|
125
|
-
t.timeout(60000)
|
|
126
|
-
const testnet = await createTestnet(3, t.teardown)
|
|
127
|
-
const key = await seed(t, {
|
|
128
|
-
bootstrap: testnet.bootstrap,
|
|
129
|
-
manifest: { name: 'tbin', version: '1.0.0', upgrade: 'pear://x', bin: 'cli.js' },
|
|
130
|
-
files: { ['/by-arch/' + arch + '/app/tbin']: 'BIN' }
|
|
131
|
-
})
|
|
132
|
-
const link = plink.serialize({ drive: { key } })
|
|
133
|
-
const target = await tmp(t)
|
|
134
|
-
const { stdout } = await run(['--to', target, '--dht-bootstrap', bootstrapArg(testnet), link])
|
|
135
|
-
t.ok(stdout.includes('Installing...'), 'installing message printed')
|
|
136
|
-
t.ok(stdout.includes('App: tbin'), 'app message printed')
|
|
137
|
-
t.ok(stdout.includes('Installed'), 'final installed message printed')
|
|
138
|
-
}
|
|
139
|
-
)
|
|
140
|
-
|
|
141
|
-
test(
|
|
142
|
-
'non-json output prints Not found for missing platform binary',
|
|
143
|
-
{ skip: isWindows },
|
|
144
|
-
async function (t) {
|
|
145
|
-
t.timeout(60000)
|
|
146
|
-
const testnet = await createTestnet(3, t.teardown)
|
|
147
|
-
const key = await seed(t, {
|
|
148
|
-
bootstrap: testnet.bootstrap,
|
|
149
|
-
manifest: { name: 'tbin', version: '1.0.0', upgrade: 'pear://x', bin: 'cli.js' },
|
|
150
|
-
files: {}
|
|
151
|
-
})
|
|
152
|
-
const link = plink.serialize({ drive: { key } })
|
|
153
|
-
const target = await tmp(t)
|
|
154
|
-
const { stdout } = await run(['--to', target, '--dht-bootstrap', bootstrapArg(testnet), link])
|
|
155
|
-
t.ok(stdout.includes('Not found: pear://'), 'not found message printed')
|
|
156
|
-
}
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
test(
|
|
160
|
-
'non-json output prints Refusing to overwrite for existing target',
|
|
161
|
-
{ skip: isWindows },
|
|
162
|
-
async function (t) {
|
|
163
|
-
t.timeout(60000)
|
|
164
|
-
const testnet = await createTestnet(3, t.teardown)
|
|
165
|
-
const key = await seed(t, {
|
|
166
|
-
bootstrap: testnet.bootstrap,
|
|
167
|
-
manifest: { name: 'tbin', version: '1.0.0', upgrade: 'pear://x', bin: 'cli.js' },
|
|
168
|
-
files: { ['/by-arch/' + arch + '/app/tbin']: 'BIN' }
|
|
169
|
-
})
|
|
170
|
-
const link = plink.serialize({ drive: { key } })
|
|
171
|
-
const target = await tmp(t)
|
|
172
|
-
fs.writeFileSync(path.join(target, 'tbin'), 'EXISTING')
|
|
173
|
-
const { stdout } = await run(['--to', target, '--dht-bootstrap', bootstrapArg(testnet), link])
|
|
174
|
-
t.ok(stdout.includes('Refusing to overwrite existing'), 'refusing message printed')
|
|
175
|
-
t.ok(stdout.includes('Manually remove first'), 'fix hint printed')
|
|
176
|
-
}
|
|
177
|
-
)
|
|
178
|
-
|
|
179
|
-
test('_move falls back to copy+rm on EXDEV', { skip: isWindows }, async function (t) {
|
|
180
|
-
t.timeout(60000)
|
|
181
|
-
const testnet = await createTestnet(3, t.teardown)
|
|
182
|
-
const key = await seed(t, {
|
|
183
|
-
bootstrap: testnet.bootstrap,
|
|
184
|
-
manifest: { name: 'tbin', version: '1.0.0', upgrade: 'pear://x', bin: 'cli.js' },
|
|
185
|
-
files: { ['/by-arch/' + arch + '/app/tbin']: 'BIN' }
|
|
186
|
-
})
|
|
187
|
-
const link = plink.serialize({ drive: { key } })
|
|
188
|
-
const target = await tmp(t)
|
|
189
|
-
const renameSync = fs.renameSync
|
|
190
|
-
fs.renameSync = function () {
|
|
191
|
-
const err = new Error('cross-device link not permitted')
|
|
192
|
-
err.code = 'EXDEV'
|
|
193
|
-
throw err
|
|
194
|
-
}
|
|
195
|
-
t.teardown(() => {
|
|
196
|
-
fs.renameSync = renameSync
|
|
197
|
-
})
|
|
198
|
-
const { events } = await run([
|
|
199
|
-
'--json',
|
|
200
|
-
'--to',
|
|
201
|
-
target,
|
|
202
|
-
'--dht-bootstrap',
|
|
203
|
-
bootstrapArg(testnet),
|
|
204
|
-
link
|
|
205
|
-
])
|
|
206
|
-
const final = events.find((e) => e.tag === 'final')
|
|
207
|
-
t.is(final?.data?.success, true, 'install succeeded via copy+rm fallback')
|
|
208
|
-
t.is(fs.readFileSync(path.join(target, 'tbin'), 'utf8'), 'BIN', 'bin content copied to target')
|
|
209
|
-
})
|
|
210
|
-
|
|
211
|
-
test('permission denied when target dir is read-only', { skip: isWindows }, async function (t) {
|
|
212
|
-
t.timeout(60000)
|
|
213
|
-
const testnet = await createTestnet(3, t.teardown)
|
|
214
|
-
const key = await seed(t, {
|
|
215
|
-
bootstrap: testnet.bootstrap,
|
|
216
|
-
manifest: { name: 'tbin', version: '1.0.0', upgrade: 'pear://x', bin: 'cli.js' },
|
|
217
|
-
files: { ['/by-arch/' + arch + '/app/tbin']: 'BIN' }
|
|
218
|
-
})
|
|
219
|
-
const link = plink.serialize({ drive: { key } })
|
|
220
|
-
const target = await tmp(t)
|
|
221
|
-
fs.chmodSync(target, 0o555)
|
|
222
|
-
t.teardown(() => {
|
|
223
|
-
try {
|
|
224
|
-
fs.chmodSync(target, 0o755)
|
|
225
|
-
} catch {}
|
|
226
|
-
})
|
|
227
|
-
const { events, stdout } = await run([
|
|
228
|
-
'--to',
|
|
229
|
-
target,
|
|
230
|
-
'--dht-bootstrap',
|
|
231
|
-
bootstrapArg(testnet),
|
|
232
|
-
link
|
|
233
|
-
])
|
|
234
|
-
// since no --json, events is empty; assert via stdout
|
|
235
|
-
t.is(events.length, 0, 'no json events (non-json mode)')
|
|
236
|
-
t.ok(stdout.includes('Permission denied'), 'permission message printed')
|
|
237
|
-
t.ok(stdout.includes('Fix:'), 'fix hint printed')
|
|
238
|
-
})
|