@fougere/cli 0.3.0-alpha.0 → 0.4.0-alpha.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/README.md +1 -1
- package/app/commands/CallCommand.ts +3 -3
- package/app/commands/ExplainCommand.ts +51 -4
- package/app/commands/FreezeCommand.ts +1 -1
- package/app/commands/KeysCommand.ts +1 -1
- package/app/commands/ServeCommand.ts +7 -16
- package/dist/bin.js +4 -5
- package/dist/bin.js.map +1 -1
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js +11 -4
- package/dist/bridge.js.map +1 -1
- package/dist/completion.d.ts +4 -1
- package/dist/completion.d.ts.map +1 -1
- package/dist/completion.js +54 -20
- package/dist/completion.js.map +1 -1
- package/dist/loader.d.ts +11 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +23 -0
- package/dist/loader.js.map +1 -0
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +4 -3
- package/dist/runner.js.map +1 -1
- package/fronds/analysis/entities/Explain.ts +2 -1
- package/fronds/analysis/handlers/ExplainHandler.ts +52 -12
- package/fronds/analysis/handlers/FreezeHandler.ts +15 -11
- package/fronds/analysis/handlers/MigrateHandler.ts +2 -2
- package/fronds/scaffold/entities/BuildFrond.ts +1 -1
- package/fronds/scaffold/entities/Call.ts +1 -1
- package/fronds/scaffold/entities/Sync.ts +1 -1
- package/fronds/scaffold/handlers/BuildFrondHandler.ts +5 -5
- package/fronds/scaffold/handlers/SyncHandler.ts +15 -15
- package/package.json +9 -8
- package/src/bin.ts +83 -0
- package/src/bridge.ts +70 -0
- package/src/completion.ts +152 -0
- package/src/index.ts +3 -0
- package/src/loader.ts +28 -0
- package/src/runner.ts +139 -0
- package/src/theme.ts +19 -0
- package/src/ui.ts +131 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell completion generator — outputs a script for bash/zsh.
|
|
3
|
+
*
|
|
4
|
+
* Commands and flags are the CLI's own entities, read once at generation. The VALUES a
|
|
5
|
+
* positional accepts belong to the project the shell sits in, so they are not written
|
|
6
|
+
* down here: the script asks `fougere explain --names` at the moment of the TAB. A list
|
|
7
|
+
* frozen into a script is stale the first time a handler is added.
|
|
8
|
+
*/
|
|
9
|
+
import type { App } from '@fougere/core';
|
|
10
|
+
import { entityToArgs } from './bridge.js';
|
|
11
|
+
|
|
12
|
+
function toKebab(name: string): string {
|
|
13
|
+
return name.replace(/[A-Z]/g, (c) => '-' + c.toLowerCase()).replace(/^-/, '');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** What a positional names, said by the field's own name — no table to keep in step. */
|
|
17
|
+
const NAMES: Record<string, 'operations' | 'fronds' | undefined> = {
|
|
18
|
+
operation: 'operations',
|
|
19
|
+
frond: 'fronds',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
interface CommandMeta {
|
|
23
|
+
name: string;
|
|
24
|
+
flags: string[];
|
|
25
|
+
names?: 'operations' | 'fronds';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function extractCommandMeta(app: App): CommandMeta[] {
|
|
29
|
+
const commands: CommandMeta[] = [];
|
|
30
|
+
|
|
31
|
+
for (const frond of app.fronds) {
|
|
32
|
+
const handlerMap = new Map(frond.handlers.map((h) => [h.address, h]));
|
|
33
|
+
|
|
34
|
+
for (const entity of frond.entities) {
|
|
35
|
+
const handler = handlerMap.get(entity.name);
|
|
36
|
+
if (!handler) continue;
|
|
37
|
+
|
|
38
|
+
let facade: Record<string, Function>;
|
|
39
|
+
try { facade = app.resolve(handler.name); } catch { continue; }
|
|
40
|
+
if (typeof facade.execute !== 'function') continue;
|
|
41
|
+
|
|
42
|
+
// The same function the runner builds citty's args from, so the positional and the
|
|
43
|
+
// flags are the ones the command actually has.
|
|
44
|
+
const args = Object.entries(entityToArgs(entity.entityClass.getFields())) as
|
|
45
|
+
[string, { type?: string }][];
|
|
46
|
+
const positional = args.find(([, def]) => def.type === 'positional')?.[0];
|
|
47
|
+
|
|
48
|
+
commands.push({
|
|
49
|
+
name: toKebab(entity.name),
|
|
50
|
+
flags: args.filter(([, def]) => def.type !== 'positional').map(([key]) => `--${key}`),
|
|
51
|
+
...(positional && NAMES[positional] ? { names: NAMES[positional]! } : {}),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return commands;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function generateZshCompletion(app: App, binName = 'fougere'): string {
|
|
60
|
+
const commands = extractCommandMeta(app);
|
|
61
|
+
|
|
62
|
+
const valueCases = commands
|
|
63
|
+
.filter((cmd) => cmd.names)
|
|
64
|
+
.map((cmd) => ` ${cmd.name}) _${binName}_names ${cmd.names} ;;`)
|
|
65
|
+
.join('\n');
|
|
66
|
+
|
|
67
|
+
const flagCases = commands
|
|
68
|
+
.map((cmd) => ` ${cmd.name}) flags="${cmd.flags.join(' ')}" ;;`)
|
|
69
|
+
.join('\n');
|
|
70
|
+
|
|
71
|
+
return `# Auto-generated by @fougere/cli
|
|
72
|
+
|
|
73
|
+
# The project answers; a stale list in this file would not.
|
|
74
|
+
_${binName}_names() {
|
|
75
|
+
local -a values
|
|
76
|
+
values=(\${(f)"$(${binName} explain --names $1 2>/dev/null)"})
|
|
77
|
+
(( \${#values} )) && compadd -- \${values}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
_${binName}() {
|
|
81
|
+
local -a commands flags
|
|
82
|
+
commands=(${commands.map((c) => `'${c.name}'`).join(' ')})
|
|
83
|
+
|
|
84
|
+
if (( CURRENT == 2 )); then
|
|
85
|
+
_describe 'command' commands
|
|
86
|
+
return
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
local cmd=\${words[2]}
|
|
90
|
+
|
|
91
|
+
if [[ "\${words[CURRENT]}" != --* ]]; then
|
|
92
|
+
case "$cmd" in
|
|
93
|
+
${valueCases}
|
|
94
|
+
esac
|
|
95
|
+
return
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
case "$cmd" in
|
|
99
|
+
${flagCases}
|
|
100
|
+
*) flags="" ;;
|
|
101
|
+
esac
|
|
102
|
+
_values 'flags' \${(s: :)flags}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
compdef _${binName} ${binName}
|
|
106
|
+
`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function generateBashCompletion(app: App, binName = 'fougere'): string {
|
|
110
|
+
const commands = extractCommandMeta(app);
|
|
111
|
+
const cmdNames = commands.map((c) => c.name).join(' ');
|
|
112
|
+
|
|
113
|
+
const valueCases = commands
|
|
114
|
+
.filter((cmd) => cmd.names)
|
|
115
|
+
.map((cmd) => ` ${cmd.name}) names="${cmd.names}" ;;`)
|
|
116
|
+
.join('\n');
|
|
117
|
+
|
|
118
|
+
const flagCases = commands
|
|
119
|
+
.map((cmd) => ` ${cmd.name}) COMPREPLY=( $(compgen -W "${cmd.flags.join(' ')}" -- "\${cur}") ) ;;`)
|
|
120
|
+
.join('\n');
|
|
121
|
+
|
|
122
|
+
return `# Auto-generated by @fougere/cli
|
|
123
|
+
_${binName}_completions() {
|
|
124
|
+
local cur cmd names
|
|
125
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
126
|
+
cmd="\${COMP_WORDS[1]}"
|
|
127
|
+
|
|
128
|
+
if [[ \${COMP_CWORD} -eq 1 ]]; then
|
|
129
|
+
COMPREPLY=( $(compgen -W "${cmdNames}" -- "\${cur}") )
|
|
130
|
+
return
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
if [[ "\${cur}" != --* ]]; then
|
|
134
|
+
names=""
|
|
135
|
+
case "$cmd" in
|
|
136
|
+
${valueCases}
|
|
137
|
+
esac
|
|
138
|
+
if [[ -n "\${names}" ]]; then
|
|
139
|
+
# The project answers; a stale list in this file would not.
|
|
140
|
+
COMPREPLY=( $(compgen -W "$(${binName} explain --names "\${names}" 2>/dev/null | tr '\\n' ' ')" -- "\${cur}") )
|
|
141
|
+
return
|
|
142
|
+
fi
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
case "$cmd" in
|
|
146
|
+
${flagCases}
|
|
147
|
+
esac
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
complete -F _${binName}_completions ${binName}
|
|
151
|
+
`;
|
|
152
|
+
}
|
package/src/index.ts
ADDED
package/src/loader.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {
|
|
2
|
+
setModuleLoader, loadConfig, resolveConventions, frondAliases,
|
|
3
|
+
} from '@fougere/core/node';
|
|
4
|
+
import type { Conventions } from '@fougere/core/node';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The loader every command needs: `alias` is what makes `@fronds/user/entities/User.js`
|
|
8
|
+
* resolve when one frond names its neighbour, in any command that loads user code.
|
|
9
|
+
*
|
|
10
|
+
* Two jitis, because the config is read BEFORE the aliases — it names the scope they are
|
|
11
|
+
* built from. `reread` drops the module cache: every loader caches by specifier, so a
|
|
12
|
+
* second boot in one process would be handed what the first one read.
|
|
13
|
+
*/
|
|
14
|
+
export async function installLoader(root: string, reread = false): Promise<Conventions> {
|
|
15
|
+
const { createJiti } = await import('jiti');
|
|
16
|
+
const bare = createJiti(import.meta.url, { interopDefault: true });
|
|
17
|
+
setModuleLoader((filePath) => bare.import(filePath) as Promise<Record<string, unknown>>);
|
|
18
|
+
|
|
19
|
+
const conventions = resolveConventions((await loadConfig(root)).conventions);
|
|
20
|
+
const jiti = createJiti(import.meta.url, {
|
|
21
|
+
interopDefault: true,
|
|
22
|
+
alias: await frondAliases(root, conventions),
|
|
23
|
+
...(reread ? { moduleCache: false } : {}),
|
|
24
|
+
});
|
|
25
|
+
setModuleLoader((filePath) => jiti.import(filePath) as Promise<Record<string, unknown>>);
|
|
26
|
+
|
|
27
|
+
return conventions;
|
|
28
|
+
}
|
package/src/runner.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI runner — scans frond entities for flags, looks for app commands
|
|
3
|
+
* for presentation, dispatches via citty.
|
|
4
|
+
*
|
|
5
|
+
* Architecture:
|
|
6
|
+
* - fronds/ → entities (flags) + handlers (domain logic)
|
|
7
|
+
* - app/ → commands (prompts, TUI, presentation)
|
|
8
|
+
* - src/ → runner + bridge (framework)
|
|
9
|
+
*/
|
|
10
|
+
import type { App } from '@fougere/core';
|
|
11
|
+
import { createAppRunner } from '@fougere/core';
|
|
12
|
+
import { lowerFirst } from '@fougere/core/contract';
|
|
13
|
+
import { defineCommand, runMain } from 'citty';
|
|
14
|
+
import { ui } from './ui.js';
|
|
15
|
+
import { entityToArgs } from './bridge.js';
|
|
16
|
+
import { readdir } from 'node:fs/promises';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
|
|
19
|
+
function toKebab(name: string): string {
|
|
20
|
+
return name.replace(/[A-Z]/g, (c) => '-' + c.toLowerCase()).replace(/^-/, '');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function toCamel(kebab: string): string {
|
|
24
|
+
return kebab.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Scan app/commands/ for command classes. */
|
|
28
|
+
async function loadAppCommands(
|
|
29
|
+
cliRoot: string,
|
|
30
|
+
loader: (path: string) => Promise<Record<string, unknown>>,
|
|
31
|
+
): Promise<Map<string, new (...args: unknown[]) => { run: (raw: Record<string, unknown>) => Promise<void> }>> {
|
|
32
|
+
const map = new Map();
|
|
33
|
+
const dir = join(cliRoot, 'app', 'commands');
|
|
34
|
+
const files = await readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
35
|
+
|
|
36
|
+
for (const f of files) {
|
|
37
|
+
if (!f.isFile() || !(f.name.endsWith('.ts') || f.name.endsWith('.js'))) continue;
|
|
38
|
+
const name = f.name.replace(/Command\.(ts|js)$/, '').replace(/\.(ts|js)$/, '');
|
|
39
|
+
const kebab = toKebab(name);
|
|
40
|
+
const mod = await loader(join(dir, f.name));
|
|
41
|
+
if (mod.default && typeof mod.default === 'function') {
|
|
42
|
+
map.set(kebab, mod.default);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return map;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function run(app: App): Promise<void> {
|
|
50
|
+
const terminal = ui();
|
|
51
|
+
const cliRoot = new URL('..', import.meta.url).pathname;
|
|
52
|
+
|
|
53
|
+
// Load app commands (presentation layer)
|
|
54
|
+
const { createJiti } = await import('jiti');
|
|
55
|
+
const jiti = createJiti(import.meta.url, { interopDefault: true });
|
|
56
|
+
const loader = (path: string) => jiti.import(path) as Promise<Record<string, unknown>>;
|
|
57
|
+
const appCommands = await loadAppCommands(cliRoot, loader);
|
|
58
|
+
|
|
59
|
+
const subCommands: Record<string, ReturnType<typeof defineCommand>> = {};
|
|
60
|
+
|
|
61
|
+
for (const frond of app.fronds) {
|
|
62
|
+
const handlerMap = new Map(frond.handlers.map((h) => [h.address, h]));
|
|
63
|
+
|
|
64
|
+
for (const entity of frond.entities) {
|
|
65
|
+
const handlerEntry = handlerMap.get(entity.name);
|
|
66
|
+
if (!handlerEntry) continue;
|
|
67
|
+
|
|
68
|
+
const handlerName = `${entity.name}Handler`;
|
|
69
|
+
let handler: Record<string, Function>;
|
|
70
|
+
try {
|
|
71
|
+
handler = app.resolve<Record<string, Function>>(handlerName);
|
|
72
|
+
} catch { continue; }
|
|
73
|
+
|
|
74
|
+
if (typeof handler.execute !== 'function') continue;
|
|
75
|
+
|
|
76
|
+
const cmdName = toKebab(entity.name);
|
|
77
|
+
const fields = entity.entityClass.getFields();
|
|
78
|
+
const args = entityToArgs(fields);
|
|
79
|
+
|
|
80
|
+
// Check for an app command (presentation layer)
|
|
81
|
+
const AppCommand = appCommands.get(cmdName);
|
|
82
|
+
|
|
83
|
+
// App commands handle their own prompting — don't let citty reject missing args
|
|
84
|
+
if (AppCommand) {
|
|
85
|
+
for (const def of Object.values(args)) {
|
|
86
|
+
if (typeof def === 'object' && def) (def as Record<string, unknown>).required = false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
subCommands[cmdName] = defineCommand({
|
|
91
|
+
meta: {
|
|
92
|
+
name: cmdName,
|
|
93
|
+
// `--help` reads the operation's own doc sentence, which the scan already
|
|
94
|
+
// carries for every door (`OperationContract.description`). A table here
|
|
95
|
+
// would be the same fact written twice, and it drifted: it described `add`
|
|
96
|
+
// and `doctor`, which do not exist, and had nothing for `call` or `serve`.
|
|
97
|
+
description: handlerEntry.operations.get('execute')?.description,
|
|
98
|
+
},
|
|
99
|
+
args,
|
|
100
|
+
run: async ({ args: parsed }) => {
|
|
101
|
+
// JSON is a protocol: a branded intro before `{` makes it unparsable. The
|
|
102
|
+
// explain command owns its machine output and therefore gets a clean stdout.
|
|
103
|
+
const machineOutput = cmdName === 'explain'
|
|
104
|
+
&& (parsed.json === true || typeof parsed.names === 'string');
|
|
105
|
+
if (cmdName !== 'completion' && !machineOutput) terminal.intro();
|
|
106
|
+
|
|
107
|
+
// citty adds `_` (raw positionals) and `--` (passthrough); strip them
|
|
108
|
+
// so only the entity's own fields reach the handler.
|
|
109
|
+
const input = { ...(parsed as Record<string, unknown>) };
|
|
110
|
+
delete input._;
|
|
111
|
+
delete input['--'];
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
if (AppCommand) {
|
|
115
|
+
const cmd = new (AppCommand as new (...a: unknown[]) => { run: (raw: Record<string, unknown>) => Promise<void> })(app, terminal);
|
|
116
|
+
await cmd.run(input);
|
|
117
|
+
} else {
|
|
118
|
+
// Ride the call contract — the same envelope every consumer uses.
|
|
119
|
+
await createAppRunner(app)(
|
|
120
|
+
{ entity: lowerFirst(entity.name), op: 'execute' },
|
|
121
|
+
{ params: {}, query: {}, body: input, state: {} },
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
} catch (err) {
|
|
125
|
+
terminal.error(err instanceof Error ? err.message : String(err));
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const main = defineCommand({
|
|
134
|
+
meta: { name: 'fougere', description: 'Fougere CLI' },
|
|
135
|
+
subCommands,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
await runMain(main);
|
|
139
|
+
}
|
package/src/theme.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
|
|
3
|
+
export interface ThemeColors {
|
|
4
|
+
brand: (text: string) => string;
|
|
5
|
+
success: (text: string) => string;
|
|
6
|
+
error: (text: string) => string;
|
|
7
|
+
warn: (text: string) => string;
|
|
8
|
+
muted: (text: string) => string;
|
|
9
|
+
bold: (text: string) => string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const defaultTheme: ThemeColors = {
|
|
13
|
+
brand: pc.green,
|
|
14
|
+
success: pc.green,
|
|
15
|
+
error: pc.red,
|
|
16
|
+
warn: pc.yellow,
|
|
17
|
+
muted: pc.dim,
|
|
18
|
+
bold: pc.bold,
|
|
19
|
+
};
|
package/src/ui.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fougere CLI UI — beautiful terminal interface.
|
|
3
|
+
*
|
|
4
|
+
* Wraps @clack/prompts + picocolors + consola into a cohesive API.
|
|
5
|
+
*
|
|
6
|
+
* This was `@fougere/cli-ui`, a published package with exactly one consumer —
|
|
7
|
+
* the CLI it is named after. A second name in the registry that nobody would
|
|
8
|
+
* ever install on purpose is a name, not a boundary. Same dependency profile,
|
|
9
|
+
* so it folds in as a module; a subpath export is one line the day something
|
|
10
|
+
* outside the CLI wants it.
|
|
11
|
+
*/
|
|
12
|
+
import * as clack from '@clack/prompts';
|
|
13
|
+
import pc from 'picocolors';
|
|
14
|
+
import { consola } from 'consola';
|
|
15
|
+
import { defaultTheme, type ThemeColors } from './theme.js';
|
|
16
|
+
|
|
17
|
+
export interface UiTheme {
|
|
18
|
+
colors?: Partial<ThemeColors>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function ui(options?: UiTheme) {
|
|
22
|
+
const c = { ...defaultTheme, ...options?.colors };
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
// ── Lifecycle ─────────────────────────────────
|
|
26
|
+
|
|
27
|
+
/** Start a new CLI session with a branded header. */
|
|
28
|
+
intro(title = 'Fougere') {
|
|
29
|
+
clack.intro(c.brand(title));
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
/** End the session with a message. */
|
|
33
|
+
outro(message: string) {
|
|
34
|
+
clack.outro(c.success(message));
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
/** Cancel and exit. */
|
|
38
|
+
cancel(message = 'Cancelled.') {
|
|
39
|
+
clack.cancel(c.muted(message));
|
|
40
|
+
process.exit(0);
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
// ── Prompts ───────────────────────────────────
|
|
44
|
+
|
|
45
|
+
/** Text input. */
|
|
46
|
+
async text(opts: { message: string; placeholder?: string; defaultValue?: string; validate?: (value: string) => string | undefined }) {
|
|
47
|
+
const result = await clack.text(opts);
|
|
48
|
+
if (clack.isCancel(result)) { this.cancel(); return ''; }
|
|
49
|
+
return result as string;
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
/** Yes/no confirmation. */
|
|
53
|
+
async confirm(opts: { message: string; initialValue?: boolean }) {
|
|
54
|
+
const result = await clack.confirm(opts);
|
|
55
|
+
if (clack.isCancel(result)) { this.cancel(); return false; }
|
|
56
|
+
return result as boolean;
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
/** Select one from a list. */
|
|
60
|
+
async select(opts: {
|
|
61
|
+
message: string;
|
|
62
|
+
options: { value: string; label?: string; hint?: string }[];
|
|
63
|
+
initialValue?: string;
|
|
64
|
+
}) {
|
|
65
|
+
const result = await clack.select(opts as Parameters<typeof clack.select>[0]);
|
|
66
|
+
if (clack.isCancel(result)) { this.cancel(); return ''; }
|
|
67
|
+
return result as string;
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
/** Multi-select from a list. */
|
|
71
|
+
async multiselect(opts: {
|
|
72
|
+
message: string;
|
|
73
|
+
options: { value: string; label?: string; hint?: string }[];
|
|
74
|
+
required?: boolean;
|
|
75
|
+
}) {
|
|
76
|
+
const result = await clack.multiselect(opts as Parameters<typeof clack.multiselect>[0]);
|
|
77
|
+
if (clack.isCancel(result)) { this.cancel(); return [] as string[]; }
|
|
78
|
+
return result as string[];
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
// ── Spinner ───────────────────────────────────
|
|
82
|
+
|
|
83
|
+
/** Start a spinner. Returns stop/update functions. */
|
|
84
|
+
spinner(message?: string) {
|
|
85
|
+
const s = clack.spinner();
|
|
86
|
+
s.start(message);
|
|
87
|
+
return {
|
|
88
|
+
update: (msg: string) => s.message(msg),
|
|
89
|
+
stop: (msg?: string) => s.stop(msg),
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
// ── Output ────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
/** Informational message. */
|
|
96
|
+
info(message: string) {
|
|
97
|
+
clack.log.info(message);
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
/** Success message. */
|
|
101
|
+
success(message: string) {
|
|
102
|
+
clack.log.success(c.success(message));
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
/** Warning message. */
|
|
106
|
+
warn(message: string) {
|
|
107
|
+
clack.log.warn(c.warn(message));
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
/** Error message. */
|
|
111
|
+
error(message: string) {
|
|
112
|
+
clack.log.error(c.error(message));
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
/** Step indicator. */
|
|
116
|
+
step(message: string) {
|
|
117
|
+
clack.log.step(message);
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
/** Note box — multiline content in a box. */
|
|
121
|
+
note(message: string, title?: string) {
|
|
122
|
+
clack.note(message, title);
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
// ── Raw colors/consola ────────────────────────
|
|
126
|
+
|
|
127
|
+
colors: c,
|
|
128
|
+
pc,
|
|
129
|
+
consola,
|
|
130
|
+
};
|
|
131
|
+
}
|