@stevezhou/sisu 0.3.14 → 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 +3 -2
- package/scripts/ensure-cli-path.js +206 -0
- package/scripts/postinstall.js +7 -0
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ sisu login
|
|
|
13
13
|
sisu
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
`npm install -g` is a small JS package.
|
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stevezhou/sisu",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.16",
|
|
4
4
|
"description": "SiSu CLI — 思溯 / SiSu · 思有所溯",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"homepage": "https://www.sisu.chat",
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"NOTICE",
|
|
27
27
|
"third_party/grok-build",
|
|
28
28
|
"scripts/postinstall.js",
|
|
29
|
-
"scripts/install-pager.js"
|
|
29
|
+
"scripts/install-pager.js",
|
|
30
|
+
"scripts/ensure-cli-path.js"
|
|
30
31
|
],
|
|
31
32
|
"publishConfig": {
|
|
32
33
|
"access": "public"
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
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 and from Windows PATH.
|
|
4
|
+
* Do not copy npm's sisu.cmd: it uses %~dp0 and breaks outside the prefix.
|
|
5
|
+
*/
|
|
6
|
+
const { execFileSync } = require('child_process')
|
|
7
|
+
const fs = require('fs')
|
|
8
|
+
const os = require('os')
|
|
9
|
+
const path = require('path')
|
|
10
|
+
|
|
11
|
+
function isWin(options = {}) {
|
|
12
|
+
return (options.platform || process.platform) === 'win32'
|
|
13
|
+
}
|
|
14
|
+
|
|
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)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function globalSisuBin(options = {}) {
|
|
41
|
+
const win = isWin(options)
|
|
42
|
+
const name = win ? 'sisu.cmd' : 'sisu'
|
|
43
|
+
const prefix = options.prefix || process.env.npm_config_prefix || ''
|
|
44
|
+
const fromPrefix = prefix
|
|
45
|
+
? (win ? path.join(prefix, name) : path.join(prefix, 'bin', name))
|
|
46
|
+
: ''
|
|
47
|
+
const fromLayout = path.resolve(
|
|
48
|
+
options.packageRoot || path.join(__dirname, '..'),
|
|
49
|
+
'..',
|
|
50
|
+
'..',
|
|
51
|
+
win ? name : path.join('bin', name),
|
|
52
|
+
)
|
|
53
|
+
const exists = options.exists || ((file) => fs.existsSync(file))
|
|
54
|
+
for (const candidate of [fromPrefix, fromLayout]) {
|
|
55
|
+
if (candidate && exists(candidate)) return candidate
|
|
56
|
+
}
|
|
57
|
+
return fromPrefix || fromLayout
|
|
58
|
+
}
|
|
59
|
+
|
|
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
|
+
}
|
|
66
|
+
return path.join(home, '.local', 'bin')
|
|
67
|
+
}
|
|
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
|
+
|
|
85
|
+
function ensureUserShim(target, options = {}) {
|
|
86
|
+
if (!target) return null
|
|
87
|
+
const dir = userLocalBin(options.home || os.homedir(), options)
|
|
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')
|
|
93
|
+
try {
|
|
94
|
+
if (fs.lstatSync(dest)) fs.unlinkSync(dest)
|
|
95
|
+
} catch {
|
|
96
|
+
// missing
|
|
97
|
+
}
|
|
98
|
+
fs.symlinkSync(path.resolve(target), dest)
|
|
99
|
+
try {
|
|
100
|
+
fs.chmodSync(dest, 0o755)
|
|
101
|
+
} catch {
|
|
102
|
+
// symlink mode follows the target on most POSIX
|
|
103
|
+
}
|
|
104
|
+
return dest
|
|
105
|
+
}
|
|
106
|
+
|
|
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 = {}) {
|
|
124
|
+
const dirs = []
|
|
125
|
+
if (localBin && !pathContains(localBin, pathEnv, options)) dirs.push(localBin)
|
|
126
|
+
if (npmBin && !pathContains(npmBin, pathEnv, options)) dirs.push(npmBin)
|
|
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
|
+
}
|
|
137
|
+
return `export PATH="${dirs.join(':')}:$PATH" && hash -r`
|
|
138
|
+
}
|
|
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
|
+
|
|
168
|
+
function installCliPath(options = {}) {
|
|
169
|
+
const writes = options.write || ((text) => process.stdout.write(text))
|
|
170
|
+
const bin = globalSisuBin(options)
|
|
171
|
+
const npmBinDir = bin ? path.dirname(bin) : ''
|
|
172
|
+
let shim = null
|
|
173
|
+
try {
|
|
174
|
+
shim = ensureUserShim(bin, options)
|
|
175
|
+
} catch (error) {
|
|
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`)
|
|
184
|
+
}
|
|
185
|
+
const hint = pathHint(npmBinDir, localDir, options.pathEnv, options)
|
|
186
|
+
if (bin) writes(`sisu: command -> ${shim || bin}\n`)
|
|
187
|
+
if (hint) {
|
|
188
|
+
writes('sisu: if `sisu` is not found in this shell, run:\n')
|
|
189
|
+
writes(` ${hint}\n`)
|
|
190
|
+
}
|
|
191
|
+
return { bin, shim, hint }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
module.exports = {
|
|
195
|
+
ensureUserShim,
|
|
196
|
+
globalSisuBin,
|
|
197
|
+
installCliPath,
|
|
198
|
+
pathContains,
|
|
199
|
+
pathHint,
|
|
200
|
+
persistUserPath,
|
|
201
|
+
userLocalBin,
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (require.main === module) {
|
|
205
|
+
installCliPath()
|
|
206
|
+
}
|
package/scripts/postinstall.js
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
* Never fail `npm install` — Node TUI still works without the binary.
|
|
4
4
|
*/
|
|
5
5
|
const { installPager } = require('./install-pager')
|
|
6
|
+
const { installCliPath } = require('./ensure-cli-path')
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
installCliPath()
|
|
10
|
+
} catch (error) {
|
|
11
|
+
process.stdout.write(`sisu: path setup skipped (${error instanceof Error ? error.message : String(error)})\n`)
|
|
12
|
+
}
|
|
6
13
|
|
|
7
14
|
installPager().then(
|
|
8
15
|
(result) => {
|