Skip to content

Commit

Permalink
Merge pull request #1500 from flexn-io/feat/interactive-wizard-improv…
Browse files Browse the repository at this point in the history
…ements

add autocomplete to interactive wizard prompt
  • Loading branch information
pavjacko authored Apr 10, 2024
2 parents fb9f073 + b218121 commit 5589cc2
Show file tree
Hide file tree
Showing 7 changed files with 172 additions and 123 deletions.
6 changes: 5 additions & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,17 @@
"@rnv/sdk-telemetry": "1.0.0-rc.13",
"chalk": "4.1.0",
"commander": "12.0.0",
"inquirer": "8.2.0"
"inquirer": "8.2.0",
"inquirer-autocomplete-prompt": "2.0.1"
},
"peerDependencies": {
"@rnv/core": "^1.0.0-rc.13"
},
"private": false,
"publishConfig": {
"access": "public"
},
"devDependencies": {
"@types/inquirer-autocomplete-prompt": "^3.0.3"
}
}
4 changes: 4 additions & 0 deletions packages/cli/src/prompt.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import inquirer from 'inquirer';
import inquirerAutocompletePrompt from 'inquirer-autocomplete-prompt';
import {
chalk,
logWarning,
Expand All @@ -10,6 +11,8 @@ import {
getContext,
} from '@rnv/core';

inquirer.registerPrompt('autocomplete', inquirerAutocompletePrompt);

export const inquirerPrompt = async (params: PromptParams): Promise<Record<string, any>> => {
const c = getContext();

Expand Down Expand Up @@ -48,6 +51,7 @@ export const inquirerPrompt = async (params: PromptParams): Promise<Record<strin
if (type === 'confirm' && !name) params.name = 'confirm';

const resp = inquirer.prompt(params);
if (params.initialValue) resp.ui.rl.input.push(params.initialValue);
return resp;
};

Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,11 @@ export type PromptParams = {
type: string;
message?: string;
choices?: Array<{ name: string; value: any } | string>;
source?: (answersSoFar: any, input: string | undefined) => Promise<any>;
validate?: (i: string) => string | boolean;
logMessage?: string;
warningMessage?: string;
initialValue?: string;
default?: any; // string | boolean | (() => string) | string[] | number | { name: string; value: any };
pageSize?: number;
loop?: boolean;
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { updateRenativeConfigs } from './plugins';
import { loadDefaultConfigTemplates } from './configs';
import { getApi } from './api/provider';
import { RnvTask } from './tasks/types';
import { runInteractiveWizard, runInteractiveWizardForSubTasks } from './tasks/wizard';
import { runInteractiveWizard } from './tasks/wizard';
import { initializeTask } from './tasks/taskExecutors';
import { getTaskNameFromCommand, selectPlatformIfRequired } from './tasks/taskHelpers';
import { logInfo } from './logger';
Expand Down Expand Up @@ -87,5 +87,5 @@ export const executeRnvCore = async () => {

// Still no task found. time to check sub tasks options via wizard
logInfo(`Did not find exact match for ${getTaskNameFromCommand()}. Running interactive wizard for sub-tasks`);
return runInteractiveWizardForSubTasks();
return runInteractiveWizard();
};
4 changes: 3 additions & 1 deletion packages/core/src/tasks/taskHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ export const selectPlatformIfRequired = async (
c.runtime.availablePlatforms = c.buildConfig.defaults?.supportedPlatforms || [];
if (typeof c.platform !== 'string') {
const taskName = getTaskNameFromCommand();
const platforms = knownTaskInstance?.platforms || c.runtime.availablePlatforms;
const platforms =
knownTaskInstance?.platforms?.filter((p) => c.runtime.availablePlatforms.includes(p)) ||
c.runtime.availablePlatforms;
if (platforms) {
if (platforms.length === 1) {
logInfo(
Expand Down
236 changes: 120 additions & 116 deletions packages/core/src/tasks/wizard.ts
Original file line number Diff line number Diff line change
@@ -1,135 +1,139 @@
import { findSuitableTask } from './taskFinder';
import { inquirerPrompt } from '../api';
import { getContext } from '../context/provider';
import { chalk, logInfo } from '../logger';
import { getEngineRunnerByPlatform } from '../engines';
import { chalk } from '../logger';
import { RnvPlatform } from '../types';
import { initializeTask } from './taskExecutors';
import { getRegisteredTasks } from './taskRegistry';
import { initializeTask } from './taskExecutors.js';
import { getTaskNameFromCommand } from './taskHelpers';
import { RnvTask } from './types';

type TaskOpt = {
name: string;
value: string;
};

const generateOptionPrompt = async (options: TaskOpt[]) => {
const ctx = getContext();

// const { selectedTask } = await inquirerPrompt({
// type: 'list',
// // default: defaultCmd,
// name: 'selectedTask',
// message: `Pick a command`,
// choices: options,
// pageSize: 15,
// logMessage: 'Welcome to the brave new world...',
// });
// const taskArr = selectedTask.split(' ');
// ctx.command = taskArr[0];
// ctx.subCommand = taskArr[1] || null;
const isTaskSupportedOnPlatform = (task: RnvTask, platform: RnvPlatform) => {
if (!task.platforms) return true;

// const initTask = await findSuitableTask();
// return initializeTask(initTask);

const { selectedTask } = await inquirerPrompt({
type: 'list',
// default: defaultCmd,
name: 'selectedTask',
message: `Pick a command`,
choices: options,
pageSize: 15,
logMessage: 'Welcome to the brave new world...',
});

const taskArr = selectedTask.split(' ');
ctx.command = taskArr[0];
ctx.subCommand = taskArr[1] || null;
// TODO
// Filtering only by platform leaves more tasks than nescessary
// But also filtering by `task.ownerID !== selectedEngineID` filters out _all_ integration tasks
// `isEngine` hackily prevents that
const isEngine = task.ownerID !== '@rnv/engine-core' && task.ownerID?.includes('/engine');

const initTask = await findSuitableTask();
return initializeTask(initTask);
const selectedEngineID = getEngineRunnerByPlatform(platform)?.config.packageName;
if (isEngine && selectedEngineID && task.ownerID && task.ownerID !== selectedEngineID) {
// If we already specified platform we can skip tasks registered to unsupported engines
return false;
}
if (platform && !task.platforms.includes(platform)) {
// We can also filter out tasks that are not supported on current platform
return false;
}
return true;
};

export const runInteractiveWizardForSubTasks = async () => {
const groupingWizard = async (tasks: RnvTask[]) => {
const ctx = getContext();

const tasks = getRegisteredTasks();
const optionsMap: Record<string, TaskOpt> = {};
const alternativeOptionsMap: Record<string, TaskOpt> = {};

const selectedEngineID = ctx.platform && ctx.runtime.enginesByPlatform[ctx.platform]?.config?.packageName;

Object.values(tasks).forEach((taskInstance) => {
if (ctx.platform) {
if (selectedEngineID && taskInstance.ownerID !== selectedEngineID) {
// If we already specified platform we can skip tasks registered to unsupported engines
return;
}
if (taskInstance.platforms && !taskInstance.platforms.includes(ctx.platform)) {
// We can also filter out tasks that are not supported on current platform
return;
}
}

const taskCmdName = taskInstance.task.split(' ')[0];

if (taskCmdName === ctx.command) {
if (!optionsMap[taskInstance.task]) {
optionsMap[taskInstance.task] = {
name: `${taskInstance.task} ${chalk().gray(taskInstance.description)}`,
value: taskInstance.task,
};
} else {
// If multiple tasks with same name we append ... to indicate there are more options coming
optionsMap[taskInstance.task].name = `${taskInstance.task}${chalk().gray('...')}`;
}
const initialValue = ctx.program.args?.join(' ');
const filteredTasks = tasks.filter((task) => isTaskSupportedOnPlatform(task, ctx.platform));
const optionsMap: Record<string, { name: string; value: RnvTask[] }> = {};
filteredTasks.forEach((taskInstance) => {
const prefix = taskInstance.task.split(' ')[0];
const sharesPrefix = filteredTasks.filter((t) => t.task.split(' ')[0] === prefix).length > 1;
if (sharesPrefix) {
optionsMap[prefix] = {
name: `${prefix}${chalk().gray('...')}`,
value: [...(optionsMap[prefix]?.value ?? []), taskInstance],
};
} else {
if (!alternativeOptionsMap[taskInstance.task]) {
alternativeOptionsMap[taskInstance.task] = {
name: `${taskInstance.task} ${chalk().gray(taskInstance.description)}`,
value: taskInstance.task,
};
} else {
// If multiple tasks with same name we append ... to indicate there are more options coming
alternativeOptionsMap[taskInstance.task].name = `${taskInstance.task}${chalk().gray('...')}`;
}
optionsMap[taskInstance.task] = {
name: `${taskInstance.task} ${chalk().gray(taskInstance.description)}`,
value: [taskInstance],
};
}
});
const options = Object.values(optionsMap).sort((a, b) =>
a.value[0].isPriorityOrder ? -1 : b.value.length - a.value.length
);
const initialValueMatch = Object.entries(optionsMap).filter(([k]) => k === initialValue)?.[0]?.[1]?.value;
const selected =
initialValueMatch ??
((
await inquirerPrompt({
type: 'autocomplete',
source: async (_, input) =>
options.filter((o) => o.name.toLowerCase().includes(input?.toLowerCase() ?? '')),
initialValue,
name: 'selected',
message: `Pick a command`,
loop: false,
choices: options,
pageSize: 15,
})
).selected as RnvTask[]);
return selected.length === 1 ? selected[0] : await disambiguatingWizard(selected);
};

const options: TaskOpt[] = Object.values(optionsMap);

if (options.length > 0) {
if (ctx.subCommand) {
logInfo(`No sub task named "${chalk().red(ctx.subCommand)}" found. Will look for available ones instead.`);
ctx.subCommand = null;
const disambiguatingWizard = async (tasks: RnvTask[]) => {
const ctx = getContext();
if (!ctx.platform) {
const uniquePlatforms = Array.from(
new Set(
tasks
.flatMap((t) => t.platforms ?? [])
.filter((p) => ctx.buildConfig.defaults?.supportedPlatforms?.includes(p))
)
);
const isPlatformDisambiguating = uniquePlatforms.some((platform) =>
tasks.some((task) => !isTaskSupportedOnPlatform(task, platform))
);
if (isPlatformDisambiguating) {
ctx.platform = (
await inquirerPrompt({
type: 'autocomplete',
source: async (_, input) =>
uniquePlatforms.filter((p) => p.toLowerCase().includes(input?.toLowerCase() ?? '')),
name: 'selected',
message: `Pick a platform`,
loop: false,
choices: uniquePlatforms,
pageSize: 15,
})
).selected;
// TODO reuse with selectPlatformIfRequired ?
// await registerPlatformEngine(c.platform);
// c.runtime.engine = getEngineRunnerByPlatform(c.platform);
// c.runtime.runtimeExtraProps = c.runtime.engine?.runtimeExtraProps || {};
}
return generateOptionPrompt(options);
}

const alternativeOptions: TaskOpt[] = Object.values(alternativeOptionsMap);
// No subtasks found but we found closest matches
if (alternativeOptions.length > 0) {
return generateOptionPrompt(alternativeOptions);
}

if (ctx.subCommand) {
logInfo(`No tasks found for "${chalk().red(getTaskNameFromCommand())}". Launching wizard...`);
ctx.subCommand = null;
}

// If nothing could be found we resort to default wizard
return runInteractiveWizard();
const filteredTasks = tasks.filter((task) => isTaskSupportedOnPlatform(task, ctx.platform));
const options = filteredTasks
.map((taskInstance) => {
const isAmbiguous = filteredTasks.filter((t) => t.task === taskInstance.task).length > 1;
return {
name: `${taskInstance.task} ${
isAmbiguous ? chalk().gray(`(${taskInstance.ownerID}) `) : ''
}${chalk().gray(taskInstance.description)}`,
value: taskInstance,
};
})
.sort((a, b) => a.name.localeCompare(b.name));
if (options.length === 1) return options[0].value;
return (
await inquirerPrompt({
type: 'autocomplete',
source: async (_, input) =>
options.filter((o) => o.name.toLowerCase().includes(input?.toLowerCase() ?? '')),
name: 'selected',
message: `Pick a command`,
loop: false,
choices: options,
pageSize: 15,
})
).selected as RnvTask;
};

export const runInteractiveWizard = async () => {
const tasks = getRegisteredTasks();

const options: TaskOpt[] = [];

Object.values(tasks).forEach((taskInstance) => {
options.push({
name: `${taskInstance.task} ${chalk().gray(taskInstance.description)}`,
value: taskInstance.task,
});
});

return generateOptionPrompt(options);
const ctx = getContext();
const selected = await groupingWizard(Object.values(getRegisteredTasks()));
const taskArr = selected.task.split(' ');
ctx.command = taskArr[0];
ctx.subCommand = taskArr[1] || null;
return initializeTask(selected);
};
Loading

0 comments on commit 5589cc2

Please sign in to comment.