Skip to content

Commit c555fb0

Browse files
committed
feat(cli): add completion uninstall command
1 parent 571e471 commit c555fb0

7 files changed

Lines changed: 221 additions & 13 deletions

File tree

README.en.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ coder-studio help start
8080
coder-studio help completion
8181
eval "$(coder-studio completion bash)"
8282
coder-studio completion install bash
83+
coder-studio completion uninstall bash
8384
```
8485

8586
For the detailed command reference, see `docs/development/cli.en.md`.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ coder-studio help start
7979
coder-studio help completion
8080
eval "$(coder-studio completion bash)"
8181
coder-studio completion install bash
82+
coder-studio completion uninstall bash
8283
```
8384

8485
详细命令说明见:`docs/development/cli.md`

docs/development/cli.en.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ coder-studio completion bash
8181
coder-studio completion zsh
8282
coder-studio completion fish
8383
coder-studio completion install bash
84+
coder-studio completion uninstall bash
8485
```
8586

8687
Version:
@@ -139,6 +140,7 @@ Install into your local shell setup automatically:
139140

140141
```bash
141142
coder-studio completion install bash
143+
coder-studio completion install bash --force
142144
coder-studio completion install zsh
143145
coder-studio completion install fish
144146
```
@@ -149,6 +151,19 @@ Install behavior:
149151
- `zsh`: writes `~/.coder-studio/completions/coder-studio.zsh` and adds a managed `source` block to `~/.zshrc`
150152
- `fish`: writes `${XDG_CONFIG_HOME:-~/.config}/fish/completions/coder-studio.fish` without editing a profile
151153

154+
Uninstall:
155+
156+
```bash
157+
coder-studio completion uninstall bash
158+
coder-studio completion uninstall zsh
159+
coder-studio completion uninstall fish
160+
```
161+
162+
Notes:
163+
164+
- `install --force` rewrites the completion file and reapplies the managed profile block
165+
- `uninstall` removes the completion script and strips the managed profile block added by the CLI
166+
152167
Manual persistent installation examples:
153168

154169
```bash
@@ -160,6 +175,7 @@ coder-studio completion fish > ~/.config/fish/completions/coder-studio.fish
160175
Notes:
161176

162177
- `coder-studio completion install <shell> --json` returns structured install output
178+
- `coder-studio completion uninstall <shell> --json` returns structured uninstall output
163179
- `coder-studio completion <shell>` does not support `--json`
164180
- `coder-studio help completion` shows command usage
165181

docs/development/cli.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ coder-studio completion bash
8181
coder-studio completion zsh
8282
coder-studio completion fish
8383
coder-studio completion install bash
84+
coder-studio completion uninstall bash
8485
```
8586

8687
版本信息:
@@ -139,6 +140,7 @@ coder-studio completion fish | source
139140

140141
```bash
141142
coder-studio completion install bash
143+
coder-studio completion install bash --force
142144
coder-studio completion install zsh
143145
coder-studio completion install fish
144146
```
@@ -149,6 +151,19 @@ coder-studio completion install fish
149151
- `zsh`:写入 `~/.coder-studio/completions/coder-studio.zsh`,并在 `~/.zshrc` 注入受管的 `source` 片段
150152
- `fish`:写入 `${XDG_CONFIG_HOME:-~/.config}/fish/completions/coder-studio.fish`,不修改 profile
151153

154+
卸载:
155+
156+
```bash
157+
coder-studio completion uninstall bash
158+
coder-studio completion uninstall zsh
159+
coder-studio completion uninstall fish
160+
```
161+
162+
说明:
163+
164+
- `install --force` 会强制重写脚本文件,并重新写入受管 profile 片段
165+
- `uninstall` 会删除补全脚本,并移除 CLI 注入的受管 profile 片段
166+
152167
手动持久化安装示例:
153168

154169
```bash
@@ -160,6 +175,7 @@ coder-studio completion fish > ~/.config/fish/completions/coder-studio.fish
160175
说明:
161176

