feat(mosaic): migrate install wizard from v0 to v1 (#103)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #103.
This commit is contained in:
152
packages/mosaic/src/prompter/clack-prompter.ts
Normal file
152
packages/mosaic/src/prompter/clack-prompter.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import * as p from '@clack/prompts';
|
||||
import { WizardCancelledError } from '../errors.js';
|
||||
import type {
|
||||
WizardPrompter,
|
||||
SelectOption,
|
||||
MultiSelectOption,
|
||||
ProgressHandle,
|
||||
} from './interface.js';
|
||||
|
||||
function guardCancel<T>(value: T | symbol): T {
|
||||
if (p.isCancel(value)) {
|
||||
throw new WizardCancelledError();
|
||||
}
|
||||
return value as T;
|
||||
}
|
||||
|
||||
export class ClackPrompter implements WizardPrompter {
|
||||
intro(message: string): void {
|
||||
p.intro(message);
|
||||
}
|
||||
|
||||
outro(message: string): void {
|
||||
p.outro(message);
|
||||
}
|
||||
|
||||
note(message: string, title?: string): void {
|
||||
p.note(message, title);
|
||||
}
|
||||
|
||||
log(message: string): void {
|
||||
p.log.info(message);
|
||||
}
|
||||
|
||||
warn(message: string): void {
|
||||
p.log.warn(message);
|
||||
}
|
||||
|
||||
async text(opts: {
|
||||
message: string;
|
||||
placeholder?: string;
|
||||
defaultValue?: string;
|
||||
validate?: (value: string) => string | void;
|
||||
}): Promise<string> {
|
||||
const validate = opts.validate
|
||||
? (v: string) => {
|
||||
const r = opts.validate!(v);
|
||||
return r === undefined ? undefined : r;
|
||||
}
|
||||
: undefined;
|
||||
const result = await p.text({
|
||||
message: opts.message,
|
||||
placeholder: opts.placeholder,
|
||||
defaultValue: opts.defaultValue,
|
||||
validate,
|
||||
});
|
||||
return guardCancel(result);
|
||||
}
|
||||
|
||||
async confirm(opts: { message: string; initialValue?: boolean }): Promise<boolean> {
|
||||
const result = await p.confirm({
|
||||
message: opts.message,
|
||||
initialValue: opts.initialValue,
|
||||
});
|
||||
return guardCancel(result);
|
||||
}
|
||||
|
||||
async select<T>(opts: {
|
||||
message: string;
|
||||
options: SelectOption<T>[];
|
||||
initialValue?: T;
|
||||
}): Promise<T> {
|
||||
const clackOptions = opts.options.map((o) => ({
|
||||
value: o.value as T,
|
||||
label: o.label,
|
||||
hint: o.hint,
|
||||
}));
|
||||
const result = await p.select({
|
||||
message: opts.message,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- clack Option conditional type needs concrete Primitive
|
||||
options: clackOptions as any,
|
||||
initialValue: opts.initialValue,
|
||||
});
|
||||
return guardCancel(result) as T;
|
||||
}
|
||||
|
||||
async multiselect<T>(opts: {
|
||||
message: string;
|
||||
options: MultiSelectOption<T>[];
|
||||
required?: boolean;
|
||||
}): Promise<T[]> {
|
||||
const clackOptions = opts.options.map((o) => ({
|
||||
value: o.value as T,
|
||||
label: o.label,
|
||||
hint: o.hint,
|
||||
}));
|
||||
const result = await p.multiselect({
|
||||
message: opts.message,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
options: clackOptions as any,
|
||||
required: opts.required,
|
||||
initialValues: opts.options.filter((o) => o.selected).map((o) => o.value),
|
||||
});
|
||||
return guardCancel(result) as T[];
|
||||
}
|
||||
|
||||
async groupMultiselect<T>(opts: {
|
||||
message: string;
|
||||
options: Record<string, MultiSelectOption<T>[]>;
|
||||
required?: boolean;
|
||||
}): Promise<T[]> {
|
||||
const grouped: Record<string, { value: T; label: string; hint?: string }[]> = {};
|
||||
for (const [group, items] of Object.entries(opts.options)) {
|
||||
grouped[group] = items.map((o) => ({
|
||||
value: o.value as T,
|
||||
label: o.label,
|
||||
hint: o.hint,
|
||||
}));
|
||||
}
|
||||
const result = await p.groupMultiselect({
|
||||
message: opts.message,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
options: grouped as any,
|
||||
required: opts.required,
|
||||
});
|
||||
return guardCancel(result) as T[];
|
||||
}
|
||||
|
||||
spinner(): ProgressHandle {
|
||||
const s = p.spinner();
|
||||
let started = false;
|
||||
return {
|
||||
update(message: string) {
|
||||
if (!started) {
|
||||
s.start(message);
|
||||
started = true;
|
||||
} else {
|
||||
s.message(message);
|
||||
}
|
||||
},
|
||||
stop(message?: string) {
|
||||
if (started) {
|
||||
s.stop(message);
|
||||
started = false;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
separator(): void {
|
||||
p.log.info('');
|
||||
}
|
||||
}
|
||||
131
packages/mosaic/src/prompter/headless-prompter.ts
Normal file
131
packages/mosaic/src/prompter/headless-prompter.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import type {
|
||||
WizardPrompter,
|
||||
SelectOption,
|
||||
MultiSelectOption,
|
||||
ProgressHandle,
|
||||
} from './interface.js';
|
||||
|
||||
export type AnswerValue = string | boolean | string[];
|
||||
|
||||
export class HeadlessPrompter implements WizardPrompter {
|
||||
private answers: Map<string, AnswerValue>;
|
||||
private logs: string[] = [];
|
||||
|
||||
constructor(answers: Record<string, AnswerValue> = {}) {
|
||||
this.answers = new Map(Object.entries(answers));
|
||||
}
|
||||
|
||||
intro(message: string): void {
|
||||
this.logs.push(`[intro] ${message}`);
|
||||
}
|
||||
outro(message: string): void {
|
||||
this.logs.push(`[outro] ${message}`);
|
||||
}
|
||||
note(message: string, title?: string): void {
|
||||
this.logs.push(`[note] ${title ?? ''}: ${message}`);
|
||||
}
|
||||
log(message: string): void {
|
||||
this.logs.push(`[log] ${message}`);
|
||||
}
|
||||
warn(message: string): void {
|
||||
this.logs.push(`[warn] ${message}`);
|
||||
}
|
||||
|
||||
async text(opts: {
|
||||
message: string;
|
||||
placeholder?: string;
|
||||
defaultValue?: string;
|
||||
validate?: (value: string) => string | void;
|
||||
}): Promise<string> {
|
||||
const answer = this.answers.get(opts.message);
|
||||
const value =
|
||||
typeof answer === 'string'
|
||||
? answer
|
||||
: opts.defaultValue !== undefined
|
||||
? opts.defaultValue
|
||||
: undefined;
|
||||
|
||||
if (value === undefined) {
|
||||
throw new Error(`HeadlessPrompter: no answer for "${opts.message}"`);
|
||||
}
|
||||
|
||||
if (opts.validate) {
|
||||
const error = opts.validate(value);
|
||||
if (error)
|
||||
throw new Error(`HeadlessPrompter validation failed for "${opts.message}": ${error}`);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
async confirm(opts: { message: string; initialValue?: boolean }): Promise<boolean> {
|
||||
const answer = this.answers.get(opts.message);
|
||||
if (typeof answer === 'boolean') return answer;
|
||||
return opts.initialValue ?? true;
|
||||
}
|
||||
|
||||
async select<T>(opts: {
|
||||
message: string;
|
||||
options: SelectOption<T>[];
|
||||
initialValue?: T;
|
||||
}): Promise<T> {
|
||||
const answer = this.answers.get(opts.message);
|
||||
if (answer !== undefined) {
|
||||
// Find matching option by value string comparison
|
||||
const match = opts.options.find((o) => String(o.value) === String(answer));
|
||||
if (match) return match.value;
|
||||
}
|
||||
if (opts.initialValue !== undefined) return opts.initialValue;
|
||||
if (opts.options.length === 0) {
|
||||
throw new Error(`HeadlessPrompter: no options for "${opts.message}"`);
|
||||
}
|
||||
const first = opts.options[0];
|
||||
if (first === undefined) {
|
||||
throw new Error(`HeadlessPrompter: no options for "${opts.message}"`);
|
||||
}
|
||||
return first.value;
|
||||
}
|
||||
|
||||
async multiselect<T>(opts: {
|
||||
message: string;
|
||||
options: MultiSelectOption<T>[];
|
||||
required?: boolean;
|
||||
}): Promise<T[]> {
|
||||
const answer = this.answers.get(opts.message);
|
||||
if (Array.isArray(answer)) {
|
||||
return opts.options
|
||||
.filter((o) => (answer as string[]).includes(String(o.value)))
|
||||
.map((o) => o.value);
|
||||
}
|
||||
return opts.options.filter((o) => o.selected).map((o) => o.value);
|
||||
}
|
||||
|
||||
async groupMultiselect<T>(opts: {
|
||||
message: string;
|
||||
options: Record<string, MultiSelectOption<T>[]>;
|
||||
required?: boolean;
|
||||
}): Promise<T[]> {
|
||||
const answer = this.answers.get(opts.message);
|
||||
if (Array.isArray(answer)) {
|
||||
const all = Object.values(opts.options).flat();
|
||||
return all.filter((o) => (answer as string[]).includes(String(o.value))).map((o) => o.value);
|
||||
}
|
||||
return Object.values(opts.options)
|
||||
.flat()
|
||||
.filter((o) => o.selected)
|
||||
.map((o) => o.value);
|
||||
}
|
||||
|
||||
spinner(): ProgressHandle {
|
||||
return {
|
||||
update(_message: string) {},
|
||||
stop(_message?: string) {},
|
||||
};
|
||||
}
|
||||
|
||||
separator(): void {}
|
||||
|
||||
getLogs(): string[] {
|
||||
return [...this.logs];
|
||||
}
|
||||
}
|
||||
49
packages/mosaic/src/prompter/interface.ts
Normal file
49
packages/mosaic/src/prompter/interface.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
export interface SelectOption<T = string> {
|
||||
value: T;
|
||||
label: string;
|
||||
hint?: string;
|
||||
}
|
||||
|
||||
export interface MultiSelectOption<T = string> extends SelectOption<T> {
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
export interface ProgressHandle {
|
||||
update(message: string): void;
|
||||
stop(message?: string): void;
|
||||
}
|
||||
|
||||
export interface WizardPrompter {
|
||||
intro(message: string): void;
|
||||
outro(message: string): void;
|
||||
note(message: string, title?: string): void;
|
||||
log(message: string): void;
|
||||
warn(message: string): void;
|
||||
|
||||
text(opts: {
|
||||
message: string;
|
||||
placeholder?: string;
|
||||
defaultValue?: string;
|
||||
validate?: (value: string) => string | void;
|
||||
}): Promise<string>;
|
||||
|
||||
confirm(opts: { message: string; initialValue?: boolean }): Promise<boolean>;
|
||||
|
||||
select<T>(opts: { message: string; options: SelectOption<T>[]; initialValue?: T }): Promise<T>;
|
||||
|
||||
multiselect<T>(opts: {
|
||||
message: string;
|
||||
options: MultiSelectOption<T>[];
|
||||
required?: boolean;
|
||||
}): Promise<T[]>;
|
||||
|
||||
groupMultiselect<T>(opts: {
|
||||
message: string;
|
||||
options: Record<string, MultiSelectOption<T>[]>;
|
||||
required?: boolean;
|
||||
}): Promise<T[]>;
|
||||
|
||||
spinner(): ProgressHandle;
|
||||
|
||||
separator(): void;
|
||||
}
|
||||
Reference in New Issue
Block a user