@nemus-cli/nemus 0.2.12 → 0.2.13
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/CHANGELOG.md +11 -0
- package/README.md +18 -0
- package/dist/commands/completion.js +143 -0
- package/dist/program.js +2 -0
- package/package.json +1 -1
- package/src/commands/completion.test.ts +60 -0
- package/src/commands/completion.ts +160 -0
- package/src/program.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.13] - 2026-08-27
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Shell completions**: `nemus completion bash|zsh|fish` prints a completion
|
|
15
|
+
script for that shell. Completes subcommands (names + aliases) and, for a
|
|
16
|
+
workspace-scoped command, live **workspace names** — the script calls back
|
|
17
|
+
into `nemus completion --workspaces`, so completions stay fresh without
|
|
18
|
+
regenerating. Registered for both the `nemus` and `nem` binaries. Install e.g.
|
|
19
|
+
`nemus completion zsh > "${fpath[1]}/_nemus"` (see README).
|
|
20
|
+
|
|
10
21
|
## [0.2.12] - 2026-08-27
|
|
11
22
|
|
|
12
23
|
### Added
|
package/README.md
CHANGED
|
@@ -243,6 +243,24 @@ The workspace-scoped ones (`status`/`doctor`/`analyze-deps`) need an explicit
|
|
|
243
243
|
workspace name with `--json` (they never prompt). On failure, `--json` prints a
|
|
244
244
|
parseable `{ "ok": false, "error": … }` to stdout and exits non-zero.
|
|
245
245
|
|
|
246
|
+
### Shell completions
|
|
247
|
+
|
|
248
|
+
Tab-complete subcommands and workspace names. `nemus completion <shell>` prints
|
|
249
|
+
a script for `bash`, `zsh`, or `fish` (works for both the `nemus` and `nem`
|
|
250
|
+
binaries):
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
# bash
|
|
254
|
+
nemus completion bash > /etc/bash_completion.d/nemus # or >> ~/.bashrc
|
|
255
|
+
# zsh (a directory on your $fpath)
|
|
256
|
+
nemus completion zsh > "${fpath[1]}/_nemus"
|
|
257
|
+
# fish
|
|
258
|
+
nemus completion fish > ~/.config/fish/completions/nemus.fish
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Workspace names are resolved live (the script calls back into the CLI), so they
|
|
262
|
+
stay current without regenerating.
|
|
263
|
+
|
|
246
264
|
### Suites (reusable repo collections)
|
|
247
265
|
|
|
248
266
|
```bash
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.COMPLETION_BINS = void 0;
|
|
4
|
+
exports.generateCompletion = generateCompletion;
|
|
5
|
+
exports.specsFromProgram = specsFromProgram;
|
|
6
|
+
exports.registerCompletionCommand = registerCompletionCommand;
|
|
7
|
+
const workspace_meta_1 = require("../utils/workspace-meta");
|
|
8
|
+
const logger_1 = require("../utils/logger");
|
|
9
|
+
/** Binaries that get completion registered (the CLI's bins). */
|
|
10
|
+
exports.COMPLETION_BINS = ['nemus', 'nem'];
|
|
11
|
+
/** Every token (name + aliases) that should complete as a subcommand. */
|
|
12
|
+
function allTokens(cmds) {
|
|
13
|
+
return cmds.flatMap((c) => [c.name, ...c.aliases]);
|
|
14
|
+
}
|
|
15
|
+
/** Tokens (names + aliases) of the commands that take a workspace argument. */
|
|
16
|
+
function workspaceTokens(cmds) {
|
|
17
|
+
return cmds.filter((c) => c.takesWorkspace).flatMap((c) => [c.name, ...c.aliases]);
|
|
18
|
+
}
|
|
19
|
+
/** Escape a description for a fish single-quoted string. */
|
|
20
|
+
function fishDesc(s) {
|
|
21
|
+
return s.replace(/\n/g, ' ').replace(/'/g, "'\\''");
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Generate a shell completion script. Pure (no I/O) so it's unit-tested. The
|
|
25
|
+
* generated script completes subcommands at position 1, and for a subcommand
|
|
26
|
+
* that takes a workspace it completes workspace names by calling back into the
|
|
27
|
+
* CLI: `<bin> completion --workspaces`. Dynamic values stay fresh without
|
|
28
|
+
* regenerating the script.
|
|
29
|
+
*/
|
|
30
|
+
function generateCompletion(shell, cmds, bins = exports.COMPLETION_BINS) {
|
|
31
|
+
const commands = allTokens(cmds).join(' ');
|
|
32
|
+
const wsCommands = workspaceTokens(cmds).join(' ');
|
|
33
|
+
if (shell === 'bash') {
|
|
34
|
+
return `# nemus bash completion. Install: nemus completion bash > /etc/bash_completion.d/nemus
|
|
35
|
+
# (or: nemus completion bash >> ~/.bashrc)
|
|
36
|
+
_nemus_complete() {
|
|
37
|
+
local cur bin sub
|
|
38
|
+
# bash does not clear COMPREPLY between completions; reset so a stale result
|
|
39
|
+
# from a previous TAB can't leak when we return without setting it.
|
|
40
|
+
COMPREPLY=()
|
|
41
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
42
|
+
bin="\${COMP_WORDS[0]}"
|
|
43
|
+
local commands="${commands}"
|
|
44
|
+
local ws_commands="${wsCommands}"
|
|
45
|
+
if [ "\$COMP_CWORD" -eq 1 ]; then
|
|
46
|
+
COMPREPLY=( \$(compgen -W "\$commands" -- "\$cur") )
|
|
47
|
+
return 0
|
|
48
|
+
fi
|
|
49
|
+
if [ "\$COMP_CWORD" -eq 2 ]; then
|
|
50
|
+
sub="\${COMP_WORDS[1]}"
|
|
51
|
+
if [[ " \$ws_commands " == *" \$sub "* ]]; then
|
|
52
|
+
local names
|
|
53
|
+
names="\$("\$bin" completion --workspaces 2>/dev/null)"
|
|
54
|
+
COMPREPLY=( \$(compgen -W "\$names" -- "\$cur") )
|
|
55
|
+
return 0
|
|
56
|
+
fi
|
|
57
|
+
fi
|
|
58
|
+
return 0
|
|
59
|
+
}
|
|
60
|
+
${bins.map((b) => `complete -F _nemus_complete ${b}`).join('\n')}
|
|
61
|
+
`;
|
|
62
|
+
}
|
|
63
|
+
if (shell === 'zsh') {
|
|
64
|
+
// Autoloaded form: save as a file named `_nemus` on your $fpath.
|
|
65
|
+
return `#compdef ${bins.join(' ')}
|
|
66
|
+
# nemus zsh completion. Install: nemus completion zsh > "\${fpath[1]}/_nemus"
|
|
67
|
+
local -a _nemus_commands
|
|
68
|
+
_nemus_commands=(${allTokens(cmds).map((t) => `'${t}'`).join(' ')})
|
|
69
|
+
local _nemus_ws_commands="${wsCommands}"
|
|
70
|
+
if (( CURRENT == 2 )); then
|
|
71
|
+
compadd -- $_nemus_commands
|
|
72
|
+
return
|
|
73
|
+
fi
|
|
74
|
+
if (( CURRENT == 3 )); then
|
|
75
|
+
local sub=\${words[2]}
|
|
76
|
+
if [[ " $_nemus_ws_commands " == *" $sub "* ]]; then
|
|
77
|
+
local -a _nemus_names
|
|
78
|
+
_nemus_names=(\${(f)"$(\${words[1]} completion --workspaces 2>/dev/null)"})
|
|
79
|
+
compadd -- $_nemus_names
|
|
80
|
+
fi
|
|
81
|
+
fi
|
|
82
|
+
`;
|
|
83
|
+
}
|
|
84
|
+
// fish
|
|
85
|
+
const lines = ['# nemus fish completion. Install: nemus completion fish > ~/.config/fish/completions/nemus.fish'];
|
|
86
|
+
for (const bin of bins) {
|
|
87
|
+
lines.push(`complete -c ${bin} -f`);
|
|
88
|
+
for (const c of cmds) {
|
|
89
|
+
for (const tok of [c.name, ...c.aliases]) {
|
|
90
|
+
lines.push(`complete -c ${bin} -n __fish_use_subcommand -a '${tok}' -d '${fishDesc(c.description)}'`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const wsToks = workspaceTokens(cmds).join(' ');
|
|
94
|
+
if (wsToks) {
|
|
95
|
+
lines.push(`complete -c ${bin} -n '__fish_seen_subcommand_from ${wsToks}' -a '(${bin} completion --workspaces)'`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return lines.join('\n') + '\n';
|
|
99
|
+
}
|
|
100
|
+
/** Distill the program's top-level commands into CommandSpecs. */
|
|
101
|
+
function specsFromProgram(program) {
|
|
102
|
+
return program.commands
|
|
103
|
+
.map((c) => {
|
|
104
|
+
const args = c.registeredArguments ?? [];
|
|
105
|
+
const firstArg = args[0]?.name?.();
|
|
106
|
+
return {
|
|
107
|
+
name: c.name(),
|
|
108
|
+
aliases: c.aliases(),
|
|
109
|
+
takesWorkspace: typeof firstArg === 'string' && firstArg.toLowerCase().includes('workspace'),
|
|
110
|
+
description: c.description() ?? '',
|
|
111
|
+
};
|
|
112
|
+
})
|
|
113
|
+
// The completion command itself and any hidden helper needn't clutter, but
|
|
114
|
+
// keeping them is harmless; only drop entries with no name.
|
|
115
|
+
.filter((s) => s.name);
|
|
116
|
+
}
|
|
117
|
+
function registerCompletionCommand(program) {
|
|
118
|
+
program
|
|
119
|
+
.command('completion [shell]')
|
|
120
|
+
.description('Output a shell completion script (bash|zsh|fish)')
|
|
121
|
+
.option('--workspaces', 'Print workspace names (used internally by completion scripts)')
|
|
122
|
+
.action(async (shell, opts) => {
|
|
123
|
+
// Data helper the generated scripts call back into.
|
|
124
|
+
if (opts.workspaces) {
|
|
125
|
+
try {
|
|
126
|
+
const workspaces = await (0, workspace_meta_1.listWorkspaces)(false);
|
|
127
|
+
for (const ws of workspaces)
|
|
128
|
+
process.stdout.write(ws.name + '\n');
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// Silent: completion must never error out the user's shell.
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const shells = ['bash', 'zsh', 'fish'];
|
|
136
|
+
if (!shell || !shells.includes(shell)) {
|
|
137
|
+
(0, logger_1.logError)(`completion: specify a shell — one of ${shells.join(', ')}`);
|
|
138
|
+
(0, logger_1.logError)('e.g. nemus completion bash');
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
process.stdout.write(generateCompletion(shell, specsFromProgram(program)));
|
|
142
|
+
});
|
|
143
|
+
}
|
package/dist/program.js
CHANGED
|
@@ -88,6 +88,7 @@ const ghq_status_1 = require("./commands/ghq-status");
|
|
|
88
88
|
const save_context_1 = require("./commands/save-context");
|
|
89
89
|
const migrate_1 = require("./commands/migrate");
|
|
90
90
|
const report_bug_1 = require("./commands/report-bug");
|
|
91
|
+
const completion_1 = require("./commands/completion");
|
|
91
92
|
(0, create_1.registerCreateCommand)(exports.program);
|
|
92
93
|
(0, list_1.registerListCommand)(exports.program);
|
|
93
94
|
(0, update_1.registerUpdateCommand)(exports.program);
|
|
@@ -111,6 +112,7 @@ const report_bug_1 = require("./commands/report-bug");
|
|
|
111
112
|
(0, save_context_1.registerSaveContextCommand)(exports.program);
|
|
112
113
|
(0, migrate_1.registerMigrateCommand)(exports.program);
|
|
113
114
|
(0, report_bug_1.registerReportBugCommand)(exports.program);
|
|
115
|
+
(0, completion_1.registerCompletionCommand)(exports.program);
|
|
114
116
|
// Register TUI (delegates to existing Ink/React implementation)
|
|
115
117
|
exports.program
|
|
116
118
|
.command('tui')
|
package/package.json
CHANGED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { generateCompletion, specsFromProgram, CommandSpec } from './completion';
|
|
4
|
+
|
|
5
|
+
const specs: CommandSpec[] = [
|
|
6
|
+
{ name: 'list', aliases: ['l'], takesWorkspace: false, description: 'List workspaces' },
|
|
7
|
+
{ name: 'status', aliases: ['st'], takesWorkspace: true, description: "Show a repo's git status" },
|
|
8
|
+
{ name: 'doctor', aliases: ['doc'], takesWorkspace: true, description: 'Health checks' },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
describe('generateCompletion — bash', () => {
|
|
12
|
+
const s = generateCompletion('bash', specs);
|
|
13
|
+
it('resets COMPREPLY, lists tokens, and registers both bins', () => {
|
|
14
|
+
expect(s).toContain('COMPREPLY=()'); // guards the stale-completion leak
|
|
15
|
+
expect(s).toContain('local commands="list l status st doctor doc"');
|
|
16
|
+
expect(s).toContain('local ws_commands="status st doctor doc"');
|
|
17
|
+
expect(s).toContain('complete -F _nemus_complete nemus');
|
|
18
|
+
expect(s).toContain('complete -F _nemus_complete nem');
|
|
19
|
+
});
|
|
20
|
+
it('calls back into the invoked bin for workspace names', () => {
|
|
21
|
+
expect(s).toContain('"$bin" completion --workspaces');
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('generateCompletion — zsh', () => {
|
|
26
|
+
const s = generateCompletion('zsh', specs);
|
|
27
|
+
it('is an autoloadable #compdef script with the tokens + callback', () => {
|
|
28
|
+
expect(s.startsWith('#compdef nemus nem')).toBe(true);
|
|
29
|
+
expect(s).toContain("_nemus_commands=('list' 'l' 'status' 'st' 'doctor' 'doc')");
|
|
30
|
+
expect(s).toContain('_nemus_ws_commands="status st doctor doc"');
|
|
31
|
+
expect(s).toContain('completion --workspaces');
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('generateCompletion — fish', () => {
|
|
36
|
+
const s = generateCompletion('fish', specs);
|
|
37
|
+
it('emits subcommand + workspace completions for both bins with escaped descriptions', () => {
|
|
38
|
+
expect(s).toContain("complete -c nemus -n __fish_use_subcommand -a 'list' -d 'List workspaces'");
|
|
39
|
+
expect(s).toContain("complete -c nem -n __fish_use_subcommand -a 'status'");
|
|
40
|
+
// apostrophe in the description is escaped for fish's single-quoted string
|
|
41
|
+
expect(s).toContain("Show a repo'\\''s git status");
|
|
42
|
+
expect(s).toContain("-n '__fish_seen_subcommand_from status st doctor doc' -a '(nemus completion --workspaces)'");
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('specsFromProgram', () => {
|
|
47
|
+
it('detects workspace args + aliases from a commander program', () => {
|
|
48
|
+
const program = new Command();
|
|
49
|
+
program.command('list').alias('l').description('list');
|
|
50
|
+
program.command('status [workspace]').alias('st').description('status');
|
|
51
|
+
program.command('create').description('create');
|
|
52
|
+
|
|
53
|
+
const out = specsFromProgram(program);
|
|
54
|
+
const byName = Object.fromEntries(out.map((s) => [s.name, s]));
|
|
55
|
+
expect(byName.status.takesWorkspace).toBe(true);
|
|
56
|
+
expect(byName.status.aliases).toEqual(['st']);
|
|
57
|
+
expect(byName.list.takesWorkspace).toBe(false);
|
|
58
|
+
expect(byName.create.takesWorkspace).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { listWorkspaces } from '../utils/workspace-meta';
|
|
3
|
+
import { logError } from '../utils/logger';
|
|
4
|
+
|
|
5
|
+
/** Binaries that get completion registered (the CLI's bins). */
|
|
6
|
+
export const COMPLETION_BINS = ['nemus', 'nem'];
|
|
7
|
+
|
|
8
|
+
export type Shell = 'bash' | 'zsh' | 'fish';
|
|
9
|
+
|
|
10
|
+
/** One top-level command, distilled to what a completion script needs. */
|
|
11
|
+
export interface CommandSpec {
|
|
12
|
+
name: string;
|
|
13
|
+
aliases: string[];
|
|
14
|
+
/** True if its first positional argument is a workspace name. */
|
|
15
|
+
takesWorkspace: boolean;
|
|
16
|
+
description: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Every token (name + aliases) that should complete as a subcommand. */
|
|
20
|
+
function allTokens(cmds: CommandSpec[]): string[] {
|
|
21
|
+
return cmds.flatMap((c) => [c.name, ...c.aliases]);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Tokens (names + aliases) of the commands that take a workspace argument. */
|
|
25
|
+
function workspaceTokens(cmds: CommandSpec[]): string[] {
|
|
26
|
+
return cmds.filter((c) => c.takesWorkspace).flatMap((c) => [c.name, ...c.aliases]);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Escape a description for a fish single-quoted string. */
|
|
30
|
+
function fishDesc(s: string): string {
|
|
31
|
+
return s.replace(/\n/g, ' ').replace(/'/g, "'\\''");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Generate a shell completion script. Pure (no I/O) so it's unit-tested. The
|
|
36
|
+
* generated script completes subcommands at position 1, and for a subcommand
|
|
37
|
+
* that takes a workspace it completes workspace names by calling back into the
|
|
38
|
+
* CLI: `<bin> completion --workspaces`. Dynamic values stay fresh without
|
|
39
|
+
* regenerating the script.
|
|
40
|
+
*/
|
|
41
|
+
export function generateCompletion(shell: Shell, cmds: CommandSpec[], bins: string[] = COMPLETION_BINS): string {
|
|
42
|
+
const commands = allTokens(cmds).join(' ');
|
|
43
|
+
const wsCommands = workspaceTokens(cmds).join(' ');
|
|
44
|
+
|
|
45
|
+
if (shell === 'bash') {
|
|
46
|
+
return `# nemus bash completion. Install: nemus completion bash > /etc/bash_completion.d/nemus
|
|
47
|
+
# (or: nemus completion bash >> ~/.bashrc)
|
|
48
|
+
_nemus_complete() {
|
|
49
|
+
local cur bin sub
|
|
50
|
+
# bash does not clear COMPREPLY between completions; reset so a stale result
|
|
51
|
+
# from a previous TAB can't leak when we return without setting it.
|
|
52
|
+
COMPREPLY=()
|
|
53
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
54
|
+
bin="\${COMP_WORDS[0]}"
|
|
55
|
+
local commands="${commands}"
|
|
56
|
+
local ws_commands="${wsCommands}"
|
|
57
|
+
if [ "\$COMP_CWORD" -eq 1 ]; then
|
|
58
|
+
COMPREPLY=( \$(compgen -W "\$commands" -- "\$cur") )
|
|
59
|
+
return 0
|
|
60
|
+
fi
|
|
61
|
+
if [ "\$COMP_CWORD" -eq 2 ]; then
|
|
62
|
+
sub="\${COMP_WORDS[1]}"
|
|
63
|
+
if [[ " \$ws_commands " == *" \$sub "* ]]; then
|
|
64
|
+
local names
|
|
65
|
+
names="\$("\$bin" completion --workspaces 2>/dev/null)"
|
|
66
|
+
COMPREPLY=( \$(compgen -W "\$names" -- "\$cur") )
|
|
67
|
+
return 0
|
|
68
|
+
fi
|
|
69
|
+
fi
|
|
70
|
+
return 0
|
|
71
|
+
}
|
|
72
|
+
${bins.map((b) => `complete -F _nemus_complete ${b}`).join('\n')}
|
|
73
|
+
`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (shell === 'zsh') {
|
|
77
|
+
// Autoloaded form: save as a file named `_nemus` on your $fpath.
|
|
78
|
+
return `#compdef ${bins.join(' ')}
|
|
79
|
+
# nemus zsh completion. Install: nemus completion zsh > "\${fpath[1]}/_nemus"
|
|
80
|
+
local -a _nemus_commands
|
|
81
|
+
_nemus_commands=(${allTokens(cmds).map((t) => `'${t}'`).join(' ')})
|
|
82
|
+
local _nemus_ws_commands="${wsCommands}"
|
|
83
|
+
if (( CURRENT == 2 )); then
|
|
84
|
+
compadd -- $_nemus_commands
|
|
85
|
+
return
|
|
86
|
+
fi
|
|
87
|
+
if (( CURRENT == 3 )); then
|
|
88
|
+
local sub=\${words[2]}
|
|
89
|
+
if [[ " $_nemus_ws_commands " == *" $sub "* ]]; then
|
|
90
|
+
local -a _nemus_names
|
|
91
|
+
_nemus_names=(\${(f)"$(\${words[1]} completion --workspaces 2>/dev/null)"})
|
|
92
|
+
compadd -- $_nemus_names
|
|
93
|
+
fi
|
|
94
|
+
fi
|
|
95
|
+
`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// fish
|
|
99
|
+
const lines: string[] = ['# nemus fish completion. Install: nemus completion fish > ~/.config/fish/completions/nemus.fish'];
|
|
100
|
+
for (const bin of bins) {
|
|
101
|
+
lines.push(`complete -c ${bin} -f`);
|
|
102
|
+
for (const c of cmds) {
|
|
103
|
+
for (const tok of [c.name, ...c.aliases]) {
|
|
104
|
+
lines.push(`complete -c ${bin} -n __fish_use_subcommand -a '${tok}' -d '${fishDesc(c.description)}'`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const wsToks = workspaceTokens(cmds).join(' ');
|
|
108
|
+
if (wsToks) {
|
|
109
|
+
lines.push(
|
|
110
|
+
`complete -c ${bin} -n '__fish_seen_subcommand_from ${wsToks}' -a '(${bin} completion --workspaces)'`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return lines.join('\n') + '\n';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Distill the program's top-level commands into CommandSpecs. */
|
|
118
|
+
export function specsFromProgram(program: Command): CommandSpec[] {
|
|
119
|
+
return program.commands
|
|
120
|
+
.map((c) => {
|
|
121
|
+
const args = (c as any).registeredArguments ?? [];
|
|
122
|
+
const firstArg: string | undefined = args[0]?.name?.();
|
|
123
|
+
return {
|
|
124
|
+
name: c.name(),
|
|
125
|
+
aliases: c.aliases(),
|
|
126
|
+
takesWorkspace: typeof firstArg === 'string' && firstArg.toLowerCase().includes('workspace'),
|
|
127
|
+
description: c.description() ?? '',
|
|
128
|
+
};
|
|
129
|
+
})
|
|
130
|
+
// The completion command itself and any hidden helper needn't clutter, but
|
|
131
|
+
// keeping them is harmless; only drop entries with no name.
|
|
132
|
+
.filter((s) => s.name);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function registerCompletionCommand(program: Command) {
|
|
136
|
+
program
|
|
137
|
+
.command('completion [shell]')
|
|
138
|
+
.description('Output a shell completion script (bash|zsh|fish)')
|
|
139
|
+
.option('--workspaces', 'Print workspace names (used internally by completion scripts)')
|
|
140
|
+
.action(async (shell: string | undefined, opts: { workspaces?: boolean }) => {
|
|
141
|
+
// Data helper the generated scripts call back into.
|
|
142
|
+
if (opts.workspaces) {
|
|
143
|
+
try {
|
|
144
|
+
const workspaces = await listWorkspaces(false);
|
|
145
|
+
for (const ws of workspaces) process.stdout.write(ws.name + '\n');
|
|
146
|
+
} catch {
|
|
147
|
+
// Silent: completion must never error out the user's shell.
|
|
148
|
+
}
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const shells: Shell[] = ['bash', 'zsh', 'fish'];
|
|
153
|
+
if (!shell || !shells.includes(shell as Shell)) {
|
|
154
|
+
logError(`completion: specify a shell — one of ${shells.join(', ')}`);
|
|
155
|
+
logError('e.g. nemus completion bash');
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
process.stdout.write(generateCompletion(shell as Shell, specsFromProgram(program)));
|
|
159
|
+
});
|
|
160
|
+
}
|
package/src/program.ts
CHANGED
|
@@ -58,6 +58,7 @@ import { registerGhqStatusCommand } from './commands/ghq-status';
|
|
|
58
58
|
import { registerSaveContextCommand } from './commands/save-context';
|
|
59
59
|
import { registerMigrateCommand } from './commands/migrate';
|
|
60
60
|
import { registerReportBugCommand } from './commands/report-bug';
|
|
61
|
+
import { registerCompletionCommand } from './commands/completion';
|
|
61
62
|
|
|
62
63
|
registerCreateCommand(program);
|
|
63
64
|
registerListCommand(program);
|
|
@@ -82,6 +83,7 @@ registerGhqStatusCommand(program);
|
|
|
82
83
|
registerSaveContextCommand(program);
|
|
83
84
|
registerMigrateCommand(program);
|
|
84
85
|
registerReportBugCommand(program);
|
|
86
|
+
registerCompletionCommand(program);
|
|
85
87
|
|
|
86
88
|
// Register TUI (delegates to existing Ink/React implementation)
|
|
87
89
|
program
|