Skip to content

Commit e3be5d3

Browse files
committed
citty update
1 parent 4c7b0a1 commit e3be5d3

File tree

2 files changed

+256
-92
lines changed

2 files changed

+256
-92
lines changed

citty.ts

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import { defineCommand } from "citty";
2+
import * as zsh from "./zsh";
3+
import * as bash from "./bash";
4+
import * as fish from "./fish";
5+
import * as powershell from "./powershell";
6+
import {
7+
flagMap,
8+
Positional,
9+
positionalMap,
10+
ShellCompDirective,
11+
} from "./shared";
12+
13+
function quoteIfNeeded(path) {
14+
return path.includes(" ") ? `'${path}'` : path;
15+
}
16+
17+
const execPath = process.execPath;
18+
const processArgs = process.argv.slice(1);
19+
const quotedExecPath = quoteIfNeeded(execPath);
20+
const quotedProcessArgs = processArgs.map(quoteIfNeeded);
21+
const quotedProcessExecArgs = process.execArgv.map(quoteIfNeeded);
22+
const x = `${quotedExecPath} ${quotedProcessExecArgs.join(" ")} ${quotedProcessArgs[0]}`;
23+
24+
export default function tab(mainCommand) {
25+
mainCommand.subCommands["complete"] = defineCommand({
26+
meta: {
27+
name: "complete",
28+
description: "Generate shell completion scripts",
29+
},
30+
args: {
31+
shell: {
32+
type: "positional",
33+
required: false,
34+
description: "Specify shell type",
35+
},
36+
},
37+
async run(ctx) {
38+
let shell: string | undefined = ctx.args.shell;
39+
if (shell?.startsWith("--")) {
40+
shell = undefined;
41+
}
42+
43+
const extra = ctx.args._ || [];
44+
45+
switch (shell) {
46+
case "zsh": {
47+
const script = zsh.generate(mainCommand.meta.name, x);
48+
console.log(script);
49+
break;
50+
}
51+
case "bash": {
52+
const script = bash.generate(mainCommand.meta.name, x);
53+
console.log(script);
54+
break;
55+
}
56+
case "fish": {
57+
const script = fish.generate(mainCommand.meta.name, x);
58+
console.log(script);
59+
break;
60+
}
61+
case "powershell": {
62+
const script = powershell.generate(mainCommand.meta.name, x);
63+
console.log(script);
64+
break;
65+
}
66+
default: {
67+
const args = extra;
68+
let directive = ShellCompDirective.ShellCompDirectiveDefault;
69+
const completions: string[] = [];
70+
const endsWithSpace = args[args.length - 1] === "";
71+
72+
if (endsWithSpace) args.pop();
73+
let toComplete = args[args.length - 1] || "";
74+
const previousArgs = args.slice(0, -1);
75+
76+
let matchedCommand = mainCommand;
77+
78+
if (previousArgs.length > 0) {
79+
const lastPrevArg = previousArgs[previousArgs.length - 1];
80+
if (lastPrevArg.startsWith("--")) {
81+
const flagCompletion = flagMap.get(lastPrevArg);
82+
if (flagCompletion) {
83+
const flagSuggestions = await flagCompletion(previousArgs, toComplete);
84+
completions.push(
85+
...flagSuggestions.map(
86+
(comp) => `${comp.action}\t${comp.description ?? ""}`
87+
)
88+
);
89+
completions.forEach((comp) => console.log(comp));
90+
console.log(`:${directive}`);
91+
return;
92+
}
93+
}
94+
}
95+
96+
if (toComplete.startsWith("--")) {
97+
if (toComplete === "--") {
98+
const allFlags = [...flagMap.keys()];
99+
completions.push(
100+
...allFlags.map(
101+
(flag) =>
102+
`${flag}\t${matchedCommand.args[flag.slice(2)]?.description ?? "Option"}`
103+
)
104+
);
105+
} else {
106+
const flagNamePartial = toComplete.slice(2);
107+
const flagKeyPartial = `--${flagNamePartial}`;
108+
109+
if (flagMap.has(toComplete)) {
110+
const flagCompletion = flagMap.get(toComplete);
111+
if (flagCompletion) {
112+
const flagSuggestions = await flagCompletion(previousArgs, "");
113+
completions.push(
114+
...flagSuggestions.map(
115+
(comp) => `${comp.action}\t${comp.description ?? ""}`
116+
)
117+
);
118+
}
119+
} else {
120+
const matchingFlags = [...flagMap.keys()].filter((flag) =>
121+
flag.startsWith(flagKeyPartial)
122+
);
123+
124+
completions.push(
125+
...matchingFlags.map(
126+
(flag) =>
127+
`${flag}\t${matchedCommand.args[flag.slice(2)]?.description ?? "Option"}`
128+
)
129+
);
130+
}
131+
}
132+
133+
completions.forEach((comp) => console.log(comp));
134+
console.log(`:${directive}`);
135+
return;
136+
}
137+
138+
if (previousArgs.length === 0) {
139+
completions.push(
140+
...Object.keys(mainCommand.subCommands || {})
141+
.filter((cmd) => cmd !== "complete")
142+
.map(
143+
(cmd) =>
144+
`${cmd}\t${mainCommand.subCommands[cmd]?.meta.description ?? ""}`
145+
)
146+
);
147+
} else {
148+
const positionalCompletions =
149+
positionalMap.get(matchedCommand.meta.name) || [];
150+
for (const positional of positionalCompletions) {
151+
const suggestions = await positional.completion(
152+
previousArgs,
153+
toComplete
154+
);
155+
completions.push(
156+
...suggestions.map(
157+
(comp) => `${comp.action}\t${comp.description ?? ""}`
158+
)
159+
);
160+
}
161+
}
162+
163+
completions.forEach((comp) => console.log(comp));
164+
console.log(`:${directive}`);
165+
}
166+
}
167+
},
168+
});
169+
}

