From e430d38da9be90ee3381c6ac6f82f3ccea2dbc8c Mon Sep 17 00:00:00 2001 From: Pawel Bartusiak <21136755+uFloppyDisk@users.noreply.github.com> Date: Mon, 24 Mar 2025 19:04:47 -0700 Subject: [PATCH 01/19] chore: install commander; --- package-lock.json | 18 ++++++++++++++---- package.json | 1 + 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7f96678..c959c1b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "AGPL-3.0-only", "dependencies": { "chalk": "^5.4.1", + "commander": "^13.1.0", "gradient-string": "^3.0.0", "ora": "^8.2.0", "prompts": "^2.4.2", @@ -2044,12 +2045,12 @@ "license": "MIT" }, "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", "license": "MIT", "engines": { - "node": ">= 6" + "node": ">=18" } }, "node_modules/concat-map": { @@ -5357,6 +5358,15 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", diff --git a/package.json b/package.json index 46d018a..b593ea3 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "description": "", "dependencies": { "chalk": "^5.4.1", + "commander": "^13.1.0", "gradient-string": "^3.0.0", "ora": "^8.2.0", "prompts": "^2.4.2", From 3d508099b7175bbcff8506356d1062ae9f013865 Mon Sep 17 00:00:00 2001 From: Pawel Bartusiak <21136755+uFloppyDisk@users.noreply.github.com> Date: Mon, 24 Mar 2025 21:48:31 -0700 Subject: [PATCH 02/19] refactor: move validator functions to file; --- src/__tests__/parameters.spec.ts | 6 +++- src/index.ts | 4 +-- src/parameters.ts | 48 +++++--------------------------- src/validations.ts | 41 +++++++++++++++++++++++++++ 4 files changed, 55 insertions(+), 44 deletions(-) create mode 100644 src/validations.ts diff --git a/src/__tests__/parameters.spec.ts b/src/__tests__/parameters.spec.ts index 41dbad0..748d00b 100644 --- a/src/__tests__/parameters.spec.ts +++ b/src/__tests__/parameters.spec.ts @@ -1,6 +1,10 @@ -import { validateNonEmptyString, validatePathDoesNotExist, validateStringIsNotPath } from "~/parameters"; import { vol } from "memfs"; import fs from "fs"; +import { + validateNonEmptyString, + validatePathDoesNotExist, + validateStringIsNotPath +} from "~/validations"; jest.mock("fs"); diff --git a/src/index.ts b/src/index.ts index b03d009..35f92b8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,7 +6,7 @@ import path from "path"; import pkg from "package.json"; import prompts from "prompts"; -import parameters from "~/parameters"; +import { interactivePrompts } from "~/parameters"; import generatePluginFiles from "~/generatePluginFiles"; import { IS_PRODUCTION, TARGET_BASE, TEMPLATE_BASE } from "~/constants"; import { createSpinner, error, renderCliInfo, renderGoodbye, renderMasthead, warn } from "~/vanity"; @@ -53,7 +53,7 @@ const generateProject = new Promise(async (resolve, reject) => { return false; } - const answers = await prompts(parameters, { onCancel }); + const answers = await prompts(interactivePrompts, { onCancel }); if (cancelled) { warn("Cancelled making a CounterStrikeSharp plugin."); return resolve(); diff --git a/src/parameters.ts b/src/parameters.ts index a8e77e5..8648714 100644 --- a/src/parameters.ts +++ b/src/parameters.ts @@ -1,46 +1,13 @@ -import { existsSync } from "fs"; import path from "path"; import { PromptObject } from "prompts"; -import { TARGET_BASE } from "~/constants"; +import { + validateNonEmptyString, + validatePathDoesNotExist, + validateStringIsNotPath, + validationBuilder +} from "./validations"; -type ValidationClosure = (input: T) => boolean | string; -type Validation = ValidationClosure; - -function validationBuilder(validations: Validation[]): Validation { - return (answer: string) => { - for (const validate of validations) { - const result = validate(answer); - if (result === true) continue; - - return result; - } - - return true; - } -} - -export function validateNonEmptyString(errorMsg: string): Validation { - return (answer) => { - return typeof answer === 'string' && answer.trim().length > 0 ? true : errorMsg; - } -} - -export function validateStringIsNotPath(errorMsg: string): Validation { - return (answer: string) => { - const found = answer.match(/[\/\\\:]/); - if (!found) return true; - - return found.length <= 0 ? true : errorMsg; - } -} - -export function validatePathDoesNotExist(errorMsg: string, base: string = TARGET_BASE): Validation { - return (answer) => { - return !existsSync(path.join(base, answer)) ? true : errorMsg; - } -} - -export default [ +export const interactivePrompts: PromptObject[] = [ { type: 'text', name: 'containingDirectoryName', @@ -110,4 +77,3 @@ export default [ } }, ]; - diff --git a/src/validations.ts b/src/validations.ts new file mode 100644 index 0000000..63fc357 --- /dev/null +++ b/src/validations.ts @@ -0,0 +1,41 @@ +import path from "path"; +import { existsSync } from "fs"; +import { TARGET_BASE } from "~/constants"; + +type ValidationClosure = (input: T) => boolean | string; +type Validation = ValidationClosure; + +export function validationBuilder(validations: Validation[]): Validation { + return (answer: string) => { + for (const validate of validations) { + const result = validate(answer); + if (result === true) continue; + + return result; + } + + return true; + } +} + +export function validateNonEmptyString(errorMsg: string): Validation { + return (answer) => { + return typeof answer === 'string' && answer.trim().length > 0 ? true : errorMsg; + } +} + +export function validateStringIsNotPath(errorMsg: string): Validation { + return (answer: string) => { + const found = answer.match(/[\/\\\:]/); + if (!found) return true; + + return found.length <= 0 ? true : errorMsg; + } +} + +export function validatePathDoesNotExist(errorMsg: string, base: string = TARGET_BASE): Validation { + return (answer) => { + return !existsSync(path.join(base, answer)) ? true : errorMsg; + } +} + From c7797cbbc18b6a4587a3bfcea4cb2e6647c94349 Mon Sep 17 00:00:00 2001 From: Pawel Bartusiak <21136755+uFloppyDisk@users.noreply.github.com> Date: Tue, 25 Mar 2025 21:36:29 -0700 Subject: [PATCH 03/19] refactor: single source of truth for cli args and interactive prompts; --- src/index.ts | 9 ++- src/parameters.ts | 197 ++++++++++++++++++++++++++++++++++----------- src/validations.ts | 2 +- 3 files changed, 161 insertions(+), 47 deletions(-) diff --git a/src/index.ts b/src/index.ts index 35f92b8..7234c3b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,11 +6,17 @@ import path from "path"; import pkg from "package.json"; import prompts from "prompts"; -import { interactivePrompts } from "~/parameters"; +import { addCommandLineArguments, programSchema, createPrompts } from "~/parameters"; import generatePluginFiles from "~/generatePluginFiles"; import { IS_PRODUCTION, TARGET_BASE, TEMPLATE_BASE } from "~/constants"; import { createSpinner, error, renderCliInfo, renderGoodbye, renderMasthead, warn } from "~/vanity"; import { promiseWithSpinner } from "./helpers"; +import { program } from "commander"; + +program.version(pkg.version); +addCommandLineArguments(program, programSchema); + +program.parse(); renderMasthead(); renderCliInfo(); @@ -53,6 +59,7 @@ const generateProject = new Promise(async (resolve, reject) => { return false; } + const interactivePrompts = createPrompts(programSchema); const answers = await prompts(interactivePrompts, { onCancel }); if (cancelled) { warn("Cancelled making a CounterStrikeSharp plugin."); diff --git a/src/parameters.ts b/src/parameters.ts index 8648714..bec0672 100644 --- a/src/parameters.ts +++ b/src/parameters.ts @@ -1,79 +1,186 @@ -import path from "path"; import { PromptObject } from "prompts"; +import { program as commanderProgram } from "commander"; import { validateNonEmptyString, validatePathDoesNotExist, validateStringIsNotPath, + Validation, validationBuilder } from "./validations"; +import { Argument, Option } from "commander"; -export const interactivePrompts: PromptObject[] = [ +type PromptOptions = Omit, "name">; +type CommandLineArgument = { + type: "argument"; + name?: string; + factory: (obj: Argument, schema: ProgramSchema) => void; +} | { + type: "option"; + flags: Option["flags"]; + factory?: (obj: Option, schema: ProgramSchema) => void; +} +type ProgramSchema = { + key: string; + description?: string; + validate?: Validation; + arg?: CommandLineArgument; + prompt?: () => PromptOptions; +} +export const programSchema: ProgramSchema[] = [ { - type: 'text', - name: 'containingDirectoryName', - message: 'What do you want to name the project directory?', + key: "containingDirectoryName", + description: + "Project name used as the project directory and plugin namespace. Accepts a path relative to current working directory.", validate: validationBuilder([ validateNonEmptyString('Your project directory must have a name!'), validatePathDoesNotExist(`A directory with that name already exists!`), ]), + arg: { + type: "argument", + name: "projectDirectory", + factory(obj) { + obj.argOptional(); + }, + }, + prompt: () => ({ + type: 'text', + message: 'What do you want to name the project directory?', + }), }, { - type: 'toggle', - name: 'pluginSameName', - message: 'Do you want your plugin to have the same name as your project directory?', - initial: true, - active: 'yes', - inactive: 'no', + key: "pluginSameName", + prompt: () => ({ + type: 'toggle', + message: 'Do you want your plugin to have the same name as your project directory?', + initial: true, + active: 'yes', + inactive: 'no', + }), }, { - type: (_, values) => values.pluginSameName === false ? 'text' : null, - name: 'pluginName', - message: 'What do you want to name your plugin?', - initial: (_, values) => path.parse(values.containingDirectoryName).base, + key: "pluginName", + description: "Use this option to set a different name for your plugin's namespace.", validate: validationBuilder([ validateNonEmptyString('Your plugin must have a name!'), validateStringIsNotPath('Your plugin name cannot be a path!'), - ]) + ]), + arg: { + type: "option", + flags: "-p, --pluginName", + }, + prompt: () => ({ + type: (_, values) => values.pluginSameName === false ? 'text' : null, + message: 'What do you want to name your plugin?', + }), }, { - type: 'text', - name: 'pluginAuthor', - message: 'Plugin author', - initial: '', + key: "pluginAuthor", + prompt: () => ({ + type: 'text', + message: 'Plugin author', + initial: '', + }), }, { - type: 'text', - name: 'pluginDescription', - message: 'Plugin description', - initial: '', + key: 'pluginDescription', + prompt: () => ({ + type: 'text', + message: 'Plugin description', + initial: '', + }), }, { - type: 'text', - name: 'pluginVersion', - message: 'Initial version', - initial: '0.0.1', + key: 'pluginVersion', + prompt: () => ({ + type: 'text', + message: 'Initial version', + initial: '0.0.1', + }), }, { - type: 'toggle', - name: 'initGitRepo', - message: 'Initialize a git repository?', - initial: true, - active: 'yes', - inactive: 'no', + key: 'initGitRepo', + prompt: () => ({ + type: 'toggle', + message: 'Initialize a git repository?', + initial: true, + active: 'yes', + inactive: 'no', + }), }, { - type: 'toggle', - name: 'setupUsingDotnetCli', - message: 'Setup plugin using dotnet?', - initial: true, - active: 'yes', - inactive: 'no', - onRender(kleur) { - //@ts-ignore - if (this.firstRender) { + key: 'setupUsingDotnetCli', + prompt: () => ({ + type: 'toggle', + message: 'Setup plugin using dotnet?', + initial: true, + active: 'yes', + inactive: 'no', + onRender(kleur) { //@ts-ignore - this.msg = `Setup plugin using dotnet? ${kleur.gray('(You must have the dotnet CLI installed and accessible via `dotnet`)')}` + if (this.firstRender) { + //@ts-ignore + this.msg = `Setup plugin using dotnet? ${kleur.gray('(You must have the dotnet CLI installed and accessible via `dotnet`)')}` + } } - } + }), + }, + { + key: "interactive", + description: "Force interactive prompting. Options set via command-line are populated as prompt defaults.", + validate: validationBuilder([ + validateNonEmptyString('Your plugin must have a name!'), + validateStringIsNotPath('Your plugin name cannot be a path!'), + ]), + arg: { + type: "option", + flags: "-i, --interactive", + }, }, ]; + +export function addCommandLineArguments( + program: typeof commanderProgram, + optionsSchema: ProgramSchema[], +): Record { + const reverseLookup: Record = {}; + + for (const schema of optionsSchema) { + if (!schema.arg) continue; + + const arg = schema.arg; + if ("argument" === arg.type) { + const argument = new Argument(arg.name ?? schema.key, schema.description); + if (arg.factory) arg.factory(argument, schema); + + program.addArgument(argument); + reverseLookup[argument.name()] = schema.key; + } else if ("option" === arg.type) { + const option = new Option(arg.flags, schema.description); + if (arg.factory) arg.factory(option, schema); + + program.addOption(option); + reverseLookup[option.name()] = schema.key; + } + } + + return reverseLookup; +} + +export function createPrompts(optionsSchema: ProgramSchema[]): PromptObject[] { + const prompts: PromptObject[] = []; + for (const schema of optionsSchema) { + if (!schema.prompt) continue; + + const prompt: PromptObject = { + name: schema.key, + message: schema.description, + validate: schema.validate, + ...schema.prompt(), + } + + + prompts.push(prompt); + } + + return prompts; +} diff --git a/src/validations.ts b/src/validations.ts index 63fc357..2eba529 100644 --- a/src/validations.ts +++ b/src/validations.ts @@ -3,7 +3,7 @@ import { existsSync } from "fs"; import { TARGET_BASE } from "~/constants"; type ValidationClosure = (input: T) => boolean | string; -type Validation = ValidationClosure; +export type Validation = ValidationClosure; export function validationBuilder(validations: Validation[]): Validation { return (answer: string) => { From ab3b0a950aded97f30812cb9ef4f0dfa91ff6320 Mon Sep 17 00:00:00 2001 From: Pawel Bartusiak <21136755+uFloppyDisk@users.noreply.github.com> Date: Thu, 27 Mar 2025 19:58:00 -0700 Subject: [PATCH 04/19] fix(cli): accept string for pluginName flag; --- src/parameters.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parameters.ts b/src/parameters.ts index bec0672..4beeb6d 100644 --- a/src/parameters.ts +++ b/src/parameters.ts @@ -66,7 +66,7 @@ export const programSchema: ProgramSchema[] = [ ]), arg: { type: "option", - flags: "-p, --pluginName", + flags: "-p, --pluginName ", }, prompt: () => ({ type: (_, values) => values.pluginSameName === false ? 'text' : null, From 1b7d497a9787217a927a0c353ea7f8e8fcc7881e Mon Sep 17 00:00:00 2001 From: Pawel Bartusiak <21136755+uFloppyDisk@users.noreply.github.com> Date: Thu, 27 Mar 2025 20:21:46 -0700 Subject: [PATCH 05/19] refactor(schema): move initial value to schema-level; --- src/parameters.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/parameters.ts b/src/parameters.ts index 4beeb6d..ea56ff8 100644 --- a/src/parameters.ts +++ b/src/parameters.ts @@ -22,6 +22,7 @@ type CommandLineArgument = { type ProgramSchema = { key: string; description?: string; + initial?: PromptObject["initial"]; validate?: Validation; arg?: CommandLineArgument; prompt?: () => PromptOptions; @@ -49,10 +50,10 @@ export const programSchema: ProgramSchema[] = [ }, { key: "pluginSameName", + initial: true, prompt: () => ({ type: 'toggle', message: 'Do you want your plugin to have the same name as your project directory?', - initial: true, active: 'yes', inactive: 'no', }), @@ -75,44 +76,44 @@ export const programSchema: ProgramSchema[] = [ }, { key: "pluginAuthor", + initial: '', prompt: () => ({ type: 'text', message: 'Plugin author', - initial: '', }), }, { key: 'pluginDescription', + initial: '', prompt: () => ({ type: 'text', message: 'Plugin description', - initial: '', }), }, { key: 'pluginVersion', + initial: '0.0.1', prompt: () => ({ type: 'text', message: 'Initial version', - initial: '0.0.1', }), }, { key: 'initGitRepo', + initial: true, prompt: () => ({ type: 'toggle', message: 'Initialize a git repository?', - initial: true, active: 'yes', inactive: 'no', }), }, { key: 'setupUsingDotnetCli', + initial: true, prompt: () => ({ type: 'toggle', message: 'Setup plugin using dotnet?', - initial: true, active: 'yes', inactive: 'no', onRender(kleur) { @@ -127,6 +128,7 @@ export const programSchema: ProgramSchema[] = [ { key: "interactive", description: "Force interactive prompting. Options set via command-line are populated as prompt defaults.", + initial: true, validate: validationBuilder([ validateNonEmptyString('Your plugin must have a name!'), validateStringIsNotPath('Your plugin name cannot be a path!'), @@ -173,6 +175,7 @@ export function createPrompts(optionsSchema: ProgramSchema[]): PromptObject[] { const prompt: PromptObject = { name: schema.key, + initial: schema.initial ?? undefined, message: schema.description, validate: schema.validate, ...schema.prompt(), From 9faa3993dd0664bcc0cf4440edfa55a73acc0e0b Mon Sep 17 00:00:00 2001 From: Pawel Bartusiak <21136755+uFloppyDisk@users.noreply.github.com> Date: Thu, 27 Mar 2025 20:58:54 -0700 Subject: [PATCH 06/19] feat: ingest cli args and options; --- src/index.ts | 15 +++++++++++++-- src/parameters.ts | 8 ++++---- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/index.ts b/src/index.ts index 7234c3b..25c1909 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,9 +14,20 @@ import { promiseWithSpinner } from "./helpers"; import { program } from "commander"; program.version(pkg.version); -addCommandLineArguments(program, programSchema); +const lookup = addCommandLineArguments(program, programSchema); -program.parse(); +const parsedOpts = program.parse().opts(); +const options = programSchema.reduce>((acc, s) => { + acc[s.key] = parsedOpts?.[lookup[s.key]] ?? (s.initial ?? null); + return acc; +}, {}); + +const positionalArgs = programSchema.filter(s => s.arg && s.arg.type === "argument"); +for (const [i, arg] of positionalArgs.entries()) { + if (!program.args[i]) continue; + + options[arg.key] = program.args[i]; +}; renderMasthead(); renderCliInfo(); diff --git a/src/parameters.ts b/src/parameters.ts index ea56ff8..a657910 100644 --- a/src/parameters.ts +++ b/src/parameters.ts @@ -144,7 +144,7 @@ export function addCommandLineArguments( program: typeof commanderProgram, optionsSchema: ProgramSchema[], ): Record { - const reverseLookup: Record = {}; + const lookup: Record = {}; for (const schema of optionsSchema) { if (!schema.arg) continue; @@ -155,17 +155,17 @@ export function addCommandLineArguments( if (arg.factory) arg.factory(argument, schema); program.addArgument(argument); - reverseLookup[argument.name()] = schema.key; + lookup[schema.key] = argument.name(); } else if ("option" === arg.type) { const option = new Option(arg.flags, schema.description); if (arg.factory) arg.factory(option, schema); program.addOption(option); - reverseLookup[option.name()] = schema.key; + lookup[schema.key] = option.name(); } } - return reverseLookup; + return lookup; } export function createPrompts(optionsSchema: ProgramSchema[]): PromptObject[] { From 6ca29fc1ee818812433742e25ed4235d88844ec6 Mon Sep 17 00:00:00 2001 From: Pawel Bartusiak <21136755+uFloppyDisk@users.noreply.github.com> Date: Thu, 27 Mar 2025 21:02:46 -0700 Subject: [PATCH 07/19] feat: merge cli args into prompt initial values; --- src/index.ts | 2 +- src/parameters.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index 25c1909..bf0ed7e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -70,7 +70,7 @@ const generateProject = new Promise(async (resolve, reject) => { return false; } - const interactivePrompts = createPrompts(programSchema); + const interactivePrompts = createPrompts(programSchema, options); const answers = await prompts(interactivePrompts, { onCancel }); if (cancelled) { warn("Cancelled making a CounterStrikeSharp plugin."); diff --git a/src/parameters.ts b/src/parameters.ts index a657910..2f04520 100644 --- a/src/parameters.ts +++ b/src/parameters.ts @@ -168,14 +168,14 @@ export function addCommandLineArguments( return lookup; } -export function createPrompts(optionsSchema: ProgramSchema[]): PromptObject[] { +export function createPrompts(optionsSchema: ProgramSchema[], options: Record): PromptObject[] { const prompts: PromptObject[] = []; for (const schema of optionsSchema) { if (!schema.prompt) continue; const prompt: PromptObject = { name: schema.key, - initial: schema.initial ?? undefined, + initial: options[schema.key] ?? (schema.initial ?? undefined), message: schema.description, validate: schema.validate, ...schema.prompt(), From 86350626b3fe7ecfbad2c13ed8578e55ea20f326 Mon Sep 17 00:00:00 2001 From: Pawel Bartusiak <21136755+uFloppyDisk@users.noreply.github.com> Date: Thu, 27 Mar 2025 21:57:49 -0700 Subject: [PATCH 08/19] feat: override prompts with cli args; --- src/index.ts | 25 +++++++++++++++++++++---- src/parameters.ts | 7 +++---- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/index.ts b/src/index.ts index bf0ed7e..e397293 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,7 +6,7 @@ import path from "path"; import pkg from "package.json"; import prompts from "prompts"; -import { addCommandLineArguments, programSchema, createPrompts } from "~/parameters"; +import { addCommandLineArguments, programSchema, createPrompts, ProgramOption } from "~/parameters"; import generatePluginFiles from "~/generatePluginFiles"; import { IS_PRODUCTION, TARGET_BASE, TEMPLATE_BASE } from "~/constants"; import { createSpinner, error, renderCliInfo, renderGoodbye, renderMasthead, warn } from "~/vanity"; @@ -17,8 +17,14 @@ program.version(pkg.version); const lookup = addCommandLineArguments(program, programSchema); const parsedOpts = program.parse().opts(); -const options = programSchema.reduce>((acc, s) => { - acc[s.key] = parsedOpts?.[lookup[s.key]] ?? (s.initial ?? null); + +const options = programSchema.reduce>((acc, s) => { + const opt = parsedOpts?.[lookup[s.key]]; + acc[s.key] = { + value: opt ?? (s.initial ?? null), + wasSet: !!opt, + } + return acc; }, {}); @@ -26,9 +32,20 @@ const positionalArgs = programSchema.filter(s => s.arg && s.arg.type === "argume for (const [i, arg] of positionalArgs.entries()) { if (!program.args[i]) continue; - options[arg.key] = program.args[i]; + options[arg.key].value = program.args[i]; + options[arg.key].wasSet = true; }; +if (!options["interactive"].value) { + prompts.override( + Object.fromEntries( + Object.entries(options) + .filter(o => o[1].wasSet) + .map(o => [o[0], o[1].value]) + ) + ); +} + renderMasthead(); renderCliInfo(); console.log(); diff --git a/src/parameters.ts b/src/parameters.ts index 2f04520..77733a2 100644 --- a/src/parameters.ts +++ b/src/parameters.ts @@ -9,6 +9,7 @@ import { } from "./validations"; import { Argument, Option } from "commander"; +export type ProgramOption = { value: T, wasSet: boolean }; type PromptOptions = Omit, "name">; type CommandLineArgument = { type: "argument"; @@ -128,7 +129,6 @@ export const programSchema: ProgramSchema[] = [ { key: "interactive", description: "Force interactive prompting. Options set via command-line are populated as prompt defaults.", - initial: true, validate: validationBuilder([ validateNonEmptyString('Your plugin must have a name!'), validateStringIsNotPath('Your plugin name cannot be a path!'), @@ -168,20 +168,19 @@ export function addCommandLineArguments( return lookup; } -export function createPrompts(optionsSchema: ProgramSchema[], options: Record): PromptObject[] { +export function createPrompts(optionsSchema: ProgramSchema[], options: Record): PromptObject[] { const prompts: PromptObject[] = []; for (const schema of optionsSchema) { if (!schema.prompt) continue; const prompt: PromptObject = { name: schema.key, - initial: options[schema.key] ?? (schema.initial ?? undefined), + initial: options[schema.key].value ?? (schema.initial ?? undefined), message: schema.description, validate: schema.validate, ...schema.prompt(), } - prompts.push(prompt); } From 5e05c8b630f13860c0e9badce7fe8523d9816873 Mon Sep 17 00:00:00 2001 From: Pawel Bartusiak <21136755+uFloppyDisk@users.noreply.github.com> Date: Thu, 27 Mar 2025 22:35:19 -0700 Subject: [PATCH 09/19] fix: validate positional arguments; --- src/index.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/index.ts b/src/index.ts index e397293..d1249b3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -32,6 +32,13 @@ const positionalArgs = programSchema.filter(s => s.arg && s.arg.type === "argume for (const [i, arg] of positionalArgs.entries()) { if (!program.args[i]) continue; + const passed = !!arg.validate ? arg.validate(program.args[i]) : true; + if (typeof passed === "string") { + program.error(`${passed}: ${program.args[i]}`); + } else if (passed === false) { + program.help(); + } + options[arg.key].value = program.args[i]; options[arg.key].wasSet = true; }; From d9fabde1040dae0cea72b8b4e8c8e39ed58068d6 Mon Sep 17 00:00:00 2001 From: Pawel Bartusiak <21136755+uFloppyDisk@users.noreply.github.com> Date: Thu, 27 Mar 2025 22:58:19 -0700 Subject: [PATCH 10/19] feat: add forced/preanswered interactive mode flags; --- src/index.ts | 21 ++++++++++++--------- src/parameters.ts | 14 +++++++++----- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/index.ts b/src/index.ts index d1249b3..be65fd6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -43,15 +43,18 @@ for (const [i, arg] of positionalArgs.entries()) { options[arg.key].wasSet = true; }; -if (!options["interactive"].value) { - prompts.override( - Object.fromEntries( - Object.entries(options) - .filter(o => o[1].wasSet) - .map(o => [o[0], o[1].value]) - ) - ); -} +(() => { + if (options["forceInteractive"].value) return; + + let preanswered = Object.entries(options); + if (options["interactive"].value) { + preanswered = preanswered.filter(o => o[1].wasSet); + } + + preanswered = preanswered.map(o => [o[0], o[1].value]); + + prompts.override(Object.fromEntries(preanswered)); +})(); renderMasthead(); renderCliInfo(); diff --git a/src/parameters.ts b/src/parameters.ts index 77733a2..5e6e390 100644 --- a/src/parameters.ts +++ b/src/parameters.ts @@ -128,16 +128,20 @@ export const programSchema: ProgramSchema[] = [ }, { key: "interactive", - description: "Force interactive prompting. Options set via command-line are populated as prompt defaults.", - validate: validationBuilder([ - validateNonEmptyString('Your plugin must have a name!'), - validateStringIsNotPath('Your plugin name cannot be a path!'), - ]), + description: "Ask prompts while skipping values set using positional arguments and flags.", arg: { type: "option", flags: "-i, --interactive", }, }, + { + key: "forceInteractive", + description: "Force ask all prompts. Options set via command-line are populated as prompt defaults.", + arg: { + type: "option", + flags: "-I, --forceInteractive", + }, + }, ]; export function addCommandLineArguments( From 04c5cd5357f7fba5e9d477f5be313a214d92e67d Mon Sep 17 00:00:00 2001 From: Pawel Bartusiak <21136755+uFloppyDisk@users.noreply.github.com> Date: Thu, 27 Mar 2025 23:25:11 -0700 Subject: [PATCH 11/19] fix: pluginName option implies pluginSameName is false; --- src/index.ts | 2 +- src/parameters.ts | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index be65fd6..c0e3d51 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,7 +22,7 @@ const options = programSchema.reduce>((acc, s) => const opt = parsedOpts?.[lookup[s.key]]; acc[s.key] = { value: opt ?? (s.initial ?? null), - wasSet: !!opt, + wasSet: opt != null, } return acc; diff --git a/src/parameters.ts b/src/parameters.ts index 5e6e390..ff61808 100644 --- a/src/parameters.ts +++ b/src/parameters.ts @@ -52,6 +52,13 @@ export const programSchema: ProgramSchema[] = [ { key: "pluginSameName", initial: true, + arg: { + type: "option", + flags: "--pluginSameName", + factory(obj) { + obj.hidden = true; + }, + }, prompt: () => ({ type: 'toggle', message: 'Do you want your plugin to have the same name as your project directory?', @@ -69,6 +76,9 @@ export const programSchema: ProgramSchema[] = [ arg: { type: "option", flags: "-p, --pluginName ", + factory(obj) { + obj.implies({ pluginSameName: false }); + }, }, prompt: () => ({ type: (_, values) => values.pluginSameName === false ? 'text' : null, From 16810a1019eaa7ea044cea20efdb8192d4b6e04e Mon Sep 17 00:00:00 2001 From: Pawel Bartusiak <21136755+uFloppyDisk@users.noreply.github.com> Date: Thu, 27 Mar 2025 23:49:26 -0700 Subject: [PATCH 12/19] feat: add author/description/initialVersion flags; --- src/parameters.ts | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/parameters.ts b/src/parameters.ts index ff61808..1afbae4 100644 --- a/src/parameters.ts +++ b/src/parameters.ts @@ -88,22 +88,59 @@ export const programSchema: ProgramSchema[] = [ { key: "pluginAuthor", initial: '', + arg: { + type: "option", + flags: "-a, --pluginAuthor ", + }, prompt: () => ({ type: 'text', message: 'Plugin author', }), }, + { + key: "noPluginAuthor", + description: "Skip prompting for plugin author.", + initial: false, + arg: { + type: "option", + flags: "-A, --noPluginAuthor", + factory(obj) { + obj.implies({ pluginAuthor: '' }) + }, + }, + }, { key: 'pluginDescription', initial: '', + arg: { + type: "option", + flags: "-d, --pluginDescription ", + }, prompt: () => ({ type: 'text', message: 'Plugin description', }), }, + { + key: "noPluginAuthor", + description: "Skip prompting for plugin description.", + initial: false, + arg: { + type: "option", + flags: "-D, --noPluginDescription", + factory(obj) { + obj.implies({ pluginDescription: '' }) + }, + }, + }, { key: 'pluginVersion', + description: "Defaults to '0.0.1'", initial: '0.0.1', + arg: { + type: "option", + flags: "-v, --initialVersion ", + }, prompt: () => ({ type: 'text', message: 'Initial version', From fc73cc3a1a76ddd1f03a1b2840add34970cb6115 Mon Sep 17 00:00:00 2001 From: Pawel Bartusiak <21136755+uFloppyDisk@users.noreply.github.com> Date: Thu, 27 Mar 2025 23:58:44 -0700 Subject: [PATCH 13/19] feat: allow skip prompts for setup tasks with flag; --- src/parameters.ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/parameters.ts b/src/parameters.ts index 1afbae4..cee0810 100644 --- a/src/parameters.ts +++ b/src/parameters.ts @@ -149,6 +149,13 @@ export const programSchema: ProgramSchema[] = [ { key: 'initGitRepo', initial: true, + arg: { + type: "option", + flags: "--initGitRepo", + factory(obj) { + obj.hidden = true; + }, + }, prompt: () => ({ type: 'toggle', message: 'Initialize a git repository?', @@ -159,6 +166,13 @@ export const programSchema: ProgramSchema[] = [ { key: 'setupUsingDotnetCli', initial: true, + arg: { + type: "option", + flags: "--setupUsingDotnetCli", + factory(obj) { + obj.hidden = true; + }, + }, prompt: () => ({ type: 'toggle', message: 'Setup plugin using dotnet?', @@ -189,6 +203,20 @@ export const programSchema: ProgramSchema[] = [ flags: "-I, --forceInteractive", }, }, + { + key: "runAllDefaultTasks", + description: "Skip prompts to run initial setup tasks.", + arg: { + type: "option", + flags: "-y, --runTasks", + factory(obj) { + obj.implies({ + setupUsingDotnetCli: true, + initGitRepo: true, + }); + }, + }, + }, ]; export function addCommandLineArguments( From 9b6162883b71970de0641fdd2000c2f6009f01b2 Mon Sep 17 00:00:00 2001 From: Pawel Bartusiak <21136755+uFloppyDisk@users.noreply.github.com> Date: Fri, 28 Mar 2025 15:46:36 -0700 Subject: [PATCH 14/19] chore(schema): kebab case flags; --- src/parameters.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/parameters.ts b/src/parameters.ts index cee0810..44622c7 100644 --- a/src/parameters.ts +++ b/src/parameters.ts @@ -54,7 +54,7 @@ export const programSchema: ProgramSchema[] = [ initial: true, arg: { type: "option", - flags: "--pluginSameName", + flags: "--plugin-same-name", factory(obj) { obj.hidden = true; }, @@ -75,7 +75,7 @@ export const programSchema: ProgramSchema[] = [ ]), arg: { type: "option", - flags: "-p, --pluginName ", + flags: "-p, --plugin-name ", factory(obj) { obj.implies({ pluginSameName: false }); }, @@ -90,7 +90,7 @@ export const programSchema: ProgramSchema[] = [ initial: '', arg: { type: "option", - flags: "-a, --pluginAuthor ", + flags: "-a, --plugin-author ", }, prompt: () => ({ type: 'text', @@ -103,7 +103,7 @@ export const programSchema: ProgramSchema[] = [ initial: false, arg: { type: "option", - flags: "-A, --noPluginAuthor", + flags: "-A, --no-plugin-author", factory(obj) { obj.implies({ pluginAuthor: '' }) }, @@ -114,7 +114,7 @@ export const programSchema: ProgramSchema[] = [ initial: '', arg: { type: "option", - flags: "-d, --pluginDescription ", + flags: "-d, --plugin-description ", }, prompt: () => ({ type: 'text', @@ -127,7 +127,7 @@ export const programSchema: ProgramSchema[] = [ initial: false, arg: { type: "option", - flags: "-D, --noPluginDescription", + flags: "-D, --no-plugin-description", factory(obj) { obj.implies({ pluginDescription: '' }) }, @@ -139,7 +139,7 @@ export const programSchema: ProgramSchema[] = [ initial: '0.0.1', arg: { type: "option", - flags: "-v, --initialVersion ", + flags: "-v, --initial-version ", }, prompt: () => ({ type: 'text', @@ -151,7 +151,7 @@ export const programSchema: ProgramSchema[] = [ initial: true, arg: { type: "option", - flags: "--initGitRepo", + flags: "--init-git-repo", factory(obj) { obj.hidden = true; }, @@ -168,7 +168,7 @@ export const programSchema: ProgramSchema[] = [ initial: true, arg: { type: "option", - flags: "--setupUsingDotnetCli", + flags: "--setup-using-dotnet-cli", factory(obj) { obj.hidden = true; }, @@ -200,7 +200,7 @@ export const programSchema: ProgramSchema[] = [ description: "Force ask all prompts. Options set via command-line are populated as prompt defaults.", arg: { type: "option", - flags: "-I, --forceInteractive", + flags: "-I, --force-interactive", }, }, { @@ -208,7 +208,7 @@ export const programSchema: ProgramSchema[] = [ description: "Skip prompts to run initial setup tasks.", arg: { type: "option", - flags: "-y, --runTasks", + flags: "-y, --run-tasks", factory(obj) { obj.implies({ setupUsingDotnetCli: true, @@ -240,7 +240,7 @@ export function addCommandLineArguments( if (arg.factory) arg.factory(option, schema); program.addOption(option); - lookup[schema.key] = option.name(); + lookup[schema.key] = option.attributeName(); } } From 4d9415a3c1a05a8a9e4bcdde1c5f97a92e6c12ac Mon Sep 17 00:00:00 2001 From: Pawel Bartusiak <21136755+uFloppyDisk@users.noreply.github.com> Date: Fri, 28 Mar 2025 16:44:35 -0700 Subject: [PATCH 15/19] feat: validate args and options; --- src/index.ts | 7 ------- src/parameters.ts | 23 +++++++++++++++++++++++ 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/index.ts b/src/index.ts index c0e3d51..30c2fc0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -32,13 +32,6 @@ const positionalArgs = programSchema.filter(s => s.arg && s.arg.type === "argume for (const [i, arg] of positionalArgs.entries()) { if (!program.args[i]) continue; - const passed = !!arg.validate ? arg.validate(program.args[i]) : true; - if (typeof passed === "string") { - program.error(`${passed}: ${program.args[i]}`); - } else if (passed === false) { - program.help(); - } - options[arg.key].value = program.args[i]; options[arg.key].wasSet = true; }; diff --git a/src/parameters.ts b/src/parameters.ts index 44622c7..83c5abb 100644 --- a/src/parameters.ts +++ b/src/parameters.ts @@ -223,6 +223,17 @@ export function addCommandLineArguments( program: typeof commanderProgram, optionsSchema: ProgramSchema[], ): Record { + function validateValue(value: string, validateFn: Validation) { + const passedOrError = validateFn(value); + if (typeof passedOrError === "string") { + program.error(`${passedOrError}: ${value}`); + } + + if (passedOrError === false) { + program.help(); + } + } + const lookup: Record = {}; for (const schema of optionsSchema) { @@ -231,12 +242,24 @@ export function addCommandLineArguments( const arg = schema.arg; if ("argument" === arg.type) { const argument = new Argument(arg.name ?? schema.key, schema.description); + if (!!schema.validate) + argument.argParser((value) => { + validateValue(value, schema.validate!); + return value; + }); + if (arg.factory) arg.factory(argument, schema); program.addArgument(argument); lookup[schema.key] = argument.name(); } else if ("option" === arg.type) { const option = new Option(arg.flags, schema.description); + if (!!schema.validate) + option.argParser((value) => { + validateValue(value, schema.validate!); + return value; + }); + if (arg.factory) arg.factory(option, schema); program.addOption(option); From 5fdcd1a1c41fad1ff38bfb663fe31cd4aa4c99af Mon Sep 17 00:00:00 2001 From: Pawel Bartusiak <21136755+uFloppyDisk@users.noreply.github.com> Date: Fri, 28 Mar 2025 17:00:13 -0700 Subject: [PATCH 16/19] refactor(addCommandLineArguments): DRY; --- src/parameters.ts | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/src/parameters.ts b/src/parameters.ts index 83c5abb..f7e30eb 100644 --- a/src/parameters.ts +++ b/src/parameters.ts @@ -239,28 +239,32 @@ export function addCommandLineArguments( for (const schema of optionsSchema) { if (!schema.arg) continue; - const arg = schema.arg; - if ("argument" === arg.type) { - const argument = new Argument(arg.name ?? schema.key, schema.description); - if (!!schema.validate) - argument.argParser((value) => { - validateValue(value, schema.validate!); - return value; - }); + const argDef = schema.arg; + const argOrOption = (() => { + if (argDef.type === "argument") + return new Argument(argDef.name ?? schema.key, schema.description); + + if (argDef.type === "option") + return new Option(argDef.flags, schema.description); + })(); + + if (!argOrOption) continue; - if (arg.factory) arg.factory(argument, schema); + if (!!schema.validate) + argOrOption.argParser((value) => { + validateValue(value, schema.validate!); + return value; + }); + + if (argDef.type === "argument") { + const argument = argOrOption as Argument; + if (argDef.factory) argDef.factory(argument, schema); program.addArgument(argument); lookup[schema.key] = argument.name(); - } else if ("option" === arg.type) { - const option = new Option(arg.flags, schema.description); - if (!!schema.validate) - option.argParser((value) => { - validateValue(value, schema.validate!); - return value; - }); - - if (arg.factory) arg.factory(option, schema); + } else if (argDef.type === "option") { + const option = argOrOption as Option; + if (argDef.factory) argDef.factory(option, schema); program.addOption(option); lookup[schema.key] = option.attributeName(); From cf4bf6608febf94c1823d75cc3fb000995ff05a0 Mon Sep 17 00:00:00 2001 From: Pawel Bartusiak <21136755+uFloppyDisk@users.noreply.github.com> Date: Fri, 28 Mar 2025 17:13:38 -0700 Subject: [PATCH 17/19] fix: prompt preanswer regression when no args passed; --- src/index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 30c2fc0..8e85f69 100644 --- a/src/index.ts +++ b/src/index.ts @@ -40,8 +40,12 @@ for (const [i, arg] of positionalArgs.entries()) { if (options["forceInteractive"].value) return; let preanswered = Object.entries(options); + + const wereSet = preanswered.filter(o => o[1].wasSet); + if (wereSet.length <= 0) return; + if (options["interactive"].value) { - preanswered = preanswered.filter(o => o[1].wasSet); + preanswered = wereSet; } preanswered = preanswered.map(o => [o[0], o[1].value]); From 30e64b86f59f7fab15e0954ab2865b491c40fa05 Mon Sep 17 00:00:00 2001 From: Pawel Bartusiak <21136755+uFloppyDisk@users.noreply.github.com> Date: Fri, 28 Mar 2025 17:17:37 -0700 Subject: [PATCH 18/19] refactor(addCommandLineArguments): remove unnecessary function; --- src/parameters.ts | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/parameters.ts b/src/parameters.ts index f7e30eb..bd8c290 100644 --- a/src/parameters.ts +++ b/src/parameters.ts @@ -223,17 +223,6 @@ export function addCommandLineArguments( program: typeof commanderProgram, optionsSchema: ProgramSchema[], ): Record { - function validateValue(value: string, validateFn: Validation) { - const passedOrError = validateFn(value); - if (typeof passedOrError === "string") { - program.error(`${passedOrError}: ${value}`); - } - - if (passedOrError === false) { - program.help(); - } - } - const lookup: Record = {}; for (const schema of optionsSchema) { @@ -252,7 +241,15 @@ export function addCommandLineArguments( if (!!schema.validate) argOrOption.argParser((value) => { - validateValue(value, schema.validate!); + const passedOrError = schema.validate!(value); + if (typeof passedOrError === "string") { + program.error(`${passedOrError}: ${value}`); + } + + if (passedOrError === false) { + program.help(); + } + return value; }); From 6b6b3329c327d47153f1045a7bb2341a30bd6427 Mon Sep 17 00:00:00 2001 From: Pawel Bartusiak <21136755+uFloppyDisk@users.noreply.github.com> Date: Fri, 28 Mar 2025 17:36:39 -0700 Subject: [PATCH 19/19] chore(cli-args): clarify -y flag effect in long name and description; --- src/parameters.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/parameters.ts b/src/parameters.ts index bd8c290..c4ff0d5 100644 --- a/src/parameters.ts +++ b/src/parameters.ts @@ -205,10 +205,10 @@ export const programSchema: ProgramSchema[] = [ }, { key: "runAllDefaultTasks", - description: "Skip prompts to run initial setup tasks.", + description: "Skip all initial setup task prompts and run them. Ex. `git init`", arg: { type: "option", - flags: "-y, --run-tasks", + flags: "-y, --run-all-tasks", factory(obj) { obj.implies({ setupUsingDotnetCli: true,