@tanstack/cli
Version:
TanStack CLI
284 lines (283 loc) • 9.14 kB
JavaScript
import { cancel, confirm, isCancel, multiselect, note, password, select, text, } from '@clack/prompts';
import { DEFAULT_PACKAGE_MANAGER, SUPPORTED_PACKAGE_MANAGERS, getAllAddOns, } from '@tanstack/create';
import { validateProjectName } from './utils.js';
export async function getProjectName() {
const value = await text({
message: 'What would you like to name your project?',
defaultValue: 'my-app',
validate(value) {
if (!value) {
return 'Please enter a name';
}
const { valid, error } = validateProjectName(value);
if (!valid) {
return error;
}
},
});
if (isCancel(value)) {
cancel('Operation cancelled.');
process.exit(0);
}
return value;
}
export async function selectPackageManager() {
const packageManager = await select({
message: 'Select package manager:',
options: SUPPORTED_PACKAGE_MANAGERS.map((pm) => ({
value: pm,
label: pm,
})),
initialValue: DEFAULT_PACKAGE_MANAGER,
});
if (isCancel(packageManager)) {
cancel('Operation cancelled.');
process.exit(0);
}
return packageManager;
}
export async function selectTemplate(templates) {
if (templates.length === 0) {
return undefined;
}
const selected = await select({
message: 'Would you like to start from a template?',
options: [
{
value: undefined,
label: 'None (base starter)',
hint: 'Two-page baseline (Home + About)',
},
...templates.map((template) => ({
value: template.id,
label: template.name,
hint: template.description,
})),
],
initialValue: undefined,
});
if (isCancel(selected)) {
cancel('Operation cancelled.');
process.exit(0);
}
return selected;
}
// Track if we've shown the multiselect help text
let hasShownMultiselectHelp = false;
export async function selectAddOns(framework, mode, type, message, forcedAddOns = [], allowMultiple = true) {
const allAddOns = await getAllAddOns(framework, mode);
const addOns = allAddOns.filter((addOn) => addOn.type === type);
if (addOns.length === 0) {
return [];
}
// Show help text only once
if (!hasShownMultiselectHelp) {
note('Use ↑/↓ to navigate • Space to select/deselect • Enter to confirm', 'Keyboard Shortcuts');
hasShownMultiselectHelp = true;
}
if (allowMultiple) {
const selectableAddOns = addOns.filter((addOn) => !forcedAddOns.includes(addOn.id));
if (selectableAddOns.length === 0) {
return [];
}
const value = await multiselect({
message,
options: selectableAddOns.map((addOn) => ({
value: addOn.id,
label: addOn.name,
hint: addOn.description,
})),
maxItems: selectableAddOns.length,
required: false,
});
if (isCancel(value)) {
cancel('Operation cancelled.');
process.exit(0);
}
return value;
}
else {
const value = await select({
message,
options: [
{
value: 'none',
label: 'None',
},
...addOns
.filter((addOn) => !forcedAddOns.includes(addOn.id))
.map((addOn) => ({
value: addOn.id,
label: addOn.name,
hint: addOn.description,
})),
],
initialValue: 'none',
});
if (isCancel(value)) {
cancel('Operation cancelled.');
process.exit(0);
}
return value === 'none' ? [] : [value];
}
}
export async function selectGit() {
const git = await confirm({
message: 'Would you like to initialize a new git repository?',
initialValue: true,
});
if (isCancel(git)) {
cancel('Operation cancelled.');
process.exit(0);
}
return git;
}
export async function selectExamples() {
const includeExamples = await confirm({
message: 'Would you like to include demo/example pages?',
initialValue: true,
});
if (isCancel(includeExamples)) {
cancel('Operation cancelled.');
process.exit(0);
}
return includeExamples;
}
export async function selectToolchain(framework, toolchain) {
if (toolchain === false) {
return undefined;
}
const toolchains = new Set();
for (const addOn of framework.getAddOns()) {
if (addOn.type === 'toolchain') {
toolchains.add(addOn);
if (toolchain && addOn.id === toolchain) {
return toolchain;
}
}
}
const tc = await select({
message: 'Select toolchain',
options: [
{
value: undefined,
label: 'None',
},
...Array.from(toolchains).map((tc) => ({
value: tc.id,
label: tc.name,
})),
],
initialValue: undefined,
});
if (isCancel(tc)) {
cancel('Operation cancelled.');
process.exit(0);
}
return tc;
}
export async function promptForAddOnOptions(addOnIds, framework) {
const addOnOptions = {};
for (const addOnId of addOnIds) {
const addOn = framework.getAddOns().find((a) => a.id === addOnId);
if (!addOn || !addOn.options)
continue;
addOnOptions[addOnId] = {};
for (const [optionName, option] of Object.entries(addOn.options)) {
if (option && typeof option === 'object' && 'type' in option) {
if (option.type === 'select') {
const selectOption = option;
const value = await select({
message: `${addOn.name}: ${selectOption.label}`,
options: selectOption.options.map((opt) => ({
value: opt.value,
label: opt.label,
})),
initialValue: selectOption.default,
});
if (isCancel(value)) {
cancel('Operation cancelled.');
process.exit(0);
}
addOnOptions[addOnId][optionName] = value;
}
// Future option types can be added here
}
}
}
return addOnOptions;
}
export async function promptForEnvVars(addOns) {
const envVars = new Map();
for (const addOn of addOns) {
for (const envVar of addOn.envVars || []) {
if (!envVars.has(envVar.name)) {
envVars.set(envVar.name, envVar);
}
}
}
const result = {};
for (const envVar of envVars.values()) {
const label = envVar.description
? `${envVar.name} (${envVar.description})`
: envVar.name;
const value = envVar.secret
? await password({
message: `Enter ${label}`,
validate: envVar.required
? (v) => v && v.trim().length > 0
? undefined
: `${envVar.name} is required`
: undefined,
})
: await text({
message: `Enter ${label}`,
defaultValue: envVar.default,
validate: envVar.required
? (v) => v && v.trim().length > 0
? undefined
: `${envVar.name} is required`
: undefined,
});
if (isCancel(value)) {
cancel('Operation cancelled.');
process.exit(0);
}
if (value && value.trim()) {
result[envVar.name] = value.trim();
}
}
return result;
}
export async function selectDeployment(framework, deployment) {
const deployments = new Set();
let initialValue = undefined;
for (const addOn of framework
.getAddOns()
.sort((a, b) => a.name.localeCompare(b.name))) {
if (addOn.type === 'deployment') {
deployments.add(addOn);
if (deployment && addOn.id === deployment) {
return deployment;
}
if (addOn.default) {
initialValue = addOn.id;
}
}
}
const dp = await select({
message: 'Select deployment adapter',
options: [
...Array.from(deployments).map((d) => ({
value: d.id,
label: d.name,
})),
],
initialValue: initialValue,
});
if (isCancel(dp)) {
cancel('Operation cancelled.');
process.exit(0);
}
return dp;
}