162177
- `coder-studio completion install <shell> --json` 会返回安装结果
178+
- `coder-studio completion uninstall <shell> --json` 会返回卸载结果
163179
- `coder-studio completion <shell>` 不支持 `--json`
164180
- `coder-studio help completion` 可查看命令说明
165181

packages/coder-studio/lib/cli.mjs

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import fs from 'node:fs/promises';
2-
import { generateCompletionScript, installCompletionScript, SUPPORTED_COMPLETION_SHELLS } from './completion.mjs';
2+
import {
3+
generateCompletionScript,
4+
installCompletionScript,
5+
uninstallCompletionScript,
6+
SUPPORTED_COMPLETION_SHELLS,
7+
} from './completion.mjs';
38
import { resolveLogPath } from './config.mjs';
49
import {
510
buildConfigPathsReport,
@@ -108,7 +113,8 @@ Usage:
108113
coder-studio config <subcommand>
109114
coder-studio auth <subcommand>
110115
coder-studio completion <bash|zsh|fish>
111-
coder-studio completion install <bash|zsh|fish> [--json]
116+
coder-studio completion install <bash|zsh|fish> [--json] [--force]
117+
coder-studio completion uninstall <bash|zsh|fish> [--json]
112118
coder-studio --version
113119
114120
Global Flags:
@@ -131,6 +137,7 @@ Examples:
131137
coder-studio auth ip list
132138
eval "$(coder-studio completion bash)"
133139
coder-studio completion install bash
140+
coder-studio completion uninstall bash
134141
135142
Run \`coder-studio config --help\`, \`coder-studio auth --help\`, or \`coder-studio help completion\` for detailed usage.
136143
`);
@@ -302,23 +309,27 @@ function printCompletionHelp() {
302309
303310
Usage:
304311
coder-studio completion <bash|zsh|fish>
305-
coder-studio completion install <bash|zsh|fish> [--json]
312+
coder-studio completion install <bash|zsh|fish> [--json] [--force]
313+
coder-studio completion uninstall <bash|zsh|fish> [--json]
306314
307315
Description:
308-
Print or install shell completion scripts.
316+
Print, install, or uninstall shell completion scripts.
309317
310318
Examples:
311319
eval "$(coder-studio completion bash)"
312320
source <(coder-studio completion zsh)
313321
coder-studio completion fish | source
314322
coder-studio completion install bash
323+
coder-studio completion install bash --force
315324
coder-studio completion install zsh --json
325+
coder-studio completion uninstall bash
316326
`);
317327
}
318328

319329
function printCompletionInstall(result) {
320330
console.log(`installed: ${result.shell}`);
321331
console.log(`scriptPath: ${result.scriptPath}`);
332+
console.log(`scriptUpdated: ${result.scriptUpdated ? 'yes' : 'no'}`);
322333
if (result.profilePath) {
323334
console.log(`profilePath: ${result.profilePath}`);
324335
console.log(`profileUpdated: ${result.profileUpdated ? 'yes' : 'no'}`);
@@ -327,6 +338,20 @@ function printCompletionInstall(result) {
327338
console.log('profileUpdated: no');
328339
}
329340
console.log(`activationCommand: ${result.activationCommand}`);
341+
console.log(`forced: ${result.forced ? 'yes' : 'no'}`);
342+
}
343+
344+
function printCompletionUninstall(result) {
345+
console.log(`uninstalled: ${result.shell}`);
346+
console.log(`scriptPath: ${result.scriptPath}`);
347+
console.log(`scriptRemoved: ${result.scriptRemoved ? 'yes' : 'no'}`);
348+
if (result.profilePath) {
349+
console.log(`profilePath: ${result.profilePath}`);
350+
console.log(`profileUpdated: ${result.profileUpdated ? 'yes' : 'no'}`);
351+
} else {
352+
console.log('profilePath: n/a');
353+
console.log('profileUpdated: no');
354+
}
330355
}
331356

332357
function printHelpTopic(topic) {
@@ -969,15 +994,39 @@ export async function runCli(argv = process.argv.slice(2)) {
969994
throw usageError(`unsupported completion shell: ${shell}`, 'completion');
970995
}
971996

972-
const result = await installCompletionScript(shell);
997+
const result = await installCompletionScript(shell, { force: Boolean(flags.force) });
973998
if (flags.json) printJson(result);
974999
else printCompletionInstall(result);
9751000
return EXIT_SUCCESS;
9761001
}
9771002

1003+
if (modeOrShell === 'uninstall') {
1004+
const shell = maybeShell;
1005+
if (!shell) {
1006+
throw usageError('completion uninstall requires <bash|zsh|fish>', 'completion');
1007+
}
1008+
if (rest.length > 0) {
1009+
throw usageError('completion uninstall accepts exactly one <shell> argument', 'completion');
1010+
}
1011+
if (flags.force) {
1012+
throw usageError('completion uninstall does not support --force', 'completion');
1013+
}
1014+
if (!SUPPORTED_COMPLETION_SHELLS.includes(shell)) {
1015+
throw usageError(`unsupported completion shell: ${shell}`, 'completion');
1016+
}
1017+
1018+
const result = await uninstallCompletionScript(shell);
1019+
if (flags.json) printJson(result);
1020+
else printCompletionUninstall(result);
1021+
return EXIT_SUCCESS;
1022+
}
1023+
9781024
if (flags.json) {
9791025
throw usageError('completion does not support --json', 'completion');
9801026
}
1027+
if (flags.force) {
1028+
throw usageError('completion does not support --force', 'completion');
1029+
}
9811030
if (maybeShell || rest.length > 0) {
9821031
throw usageError('completion accepts exactly one <shell> argument', 'completion');
9831032
}

packages/coder-studio/lib/completion.mjs

Lines changed: 71 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,9 @@ const DOCTOR_FLAGS = ['--host', '--port', '--json', '--help', '-h'];
3939
const CONFIG_FLAGS = ['--json', '--help', '-h'];
4040
const AUTH_FLAGS = ['--json', '--help', '-h'];
4141
const COMPLETION_FLAGS = ['--help', '-h'];
42-
const COMPLETION_COMMANDS = ['install', ...SUPPORTED_COMPLETION_SHELLS];
43-
const COMPLETION_INSTALL_FLAGS = ['--json', '--help', '-h'];
42+
const COMPLETION_COMMANDS = ['install', 'uninstall', ...SUPPORTED_COMPLETION_SHELLS];
43+
const COMPLETION_INSTALL_FLAGS = ['--json', '--force', '--help', '-h'];
44+
const COMPLETION_UNINSTALL_FLAGS = ['--json', '--help', '-h'];
4445
const CONFIG_PASSWORD_SET_FLAGS = ['--stdin', '--help', '-h'];
4546
const AUTH_IP_UNBLOCK_FLAGS = ['--all', '--json', '--help', '-h'];
4647

@@ -128,6 +129,14 @@ function generateBashScript() {
128129
' fi',
129130
' return 0',
130131
' fi',
132+
' if [[ "$subcommand" == "uninstall" ]]; then',
133+
' if [[ $COMP_CWORD -eq 3 ]]; then',
134+
` COMPREPLY=( $(compgen -W "${words(SUPPORTED_COMPLETION_SHELLS)}" -- "$cur") )`,
135+
' else',
136+
` COMPREPLY=( $(compgen -W "${words(COMPLETION_UNINSTALL_FLAGS)}" -- "$cur") )`,
137+
' fi',
138+
' return 0',
139+
' fi',
131140
` COMPREPLY=( $(compgen -W "${words(SUPPORTED_COMPLETION_SHELLS.concat(COMPLETION_FLAGS))}" -- "$cur") )`,
132141
' return 0',
133142
' ;;',
@@ -187,7 +196,7 @@ function generateBashScript() {
187196
' ;;',
188197
' ip)',
189198
' if [[ $COMP_CWORD -eq 3 ]]; then',
190-
` COMPREPLY=( $(compgen -W "${words(AUTH_IP_SUBCOMMANDS.concat(COMPLETION_FLAGS))}" -- "$cur") )`,
199+
` COMPREPLY=( $(compgen -W "${words(AUTH_IP_SUBCOMMANDS.concat(COMPLETION_FLAGS))}" -- "$cur") )`,
191200
' return 0',
192201
' fi',
193202
' if [[ "$nested" == "list" ]]; then',
@@ -267,6 +276,12 @@ function generateZshScript() {
267276
' else',
268277
` suggestions=(${words(COMPLETION_INSTALL_FLAGS)})`,
269278
' fi',
279+
' elif [[ "$subcommand" == "uninstall" ]]; then',
280+
' if (( CURRENT == 4 )); then',
281+
` suggestions=(${words(SUPPORTED_COMPLETION_SHELLS)})`,
282+
' else',
283+
` suggestions=(${words(COMPLETION_UNINSTALL_FLAGS)})`,
284+
' fi',
270285
' else',
271286
` suggestions=(${words(SUPPORTED_COMPLETION_SHELLS)} ${words(COMPLETION_FLAGS)})`,
272287
' fi',
@@ -400,10 +415,13 @@ function generateFishScript() {
400415
'complete -c coder-studio -n "__fish_seen_subcommand_from doctor" -l json',
401416
'complete -c coder-studio -n "__fish_seen_subcommand_from doctor" -l help -s h',
402417
'',
403-
'complete -c coder-studio -n "__fish_seen_subcommand_from completion; and not __fish_seen_subcommand_from install bash zsh fish" -a "install bash zsh fish"',
418+
'complete -c coder-studio -n "__fish_seen_subcommand_from completion; and not __fish_seen_subcommand_from install uninstall bash zsh fish" -a "install uninstall bash zsh fish"',
404419
'complete -c coder-studio -n "__fish_seen_subcommand_from completion" -l help -s h',
405420
'complete -c coder-studio -n "__fish_seen_subcommand_from completion; and __fish_seen_subcommand_from install; and not __fish_seen_subcommand_from bash zsh fish" -a "bash zsh fish"',
421+
'complete -c coder-studio -n "__fish_seen_subcommand_from completion; and __fish_seen_subcommand_from install" -l force',
406422
'complete -c coder-studio -n "__fish_seen_subcommand_from completion; and __fish_seen_subcommand_from install" -l json',
423+
'complete -c coder-studio -n "__fish_seen_subcommand_from completion; and __fish_seen_subcommand_from uninstall; and not __fish_seen_subcommand_from bash zsh fish" -a "bash zsh fish"',
424+
'complete -c coder-studio -n "__fish_seen_subcommand_from completion; and __fish_seen_subcommand_from uninstall" -l json',
407425
'',
408426
'complete -c coder-studio -n "__fish_seen_subcommand_from config; and not __fish_seen_subcommand_from path show get set unset validate root password auth" -a "path show get set unset validate root password auth"',
409427
'complete -c coder-studio -n "__fish_seen_subcommand_from config" -l json',
@@ -463,8 +481,12 @@ function buildManagedBlock(sourceLine) {
463481
return `${MANAGED_BLOCK_START}\n${sourceLine}\n${MANAGED_BLOCK_END}`;
464482
}
465483

484+
function escapeRegex(value) {
485+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
486+
}
487+
466488
function upsertManagedBlock(currentText, block) {
467-
const pattern = new RegExp(`${MANAGED_BLOCK_START}[\\s\\S]*?${MANAGED_BLOCK_END}`, 'm');
489+
const pattern = new RegExp(`${escapeRegex(MANAGED_BLOCK_START)}[\\s\\S]*?${escapeRegex(MANAGED_BLOCK_END)}`, 'm');
468490
if (pattern.test(currentText)) {
469491
return currentText.replace(pattern, block);
470492
}
@@ -473,6 +495,16 @@ function upsertManagedBlock(currentText, block) {
473495
return normalized ? `${normalized}\n\n${block}\n` : `${block}\n`;
474496
}
475497

498+
function removeManagedBlock(currentText) {
499+
const pattern = new RegExp(`\\n*${escapeRegex(MANAGED_BLOCK_START)}\\n[\\s\\S]*?\\n${escapeRegex(MANAGED_BLOCK_END)}\\n*`, 'm');
500+
if (!pattern.test(currentText)) {
501+
return currentText;
502+
}
503+
504+
const next = currentText.replace(pattern, '\n').replace(/\n{3,}/g, '\n\n').trimEnd();
505+
return next ? `${next}\n` : '';
506+
}
507+
476508
async function readOptionalText(filePath) {
477509
try {
478510
return await fs.readFile(filePath, 'utf8');
@@ -484,18 +516,22 @@ async function readOptionalText(filePath) {
484516
}
485517
}
486518

487-
export async function installCompletionScript(shell, { env = process.env } = {}) {
519+
export async function installCompletionScript(shell, { env = process.env, force = false } = {}) {
488520
const plan = resolveInstallPlan(shell, env);
489521
const script = generateCompletionScript(shell);
490522

491523
await fs.mkdir(path.dirname(plan.scriptPath), { recursive: true });
492-
await fs.writeFile(plan.scriptPath, script, 'utf8');
524+
const currentScript = await readOptionalText(plan.scriptPath);
525+
const scriptUpdated = force || currentScript !== script;
526+
if (scriptUpdated) {
527+
await fs.writeFile(plan.scriptPath, script, 'utf8');
528+
}
493529

494530
let profileUpdated = false;
495531
if (plan.profilePath && plan.sourceLine) {
496532
const currentProfile = await readOptionalText(plan.profilePath);
497533
const nextProfile = upsertManagedBlock(currentProfile, buildManagedBlock(plan.sourceLine));
498-
profileUpdated = nextProfile !== currentProfile;
534+
profileUpdated = force || nextProfile !== currentProfile;
499535
if (profileUpdated) {
500536
await fs.writeFile(plan.profilePath, nextProfile, 'utf8');
501537
}
@@ -504,9 +540,36 @@ export async function installCompletionScript(shell, { env = process.env } = {})
504540
return {
505541
shell: plan.shell,
506542
scriptPath: plan.scriptPath,
543+
scriptUpdated,
507544
profilePath: plan.profilePath,
508545
profileUpdated,
509546
activationCommand: plan.activationCommand,
547+
forced: Boolean(force),
548+
};
549+
}
550+
551+
export async function uninstallCompletionScript(shell, { env = process.env } = {}) {
552+
const plan = resolveInstallPlan(shell, env);
553+
const currentScript = await readOptionalText(plan.scriptPath);
554+
const scriptRemoved = currentScript.length > 0;
555+
await fs.rm(plan.scriptPath, { force: true });
556+
557+
let profileUpdated = false;
558+
if (plan.profilePath) {
559+
const currentProfile = await readOptionalText(plan.profilePath);
560+
const nextProfile = removeManagedBlock(currentProfile);
561+
profileUpdated = nextProfile !== currentProfile;
562+
if (profileUpdated) {
563+
await fs.writeFile(plan.profilePath, nextProfile, 'utf8');
564+
}
565+
}
566+
567+
return {
568+
shell: plan.shell,
569+
scriptPath: plan.scriptPath,
570+
scriptRemoved,
571+
profilePath: plan.profilePath,
572+
profileUpdated,
510573
};
511574
}
512575

0 commit comments

Comments
 (0)