@mosaic/mosaic is now the single package providing both: - 'mosaic' binary (CLI: yolo, coord, prdy, tui, gateway, etc.) - 'mosaic-wizard' binary (installation wizard) Changes: - Move packages/cli/src/* into packages/mosaic/src/ - Convert dynamic @mosaic/mosaic imports to static relative imports - Add CLI deps (ink, react, socket.io-client, @mosaic/config) to mosaic - Add jsx: react-jsx to mosaic's tsconfig - Exclude packages/cli from workspace (pnpm-workspace.yaml) - Update install.sh to install @mosaic/mosaic instead of @mosaic/cli - Bump version to 0.0.17 This eliminates the circular dependency between @mosaic/cli and @mosaic/mosaic that was blocking the build graph.
59 lines
1.5 KiB
TypeScript
59 lines
1.5 KiB
TypeScript
/**
|
|
* Interactive item selection. Uses @clack/prompts when TTY, falls back to numbered list.
|
|
*/
|
|
export async function selectItem<T>(
|
|
items: T[],
|
|
opts: {
|
|
message: string;
|
|
render: (item: T) => string;
|
|
emptyMessage?: string;
|
|
},
|
|
): Promise<T | undefined> {
|
|
if (items.length === 0) {
|
|
console.log(opts.emptyMessage ?? 'No items found.');
|
|
return undefined;
|
|
}
|
|
|
|
const isTTY = process.stdin.isTTY;
|
|
|
|
if (isTTY) {
|
|
try {
|
|
const { select } = await import('@clack/prompts');
|
|
const result = await select({
|
|
message: opts.message,
|
|
options: items.map((item, i) => ({
|
|
value: i,
|
|
label: opts.render(item),
|
|
})),
|
|
});
|
|
|
|
if (typeof result === 'symbol') {
|
|
return undefined;
|
|
}
|
|
|
|
return items[result as number];
|
|
} catch {
|
|
// Fall through to non-interactive
|
|
}
|
|
}
|
|
|
|
// Non-interactive: display numbered list and read a number
|
|
console.log(`\n${opts.message}\n`);
|
|
for (let i = 0; i < items.length; i++) {
|
|
console.log(` ${i + 1}. ${opts.render(items[i]!)}`);
|
|
}
|
|
|
|
const readline = await import('node:readline');
|
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
const answer = await new Promise<string>((resolve) => rl.question('\nSelect: ', resolve));
|
|
rl.close();
|
|
|
|
const index = parseInt(answer, 10) - 1;
|
|
if (isNaN(index) || index < 0 || index >= items.length) {
|
|
console.error('Invalid selection.');
|
|
return undefined;
|
|
}
|
|
|
|
return items[index];
|
|
}
|