@stevezhou/sisu 0.3.15 → 0.3.16
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 +1 -1
- package/package.json +1 -1
- package/scripts/ensure-cli-path.js +125 -26
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ sisu login
|
|
|
13
13
|
sisu
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
Postinstall
|
|
16
|
+
Postinstall puts `sisu` on a PATH users actually have: `~/.local/bin` on Unix, `%LOCALAPPDATA%\sisu\bin` on Windows (wrappers that call npm's `sisu.cmd`, plus a Git Bash `sisu`). It also appends that Windows directory to the user PATH. If this shell still cannot see the command, it prints `export PATH=...` (Unix) or `set PATH=` / `$env:Path` (Windows). `npm install -g` is a small JS package. It also fetches the stamped SiSu TUI pager for **this package version** into `~/.sisu/bin` when a prebuilt exists. GitHub Release tags ship `darwin-arm64`, `linux-x64`, and `linux-arm64`. `darwin-x64` is opt-in (`workflow_dispatch` with `platforms` containing `darwin-x64`) and often missing; platforms without a binary, or a missing GitHub Release asset, keep the Node TUI.
|
|
17
17
|
|
|
18
18
|
Requires Node.js 20 or newer. `npx sisu` works without a global install.
|
|
19
19
|
|
package/package.json
CHANGED
|
@@ -1,37 +1,54 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/** After `npm i -g`, put `sisu` on a PATH users actually have.
|
|
3
|
-
* npm's global bin is often missing from Debian/login PATH.
|
|
3
|
+
* npm's global bin is often missing from Debian/login PATH and from Windows PATH.
|
|
4
|
+
* Do not copy npm's sisu.cmd: it uses %~dp0 and breaks outside the prefix.
|
|
4
5
|
*/
|
|
6
|
+
const { execFileSync } = require('child_process')
|
|
5
7
|
const fs = require('fs')
|
|
6
8
|
const os = require('os')
|
|
7
9
|
const path = require('path')
|
|
8
10
|
|
|
9
|
-
function
|
|
10
|
-
return
|
|
11
|
+
function isWin(options = {}) {
|
|
12
|
+
return (options.platform || process.platform) === 'win32'
|
|
11
13
|
}
|
|
12
14
|
|
|
13
|
-
function
|
|
14
|
-
|
|
15
|
-
return
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
function pathDelim(options = {}) {
|
|
16
|
+
if (options.delimiter) return options.delimiter
|
|
17
|
+
return isWin(options) ? ';' : options.platform ? ':' : path.delimiter
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function pathDirs(pathEnv = process.env.PATH || '', options = {}) {
|
|
21
|
+
return pathEnv.split(pathDelim(options)).filter(Boolean)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function normalizePathEntry(dir, options = {}) {
|
|
25
|
+
if (isWin(options)) {
|
|
26
|
+
return String(dir).replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase()
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
return path.resolve(dir)
|
|
30
|
+
} catch {
|
|
31
|
+
return dir
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function pathContains(dir, pathEnv = process.env.PATH || '', options = {}) {
|
|
36
|
+
const target = normalizePathEntry(dir, options)
|
|
37
|
+
return pathDirs(pathEnv, options).some((entry) => normalizePathEntry(entry, options) === target)
|
|
22
38
|
}
|
|
23
39
|
|
|
24
40
|
function globalSisuBin(options = {}) {
|
|
25
|
-
const
|
|
41
|
+
const win = isWin(options)
|
|
42
|
+
const name = win ? 'sisu.cmd' : 'sisu'
|
|
26
43
|
const prefix = options.prefix || process.env.npm_config_prefix || ''
|
|
27
44
|
const fromPrefix = prefix
|
|
28
|
-
? (
|
|
45
|
+
? (win ? path.join(prefix, name) : path.join(prefix, 'bin', name))
|
|
29
46
|
: ''
|
|
30
47
|
const fromLayout = path.resolve(
|
|
31
48
|
options.packageRoot || path.join(__dirname, '..'),
|
|
32
49
|
'..',
|
|
33
50
|
'..',
|
|
34
|
-
|
|
51
|
+
win ? name : path.join('bin', name),
|
|
35
52
|
)
|
|
36
53
|
const exists = options.exists || ((file) => fs.existsSync(file))
|
|
37
54
|
for (const candidate of [fromPrefix, fromLayout]) {
|
|
@@ -40,16 +57,39 @@ function globalSisuBin(options = {}) {
|
|
|
40
57
|
return fromPrefix || fromLayout
|
|
41
58
|
}
|
|
42
59
|
|
|
43
|
-
function userLocalBin(home = os.homedir()) {
|
|
60
|
+
function userLocalBin(home = os.homedir(), options = {}) {
|
|
61
|
+
if (isWin(options)) {
|
|
62
|
+
const localAppData =
|
|
63
|
+
options.localAppData || process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local')
|
|
64
|
+
return path.join(localAppData, 'sisu', 'bin')
|
|
65
|
+
}
|
|
44
66
|
return path.join(home, '.local', 'bin')
|
|
45
67
|
}
|
|
46
68
|
|
|
69
|
+
function toGitBashPath(winPath) {
|
|
70
|
+
const normalized = String(winPath).replace(/\\/g, '/')
|
|
71
|
+
const drive = normalized.match(/^([A-Za-z]):\/(.*)$/)
|
|
72
|
+
if (drive) return `/${drive[1].toLowerCase()}/${drive[2]}`
|
|
73
|
+
return normalized
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function writeShim(dest, body) {
|
|
77
|
+
fs.writeFileSync(dest, body)
|
|
78
|
+
try {
|
|
79
|
+
fs.chmodSync(dest, 0o755)
|
|
80
|
+
} catch {
|
|
81
|
+
// Windows may ignore chmod; the file still runs via PATHEXT / shebang
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
47
85
|
function ensureUserShim(target, options = {}) {
|
|
48
|
-
if ((options.platform || process.platform) === 'win32') return null
|
|
49
86
|
if (!target) return null
|
|
50
|
-
const dir = userLocalBin(options.home || os.homedir())
|
|
51
|
-
const dest = path.join(dir, 'sisu')
|
|
87
|
+
const dir = userLocalBin(options.home || os.homedir(), options)
|
|
52
88
|
fs.mkdirSync(dir, { recursive: true, mode: 0o755 })
|
|
89
|
+
if (isWin(options)) {
|
|
90
|
+
return writeWindowsWrappers(target, dir, options)
|
|
91
|
+
}
|
|
92
|
+
const dest = path.join(dir, 'sisu')
|
|
53
93
|
try {
|
|
54
94
|
if (fs.lstatSync(dest)) fs.unlinkSync(dest)
|
|
55
95
|
} catch {
|
|
@@ -64,27 +104,85 @@ function ensureUserShim(target, options = {}) {
|
|
|
64
104
|
return dest
|
|
65
105
|
}
|
|
66
106
|
|
|
67
|
-
function
|
|
107
|
+
function writeWindowsWrappers(target, dir, options = {}) {
|
|
108
|
+
const exists = options.exists || ((file) => fs.existsSync(file))
|
|
109
|
+
const cmdTarget = String(target).replace(/"/g, '')
|
|
110
|
+
const cmdDest = path.join(dir, 'sisu.cmd')
|
|
111
|
+
writeShim(cmdDest, `@echo off\r\ncall "${cmdTarget}" %*\r\n`)
|
|
112
|
+
|
|
113
|
+
const ps1Source = cmdTarget.replace(/\.cmd$/i, '.ps1')
|
|
114
|
+
const ps1Target = exists(ps1Source) ? ps1Source : cmdTarget
|
|
115
|
+
writeShim(path.join(dir, 'sisu.ps1'), `& "${ps1Target.replace(/"/g, '')}" @args\r\n`)
|
|
116
|
+
|
|
117
|
+
const shSource = cmdTarget.replace(/\.cmd$/i, '')
|
|
118
|
+
const shTarget = exists(shSource) ? shSource : cmdTarget
|
|
119
|
+
writeShim(path.join(dir, 'sisu'), `#!/bin/sh\nexec "${toGitBashPath(shTarget).replace(/"/g, '')}" "$@"\n`)
|
|
120
|
+
return cmdDest
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function pathHint(npmBin, localBin, pathEnv = process.env.PATH || '', options = {}) {
|
|
68
124
|
const dirs = []
|
|
69
|
-
if (localBin && !pathContains(localBin, pathEnv)) dirs.push(localBin)
|
|
70
|
-
if (npmBin && !pathContains(npmBin, pathEnv)) dirs.push(npmBin)
|
|
125
|
+
if (localBin && !pathContains(localBin, pathEnv, options)) dirs.push(localBin)
|
|
126
|
+
if (npmBin && !pathContains(npmBin, pathEnv, options)) dirs.push(npmBin)
|
|
71
127
|
if (!dirs.length) return ''
|
|
128
|
+
if (isWin(options)) {
|
|
129
|
+
const joined = dirs.join(';')
|
|
130
|
+
const git = dirs.map(toGitBashPath).join(':')
|
|
131
|
+
return [
|
|
132
|
+
`set PATH=${joined};%PATH%`,
|
|
133
|
+
`$env:Path = "${joined};" + $env:Path`,
|
|
134
|
+
`export PATH="${git}:$PATH"`,
|
|
135
|
+
].join('\n ')
|
|
136
|
+
}
|
|
72
137
|
return `export PATH="${dirs.join(':')}:$PATH" && hash -r`
|
|
73
138
|
}
|
|
74
139
|
|
|
140
|
+
function readWindowsUserPath() {
|
|
141
|
+
const out = execFileSync('reg', ['query', 'HKCU\\Environment', '/v', 'Path'], {
|
|
142
|
+
encoding: 'utf8',
|
|
143
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
144
|
+
})
|
|
145
|
+
const line = out.split(/\r?\n/).find((row) => /\bPath\s+REG_/i.test(row))
|
|
146
|
+
if (!line) return ''
|
|
147
|
+
const match = line.match(/REG_\w+\s+(.*)$/)
|
|
148
|
+
return match ? match[1].trim() : ''
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function writeWindowsUserPath(value) {
|
|
152
|
+
execFileSync('reg', ['add', 'HKCU\\Environment', '/v', 'Path', '/t', 'REG_EXPAND_SZ', '/d', value, '/f'], {
|
|
153
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function persistUserPath(dir, options = {}) {
|
|
158
|
+
if (!isWin(options) || !dir) return { added: false }
|
|
159
|
+
const read = options.readUserPath || readWindowsUserPath
|
|
160
|
+
const write = options.writeUserPath || writeWindowsUserPath
|
|
161
|
+
const current = String(read() || '')
|
|
162
|
+
if (pathContains(dir, current, options)) return { added: false }
|
|
163
|
+
const next = current ? `${current.replace(/;+$/, '')};${dir}` : dir
|
|
164
|
+
write(next)
|
|
165
|
+
return { added: true }
|
|
166
|
+
}
|
|
167
|
+
|
|
75
168
|
function installCliPath(options = {}) {
|
|
76
169
|
const writes = options.write || ((text) => process.stdout.write(text))
|
|
77
|
-
const platform = options.platform || process.platform
|
|
78
170
|
const bin = globalSisuBin(options)
|
|
79
171
|
const npmBinDir = bin ? path.dirname(bin) : ''
|
|
80
172
|
let shim = null
|
|
81
173
|
try {
|
|
82
174
|
shim = ensureUserShim(bin, options)
|
|
83
175
|
} catch (error) {
|
|
84
|
-
writes(`sisu: could not
|
|
176
|
+
writes(`sisu: could not install user command (${error instanceof Error ? error.message : String(error)})\n`)
|
|
177
|
+
}
|
|
178
|
+
const localDir = shim ? path.dirname(shim) : userLocalBin(options.home, options)
|
|
179
|
+
try {
|
|
180
|
+
const persisted = persistUserPath(localDir, options)
|
|
181
|
+
if (persisted.added) writes('sisu: added to user PATH (new terminals will see it)\n')
|
|
182
|
+
} catch (error) {
|
|
183
|
+
writes(`sisu: could not update user PATH (${error instanceof Error ? error.message : String(error)})\n`)
|
|
85
184
|
}
|
|
86
|
-
const
|
|
87
|
-
const hint = pathHint(npmBinDir, localDir, options.pathEnv)
|
|
185
|
+
const hint = pathHint(npmBinDir, localDir, options.pathEnv, options)
|
|
88
186
|
if (bin) writes(`sisu: command -> ${shim || bin}\n`)
|
|
89
187
|
if (hint) {
|
|
90
188
|
writes('sisu: if `sisu` is not found in this shell, run:\n')
|
|
@@ -99,6 +197,7 @@ module.exports = {
|
|
|
99
197
|
installCliPath,
|
|
100
198
|
pathContains,
|
|
101
199
|
pathHint,
|
|
200
|
+
persistUserPath,
|
|
102
201
|
userLocalBin,
|
|
103
202
|
}
|
|
104
203
|
|