@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 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
- npm install -g @stevezhou/sisu
11
- sisu --help
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
- Postinstall links `sisu` into `~/.local/bin` (Debian/login PATH) and prints an `export PATH=...` line if this shell still cannot see the command. `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.
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
- Requires Node.js 20 or newer. `npx sisu` works without a global install.
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@stevezhou/sisu",
3
- "version": "0.3.15",
3
+ "version": "0.3.17",
4
4
  "description": "SiSu CLI — 思溯 / SiSu · 思有所溯",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://www.sisu.chat",
@@ -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 pathDirs(pathEnv = process.env.PATH || '') {
10
- return pathEnv.split(path.delimiter).filter(Boolean)
11
+ function isWin(options = {}) {
12
+ return (options.platform || process.platform) === 'win32'
11
13
  }
12
14
 
13
- function pathContains(dir, pathEnv = process.env.PATH || '') {
14
- const target = path.resolve(dir)
15
- return pathDirs(pathEnv).some((entry) => {
16
- try {
17
- return path.resolve(entry) === target
18
- } catch {
19
- return false
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 name = options.platform === 'win32' ? 'sisu.cmd' : 'sisu'
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
- ? (options.platform === 'win32' ? path.join(prefix, name) : path.join(prefix, 'bin', name))
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
- options.platform === 'win32' ? name : path.join('bin', name),
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 pathHint(npmBin, localBin, pathEnv = process.env.PATH || '') {
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 link ~/.local/bin/sisu (${error instanceof Error ? error.message : String(error)})\n`)
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 localDir = shim ? path.dirname(shim) : userLocalBin(options.home)
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