Skip to content

Commit 70b8753

Browse files
committed
fix(windows): resolve CLI compatibility issues on Windows
- Replace chmod +x with cross-platform fs.chmodSync in build script - Replace c.kill('SIGTERM') with c.kill() for cross-platform process termination - Guard process.on('SIGTERM') listener behind platform !== 'win32' check - Wrap all setRawMode(true) calls in try/catch to handle Windows terminals that report isTTY=true but do not support raw mode - Add PowerShell completion script support via termui completion powershell
1 parent e70734d commit 70b8753

6 files changed

Lines changed: 121 additions & 28 deletions

File tree

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "termui",
33
"private": false,
4-
"version": "1.4.2",
4+
"version": "1.4.3",
55
"description": "A TypeScript/React terminal UI framework with 100+ components, 8 themes, and a shadcn-style CLI",
66
"keywords": [
77
"terminal",
@@ -221,7 +221,7 @@
221221
],
222222
"scripts": {
223223
"build": "turbo run build && tsup",
224-
"build:root": "tsup && chmod +x dist/cli.js",
224+
"build:root": "tsup && node -e \"require('fs').chmodSync('dist/cli.js', '755')\"",
225225
"dev": "turbo run dev --parallel",
226226
"test": "turbo run test",
227227
"lint": "turbo run lint",

packages/cli/src/cli.ts

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ const cli = createCLI({
4949
publish: { name: 'publish', description: 'Submit a component to the community registry' },
5050
completion: {
5151
name: 'completion',
52-
description: 'Print shell completion script (bash/zsh/fish)',
53-
args: { shell: { description: 'Shell: bash | zsh | fish', required: true } },
52+
description: 'Print shell completion script (bash/zsh/fish/powershell)',
53+
args: { shell: { description: 'Shell: bash | zsh | fish | powershell', required: true } },
5454
},
5555
},
5656
});
@@ -134,11 +134,11 @@ async function main() {
134134

135135
case 'completion': {
136136
const shell = cmdArgs[0];
137-
if (!shell || !['bash', 'zsh', 'fish'].includes(shell)) {
138-
console.error(`\x1b[31mError:\x1b[0m Usage: npx termui completion <bash|zsh|fish>`);
137+
if (!shell || !['bash', 'zsh', 'fish', 'powershell'].includes(shell)) {
138+
console.error(`\x1b[31mError:\x1b[0m Usage: npx termui completion <bash|zsh|fish|powershell>`);
139139
process.exit(1);
140140
}
141-
await runCompletion(shell as 'bash' | 'zsh' | 'fish');
141+
await runCompletion(shell as 'bash' | 'zsh' | 'fish' | 'powershell');
142142
break;
143143
}
144144

@@ -252,7 +252,7 @@ async function interactiveMenu(): Promise<void> {
252252
}
253253
}
254254

255-
async function runCompletion(shell: 'bash' | 'zsh' | 'fish'): Promise<void> {
255+
async function runCompletion(shell: 'bash' | 'zsh' | 'fish' | 'powershell'): Promise<void> {
256256
const { getLocalRegistry } = await import('./registry/client.js');
257257
const registry = getLocalRegistry();
258258
const comps = Object.keys(registry.components).join(' ');
@@ -261,7 +261,43 @@ async function runCompletion(shell: 'bash' | 'zsh' | 'fish'): Promise<void> {
261261
const CMDS =
262262
'init add list diff update theme preview dev docs create publish completion help --help --version';
263263

264-
if (shell === 'bash') {
264+
if (shell === 'powershell') {
265+
const cmdList = CMDS.split(' ').map((c) => `'${c}'`).join(', ');
266+
const compList = comps.split(' ').filter(Boolean).map((c) => `'${c}'`).join(', ');
267+
const themeList = THEMES.split(' ').map((t) => `'${t}'`).join(', ');
268+
process.stdout.write(`# TermUI PowerShell completion
269+
# Add the following to your PowerShell profile ($PROFILE):
270+
# npx termui completion powershell | Out-String | Invoke-Expression
271+
272+
Register-ArgumentCompleter -Native -CommandName @('termui', 'npx') -ScriptBlock {
273+
param($wordToComplete, $commandAst, $cursorPosition)
274+
$cmds = @(${cmdList})
275+
$comps = @(${compList})
276+
$themes = @(${themeList})
277+
$words = $commandAst.CommandElements
278+
if ($words.Count -le 2) {
279+
$cmds | Where-Object { $_ -like "$wordToComplete*" } |
280+
ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }
281+
return
282+
}
283+
$prev = $words[$words.Count - 2].ToString()
284+
switch ($prev) {
285+
{ $_ -in 'add','update','diff' } {
286+
$comps | Where-Object { $_ -like "$wordToComplete*" } |
287+
ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }
288+
}
289+
'theme' {
290+
$themes | Where-Object { $_ -like "$wordToComplete*" } |
291+
ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }
292+
}
293+
'completion' {
294+
@('bash','zsh','fish','powershell') | Where-Object { $_ -like "$wordToComplete*" } |
295+
ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }
296+
}
297+
}
298+
}
299+
`);
300+
} else if (shell === 'bash') {
265301
process.stdout.write(`# TermUI bash completion — source <(npx termui completion bash)
266302
_termui_completions() {
267303
local cur=\${COMP_WORDS[COMP_CWORD]}
@@ -273,7 +309,7 @@ _termui_completions() {
273309
case "\${prev}" in
274310
add|update|diff) COMPREPLY=( $(compgen -W "${comps}" -- "\${cur}") ); return ;;
275311
theme) COMPREPLY=( $(compgen -W "${THEMES}" -- "\${cur}") ); return ;;
276-
completion) COMPREPLY=( $(compgen -W "bash zsh fish" -- "\${cur}") ); return ;;
312+
completion) COMPREPLY=( $(compgen -W "bash zsh fish powershell" -- "\${cur}") ); return ;;
277313
esac
278314
}
279315
complete -F _termui_completions termui npx
@@ -290,7 +326,7 @@ _termui() {
290326
case $words[1] in
291327
add|update|diff) _arguments '*: :(${comps})' ;;
292328
theme) _arguments '*: :(${THEMES})' ;;
293-
completion) _arguments '*: :(bash zsh fish)' ;;
329+
completion) _arguments '*: :(bash zsh fish powershell)' ;;
294330
esac
295331
;;
296332
esac

packages/cli/src/commands/dev.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,11 @@ export async function dev(args: string[]): Promise<void> {
4848
const c = child;
4949
child = null;
5050
c.once('exit', () => resolve());
51-
c.kill('SIGTERM');
52-
// Force-kill after 2s if SIGTERM not enough
51+
c.kill(); // SIGTERM on Unix, TerminateProcess on Windows
52+
// Force-kill after 2s if graceful kill wasn't enough
5353
setTimeout(() => {
5454
try {
55-
c.kill('SIGKILL');
55+
c.kill('SIGKILL'); // no-op on Windows (process already gone or will error)
5656
} catch {
5757
/* already dead */
5858
}
@@ -80,8 +80,8 @@ export async function dev(args: string[]): Promise<void> {
8080
env: { ...process.env, FORCE_COLOR: '1' },
8181
});
8282

83-
child.on('exit', (code, signal) => {
84-
if (!isExiting && signal !== 'SIGTERM' && signal !== 'SIGKILL') {
83+
child.on('exit', (_code, _signal) => {
84+
if (!isExiting) {
8585
// Preview exited on its own (user pressed q) — keep watching
8686
console.log(`\n${ansi.dim}[dev] preview closed — waiting for file changes…${ansi.reset}`);
8787
}
@@ -121,11 +121,18 @@ export async function dev(args: string[]): Promise<void> {
121121
}
122122

123123
process.on('SIGINT', cleanup);
124-
process.on('SIGTERM', cleanup);
124+
// SIGTERM is not raised by the OS on Windows; guard to avoid misleading listener
125+
if (process.platform !== 'win32') {
126+
process.on('SIGTERM', cleanup);
127+
}
125128

126129
// Allow 'q' to quit (raw mode)
127130
if (process.stdin.isTTY) {
128-
process.stdin.setRawMode(true);
131+
try {
132+
process.stdin.setRawMode(true);
133+
} catch {
134+
// Raw mode not supported (some Windows terminals) — q-to-quit disabled, Ctrl+C still works
135+
}
129136
process.stdin.resume();
130137
process.stdin.setEncoding('utf8');
131138
process.stdin.on('data', (key: string) => {

packages/cli/src/commands/preview.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,12 @@ export async function preview(_args: string[]): Promise<void> {
4747

4848
process.stdout.write(ansi.hideCursor);
4949
readline.emitKeypressEvents(process.stdin);
50-
process.stdin.setRawMode(true);
50+
try {
51+
process.stdin.setRawMode(true);
52+
} catch {
53+
console.error('termui preview requires a terminal that supports raw mode (e.g. Windows Terminal).');
54+
process.exit(1);
55+
}
5156
process.stdin.resume();
5257

5358
const cleanup = () => {
@@ -58,7 +63,9 @@ export async function preview(_args: string[]): Promise<void> {
5863
};
5964

6065
process.on('SIGINT', cleanup);
61-
process.on('SIGTERM', cleanup);
66+
if (process.platform !== 'win32') {
67+
process.on('SIGTERM', cleanup);
68+
}
6269

6370
render(state);
6471

packages/cli/src/utils/clack.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,14 @@ export async function text(opts: {
152152
return;
153153
}
154154

155-
process.stdin.setRawMode(true);
155+
try {
156+
process.stdin.setRawMode(true);
157+
} catch {
158+
// Windows terminals that report isTTY but don't support raw mode
159+
console.log('');
160+
resolve(placeholder);
161+
return;
162+
}
156163
process.stdin.resume();
157164
process.stdin.setEncoding('utf8');
158165

@@ -209,7 +216,12 @@ export async function confirm(opts: { message: string; initialValue?: boolean })
209216
return;
210217
}
211218

212-
process.stdin.setRawMode(true);
219+
try {
220+
process.stdin.setRawMode(true);
221+
} catch {
222+
resolve(defaultValue);
223+
return;
224+
}
213225
process.stdin.resume();
214226
process.stdin.setEncoding('utf8');
215227

@@ -279,7 +291,12 @@ export async function select<T>(opts: {
279291
return;
280292
}
281293

282-
process.stdin.setRawMode(true);
294+
try {
295+
process.stdin.setRawMode(true);
296+
} catch {
297+
resolve(options[0]!.value);
298+
return;
299+
}
283300
process.stdin.resume();
284301
process.stdin.setEncoding('utf8');
285302

@@ -354,7 +371,12 @@ export async function multiselect<T>(opts: {
354371
return;
355372
}
356373

357-
process.stdin.setRawMode(true);
374+
try {
375+
process.stdin.setRawMode(true);
376+
} catch {
377+
resolve([...selected]);
378+
return;
379+
}
358380
process.stdin.resume();
359381
process.stdin.setEncoding('utf8');
360382

packages/cli/src/utils/ui.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,12 @@ export async function select<T extends string>(
279279
return;
280280
}
281281

282-
process.stdin.setRawMode(true);
282+
try {
283+
process.stdin.setRawMode(true);
284+
} catch {
285+
resolve(options[0]!.value);
286+
return;
287+
}
283288
process.stdin.resume();
284289
process.stdin.setEncoding('utf8');
285290

@@ -324,7 +329,12 @@ export async function confirm(prompt: string, defaultValue = true): Promise<bool
324329
return;
325330
}
326331

327-
process.stdin.setRawMode(true);
332+
try {
333+
process.stdin.setRawMode(true);
334+
} catch {
335+
resolve(defaultValue);
336+
return;
337+
}
328338
process.stdin.resume();
329339
process.stdin.setEncoding('utf8');
330340

@@ -391,7 +401,13 @@ export async function text(
391401
return;
392402
}
393403

394-
process.stdin.setRawMode(true);
404+
try {
405+
process.stdin.setRawMode(true);
406+
} catch {
407+
console.log('');
408+
resolve(defaultValue);
409+
return;
410+
}
395411
process.stdin.resume();
396412
process.stdin.setEncoding('utf8');
397413

@@ -477,7 +493,12 @@ export async function multiselect<T extends string>(
477493
return;
478494
}
479495

480-
process.stdin.setRawMode(true);
496+
try {
497+
process.stdin.setRawMode(true);
498+
} catch {
499+
resolve([...selected]);
500+
return;
501+
}
481502
process.stdin.resume();
482503
process.stdin.setEncoding('utf8');
483504

0 commit comments

Comments
 (0)