@stevezhou/sisu 0.3.15 → 0.3.17
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 +27 -4
- package/package.json +1 -1
- package/scripts/ensure-cli-path.js +125 -26
package/README.md
CHANGED
|
@@ -6,16 +6,39 @@ One login. Cloud quota. Local runtime. Auth lives in `~/.sisu`, shared with SiSu
|
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
|
+
macOS / Linux / WSL:
|
|
10
|
+
|
|
9
11
|
```bash
|
|
10
|
-
|
|
11
|
-
sisu
|
|
12
|
+
curl -fsSL https://www.sisu.chat/install.sh | bash
|
|
13
|
+
sisu login
|
|
14
|
+
sisu
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Windows PowerShell:
|
|
18
|
+
|
|
19
|
+
```powershell
|
|
20
|
+
irm https://www.sisu.chat/install.ps1 | iex
|
|
12
21
|
sisu login
|
|
13
22
|
sisu
|
|
14
23
|
```
|
|
15
24
|
|
|
16
|
-
|
|
25
|
+
Windows CMD:
|
|
26
|
+
|
|
27
|
+
```bat
|
|
28
|
+
curl -fsSL https://www.sisu.chat/install.cmd -o install.cmd && install.cmd && del install.cmd
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The installer does **not** apt/brew/winget a system Node. If `node` ≥ 20 is already on PATH, it uses that. Otherwise it unpacks official Node 22 into `~/.sisu/node` (user-local, no sudo) and `npm install -g --prefix ~/.sisu @stevezhou/sisu`. Unix gets `~/.sisu/bin` plus a `~/.local/bin/sisu` link; Windows adds `~/.sisu` to the user PATH.
|
|
32
|
+
|
|
33
|
+
Already have Node 20+ and prefer npm:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install -g @stevezhou/sisu
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Postinstall also 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`). 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
40
|
|
|
18
|
-
|
|
41
|
+
`npx sisu` works without a global install.
|
|
19
42
|
|
|
20
43
|
`sisu update` reinstalls that stamped pager for the installed CLI version. It is not a grok-style background auto-updater.
|
|
21
44
|
|
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
|
|