sido-askpass 0.1.0
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/LICENSE +21 -0
- package/README.md +65 -0
- package/dist/sido-askpass.js +371 -0
- package/package.json +47 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 patdx
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# sido-askpass
|
|
2
|
+
|
|
3
|
+
`SUDO_ASKPASS` shim for environments without a usable TTY — tmux, Herdr, and coding agents (Codex, OpenCode, Pi).
|
|
4
|
+
|
|
5
|
+
When sudo needs a password and can't read from the terminal, it runs this program instead. Passwords never touch disk (FIFOs / stdout only).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm i -g sido-askpass
|
|
11
|
+
# or: pnpm add -g sido-askpass
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Requires **Node.js 24+**.
|
|
15
|
+
|
|
16
|
+
## Setup
|
|
17
|
+
|
|
18
|
+
Session-only:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
export SUDO_ASKPASS=$(command -v sido-askpass)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Persist:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
sido-askpass install --user # ~/.profile → SUDO_ASKPASS
|
|
28
|
+
# or
|
|
29
|
+
sido-askpass install --system # /etc/sudo.conf → Path askpass
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Then:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
sudo -A <command>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
On modern Fedora, plain `sudo` often falls back to askpass automatically when there is no TTY.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
sido-askpass status [--user|--system]
|
|
42
|
+
sido-askpass uninstall --user|--system
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## How it prompts
|
|
46
|
+
|
|
47
|
+
| Context | Method |
|
|
48
|
+
| ---------------------------- | ------------------------------------------------------ |
|
|
49
|
+
| `$TMUX` set | `tmux command-prompt -N` (secret), FIFO back to parent |
|
|
50
|
+
| `$HERDR_ENV=1` | Herdr pane split + bash `read -s`, FIFO back |
|
|
51
|
+
| macOS + GUI | `osascript` hidden dialog |
|
|
52
|
+
| Linux + `$DISPLAY` / Wayland | `zenity` → `kdialog` |
|
|
53
|
+
| else | `read -s` on `/dev/tty` |
|
|
54
|
+
|
|
55
|
+
### tmux
|
|
56
|
+
|
|
57
|
+
Inside a tmux session, sudo password entry uses tmux's built-in secret prompt — no GUI dialog, no extra pane. Works well when agents or nested shells run under tmux and can't steal the TTY.
|
|
58
|
+
|
|
59
|
+
### Agents / Herdr
|
|
60
|
+
|
|
61
|
+
In Herdr (`HERDR_ENV=1`), a temporary pane collects the password. Same idea for Codex / OpenCode / Pi: point `SUDO_ASKPASS` at this binary so privilege prompts don't hang the session.
|
|
62
|
+
|
|
63
|
+
## Security
|
|
64
|
+
|
|
65
|
+
Passwords are passed through kernel FIFOs (tmux / Herdr) or process stdout (GUI / TTY). Nothing is written to a regular file.
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from 'node:child_process'
|
|
4
|
+
import {
|
|
5
|
+
existsSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
unlinkSync,
|
|
9
|
+
chmodSync,
|
|
10
|
+
} from 'node:fs'
|
|
11
|
+
import { tmpdir, userInfo, hostname, homedir } from 'node:os'
|
|
12
|
+
import { join, resolve } from 'node:path'
|
|
13
|
+
import { randomUUID } from 'node:crypto'
|
|
14
|
+
import { parseArgs } from 'node:util'
|
|
15
|
+
|
|
16
|
+
const self_path = resolve(process.argv[1] )
|
|
17
|
+
const command = process.argv[2]
|
|
18
|
+
|
|
19
|
+
// ── install / uninstall / status ────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
if (command === 'install' || command === 'uninstall') {
|
|
22
|
+
const { values } = parseArgs({
|
|
23
|
+
args: process.argv.slice(3),
|
|
24
|
+
options: {
|
|
25
|
+
user: { type: 'boolean' },
|
|
26
|
+
system: { type: 'boolean' },
|
|
27
|
+
},
|
|
28
|
+
})
|
|
29
|
+
if ((values.user && values.system) || (!values.user && !values.system)) {
|
|
30
|
+
console.error(`[sido] usage: ${self_path} ${command} --user|--system`)
|
|
31
|
+
process.exit(1)
|
|
32
|
+
}
|
|
33
|
+
const scope = values.user ? '--user' : '--system'
|
|
34
|
+
if (command === 'install') do_install(scope)
|
|
35
|
+
else do_uninstall(scope)
|
|
36
|
+
process.exit(0)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (command === 'status') {
|
|
40
|
+
const { values } = parseArgs({
|
|
41
|
+
args: process.argv.slice(3),
|
|
42
|
+
options: {
|
|
43
|
+
user: { type: 'boolean' },
|
|
44
|
+
system: { type: 'boolean' },
|
|
45
|
+
},
|
|
46
|
+
strict: false,
|
|
47
|
+
})
|
|
48
|
+
const scope = values.user ? '--user' : values.system ? '--system' : undefined
|
|
49
|
+
do_status(scope)
|
|
50
|
+
process.exit(0)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── askpass mode ───────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
const prompt = (command || `[sudo] password for ${userInfo().username}: `)
|
|
56
|
+
.replace(/%u/g, userInfo().username)
|
|
57
|
+
.replace(/%h/g, hostname())
|
|
58
|
+
|
|
59
|
+
const isTmux = !!process.env.TMUX
|
|
60
|
+
const isHerdr = process.env.HERDR_ENV === '1'
|
|
61
|
+
const isMac = process.platform === 'darwin'
|
|
62
|
+
const isLinux = process.platform === 'linux'
|
|
63
|
+
const has_display = !!(process.env.DISPLAY || process.env.WAYLAND_DISPLAY)
|
|
64
|
+
const can_gui = isMac || (isLinux && has_display)
|
|
65
|
+
|
|
66
|
+
function tmp() {
|
|
67
|
+
return join(tmpdir(), `sido-${randomUUID()}`)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function mkfifo(p ) {
|
|
71
|
+
spawnSync('mkfifo', [p], { stdio: ['pipe', 'pipe', 'pipe'] })
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function read_stdout(r
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
) {
|
|
78
|
+
if (r.status !== 0 || !r.stdout || r.stdout.length === 0) process.exit(1)
|
|
79
|
+
return r.stdout.toString().replace(/\r?\n$/, '')
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function main() {
|
|
83
|
+
if (isTmux) return void tmux_prompt()
|
|
84
|
+
if (isHerdr) return void herdr_prompt()
|
|
85
|
+
if (can_gui) return void gui_prompt()
|
|
86
|
+
tty_prompt()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function sudo_conf_path() {
|
|
90
|
+
return '/etc/sudo.conf'
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function user_profile_path() {
|
|
94
|
+
return join(homedir(), '.profile')
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function do_install(scope ) {
|
|
98
|
+
if (scope === '--user') {
|
|
99
|
+
const path = user_profile_path()
|
|
100
|
+
const line = `export SUDO_ASKPASS="${self_path}"`
|
|
101
|
+
let content = existsSync(path) ? readFileSync(path, 'utf-8') + '\n' : ''
|
|
102
|
+
if (/^export SUDO_ASKPASS=/m.test(content)) {
|
|
103
|
+
content = content.replace(/^export SUDO_ASKPASS=.*$/gm, line)
|
|
104
|
+
} else {
|
|
105
|
+
content += `\n# sido\nexport SUDO_ASKPASS="${self_path}"\n`
|
|
106
|
+
}
|
|
107
|
+
writeFileSync(path, content)
|
|
108
|
+
console.error(`[sido] installed to ${path}`)
|
|
109
|
+
console.error(`[sido] run: source ${path}`)
|
|
110
|
+
} else {
|
|
111
|
+
const path = sudo_conf_path()
|
|
112
|
+
const line = `Path askpass ${self_path}`
|
|
113
|
+
let content = ''
|
|
114
|
+
if (existsSync(path)) {
|
|
115
|
+
const r = spawnSync('cat', [path], { stdio: ['pipe', 'pipe', 'pipe'] })
|
|
116
|
+
content = r.stdout?.toString() ?? ''
|
|
117
|
+
}
|
|
118
|
+
if (/^Path askpass /m.test(content)) {
|
|
119
|
+
content = content.replace(/^Path askpass .*$/gm, line)
|
|
120
|
+
} else {
|
|
121
|
+
content += `\n# sido\n${line}\n`
|
|
122
|
+
}
|
|
123
|
+
const proc = spawnSync('sudo', ['tee', path], {
|
|
124
|
+
input: content,
|
|
125
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
126
|
+
})
|
|
127
|
+
if (proc.status === 0) {
|
|
128
|
+
console.error(`[sido] installed to ${path}`)
|
|
129
|
+
} else {
|
|
130
|
+
console.error('[sido] install failed — do you have sudo?')
|
|
131
|
+
process.exit(1)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function do_uninstall(scope ) {
|
|
137
|
+
if (scope === '--user') {
|
|
138
|
+
const path = user_profile_path()
|
|
139
|
+
if (!existsSync(path)) {
|
|
140
|
+
console.error('[sido] nothing to uninstall')
|
|
141
|
+
process.exit(1)
|
|
142
|
+
}
|
|
143
|
+
let content = readFileSync(path, 'utf-8')
|
|
144
|
+
content = content.replace(/^# sido\n/gm, '')
|
|
145
|
+
content = content.replace(/^export SUDO_ASKPASS=.*$/gm, '')
|
|
146
|
+
content = content.replace(/\n{3,}/g, '\n\n').trim()
|
|
147
|
+
writeFileSync(path, content + '\n')
|
|
148
|
+
console.error(`[sido] removed from ${path}`)
|
|
149
|
+
} else {
|
|
150
|
+
const path = sudo_conf_path()
|
|
151
|
+
if (!existsSync(path)) {
|
|
152
|
+
console.error('[sido] nothing to uninstall')
|
|
153
|
+
process.exit(1)
|
|
154
|
+
}
|
|
155
|
+
const r = spawnSync('cat', [path], { stdio: ['pipe', 'pipe', 'pipe'] })
|
|
156
|
+
let content = r.stdout?.toString() ?? ''
|
|
157
|
+
content = content.replace(/^# sido\n/gm, '')
|
|
158
|
+
content = content.replace(/^Path askpass .*$/gm, '')
|
|
159
|
+
content = content.replace(/\n{3,}/g, '\n\n').trim()
|
|
160
|
+
const proc = spawnSync('sudo', ['tee', path], {
|
|
161
|
+
input: content + '\n',
|
|
162
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
163
|
+
})
|
|
164
|
+
if (proc.status === 0) {
|
|
165
|
+
console.error(`[sido] removed from ${path}`)
|
|
166
|
+
} else {
|
|
167
|
+
console.error('[sido] uninstall failed')
|
|
168
|
+
process.exit(1)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function do_status(scope ) {
|
|
174
|
+
if (!scope || scope === '--system') {
|
|
175
|
+
const sys_path = sudo_conf_path()
|
|
176
|
+
if (existsSync(sys_path)) {
|
|
177
|
+
const r = spawnSync('cat', [sys_path], {
|
|
178
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
179
|
+
})
|
|
180
|
+
const m = r.stdout?.toString().match(/^Path askpass (.*)$/m)
|
|
181
|
+
console.error(
|
|
182
|
+
m
|
|
183
|
+
? `[sido] system askpass: ${m[1]}`
|
|
184
|
+
: `[sido] no system askpass in ${sys_path}`,
|
|
185
|
+
)
|
|
186
|
+
} else {
|
|
187
|
+
console.error(`[sido] ${sys_path} does not exist`)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (!scope || scope === '--user') {
|
|
192
|
+
const user_path = user_profile_path()
|
|
193
|
+
if (existsSync(user_path)) {
|
|
194
|
+
const content = readFileSync(user_path, 'utf-8')
|
|
195
|
+
const m = content.match(/^export SUDO_ASKPASS=(.*)$/m)
|
|
196
|
+
console.error(
|
|
197
|
+
m
|
|
198
|
+
? `[sido] user askpass in ~/.profile: ${m[1]}`
|
|
199
|
+
: `[sido] no SUDO_ASKPASS in ~/.profile`,
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const env_active = process.env.SUDO_ASKPASS
|
|
205
|
+
if (env_active) {
|
|
206
|
+
console.error(`[sido] active (env): ${env_active}`)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ── tmux ────────────────────────────────────────────────────────────────────
|
|
211
|
+
//
|
|
212
|
+
// We use a single bash spawnSync combining a background cat on the FIFO
|
|
213
|
+
// (reader) and the blocking tmux command-prompt (writer). Shell job control
|
|
214
|
+
// (&, wait) keeps both in one sync call. Native fs.readFile on a FIFO
|
|
215
|
+
// can't be cancelled mid-open (libuv threadpool blocking op), whereas the
|
|
216
|
+
// shell naturally reaps the background cat on cancel.
|
|
217
|
+
|
|
218
|
+
function tmux_prompt() {
|
|
219
|
+
const fifo = tmp()
|
|
220
|
+
mkfifo(fifo)
|
|
221
|
+
|
|
222
|
+
const safe_prompt = prompt.replace(/"/g, '\\"')
|
|
223
|
+
const script = [
|
|
224
|
+
`cat "${fifo}" &`,
|
|
225
|
+
`reader=$!`,
|
|
226
|
+
`tmux command-prompt -N -p "${safe_prompt}" "run-shell 'echo \\"%1\\" > \\"${fifo}\\"'"`,
|
|
227
|
+
`wait "$reader" 2>/dev/null`,
|
|
228
|
+
].join('\n')
|
|
229
|
+
|
|
230
|
+
const r = spawnSync('bash', ['-c', script], {
|
|
231
|
+
stdio: ['inherit', 'pipe', 'inherit'],
|
|
232
|
+
})
|
|
233
|
+
try {
|
|
234
|
+
unlinkSync(fifo)
|
|
235
|
+
} catch {}
|
|
236
|
+
process.stdout.write(read_stdout(r))
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ── Herdr ───────────────────────────────────────────────────────────────────
|
|
240
|
+
// Same bash spawnSync pattern as tmux: background cat on the FIFO + blocking
|
|
241
|
+
// herdr pane run. Shell job control (&, wait) keeps coordination in one sync
|
|
242
|
+
// call and reaps the background cat on cancel.
|
|
243
|
+
|
|
244
|
+
function herdr_prompt() {
|
|
245
|
+
const fifo = tmp()
|
|
246
|
+
const prompt_file = tmp()
|
|
247
|
+
const script_file = tmp()
|
|
248
|
+
|
|
249
|
+
mkfifo(fifo)
|
|
250
|
+
writeFileSync(prompt_file, prompt)
|
|
251
|
+
writeFileSync(
|
|
252
|
+
script_file,
|
|
253
|
+
[
|
|
254
|
+
`#!/usr/bin/env bash`,
|
|
255
|
+
`prompt=$(cat "${prompt_file}")`,
|
|
256
|
+
`read -s -p "$prompt" pw`,
|
|
257
|
+
`echo "$pw" > "${fifo}"`,
|
|
258
|
+
].join('\n'),
|
|
259
|
+
)
|
|
260
|
+
chmodSync(script_file, 0o755)
|
|
261
|
+
|
|
262
|
+
let direction = 'right'
|
|
263
|
+
try {
|
|
264
|
+
const layout = spawnSync('herdr', ['pane', 'layout', '--current'], {
|
|
265
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
266
|
+
})
|
|
267
|
+
if (layout.status === 0) {
|
|
268
|
+
const info = JSON.parse(layout.stdout?.toString() ?? '{}')
|
|
269
|
+
if ((info.result?.pane?.width ?? 0) <= 150) direction = 'down'
|
|
270
|
+
}
|
|
271
|
+
} catch {}
|
|
272
|
+
|
|
273
|
+
console.error(
|
|
274
|
+
`[sido] password requested — see new pane (${direction === 'right' ? 'right' : 'bottom'})`,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
const split = spawnSync(
|
|
278
|
+
'herdr',
|
|
279
|
+
['pane', 'split', '--current', '--direction', direction, '--cwd', '/'],
|
|
280
|
+
{ stdio: ['inherit', 'pipe', 'pipe'] },
|
|
281
|
+
)
|
|
282
|
+
if (split.status !== 0) return void fallback_herdr()
|
|
283
|
+
const pane_id = JSON.parse(split.stdout?.toString() ?? '{}').result?.pane
|
|
284
|
+
?.pane_id
|
|
285
|
+
if (!pane_id) return void fallback_herdr()
|
|
286
|
+
|
|
287
|
+
const runner_script = [
|
|
288
|
+
`cat "${fifo}" &`,
|
|
289
|
+
`reader=$!`,
|
|
290
|
+
`herdr pane run "${pane_id}" bash "${script_file}"`,
|
|
291
|
+
`wait "$reader" 2>/dev/null`,
|
|
292
|
+
].join('\n')
|
|
293
|
+
|
|
294
|
+
const r = spawnSync('bash', ['-c', runner_script], {
|
|
295
|
+
stdio: ['inherit', 'pipe', 'inherit'],
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
unlinkSync(fifo)
|
|
300
|
+
} catch {}
|
|
301
|
+
try {
|
|
302
|
+
unlinkSync(prompt_file)
|
|
303
|
+
} catch {}
|
|
304
|
+
try {
|
|
305
|
+
unlinkSync(script_file)
|
|
306
|
+
} catch {}
|
|
307
|
+
spawnSync('herdr', ['pane', 'close', pane_id], {
|
|
308
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
process.stdout.write(read_stdout(r))
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function fallback_herdr() {
|
|
315
|
+
console.error('[sido] herdr pane split failed, falling back')
|
|
316
|
+
if (can_gui) return void gui_prompt()
|
|
317
|
+
tty_prompt()
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ── GUI (macOS / Linux) ────────────────────────────────────────────────────
|
|
321
|
+
|
|
322
|
+
function gui_prompt() {
|
|
323
|
+
if (isMac) return void mac_gui()
|
|
324
|
+
linux_gui()
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function mac_gui() {
|
|
328
|
+
const safe_prompt = prompt.replace(/"/g, '\\"')
|
|
329
|
+
const r = spawnSync(
|
|
330
|
+
'osascript',
|
|
331
|
+
[
|
|
332
|
+
'-e',
|
|
333
|
+
`display dialog "${safe_prompt}" with hidden answer default answer ""`,
|
|
334
|
+
'-e',
|
|
335
|
+
'text returned of result',
|
|
336
|
+
],
|
|
337
|
+
{ stdio: ['inherit', 'pipe', 'inherit'] },
|
|
338
|
+
)
|
|
339
|
+
if (r.status !== 0) process.exit(1)
|
|
340
|
+
process.stdout.write(r.stdout?.toString().trim() ?? '')
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function linux_gui() {
|
|
344
|
+
for (const prog of [
|
|
345
|
+
['zenity', '--password', '--title', prompt],
|
|
346
|
+
['kdialog', '--password', prompt],
|
|
347
|
+
]) {
|
|
348
|
+
const [cmd, ...args] = prog
|
|
349
|
+
const r = spawnSync(cmd, args, { stdio: ['inherit', 'pipe', 'inherit'] })
|
|
350
|
+
if (r.status === 0) {
|
|
351
|
+
process.stdout.write(r.stdout?.toString().replace(/\n$/, '') ?? '')
|
|
352
|
+
return
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
console.error('[sido] no GUI dialog found')
|
|
356
|
+
tty_prompt()
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ── TTY fallback ───────────────────────────────────────────────────────────
|
|
360
|
+
|
|
361
|
+
function tty_prompt() {
|
|
362
|
+
const safe_prompt = prompt.replace(/"/g, '\\"')
|
|
363
|
+
const r = spawnSync(
|
|
364
|
+
'bash',
|
|
365
|
+
['-c', `read -s -p "${safe_prompt}" pw && echo "$pw"`],
|
|
366
|
+
{ stdio: ['inherit', 'pipe', 'inherit'] },
|
|
367
|
+
)
|
|
368
|
+
process.stdout.write(read_stdout(r))
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
main()
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sido-askpass",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "SUDO_ASKPASS shim for headless agent environments (tmux, Herdr, GUI, TTY)",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"sido-askpass": "dist/sido-askpass.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=24"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/patdx/sido-askpass.git"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/patdx/sido-askpass#readme",
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/patdx/sido-askpass/issues"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"sudo",
|
|
28
|
+
"askpass",
|
|
29
|
+
"SUDO_ASKPASS",
|
|
30
|
+
"tmux",
|
|
31
|
+
"herdr"
|
|
32
|
+
],
|
|
33
|
+
"packageManager": "pnpm@11.18.0+sha512.33d83c77da82f49fba836925c6f1b841181ec3132b670639bd012f7075f5c7cf634c5f870147c19aae7478fac01df09d8892e880454896edd23ee9b33757563c",
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "node scripts/build.mjs",
|
|
36
|
+
"prepublishOnly": "pnpm typecheck && pnpm build",
|
|
37
|
+
"typecheck": "tsc",
|
|
38
|
+
"format": "prettier --write .",
|
|
39
|
+
"format:check": "prettier --check ."
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "^24.0.0",
|
|
43
|
+
"amaro": "^1.1.11",
|
|
44
|
+
"prettier": "^3.9.6",
|
|
45
|
+
"typescript": "^7.0.2"
|
|
46
|
+
}
|
|
47
|
+
}
|