demo.citty.ts

Lines changed: 87 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,25 @@
11
import { defineCommand, createMain } from "citty";
2-
// import { tab } from "./citty";
2+
import tab from "./citty";
3+
import { flagMap, positionalMap } from "./shared";
4+
35
const main = defineCommand({
46
meta: {
57
name: "vite",
68
description: "Vite CLI tool",
79
},
810
args: {
9-
config: {
10-
type: "string",
11-
description: "Use specified config file",
12-
alias: "c",
13-
},
14-
base: {
15-
type: "string",
16-
description: "Public base path (default: /)",
17-
},
18-
logLevel: {
19-
type: "string",
20-
description: "info | warn | error | silent",
21-
alias: "l",
22-
},
23-
clearScreen: {
24-
type: "boolean",
25-
description: "Allow/disable clear screen when logging",
26-
},
27-
debug: {
28-
type: "string",
29-
description: "Show debug logs",
30-
alias: "d",
31-
},
32-
filter: {
33-
type: "string",
34-
description: "Filter debug logs",
35-
alias: "f",
36-
},
37-
mode: {
38-
type: "string",
39-
description: "Set env mode",
40-
alias: "m",
41-
},
11+
config: { type: "string", description: "Use specified config file", alias: "c" },
12+
base: { type: "string", description: "Public base path (default: /)" },
13+
logLevel: { type: "string", description: "info | warn | error | silent", alias: "l" },
14+
clearScreen: { type: "boolean", description: "Allow/disable clear screen when logging" },
15+
debug: { type: "string", description: "Show debug logs", alias: "d" },
16+
filter: { type: "string", description: "Filter debug logs", alias: "f" },
17+
mode: { type: "string", description: "Set env mode", alias: "m" },
4218
},
4319
run(ctx) {
44-
if (ctx.args._?.[0] !== "complete" && devCommand.run) {
45-
devCommand.run(ctx);
46-
} else if (!devCommand.run) {
47-
console.error("Error: dev command is not defined.");
20+
const firstArg = ctx.args._?.[0];
21+
if (firstArg && devCommand?.run) {
22+
devCommand.run({ ...ctx, args: { ...ctx.args, root: firstArg } });
4823
}
4924
},
5025
});
@@ -55,71 +30,91 @@ const devCommand = defineCommand({
5530
description: "Start dev server",
5631
},
5732
args: {
58-
root: {
59-
type: "positional",
60-
description: "Root directory",
61-
required: false,
62-
default: ".",
63-
},
64-
host: {
65-
type: "string",
66-
description: "Specify hostname",
67-
},
68-
port: {
69-
type: "string",
70-
description: "Specify port",
71-
},
72-
open: {
73-
type: "boolean",
74-
description: "Open browser on startup",
75-
},
76-
cors: {
77-
type: "boolean",
78-
description: "Enable CORS",
79-
},
80-
strictPort: {
81-
type: "boolean",
82-
description: "Exit if specified port is already in use",
83-
},
84-
force: {
85-
type: "boolean",
86-
description: "Force the optimizer to ignore the cache and re-bundle",
87-
},
33+
root: { type: "positional", description: "Root directory", default: "." },
34+
host: { type: "string", description: "Specify hostname" },
35+
port: { type: "string", description: "Specify port" },
36+
open: { type: "boolean", description: "Open browser on startup" },
37+
cors: { type: "boolean", description: "Enable CORS" },
38+
strictPort: { type: "boolean", description: "Exit if specified port is already in use" },
39+
force: { type: "boolean", description: "Force the optimizer to ignore the cache and re-bundle" },
8840
},
8941
run({ args }) {
9042
const { root, port, ...options } = args;
9143
const parsedPort = port ? parseInt(port, 10) : undefined;
9244

93-
if (args._?.[0] !== "complete") {
94-
if (!root || root === ".") {
95-
console.log("Suggested root directories:");
96-
console.log("- src/");
97-
console.log("- ./");
98-
}
45+
if (!root || root === ".") {
46+
console.log("Suggested root directories:");
47+
console.log("- src/");
48+
console.log("- ./");
49+
}
50+
},
51+
});
9952

100-
const formattedOptions = { ...options, port: parsedPort };
101-
if (formattedOptions._) {
102-
formattedOptions["--"] = formattedOptions._;
103-
delete formattedOptions._;
104-
}
53+
main.subCommands = { dev: devCommand };
10554

106-
const cleanedOptions = Object.fromEntries(
107-
Object.entries(formattedOptions).filter(([_, v]) => v !== undefined)
108-
);
55+
for (const command of [main, ...Object.values(main.subCommands)]) {
56+
const commandName = command.meta.name;
10957

110-
console.log(
111-
`Starting dev server at ${root || "."} with options:`,
112-
cleanedOptions
113-
);
58+
for (const [argName, argConfig] of Object.entries(command.args || {})) {
59+
const optionKey = `--${argName}`;
60+
if (argName === "port") {
61+
flagMap.set(optionKey, async (_, toComplete) => {
62+
const options = [
63+
{ action: "3000", description: "Development server port" },
64+
{ action: "8080", description: "Alternative port" },
65+
];
66+
return toComplete
67+
? options.filter(comp => comp.action.startsWith(toComplete))
68+
: options;
69+
});
11470
}
115-
},
116-
});
11771

