refactor(tools) : migrate inquirer prompts (#66139)

Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
Co-authored-by: Sem Bauke <sem@freecodecamp.org>
This commit is contained in:
Jeevankumar S
2026-03-05 17:34:01 +05:30
committed by GitHub
parent 80cace6f06
commit e72a5dc1bb
15 changed files with 1029 additions and 881 deletions
+495 -375
View File
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,6 @@
import fs from 'fs/promises';
import path from 'path';
import { prompt } from 'inquirer';
import { select, input, number } from '@inquirer/prompts';
import { format } from 'prettier';
import { ObjectId } from 'bson';
@@ -335,231 +335,276 @@ function getBlockPrefix(
}
void getAllBlocks()
.then(existingBlocks =>
prompt([
{
name: 'superBlock',
message: 'Which certification does this belong to?',
default: SuperBlocks.A2English,
type: 'list',
choices: Object.values(languageSuperBlocks)
},
{
name: 'blockLabel',
message: 'Choose a block label',
default: BlockLabel.learn,
type: 'list',
choices: Object.values(BlockLabel)
},
{
name: 'block',
message: (answers: CreateBlockArgs) => {
const prefix = getBlockPrefix(answers.superBlock, answers.blockLabel);
return prefix
? `Complete the dashed name after the prefix below.\nPrefix: ${prefix}`
: 'What is the dashed name (in kebab-case) for this block?';
},
validate: (block: string, answers: CreateBlockArgs) => {
const prefix = getBlockPrefix(answers.superBlock, answers.blockLabel);
.then(async existingBlocks => {
const superBlock = await select<SuperBlocks>({
message: 'Which certification does it this belong to?',
default: SuperBlocks.A2English,
choices: Object.values(languageSuperBlocks).map(value => ({
name: value,
value
}))
});
if (prefix) {
const uniquePart = block.slice(prefix.length);
const blockLabel = await select<BlockLabel>({
message: 'Choose a block label',
default: BlockLabel.learn,
choices: Object.values(BlockLabel).map(value => ({
name: value,
value
}))
});
// Check if user accidentally included block label at the end
if (answers.blockLabel) {
// Exclude exam as it is an exception
const blockLabelValues = Object.values(BlockLabel).filter(
label => label !== BlockLabel.exam
);
const prefix = getBlockPrefix(superBlock, blockLabel);
const endsWithLabel = blockLabelValues.some(label =>
uniquePart.endsWith(`-${label}`)
);
const rawBlock = await input({
message: prefix
? `Complete the dashed name after the prefix below.\nPrefix: ${prefix}`
: 'What is the dashed name (in kebab-case) for this block?',
if (endsWithLabel) {
return `Block name should not end with a block label (e.g., '-${answers.blockLabel}'). The label is already in the prefix.`;
}
}
validate: (value: string) => {
if (prefix) {
const uniquePart = value.slice(prefix.length);
const blockLabelValues = Object.values(BlockLabel).filter(
label => label !== BlockLabel.exam
);
const endsWithLabel = blockLabelValues.some(label =>
uniquePart.endsWith(`-${label}`)
);
if (endsWithLabel) {
return `Block name should not end with a block label (e.g., '-${blockLabel}'). The label is already in the prefix.`;
}
return validateBlockName(block, existingBlocks);
},
filter: (block: string, answers: CreateBlockArgs) => {
const prefix = getBlockPrefix(answers.superBlock, answers.blockLabel);
const normalized = block.toLowerCase().trim();
if (prefix) {
// Strip prefix if already present (happens on re-validation), then re-add it
const withoutPrefix = normalized.startsWith(prefix)
? normalized.slice(prefix.length)
: normalized;
return prefix + withoutPrefix;
}
return normalized;
}
},
{
name: 'title',
default: ({ block }: { block: string }) => block
},
{
name: 'helpCategory',
message: 'Choose a help category',
default: 'English',
type: 'list',
choices: helpCategories
},
{
name: 'blockLayout',
return validateBlockName(value, existingBlocks);
}
});
const block = prefix
? `${prefix}${rawBlock.toLowerCase().trim()}`
: rawBlock.toLowerCase().trim();
const title = await input({
message: 'Enter a title for this block:',
default: block
});
const helpCategory = await select<string>({
message: 'Choose a help category',
default: 'English',
choices: helpCategories.map(value => ({
name: value,
value
}))
});
let blockLayout: string | undefined;
if (
chapterBasedSuperBlocks.includes(superBlock) &&
blockLabel !== BlockLabel.quiz
) {
blockLayout = await select<BlockLayouts>({
message: 'Choose a block layout',
default: BlockLayouts.DialogueGrid,
type: 'list',
choices: Object.values(BlockLayouts),
when: (answers: CreateBlockArgs) =>
chapterBasedSuperBlocks.includes(answers.superBlock) &&
answers.blockLabel !== BlockLabel.quiz
},
{
name: 'questionCount',
choices: Object.values(BlockLayouts).map(value => ({
name: value,
value
}))
});
}
let questionCount: number | undefined;
if (blockLabel === BlockLabel.quiz) {
questionCount = await select<number>({
message: 'Choose a question count',
default: 20,
type: 'list',
choices: [10, 20],
when: (answers: CreateBlockArgs) =>
answers.blockLabel === BlockLabel.quiz
},
{
name: 'chapter',
choices: [
{ value: 10, name: '10' },
{ value: 20, name: '20' }
]
});
}
let chapter: string | undefined;
if (chapterBasedSuperBlocks.includes(superBlock)) {
const superblockFilename = (
superBlockToFilename as Record<SuperBlocks, string>
)[superBlock];
const structure = getSuperblockStructure(superblockFilename) as {
chapters: {
dashedName: string;
modules: { dashedName: string; blocks: string[] }[];
}[];
};
chapter = await select({
message: 'What chapter should this language block go in?',
type: 'list',
choices: (answers: CreateBlockArgs) => {
const superblockFilename = (
superBlockToFilename as Record<SuperBlocks, string>
)[answers.superBlock];
const structure = getSuperblockStructure(superblockFilename) as {
chapters: {
dashedName: string;
modules: { dashedName: string; blocks: string[] }[];
}[];
};
return [
...structure.chapters.map(chapter => chapter.dashedName),
'-- Create new chapter --'
];
},
when: (answers: CreateBlockArgs) =>
chapterBasedSuperBlocks.includes(answers.superBlock)
},
{
name: 'newChapterName',
choices: [
...structure.chapters.map(ch => ({
value: ch.dashedName,
name: ch.dashedName
})),
{
value: '-- Create new chapter --',
name: '-- Create new chapter --'
}
]
});
}
let newChapterName: string | undefined;
if (
chapterBasedSuperBlocks.includes(superBlock) &&
chapter === '-- Create new chapter --'
) {
const rawName = await input({
message: 'Enter the dashed name for the new chapter (in kebab-case):',
validate: (name: string) => {
if (!name || name.trim() === '') {
return 'Chapter name cannot be empty.';
}
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name.trim())) {
return 'Chapter name must be in kebab-case (e.g., "chapter-one").';
}
return true;
},
filter: (name: string) => name.toLowerCase().trim(),
when: (answers: CreateBlockArgs) =>
chapterBasedSuperBlocks.includes(answers.superBlock) &&
answers.chapter === '-- Create new chapter --'
},
{
name: 'newChapterTitle',
}
});
newChapterName = rawName.toLowerCase().trim();
}
let newChapterTitle: string | undefined;
if (
chapterBasedSuperBlocks.includes(superBlock) &&
chapter === '-- Create new chapter --'
) {
newChapterTitle = await input({
message: 'Enter the title for the new chapter:',
default: ({ newChapterName }: { newChapterName: string }) =>
newChapterName,
default: newChapterName,
validate: (title: string) => {
if (!title || title.trim() === '') {
return 'Chapter title cannot be empty.';
}
return true;
},
when: (answers: CreateBlockArgs) =>
chapterBasedSuperBlocks.includes(answers.superBlock) &&
answers.chapter === '-- Create new chapter --'
},
{
name: 'module',
message: 'What module should this language block go in?',
type: 'list',
choices: (answers: CreateBlockArgs) => {
if (answers.chapter === '-- Create new chapter --') {
return ['-- Create new module --'];
}
});
}
let module: string | undefined;
if (chapterBasedSuperBlocks.includes(superBlock)) {
const superblockFilename = (
superBlockToFilename as Record<SuperBlocks, string>
)[superBlock];
const structure = getSuperblockStructure(superblockFilename) as {
chapters: {
dashedName: string;
modules: { dashedName: string; blocks: string[] }[];
}[];
};
let moduleChoices: { value: string; name: string }[];
if (chapter === '-- Create new chapter --') {
moduleChoices = [
{
value: '-- Create new module --',
name: '-- Create new module --'
}
const superblockFilename = (
superBlockToFilename as Record<SuperBlocks, string>
)[answers.superBlock];
const structure = getSuperblockStructure(superblockFilename) as {
chapters: {
dashedName: string;
modules: { dashedName: string; blocks: string[] }[];
}[];
};
const existingModules =
structure.chapters
.find(chapter => chapter.dashedName === answers.chapter)
?.modules.map(module => module.dashedName) ?? [];
return [...existingModules, '-- Create new module --'];
},
when: (answers: CreateBlockArgs) =>
chapterBasedSuperBlocks.includes(answers.superBlock)
},
{
name: 'newModuleName',
];
} else {
const existingModules =
structure.chapters
.find(ch => ch.dashedName === chapter)
?.modules.map(m => m.dashedName) ?? [];
moduleChoices = [
...existingModules.map(m => ({
value: m,
name: m
})),
{
value: '-- Create new module --',
name: '-- Create new module --'
}
];
}
module = await select({
message: 'What module should this language block go in?',
choices: moduleChoices
});
}
let newModuleName: string | undefined;
if (
chapterBasedSuperBlocks.includes(superBlock) &&
module === '-- Create new module --'
) {
const rawName = await input({
message: 'Enter the dashed name for the new module (in kebab-case):',
validate: (name: string) => {
if (!name || name.trim() === '') {
return 'Module name cannot be empty.';
}
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name.trim())) {
return 'Module name must be in kebab-case (e.g., "module-one").';
}
return true;
},
filter: (name: string) => name.toLowerCase().trim(),
when: (answers: CreateBlockArgs) =>
chapterBasedSuperBlocks.includes(answers.superBlock) &&
answers.module === '-- Create new module --'
},
{
name: 'newModuleTitle',
}
});
newModuleName = rawName.toLowerCase().trim();
}
let newModuleTitle: string | undefined;
if (
chapterBasedSuperBlocks.includes(superBlock) &&
module === '-- Create new module --'
) {
newModuleTitle = await input({
message: 'Enter the title for the new module:',
default: ({ newModuleName }: { newModuleName: string }) =>
newModuleName,
default: newModuleName,
validate: (title: string) => {
if (!title || title.trim() === '') {
return 'Module title cannot be empty.';
}
return true;
},
when: (answers: CreateBlockArgs) =>
chapterBasedSuperBlocks.includes(answers.superBlock) &&
answers.module === '-- Create new module --'
},
{
name: 'position',
}
});
}
let position: number | undefined;
if (chapterBasedSuperBlocks.includes(superBlock)) {
position = await number({
message: 'At which position does this new block appear in the module?',
default: 1,
validate: (position: string) => {
return parseInt(position, 10) > 0
? true
: 'Position must be an number greater than zero.';
},
when: (answers: CreateBlockArgs) =>
chapterBasedSuperBlocks.includes(answers.superBlock),
filter: (position: string) => {
return parseInt(position, 10);
validate: (value: number | undefined) => {
if (!value || value <= 0) {
return 'Position must be a number greater than zero.';
}
return true;
}
}
])
)
.then(
async ({
});
}
return {
superBlock,
block,
helpCategory,
@@ -574,33 +619,57 @@ void getAllBlocks()
blockLabel,
blockLayout,
questionCount
}: CreateBlockArgs) => {
const resolvedChapter =
chapter === '-- Create new chapter --' ? newChapterName : chapter;
const resolvedModule =
module === '-- Create new module --' ? newModuleName : module;
};
})
.then(async (answers: CreateBlockArgs) => {
const {
superBlock,
block,
helpCategory,
title,
chapter,
module,
newChapterName,
newModuleTitle,
newChapterTitle,
newModuleName,
position,
blockLabel,
blockLayout,
questionCount
} = answers;
// Only pass chapter title if we're creating a new chapter
const chapterTitle =
chapter === '-- Create new chapter --' ? newChapterTitle : undefined;
// Only pass module title if we're creating a new module
const moduleTitle =
module === '-- Create new module --' ? newModuleTitle : undefined;
const resolvedChapter =
chapter === '-- Create new chapter --' ? newChapterName : chapter;
const resolvedModule =
module === '-- Create new module --' ? newModuleName : module;
await createLanguageBlock(
superBlock,
block,
helpCategory,
title,
resolvedChapter,
resolvedModule,
chapterTitle,
moduleTitle,
position,
blockLabel,
blockLayout,
questionCount
);
}
)
.then(() => console.log('All set. Refresh the page to see the changes.'));
// Only pass chapter title if we're creating a new chapter
const chapterTitle =
chapter === '-- Create new chapter --' ? newChapterTitle : undefined;
// Only pass module title if we're creating a new module
const moduleTitle =
module === '-- Create new module --' ? newModuleTitle : undefined;
await createLanguageBlock(
superBlock,
block,
helpCategory,
title,
resolvedChapter,
resolvedModule,
chapterTitle,
moduleTitle,
position,
blockLabel,
blockLayout,
questionCount
);
})
.then(() => console.log('All set. Refresh the page to see the changes.'))
.catch((err: unknown) =>
console.error(
'Error creating language block:',
err instanceof Error ? err.message : String(err)
)
);
+120 -133
View File
@@ -1,6 +1,6 @@
import fs from 'fs/promises';
import path from 'path';
import { prompt } from 'inquirer';
import { select, input, number } from '@inquirer/prompts';
import { format } from 'prettier';
import { ObjectId } from 'bson';
@@ -267,149 +267,136 @@ async function getModules(superBlock: string, chapterName: string) {
}
void getAllBlocks()
.then(existingBlocks =>
prompt([
{
name: 'superBlock',
message: 'Which certification does this belong to?',
default: SuperBlocks.RespWebDesignV9,
type: 'list',
choices: Object.values(SuperBlocks)
},
{
name: 'block',
message: 'What is the dashed name (in kebab-case) for this project?',
validate: (block: string) => validateBlockName(block, existingBlocks),
filter: (block: string) => {
return block.toLowerCase().trim();
}
},
{
name: 'title',
default: ({ block }: { block: string }) => block
},
{
name: 'helpCategory',
message: 'Choose a help category',
default: 'HTML-CSS',
type: 'list',
choices: helpCategories
},
{
name: 'blockLabel',
.then(async existingBlocks => {
const superBlock = await select<SuperBlocks>({
message: 'Which certification does this belong to?',
default: SuperBlocks.RespWebDesignV9,
choices: Object.values(SuperBlocks).map(value => ({
name: value,
value
}))
});
const rawBlock = await input({
message: 'What is the dashed name (in kebab-case) for this project?',
validate: (value: string) => validateBlockName(value, existingBlocks)
});
const block = rawBlock.toLowerCase().trim();
const title = await input({
message: 'Enter a title for this project:',
default: block
});
const helpCategory = await select<string>({
message: 'Choose a help category',
default: 'HTML-CSS',
choices: helpCategories.map(value => ({
name: value,
value
}))
});
let blockLabel: BlockLabel | undefined;
let blockLayout: BlockLayouts | undefined;
let questionCount: number | undefined;
let chapter: string | undefined;
let module: string | undefined;
let position: number | undefined;
let order: number | undefined;
if (chapterBasedSuperBlocks.includes(superBlock)) {
blockLabel = await select<BlockLabel>({
message: 'Choose a block label',
default: BlockLabel.lab,
type: 'list',
choices: Object.values(BlockLabel),
when: (answers: CreateProjectArgs) =>
chapterBasedSuperBlocks.includes(answers.superBlock)
},
{
name: 'blockLayout',
message: 'Choose a block layout',
choices: Object.values(BlockLabel).map(value => ({
name: value,
value
}))
});
default: (answers: { blockLabel: BlockLabel }) =>
answers.blockLabel == BlockLabel.quiz
blockLayout = await select<BlockLayouts>({
message: 'Choose a block layout',
default:
blockLabel === BlockLabel.quiz
? BlockLayouts.Link
: BlockLayouts.ChallengeList,
type: 'list',
choices: Object.values(BlockLayouts),
when: (answers: CreateProjectArgs) =>
chapterBasedSuperBlocks.includes(answers.superBlock)
},
{
name: 'questionCount',
message: 'Choose a question count',
default: 20,
type: 'list',
choices: [10, 20],
when: (answers: CreateProjectArgs) =>
answers.blockLabel === BlockLabel.quiz
},
{
name: 'chapter',
choices: Object.values(BlockLayouts).map(value => ({
name: value,
value
}))
});
if (blockLabel === BlockLabel.quiz) {
questionCount = await select<number>({
message: 'Choose a question count',
default: 20,
choices: [
{ name: '10', value: 10 },
{ name: '20', value: 20 }
]
});
}
const chapters = await getChapters(superBlock);
chapter = await select({
message: 'What chapter should this project go in?',
default: 'html',
type: 'list',
choices: async (answers: CreateProjectArgs) => {
const chapters = await getChapters(answers.superBlock);
return chapters.map(x => x.dashedName);
},
when: (answers: CreateProjectArgs) =>
chapterBasedSuperBlocks.includes(answers.superBlock)
},
{
name: 'module',
choices: chapters.map(x => ({
name: x.dashedName,
value: x.dashedName
}))
});
const modules = await getModules(superBlock, chapter);
module = await select({
message: 'What module should this project go in?',
default: 'html',
type: 'list',
choices: async (answers: CreateProjectArgs) => {
const modules = await getModules(
answers.superBlock,
answers.chapter!
);
return modules!.map(x => x.dashedName);
},
when: (answers: CreateProjectArgs) =>
chapterBasedSuperBlocks.includes(answers.superBlock)
},
{
name: 'position',
choices: modules!.map(x => ({
name: x.dashedName,
value: x.dashedName
}))
});
position = await number({
message: 'At which position does this appear in the module?',
default: 1,
validate: (position: string) => {
return parseInt(position, 10) > 0
validate: (value: number | undefined) =>
value && value > 0
? true
: 'Position must be an number greater than zero.';
},
when: (answers: CreateProjectArgs) =>
chapterBasedSuperBlocks.includes(answers.superBlock),
filter: (position: string) => {
return parseInt(position, 10);
}
},
{
name: 'order',
: 'Position must be a number greater than zero.'
});
} else {
order = await number({
message: 'Which position does this appear in the certificate?',
default: 42,
validate: (order: string) => {
return parseInt(order, 10) > 0
validate: (value: number | undefined) =>
value && value > 0
? true
: 'Order must be an number greater than zero.';
},
when: (answers: CreateProjectArgs) =>
!chapterBasedSuperBlocks.includes(answers.superBlock),
filter: (order: string) => {
return parseInt(order, 10);
}
}
]).then(
async ({
superBlock,
block,
title,
helpCategory,
blockLabel,
blockLayout,
questionCount,
chapter,
module,
position,
order
}: CreateProjectArgs) =>
await createProject({
superBlock,
block,
helpCategory,
blockLabel,
blockLayout,
questionCount,
title,
chapter,
module,
position,
order
})
: 'Order must be a number greater than zero.'
});
}
return {
superBlock,
block,
title,
helpCategory,
blockLabel,
blockLayout,
questionCount,
chapter,
module,
position,
order
};
})
.then(async (answers: CreateProjectArgs) => {
await createProject(answers);
})
.then(() => console.log('All set. Refresh the page to see the changes.'))
.catch((err: unknown) =>
console.error(
'Error creating project:',
err instanceof Error ? err.message : String(err)
)
)
.then(() => console.log('All set. Refresh the page to see the changes.'));
);
+58 -56
View File
@@ -1,6 +1,6 @@
import fs from 'fs/promises';
import path from 'path';
import { prompt } from 'inquirer';
import { select, input } from '@inquirer/prompts';
import { format } from 'prettier';
import { ObjectId } from 'bson';
@@ -32,21 +32,13 @@ type SuperBlockInfo = {
type IntroJson = Record<SuperBlocks, SuperBlockInfo>;
interface CreateQuizArgs {
superBlock: SuperBlocks;
block: string;
helpCategory: string;
title?: string;
questionCount: number;
}
async function createQuiz({
superBlock,
block,
helpCategory,
questionCount,
title
}: CreateQuizArgs) {
async function createQuiz(
superBlock: SuperBlocks,
block: string,
helpCategory: string,
questionCount: number,
title?: string
) {
if (!title) {
title = block;
}
@@ -137,43 +129,53 @@ function withTrace<Args extends unknown[], Result>(
});
}
void getAllBlocks()
.then(existingBlocks =>
prompt([
{
name: 'superBlock',
message: 'Which certification does this belong to?',
default: SuperBlocks.RespWebDesignV9,
type: 'list',
choices: Object.values(SuperBlocks)
},
{
name: 'block',
message: 'What is the dashed name (in kebab-case) for this quiz?',
validate: (block: string) => validateBlockName(block, existingBlocks),
filter: (block: string) => {
return block.toLowerCase().trim();
}
},
{
name: 'title',
default: ({ block }: { block: string }) => block
},
{
name: 'helpCategory',
message: 'Choose a help category',
default: 'HTML-CSS',
type: 'list',
choices: helpCategories
},
{
name: 'questionCount',
message: 'Should this quiz have either ten or twenty questions?',
default: 20,
type: 'list',
choices: [20, 10]
}
])
)
.then(async (args: CreateQuizArgs) => await createQuiz(args))
.then(() => console.log('All set. Restart the client to see the changes.'));
void getAllBlocks().then(async existingBlocks => {
const superBlock = await select<SuperBlocks>({
message: 'Which certification does this belong to?',
default: SuperBlocks.RespWebDesignV9,
choices: Object.values(SuperBlocks).map(value => ({
name: value,
value
}))
});
const block = await input({
message: 'What is the dashed name (in kebab-case) for this quiz?',
validate: (block: string) => validateBlockName(block, existingBlocks)
});
const transformedBlock = block.toLowerCase().trim();
const title = await input({
message: 'What is the new name?',
default: transformedBlock
});
const helpCategory = await select<string>({
message: 'Choose a help category',
default: 'HTML-CSS',
choices: helpCategories.map(value => ({
name: value,
value
}))
});
const questionCount = await select<number>({
message: 'Should this quiz have either ten or twenty questions?',
default: 20,
choices: [
{ name: '20 questions', value: 20 },
{ name: '10 questions', value: 10 }
]
});
await createQuiz(
superBlock,
transformedBlock,
helpCategory,
questionCount,
title
);
console.log('All set. Refresh the page to see the changes.');
});
@@ -1,5 +1,5 @@
import { unlink } from 'fs/promises';
import { prompt } from 'inquirer';
import { select } from '@inquirer/prompts';
import { getProjectPath } from './helpers/get-project-info.js';
import { getMetaData, updateMetaData } from './helpers/project-metadata.js';
import { getFileName } from './helpers/get-file-name.js';
@@ -9,22 +9,22 @@ const deleteChallenge = async () => {
const challenges = getMetaData().challengeOrder;
const challengeToDelete = (await prompt({
name: 'id',
const challengeToDeleteId = await select<string>({
message: 'Which challenge should be deleted?',
type: 'list',
choices: challenges.map(({ id, title }) => ({
name: title,
value: id
}))
})) as { id: string };
});
const indexToDelete = challenges.findIndex(
({ id }) => id === challengeToDelete.id
({ id }) => id === challengeToDeleteId
);
const fileToDelete = await getFileName(challengeToDelete.id);
const fileToDelete = await getFileName(challengeToDeleteId);
if (!fileToDelete) {
throw new Error(`File not found for challenge ${challengeToDelete.id}`);
throw new Error(`File not found for challenge ${challengeToDeleteId}`);
}
await unlink(`${path}${fileToDelete}`);
@@ -1,5 +1,5 @@
import { unlink } from 'fs/promises';
import { prompt } from 'inquirer';
import { select } from '@inquirer/prompts';
import { getProjectPath } from './helpers/get-project-info.js';
import { getFileName } from './helpers/get-file-name.js';
import {
@@ -14,23 +14,21 @@ const deleteTask = async () => {
const path = getProjectPath();
const challenges = getMetaData().challengeOrder;
const challengeToDelete = (await prompt({
name: 'id',
const challengeToDeleteId = await select<string>({
message: 'Which challenge should be deleted?',
type: 'list',
choices: challenges.map(({ id, title }) => ({
name: title,
value: id
}))
})) as { id: string };
});
const indexToDelete = challenges.findIndex(
({ id }) => id === challengeToDelete.id
({ id }) => id === challengeToDeleteId
);
const fileToDelete = await getFileName(challengeToDelete.id);
const fileToDelete = await getFileName(challengeToDeleteId);
if (!fileToDelete) {
throw new Error(`File not found for challenge ${challengeToDelete.id}`);
throw new Error(`File not found for challenge ${challengeToDeleteId}`);
}
await unlink(`${path}${fileToDelete}`);
@@ -1,4 +1,4 @@
import { prompt } from 'inquirer';
import { select } from '@inquirer/prompts';
import { ChallengeLang } from '@freecodecamp/shared/config/curriculum';
import { challengeTypes } from '@freecodecamp/shared/config/challenge-types';
@@ -14,12 +14,13 @@ export const getInputType = async (
return;
}
const inputType = await prompt<{ value: string }>({
name: 'value',
const inputType = await select<string>({
message: 'What input type is challenge using?',
type: 'list',
choices: ['pinyin-tone', 'pinyin-to-hanzi']
choices: [
{ name: 'pinyin-tone', value: 'pinyin-tone' },
{ name: 'pinyin-to-hanzi', value: 'pinyin-to-hanzi' }
]
});
return inputType.value;
return inputType;
};
@@ -1,4 +1,4 @@
import { prompt } from 'inquirer';
import { input, select } from '@inquirer/prompts';
import { challengeTypes } from '@freecodecamp/shared/config/challenge-types';
import { getLastStep } from './get-last-step-file-number.js';
@@ -7,13 +7,11 @@ export const newChallengePrompts = async (): Promise<{
dashedName: string;
challengeType: string;
}> => {
const challengeType = await prompt<{ value: string }>({
name: 'value',
const challengeType = await select<string>({
message: 'What type of challenge is this?',
type: 'list',
choices: Object.entries(challengeTypes).map(([key, value]) => ({
name: key,
value
value: value.toString()
}))
});
@@ -21,9 +19,9 @@ export const newChallengePrompts = async (): Promise<{
const defaultTitle = `Step ${lastStep + 1}`;
const defaultDashedName = `step-${lastStep + 1}`;
const dashedName = await prompt<{ value: string }>({
name: 'value',
const dashedName = await input({
message: 'What is the dashed name (in kebab-case) for this challenge?',
default: defaultDashedName,
validate: (block: string) => {
if (!block.length) {
return 'please enter a dashed name';
@@ -33,20 +31,16 @@ export const newChallengePrompts = async (): Promise<{
}
return true;
},
filter: (block: string) => {
return block.toLowerCase();
},
default: defaultDashedName
transformer: (block: string) => block.toLowerCase()
});
const title = await prompt<{ value: string }>({
name: 'value',
const title = await input({
message: 'What is the title of this challenge?',
default: defaultTitle
});
return {
title: title.value,
dashedName: dashedName.value,
challengeType: challengeType.value
title,
dashedName,
challengeType
};
};
@@ -1,4 +1,4 @@
import { prompt } from 'inquirer';
import { select } from '@inquirer/prompts';
import { challengeTypes } from '@freecodecamp/shared/config/challenge-types';
const taskChallenges = [
@@ -10,19 +10,17 @@ const taskChallenges = [
export const newTaskPrompts = async (): Promise<{
challengeType: string;
}> => {
const challengeType = await prompt<{ value: string }>({
name: 'value',
const challengeType = await select<string>({
message: 'What type of task challenge is this?',
type: 'list',
choices: Object.entries(challengeTypes)
.filter(entry => taskChallenges.includes(entry[1]))
.map(([key, value]) => ({
name: key,
value
value: value.toString()
}))
});
return {
challengeType: challengeType.value
challengeType
};
};
@@ -1,5 +1,5 @@
import { ObjectId } from 'bson';
import { prompt } from 'inquirer';
import { select } from '@inquirer/prompts';
import { getTemplate } from './helpers/get-challenge-template.js';
import { newChallengePrompts } from './helpers/new-challenge-prompts.js';
import { getProjectPath } from './helpers/get-project-info.js';
@@ -13,17 +13,15 @@ const insertChallenge = async () => {
const challenges = getMetaData().challengeOrder;
const challengeAfter = await prompt<{ id: string }>({
name: 'id',
const challengeAfterId = await select<string>({
message: 'Which challenge should come AFTER this new one?',
type: 'list',
choices: challenges.map(({ id, title }) => ({
name: title,
value: id
}))
});
const indexToInsert = challenges.findIndex(
({ id }) => id === challengeAfter.id
({ id }) => id === challengeAfterId
);
const template = getTemplate(options.challengeType);
@@ -1,5 +1,5 @@
import { ObjectId } from 'bson';
import { prompt } from 'inquirer';
import { select } from '@inquirer/prompts';
import { getTemplate } from './helpers/get-challenge-template.js';
import { newTaskPrompts } from './helpers/new-task-prompts.js';
import { getProjectPath } from './helpers/get-project-info.js';
@@ -15,19 +15,17 @@ import { getInputType } from './helpers/get-input-type.js';
const insertChallenge = async () => {
const challenges = getMetaData().challengeOrder;
const challengeAfter = await prompt<{ id: string }>({
name: 'id',
const challengeAfterId = await select<string>({
message: 'Which challenge should come AFTER this new one?',
type: 'list',
choices: challenges.map(({ id, title }) => ({
name: title,
value: id
}))
});
const challengeLang = getChallenge(challengeAfter.id)?.lang;
const challengeLang = getChallenge(challengeAfterId)?.lang;
const indexToInsert = challenges.findIndex(
({ id }) => id === challengeAfter.id
({ id }) => id === challengeAfterId
);
const newTaskTitle = 'Task 0';
+1 -2
View File
@@ -34,15 +34,14 @@
"@freecodecamp/curriculum": "workspace:*",
"@freecodecamp/eslint-config": "workspace:*",
"@freecodecamp/shared": "workspace:*",
"@inquirer/prompts": "^7.8.3",
"@total-typescript/ts-reset": "^0.6.1",
"@types/glob": "^8.0.1",
"@types/inquirer": "^8.2.5",
"@vitest/ui": "^4.0.15",
"bson": "^7.0.0",
"eslint": "^9.39.1",
"glob": "^8.1.0",
"gray-matter": "4.0.3",
"inquirer": "8.2.6",
"prettier": "3.2.5",
"typescript": "5.9.3",
"vitest": "^4.0.15"
+22 -30
View File
@@ -1,6 +1,6 @@
import fs from 'fs/promises';
import path, { join } from 'path';
import { prompt } from 'inquirer';
import { input } from '@inquirer/prompts';
import { format } from 'prettier';
import { IntroJson, parseJson } from './helpers/parse-json';
@@ -80,32 +80,24 @@ async function renameBlock({ newBlock, newName, oldBlock }: RenameBlockArgs) {
}
}
void getAllBlocks()
.then(existingBlocks =>
prompt([
{
name: 'oldBlock',
message: 'What is the dashed name of block to rename?',
type: 'input',
validate: (block: string) => existingBlocks.includes(block)
},
{
name: 'newName',
message: 'What is the new name?',
type: 'input',
default: ({ oldBlock }: RenameBlockArgs) =>
getBlockStructure(oldBlock).name
},
{
name: 'newBlock',
message: 'What is the new dashed name (in kebab-case)?',
validate: (newBlock: string) =>
validateBlockName(newBlock, existingBlocks)
}
])
)
.then(
async ({ newBlock, newName, oldBlock }: RenameBlockArgs) =>
await renameBlock({ newBlock, newName, oldBlock })
)
.then(() => console.log('All set. Refresh the page to see the changes.'));
void getAllBlocks().then(async existingBlocks => {
const oldBlock = await input({
message: 'What is the dashed name of block to rename?',
validate: (block: string) =>
existingBlocks.includes(block) || 'Block not found in existing blocks.'
});
const newName = await input({
message: 'What is the new name?',
default: getBlockStructure(oldBlock).name
});
const newBlock = await input({
message: 'What is the new dashed name (in kebab-case)?',
validate: (block: string) => validateBlockName(block, existingBlocks)
});
await renameBlock({ newBlock, newName, oldBlock });
console.log('All set. Refresh the page to see the changes.');
});
@@ -4,7 +4,7 @@ import { join } from 'path';
import { promisify } from 'util';
import gray from 'gray-matter';
import { prompt } from 'inquirer';
import { select } from '@inquirer/prompts';
const asyncExec = promisify(exec);
@@ -13,23 +13,19 @@ void (async () => {
join(process.cwd(), 'curriculum', 'challenges', 'english')
);
const { superblock } = (await prompt({
name: 'superblock',
const superblock = await select<string>({
message: 'Select target superblock:',
type: 'list',
choices: superblocks.map(e => ({ name: e, value: e }))
})) as { superblock: string };
choices: superblocks.map(value => ({ name: value, value }))
});
const blocks = await readdir(
join(process.cwd(), 'curriculum', 'challenges', 'english', superblock)
);
const { block } = (await prompt({
name: 'block',
const block = await select<string>({
message: 'Select target block:',
type: 'list',
choices: blocks.map(e => ({ name: e, value: e }))
})) as { block: string };
choices: blocks.map(value => ({ name: value, value }))
});
const files = await readdir(
join(
@@ -1,4 +1,4 @@
import { prompt } from 'inquirer';
import { select, confirm } from '@inquirer/prompts';
import { getMetaData, updateMetaData } from './helpers/project-metadata.js';
@@ -10,21 +10,19 @@ const updateChallengeOrder = async () => {
const newChallengeOrder: { id: string; title: string }[] = [];
while (oldChallengeOrder.length) {
const nextChallenge = (await prompt({
name: 'id',
const nextChallengeId = await select<string>({
message: newChallengeOrder.length
? `What challenge comes after ${
newChallengeOrder[newChallengeOrder.length - 1].title
}?`
: 'What is the first challenge?',
type: 'list',
choices: oldChallengeOrder.map(({ id, title }) => ({
name: title,
value: id
}))
})) as { id: string };
});
const nextChallengeIndex = oldChallengeOrder.findIndex(
({ id }) => id === nextChallenge.id
({ id }) => id === nextChallengeId
);
const targetChallenge = oldChallengeOrder[nextChallengeIndex];
oldChallengeOrder.splice(nextChallengeIndex, 1);
@@ -34,14 +32,12 @@ const updateChallengeOrder = async () => {
console.log('New challenge order is: ');
console.table(newChallengeOrder.map(({ title }) => ({ title })));
const confirm = await prompt({
name: 'correct',
const isCorrect = await confirm({
message: 'Is this correct?',
type: 'confirm',
default: false
});
if (!confirm.correct) {
if (!isCorrect) {
console.error('Aborting.');
return;
}