118-
main.subCommands = {
119-
dev: devCommand,
120-
};
72+
if (argName === "host") {
73+
flagMap.set(optionKey, async (_, toComplete) => {
74+
const options = [
75+
{ action: "localhost", description: "Localhost" },
76+
{ action: "0.0.0.0", description: "All interfaces" },
77+
];
78+
return toComplete
79+
? options.filter(comp => comp.action.startsWith(toComplete))
80+
: options;
81+
});
82+
}
83+
84+
if (argName === "mode") {
85+
flagMap.set(optionKey, async (_, toComplete) => {
86+
const options = [
87+
{ action: "development", description: "Development mode" },
88+
{ action: "production", description: "Production mode" },
89+
];
90+
return toComplete
91+
? options.filter(comp => comp.action.startsWith(toComplete))
92+
: options;
93+
});
94+
}
95+
}
96+
97+
if (command.args) {
98+
const positionals = Object.entries(command.args)
99+
.filter(([, config]) => config.type === "positional")
100+
.map(([argName, argConfig]) => ({
101+
value: argName,
102+
required: !!argConfig.required,
103+
completion: async (_, toComplete) => {
104+
const options = [
105+
{ action: "src/", description: "Source directory" },
106+
{ action: "./", description: "Current directory" },
107+
];
108+
return toComplete
109+
? options.filter(comp => comp.action.startsWith(toComplete))
110+
: options;
111+
},
112+
}));
113+
positionalMap.set(commandName, positionals);
114+
}
115+
}
121116

122-
// tab(main);
117+
tab(main);
123118

124119
const cli = createMain(main);
125120
cli();

0 commit comments

Comments
 (0)