From 1b77803da09500cfb47ff4707a68d46ff1ece0a7 Mon Sep 17 00:00:00 2001 From: Craigory Coppola Date: Tue, 10 Sep 2024 00:13:05 -0400 Subject: [PATCH] chore(core): replace commander and ts-command-line-args with cli-forge --- packages/core/package.json | 3 +- packages/core/src/cli/lib/addDebugOption.ts | 23 +- packages/core/src/cli/lib/cli-options.ts | 42 ++- packages/core/src/cli/lib/params.global.ts | 116 ++++----- packages/core/src/cli/lib/params.merge.ts | 164 ++++++------ packages/core/src/cli/lib/params.ts | 43 +-- packages/core/src/cli/stitch-add-files.ts | 56 ++-- packages/core/src/cli/stitch-add-sounds.ts | 73 +++--- packages/core/src/cli/stitch-add-sprites.ts | 136 +++++----- packages/core/src/cli/stitch-add.ts | 24 +- packages/core/src/cli/stitch-archive.ts | 34 +-- packages/core/src/cli/stitch-debork.ts | 34 +-- packages/core/src/cli/stitch-issues-create.ts | 246 +++++++++--------- packages/core/src/cli/stitch-issues-open.ts | 78 +++--- packages/core/src/cli/stitch-issues-submit.ts | 64 +++-- packages/core/src/cli/stitch-issues.ts | 22 +- packages/core/src/cli/stitch-lint.ts | 60 ++--- packages/core/src/cli/stitch-merge.ts | 38 +-- packages/core/src/cli/stitch-open.ts | 149 +++++------ .../core/src/cli/stitch-set-audio-group.ts | 50 ++-- .../core/src/cli/stitch-set-texture-group.ts | 50 ++-- packages/core/src/cli/stitch-set-version.ts | 31 +-- packages/core/src/cli/stitch-set.ts | 17 +- packages/core/src/cli/stitch.ts | 56 ++-- pnpm-lock.yaml | 25 ++ 25 files changed, 806 insertions(+), 828 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index e80ae728..7faed31c 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -47,7 +47,7 @@ "types": "dist/index.d.ts", "bin": "./stitch.mjs", "scripts": { - "build": "tsc --build", + "build": "tsc --build tsconfig.json", "clean": "rimraf build dist *.tsbuildinfo **/*.tsbuildinfo", "test": "mocha --config ../../config/.mocharc.cjs --parallel=false --timeout=30000", "test:dev": "mocha --config ../../config/.mocharc.cjs --forbid-only=false --parallel=false --timeout=9999999999", @@ -68,6 +68,7 @@ "archiver": "6.0.1", "chalk": "5.3.0", "change-case": "5.1.2", + "cli-forge": "0.8.0", "commander": "11.1.0", "debug": "4.3.4", "fs-extra": "11.1.1", diff --git a/packages/core/src/cli/lib/addDebugOption.ts b/packages/core/src/cli/lib/addDebugOption.ts index 82802e80..a0399bd7 100644 --- a/packages/core/src/cli/lib/addDebugOption.ts +++ b/packages/core/src/cli/lib/addDebugOption.ts @@ -1,16 +1,17 @@ -import type { Command } from 'commander'; import { debug } from '../../utility/log.js'; +import { makeComposableBuilder } from 'cli-forge'; -export function addDebugOptions(cli: Command) { - return cli - .option( - '--debug', - 'Run in debug mode, which writes more logs to help triangulate bugs.', - ) - .action(function (options) { - if (options.debug) { +export const addDebugOptions = makeComposableBuilder((cli) => + cli + .option('debug', { + type: 'boolean', + description: + 'Run in debug mode, which writes more logs to help triangulate bugs.', + }) + .middleware((args) => { + if (args.debug) { process.env.DEBUG = 'true'; debug('Running in debug mode'); } - }); -} + }), +); diff --git a/packages/core/src/cli/lib/cli-options.ts b/packages/core/src/cli/lib/cli-options.ts index f99953aa..a7a248b7 100644 --- a/packages/core/src/cli/lib/cli-options.ts +++ b/packages/core/src/cli/lib/cli-options.ts @@ -1,26 +1,40 @@ import { oneline } from '@bscotch/utility'; +import { makeComposableBuilder } from 'cli-forge'; -export default { - force: [ - '-f --force', - oneline` +export const force = makeComposableBuilder((argv) => + argv.option('force', { + type: 'boolean', + alias: ['f'], + description: oneline` Bypass safety checks, including the normal requirement that the project be in a clean git state. Only use this option if you know what you're doing. `, - ], - targetProject: [ - '-t --target-project ', - oneline` + }), +); +export const targetProject = makeComposableBuilder((argv) => + argv.option('targetProject', { + type: 'string', + alias: ['t'], + description: oneline` Path to the target GameMaker Studio 2 project. If not set, will auto-search the current directory. `, - ], - watch: [ - '--watch', - oneline` + }), +); +export const watch = makeComposableBuilder((argv) => + argv.option('watch', { + type: 'boolean', + alias: ['w'], + description: oneline` Run the command with a watcher, so that it will re-run any time there is a change to the source files that might warrant a re-run. `, - ], -} as const; + }), +); + +export default { + force, + targetProject, + watch, +}; diff --git a/packages/core/src/cli/lib/params.global.ts b/packages/core/src/cli/lib/params.global.ts index 2793c79d..3ded6ad5 100644 --- a/packages/core/src/cli/lib/params.global.ts +++ b/packages/core/src/cli/lib/params.global.ts @@ -1,54 +1,50 @@ import { oneline } from '@bscotch/utility'; -import type { ArgumentConfig } from 'ts-command-line-args'; -import type { - StitchCliGlobalParams, - StitchCliTargetParams, -} from './params.types.js'; +import { makeComposableBuilder } from 'cli-forge'; const globalParamsGroup = 'General Options'; -export const targetProjectParam: ArgumentConfig<{ targetProject?: string }> = { - targetProject: { - alias: 't', - type: String, - optional: true, - defaultValue: process.cwd(), +export const withTargetProjectParam = makeComposableBuilder((args) => + args.option('targetProject', { + alias: ['t'], + type: 'string', + default: { + value: process.cwd(), + description: 'Current directory', + }, description: oneline` - Path to the target GameMaker Studio 2 project. - If not set, will auto-search the current directory. - `, + Path to the target GameMaker Studio 2 project. + If not set, will auto-search the current directory. + `, group: globalParamsGroup, - }, -}; + }), +); -export const targetParams: ArgumentConfig = { - ...targetProjectParam, - force: { - alias: 'f', - type: Boolean, - optional: true, - description: oneline` - Bypass safety checks, including the normal requirement that the project be - in a clean git state. Only use this option if you know what you're doing. - `, - group: globalParamsGroup, - }, - readOnly: { - type: Boolean, - optional: true, - description: oneline` - Prevent any file-writes from occurring. Useful to prevent - automatic fixes from being applied and for testing purposes. - Commands may behave unexpectedly when this option is enabled. - `, - group: globalParamsGroup, - }, -}; +export const withTargetParams = makeComposableBuilder((args) => + withTargetProjectParam(args) + .option('force', { + alias: ['f'], + type: 'boolean', + description: oneline` + Bypass safety checks, including the normal requirement that the project be + in a clean git state. Only use this option if you know what you're doing. + `, + group: globalParamsGroup, + }) + .option('readOnly', { + type: 'boolean', + description: oneline` + Prevent any file-writes from occurring. Useful to prevent + automatic fixes from being applied and for testing purposes. + Commands may behave unexpectedly when this option is enabled. + `, + group: globalParamsGroup, + }), +); -export const watchParam: ArgumentConfig<{ watch?: boolean }> = { - watch: { - alias: 'w', - type: Boolean, +export const withWatchParam = makeComposableBuilder((args) => + args.option('watch', { + alias: ['w'], + type: 'boolean', optional: true, description: oneline` Run the command with a watcher, so that it will re-run @@ -56,21 +52,21 @@ export const watchParam: ArgumentConfig<{ watch?: boolean }> = { warrant a re-run. `, group: globalParamsGroup, - }, -}; + }), +); -export const globalParams: ArgumentConfig = { - help: { - alias: 'h', - type: Boolean, - optional: true, - group: globalParamsGroup, - }, - debug: { - alias: 'd', - type: Boolean, - optional: true, - defaultValue: !!process.env.DEBUG, - group: globalParamsGroup, - }, -}; +export const withGlobalParams = makeComposableBuilder((args) => + args + .option('help', { + alias: ['h'], + type: 'boolean', + description: 'Show help', + group: globalParamsGroup, + }) + .option('debug', { + alias: ['d'], + type: 'boolean', + description: 'Run in debug mode', + group: globalParamsGroup, + }), +); diff --git a/packages/core/src/cli/lib/params.merge.ts b/packages/core/src/cli/lib/params.merge.ts index 592c2fc1..c8078d53 100644 --- a/packages/core/src/cli/lib/params.merge.ts +++ b/packages/core/src/cli/lib/params.merge.ts @@ -1,33 +1,28 @@ import { oneline } from '@bscotch/utility'; -import { ArgumentConfig } from 'ts-command-line-args'; import { Gms2ResourceArray, Gms2ResourceType, } from '../../lib/components/Gms2ResourceArray.js'; -import { - ClobberAction, - StitchMergerOptions, -} from '../../lib/StitchProjectMerger.js'; import { assert } from '../../utility/errors.js'; -import type { Gms2MergeCliSourceOptions } from './merge.js'; import { parseGitHubString } from './parseGitHubString.js'; +import { makeComposableBuilder } from 'cli-forge'; const mergeSourceGroup = 'Merge Source'; -export const mergeSourceParams: ArgumentConfig = { - source: { - alias: 's', - type: String, - optional: true, - group: mergeSourceGroup, - description: 'Local path to the source GameMaker Studio 2 project.', - }, - sourceGithub: { - alias: 'g', - type: parseGitHubString, - optional: true, - group: mergeSourceGroup, - description: oneline` +export const withMergeSourceParams = makeComposableBuilder((argv) => + argv + .option('source', { + alias: ['s'], + type: 'string', + group: mergeSourceGroup, + description: 'Local path to the source GameMaker Studio 2 project.', + }) + .option('sourceGithub', { + alias: ['g'], + type: 'string', + coerce: parseGitHubString, + group: mergeSourceGroup, + description: oneline` Repo owner and name for a Gamemaker Studio 2 project on GitHub in format \`owner/repo@revision\`. The revision suffix is optional, and @@ -41,50 +36,47 @@ export const mergeSourceParams: ArgumentConfig = { tagPattern is provided, Stitch uses HEAD. To provide credentials for private GitHub repos, see the README. `, - }, - sourceUrl: { - alias: 'u', - type: String, - optional: true, - group: mergeSourceGroup, - description: 'URL to a zipped GameMaker Studio 2 project.', - }, -}; + }) + .option('sourceUrl', { + alias: ['u'], + type: 'string', + group: mergeSourceGroup, + description: 'URL to a zipped GameMaker Studio 2 project.', + }), +); const mergeOptionsGroup = 'Merge Options'; -export const mergeOptionsParams: ArgumentConfig = { - ifFolderMatches: { - type: String, - optional: true, - multiple: true, - group: mergeOptionsGroup, - description: oneline` +export const withMergeOptionsParams = makeComposableBuilder((argv) => + argv + .option('ifFolderMatches', { + type: 'array', + items: 'string', + group: mergeOptionsGroup, + description: oneline` List of source folder patterns that, if matched, should have all child assets imported (recursive). Will be passed to \`new RegExp()\` and tested against the parent folder of every source resource. Independent from ifNameMatches. Case is ignored. `, - }, - ifNameMatches: { - type: String, - optional: true, - multiple: true, - group: mergeOptionsGroup, - description: oneline` + }) + .option('ifNameMatches', { + type: 'array', + items: 'string', + group: mergeOptionsGroup, + description: oneline` List of source resource name patterns that, if matched, should have all child assets imported (recursive). Will be passed to \`new RegExp()\` and tested against the name of every source resource. Independent from ifFolderMatches. Case is ignored. `, - }, - moveConflicting: { - type: Boolean, - optional: true, - group: mergeOptionsGroup, - description: oneline` + }) + .option('moveConflicting', { + type: 'boolean', + group: mergeOptionsGroup, + description: oneline` The target project may have assets matching your merge pattern, but that aren't in the source. By default these are left alone, which can create some @@ -95,19 +87,13 @@ export const mergeOptionsParams: ArgumentConfig = { this flag if your source and target projects are using unique folder names for their assets. `, - }, - onClobber: { - type: (input: string) => { - assert( - ['error', 'skip', 'overwrite'].includes(input), - 'Invalid onClobber value', - ); - return input as ClobberAction; - }, - optional: true, - defaultValue: 'overwrite', - group: mergeOptionsGroup, - description: oneline` + }) + .option('onClobber', { + type: 'string', + defaultValue: 'overwrite', + group: mergeOptionsGroup, + choices: ['overwrite', 'skip', 'error'], + description: oneline` If source assets match target assets by name, but those matching assets are not matched by the merge options, it's possible that the two assets are not @@ -115,44 +101,44 @@ export const mergeOptionsParams: ArgumentConfig = { You can change the behavior to instead skip importing those assets (keeping the target version) or throw an error. `, - }, - skipDependencyCheck: { - type: Boolean, - optional: true, - group: mergeOptionsGroup, - description: oneline` + }) + .option('skipDependencyCheck', { + type: 'boolean', + group: mergeOptionsGroup, + description: oneline` If an object in your source has dependencies (parent objects or sprites) that are *not* being merged, import will be blocked. This prevents accidentally importing broken assets. If you know that those missing dependencies will be found in the target project, you can skip this check. `, - }, - skipIncludedFiles: { - type: Boolean, - optional: true, - group: mergeOptionsGroup, - description: oneline` + }) + .option('skipIncludedFiles', { + type: 'boolean', + group: mergeOptionsGroup, + description: oneline` By default, "Included Files" are also merged if they match filters. These can be skipped. `, - }, - types: { - type: (input: string) => { - assert( - Gms2ResourceArray.resourceTypeNames.includes(input as any), - `Invalid resource type: ${input}`, - ); - return input as Gms2ResourceType; - }, - optional: true, - multiple: true, - group: mergeOptionsGroup, - description: oneline` + }) + .option('types', { + type: 'array', + items: 'string', + group: mergeOptionsGroup, + description: oneline` All resource types are included by default. You can optionally change to a whitelist pattern and only include specific types. Types are: ${Object.keys(Gms2ResourceArray.resourceClassMap).join(', ')} `, - }, -}; + coerce: (inputs: string[]) => { + return inputs.map((input) => { + assert( + Gms2ResourceArray.resourceTypeNames.includes(input as any), + `Invalid resource type: ${input}`, + ); + return input as Gms2ResourceType; + }); + }, + }), +); diff --git a/packages/core/src/cli/lib/params.ts b/packages/core/src/cli/lib/params.ts index 439684ba..38f12c34 100644 --- a/packages/core/src/cli/lib/params.ts +++ b/packages/core/src/cli/lib/params.ts @@ -1,9 +1,5 @@ -import { keysOf } from '@bscotch/utility'; -import { ArgumentConfig, parse as parseArgs } from 'ts-command-line-args'; import { StitchProject } from '../../index.js'; -import { assert } from '../../utility/errors.js'; -import { globalParams } from './params.global.js'; -import { StitchCliParams, StitchCliTargetParams } from './params.types.js'; +import { StitchCliTargetParams } from './params.types.js'; export * from './params.global.js'; export * from './params.merge.js'; @@ -18,40 +14,3 @@ export async function loadProjectFromArgs( readOnly: options.readOnly, }); } - -export function parseStitchArgs>( - args: ArgumentConfig, - info: { - title: string; - description: string; - }, -): StitchCliParams { - const argsConfig: ArgumentConfig> = { - ...args, - ...globalParams, - }; - const argNames = keysOf(argsConfig); - const groups: string[] = argNames.map((name) => { - // @ts-expect-error - const { group } = argsConfig[name]; - assert( - group, - `Argument definition for ${name as any} does not have a group.`, - ); - return group; - }); - return parseArgs>(argsConfig, { - // @ts-expect-error - helpArg: 'help', - headerContentSections: [ - { - header: info.title, - content: info.description, - }, - ], - optionSections: [...new Set(groups)].map((groupName) => ({ - group: groupName, - header: groupName, - })), - }); -} diff --git a/packages/core/src/cli/stitch-add-files.ts b/packages/core/src/cli/stitch-add-files.ts index 79be38ed..0b7478c2 100644 --- a/packages/core/src/cli/stitch-add-files.ts +++ b/packages/core/src/cli/stitch-add-files.ts @@ -1,35 +1,41 @@ #!/usr/bin/env node import { oneline, undent } from '@bscotch/utility'; -import { program as cli } from 'commander'; -import { ImportBaseOptions } from './lib/add-base-options.js'; +import { cli, chain } from 'cli-forge'; import importFiles from './lib/add-files.js'; import { addDebugOptions } from './lib/addDebugOption.js'; -import options from './lib/cli-options.js'; +import * as options from './lib/cli-options.js'; import { runOrWatch } from './watch.js'; -cli - .description( - undent` +export const addFilesCommand = cli('files', { + description: undent` Create/update included file assets from a file or a path. If the asset does not already exists in the target project, it will be placed in the "NEW" folder. Otherwise, the asset will be replaced by the source asset.`, - ) - .requiredOption( - '--source ', - oneline` - Path to the file or the folder containing the files to import. - `, - ) - .option( - '--extensions ', - oneline` - Only allow certain extensions to be imported. - If not set, Will attempt to import all files. - `, - ) - .option(...options.targetProject) - .option(...options.force); -addDebugOptions(cli).parse(process.argv); -const opts = cli.opts() as ImportBaseOptions & { extensions?: string }; -runOrWatch(opts, () => importFiles(opts), opts.source, opts.extensions); + builder: (argv) => + chain(argv, options.force, options.targetProject, addDebugOptions) + .option('extensions', { + type: 'array', + items: 'string', + description: oneline` + Only allow certain extensions to be imported. + If not set, Will attempt to import all files. + `, + }) + .option('source', { + type: 'string', + description: oneline` + Path to the file or the folder containing the files to import. + `, + }), + handler: (argv) => { + runOrWatch( + // argv doesn't have watch, so we are casting it here... I don't love that, + // but I think adding watch wouldn't make sense here either. + argv as { watch?: boolean }, + () => importFiles(argv), + argv.source, + argv.extensions, + ); + }, +}); diff --git a/packages/core/src/cli/stitch-add-sounds.ts b/packages/core/src/cli/stitch-add-sounds.ts index 3c95af58..5a0a6c8b 100644 --- a/packages/core/src/cli/stitch-add-sounds.ts +++ b/packages/core/src/cli/stitch-add-sounds.ts @@ -1,44 +1,49 @@ #!/usr/bin/env node import { oneline, undent } from '@bscotch/utility'; -import { program as cli } from 'commander'; import { StitchProject } from '../lib/StitchProject.js'; -import { ImportBaseOptions } from './lib/add-base-options.js'; import importSounds from './lib/add-sounds.js'; import { addDebugOptions } from './lib/addDebugOption.js'; -import options from './lib/cli-options.js'; +import * as options from './lib/cli-options.js'; import { runOrWatch } from './watch.js'; +import { cli, chain } from 'cli-forge'; -cli - .description( - undent` +export const addSoundsCommand = cli('sounds', { + description: undent` Create/update sound assets from a file or a path. If the asset does not already exists in the target project, it will be placed in the "NEW" folder. Otherwise, the asset will be replaced by the source asset.`, - ) - .requiredOption( - '--source ', - oneline` - Path to the sound file or the folder containing the sounds files. - `, - ) - .option( - '--extensions ', - oneline` - input one or more of the supported extensions: mp3, wav, ogg, wma. - If not set, Will attempt to import all supported extensions. - `, - ) - .option(...options.targetProject) - .option(...options.force) - .option(...options.watch); -addDebugOptions(cli).parse(process.argv); - -const opts = cli.opts() as ImportBaseOptions & { extensions?: string }; -runOrWatch( - opts, - () => importSounds(opts), - opts.source, - opts.extensions - ? opts.extensions.split(',') - : StitchProject.supportedSoundFileExtensions, -); + builder: (argv) => + chain( + argv, + addDebugOptions, + options.force, + options.targetProject, + options.watch, + ) + .option('source', { + type: 'string', + required: true, + description: oneline` + Path to the sound file or the folder containing the sounds files. + `, + }) + .option('extensions', { + type: 'array', + items: 'string', + description: oneline` + input one or more of the supported extensions: mp3, wav, ogg, wma. + If not set, Will attempt to import all supported extensions. + `, + }), + // eslint-disable-next-line @typescript-eslint/no-empty-function + handler: (argv) => { + runOrWatch( + argv, + () => importSounds(argv), + argv.source, + argv.extensions + ? argv.extensions + : StitchProject.supportedSoundFileExtensions, + ); + }, +}); diff --git a/packages/core/src/cli/stitch-add-sprites.ts b/packages/core/src/cli/stitch-add-sprites.ts index 7aaabcef..6171ce34 100644 --- a/packages/core/src/cli/stitch-add-sprites.ts +++ b/packages/core/src/cli/stitch-add-sprites.ts @@ -1,16 +1,14 @@ #!/usr/bin/env node import { oneline, undent } from '@bscotch/utility'; -import { program } from 'commander'; -import { SpriteImportOptions } from '../lib/StitchProject.js'; -import { ImportBaseOptions } from './lib/add-base-options.js'; + import importSprites from './lib/add-sprites.js'; import { addDebugOptions } from './lib/addDebugOption.js'; import options from './lib/cli-options.js'; import { runOrWatch } from './watch.js'; +import { cli, chain } from 'cli-forge'; -program - .description( - undent` +export const addSpritesCommand = cli('sprites', { + description: undent` Create/update sprite assets collection of images. A 'sprite' source is any folder whose immediate children are all PNGs with identical dimensions. Sprites can be @@ -18,66 +16,66 @@ program If the asset does not already exists in the target project, it will be placed in the "NEW" folder. Otherwise, the asset will be replaced by the source asset.`, - ) - .requiredOption( - '--source ', - oneline` - Path to the sprite folder or root folder containing multiple sprites. - `, - ) - .option(...options.targetProject) - .option( - '--prefix ', - oneline` - Prefix the source names when creating/updateing sprites - based on the source folders. Prefixing is performed after - casing, so it will be used as-is. - `, - ) - .option( - '--postfix ', - oneline` - Postfix the source names when creating/updateing sprites - based on the source folders. Postfixing is performed after - casing, so it will be used as-is. - `, - ) - .option( - '--case ', - oneline` - Normalize the casing upon import. This ensures consistent - casing of assets even if the source is either inconsistent - or uses a different casing than intended in the game project. - `, - 'keep', - ) - .option( - '--flatten', - oneline` - By default each sprite resource is named by its final folder - (e.g. a sprite at 'root/my/sprite' will be called 'sprite'). - Use this flag to convert the entire post-root path to the - sprite's name (e.g. 'root/my/sprite' will be called 'my_sprite' - if using snake case). - `, - ) - .option( - '--exclude ', - oneline` - The provided pattern will be converted to a RegEx using - JavaScript's \`new RegExp()\` function. Any sprites whose - *original* names match the pattern will not be imported. - `, - ) - .option(...options.force) - .option(...options.watch); -addDebugOptions(program).parse(process.argv); - -const opts = program.opts(); - -runOrWatch( - opts, - () => importSprites(opts as ImportBaseOptions & SpriteImportOptions), - opts.source, - 'png', -); + builder: (argv) => + chain( + argv, + addDebugOptions, + options.force, + options.targetProject, + options.watch, + ) + .option('source', { + type: 'string', + required: true, + description: oneline` + Path to the sprite folder or root folder containing multiple sprites. + `, + }) + .option('prefix', { + type: 'string', + description: oneline` + Prefix the source names when creating/updateing sprites + based on the source folders. Prefixing is performed after + casing, so it will be used as-is. + `, + }) + .option('postfix', { + type: 'string', + description: oneline` + Postfix the source names when creating/updateing sprites + based on the source folders. Postfixing is performed after + casing, so it will be used as-is. + `, + }) + .option('case', { + type: 'string', + description: oneline` + Normalize the casing upon import. This ensures consistent + casing of assets even if the source is either inconsistent + or uses a different casing than intended in the game project. + `, + default: 'keep', + choices: ['keep', 'snake', 'camel', 'pascal'], + }) + .option('flatten', { + type: 'boolean', + description: oneline` + By default each sprite resource is named by its final folder + (e.g. a sprite at 'root/my/sprite' will be called 'sprite'). + Use this flag to convert the entire post-root path to the + sprite's name (e.g. 'root/my/sprite' will be called 'my_sprite' + if using snake case). + `, + }) + .option('exclude', { + type: 'string', + description: oneline` + The provided pattern will be converted to a RegEx using + JavaScript's \`new RegExp()\` function. Any sprites whose + *original* names match the pattern will not be imported. + `, + }), + handler: (opts) => { + runOrWatch(opts, () => importSprites(opts), opts.source, 'png'); + }, +}); diff --git a/packages/core/src/cli/stitch-add.ts b/packages/core/src/cli/stitch-add.ts index c98e34de..727aa019 100644 --- a/packages/core/src/cli/stitch-add.ts +++ b/packages/core/src/cli/stitch-add.ts @@ -1,15 +1,13 @@ #!/usr/bin/env node -import { program as cli } from 'commander'; +import { cli } from 'cli-forge'; -cli - .description('Create GameMaker Studio 2 resources.') - .command( - 'sounds', - 'Create sound assets from a file or a path to a target project.', - ) - .command('sprites', 'Create sprite assets from a collection of images.') - .command( - 'files', - 'Create included files assets from a file or a path to a target project.', - ) - .parse(process.argv); +import { addFilesCommand } from './stitch-add-files.js'; +import { addSoundsCommand } from './stitch-add-sounds.js'; +import { addSpritesCommand } from './stitch-add-sprites.js'; + +export const addCommand = cli('add', { + description: + 'Create assets (e.g. sprites) using external resources (e.g. images).', + builder: (cli) => + cli.commands(addFilesCommand, addSoundsCommand, addSpritesCommand), +}); diff --git a/packages/core/src/cli/stitch-archive.ts b/packages/core/src/cli/stitch-archive.ts index 42204933..eaeee39f 100644 --- a/packages/core/src/cli/stitch-archive.ts +++ b/packages/core/src/cli/stitch-archive.ts @@ -1,26 +1,20 @@ #!/usr/bin/env node +import { cli, chain } from 'cli-forge'; import { - globalParams, loadProjectFromArgs, - parseStitchArgs, - targetProjectParam, + withGlobalParams, + withTargetProjectParam, } from './lib/params.js'; -const args = parseStitchArgs( - { - ...globalParams, - ...targetProjectParam, +export const archiveCommand = cli('archive', { + description: `Create a .yyz archive of a GameMaker project.`, + builder: (cli) => chain(cli, withGlobalParams, withTargetProjectParam), + handler: async (args) => { + // Note: Adding an issue template doesn't have any impact + // on project state, since it could be submitted at some + // other time, so we can always bypass the dirty working dir + // check. + const targetProject = await loadProjectFromArgs({ ...args, force: true }); + await targetProject.exportYyz(); }, - { - title: 'Archive', - description: `Create a .yyz archive of a GameMaker project. This is useful for sharing a project with others, including for GameMaker support tickets.`, - }, -); - -// Note: Adding an issue template doesn't have any impact -// on project state, since it could be submitted at some -// other time, so we can always bypass the dirty working dir -// check. -const targetProject = await loadProjectFromArgs({ ...args, force: true }); - -await targetProject.exportYyz(); +}); diff --git a/packages/core/src/cli/stitch-debork.ts b/packages/core/src/cli/stitch-debork.ts index 0396be0a..8e8f367d 100644 --- a/packages/core/src/cli/stitch-debork.ts +++ b/packages/core/src/cli/stitch-debork.ts @@ -1,21 +1,23 @@ #!/usr/bin/env node -import { program as cli } from 'commander'; +import { cli, chain } from 'cli-forge'; import { StitchProject } from '../lib/StitchProject.js'; import { addDebugOptions } from './lib/addDebugOption.js'; import options from './lib/cli-options.js'; +import { oneline } from '@bscotch/utility'; -cli - .description( - 'Fix and normalize common issues in a GameMaker Studio 2.3+ Project.', - ) - .option(...options.targetProject) - .option(...options.force); -addDebugOptions(cli).parse(process.argv); - -const opts = cli.opts(); -( - await StitchProject.load({ - projectPath: opts.targetProject, - dangerouslyAllowDirtyWorkingDir: opts.force, - }) -).save(); +export const deborkCommand = cli('debork', { + description: oneline` + Run Stitch on the project without making any changes, + which will clean up some common issues and normalize the file content. +`, + builder: (cli) => + chain(cli, options.targetProject, options.force, addDebugOptions), + handler: async (args) => { + ( + await StitchProject.load({ + projectPath: args.targetProject, + dangerouslyAllowDirtyWorkingDir: args.force, + }) + ).save(); + }, +}); diff --git a/packages/core/src/cli/stitch-issues-create.ts b/packages/core/src/cli/stitch-issues-create.ts index b05170ef..450f50ab 100644 --- a/packages/core/src/cli/stitch-issues-create.ts +++ b/packages/core/src/cli/stitch-issues-create.ts @@ -15,129 +15,137 @@ import { listLocalProjectChoices, openGameMakerIssue, } from './lib/issuesLib.js'; +import { cli } from 'cli-forge'; -const answers = await inquirer.prompt< - { - name?: string; - allPlatforms?: boolean; - template: string; - newTemplate?: string; - } & GameMakerIssueForm ->([ - { - type: 'input', - name: 'summary', - message: 'Title for the issue (short, precise, and descriptive)', - validate(value: string) { - return value?.length < 128 - ? true - : 'Title must be less than 128 characters'; - }, - }, - { - type: 'input', - name: 'description', - message: - 'Fully describe the issue, including information about replication and context.', - }, - { - type: 'list', - name: 'type', - message: 'What type of issue is this?', - choices: Object.entries(issueTypes).map((entry) => ({ - name: entry[1], - value: entry[0], - })), - }, - { - type: 'checkbox', - name: 'affected', - message: 'What features are impacted?', +export const createCommand = cli('create', { + description: 'Create a new GameMaker issue.', + handler: async () => { + const answers = await inquirer.prompt< + { + name?: string; + allPlatforms?: boolean; + template: string; + newTemplate?: string; + } & GameMakerIssueForm + >([ + { + type: 'input', + name: 'summary', + message: 'Title for the issue (short, precise, and descriptive)', + validate(value: string) { + return value?.length < 128 + ? true + : 'Title must be less than 128 characters'; + }, + }, + { + type: 'input', + name: 'description', + message: + 'Fully describe the issue, including information about replication and context.', + }, + { + type: 'list', + name: 'type', + message: 'What type of issue is this?', + choices: Object.entries(issueTypes).map((entry) => ({ + name: entry[1], + value: entry[0], + })), + }, + { + type: 'checkbox', + name: 'affected', + message: 'What features are impacted?', - choices(answers) { - return issueAffectedAreaOptions[answers.type]; - }, - }, - { - type: 'checkbox', - name: 'platforms', - when(answers) { - return !answers.allPlatforms; - }, - message: 'Which platforms are affected?', - choices: issueAffectedPlatforms, - validate(value: string[]) { - return value.length > 0 ? true : 'You must select at least one platform'; - }, - default: issueAffectedPlatforms, - }, - { - type: 'list', - name: 'template', - message: 'Choose a template to clone for this issue.', - async choices() { - const templates = [ - new inquirer.Separator('\n--- Templates ---'), - // Add the default template, shipped with the package - { - name: "🚀 Use Stitch's template", - value: Gms2Project.defaultProjectTemplatePath, + choices(answers) { + return issueAffectedAreaOptions[answers.type]; }, - // Add an option to specify a custom template - { - name: '🆕 Use a new template', - value: '', + }, + { + type: 'checkbox', + name: 'platforms', + when(answers) { + return !answers.allPlatforms; }, - new inquirer.Separator('\n--- Existing Issues ---'), - ...(await listIssueProjectChoices()), - new inquirer.Separator('\n--- Local Projects ---'), - ...(await listLocalProjectChoices()), - ]; - return templates; - }, - }, - { - type: 'input', - name: 'newTemplate', - message: 'Enter the path to the template to use', - when(answers) { - return answers.template === ''; - }, - validate(value: string) { - const templatePath = new Pathy(value); - return templatePath.existsSync() ? true : 'Template does not exist'; - }, - }, - { - type: 'input', - name: 'name', - message: 'Name the new GameMaker project:', - async validate(name: string) { - // Make sure it won't clobber - if (name.length === 0) { - return 'You must enter a name!'; - } - return (await GameMakerIssue.issuesDirectory - .join(kebabCase(name)) - .isEmptyDirectory({ allowNotFound: true })) - ? true - : 'An issue with that name already exists'; - }, - }, -]); + message: 'Which platforms are affected?', + choices: issueAffectedPlatforms, + validate(value: string[]) { + return value.length > 0 + ? true + : 'You must select at least one platform'; + }, + default: issueAffectedPlatforms, + }, + { + type: 'list', + name: 'template', + message: 'Choose a template to clone for this issue.', + async choices() { + const templates = [ + new inquirer.Separator('\n--- Templates ---'), + // Add the default template, shipped with the package + { + name: "🚀 Use Stitch's template", + value: Gms2Project.defaultProjectTemplatePath, + }, + // Add an option to specify a custom template + { + name: '🆕 Use a new template', + value: '', + }, + new inquirer.Separator('\n--- Existing Issues ---'), + ...(await listIssueProjectChoices()), + new inquirer.Separator('\n--- Local Projects ---'), + ...(await listLocalProjectChoices()), + ]; + return templates; + }, + }, + { + type: 'input', + name: 'newTemplate', + message: 'Enter the path to the template to use', + when(answers) { + return answers.template === ''; + }, + validate(value: string) { + const templatePath = new Pathy(value); + return templatePath.existsSync() ? true : 'Template does not exist'; + }, + }, + { + type: 'input', + name: 'name', + message: 'Name the new GameMaker project:', + async validate(name: string) { + // Make sure it won't clobber + if (name.length === 0) { + return 'You must enter a name!'; + } + return (await GameMakerIssue.issuesDirectory + .join(kebabCase(name)) + .isEmptyDirectory({ allowNotFound: true })) + ? true + : 'An issue with that name already exists'; + }, + }, + ]); -const issueProject = await Gms2Project.cloneProject({ - templatePath: (answers.newTemplate || answers.template).toString(), - name: answers.name, - where: GameMakerIssue.issuesDirectory.absolute, -}); + const issueProject = await Gms2Project.cloneProject({ + templatePath: (answers.newTemplate || answers.template).toString(), + name: answers.name, + where: GameMakerIssue.issuesDirectory.absolute, + }); -await issueProject.issue.updateForm({ - affected: answers.affected, - platforms: answers.platforms, - summary: answers.summary, - description: answers.description, - type: answers.type, -}); + await issueProject.issue.updateForm({ + affected: answers.affected, + platforms: answers.platforms, + summary: answers.summary, + description: answers.description, + type: answers.type, + }); -await openGameMakerIssue(issueProject); + await openGameMakerIssue(issueProject); + }, +}); diff --git a/packages/core/src/cli/stitch-issues-open.ts b/packages/core/src/cli/stitch-issues-open.ts index 872cb8de..91103c84 100644 --- a/packages/core/src/cli/stitch-issues-open.ts +++ b/packages/core/src/cli/stitch-issues-open.ts @@ -6,43 +6,51 @@ import { openGameMakerIssue, openPaths, } from './lib/issuesLib.js'; +import { cli } from 'cli-forge'; -const answers = await inquirer.prompt<{ - targetProject?: string; -}>([ - { - type: 'list', - name: 'targetProject', - message: 'Which Issue do you want to open?', - async choices() { - return [ - { - name: '📁 Issues Folder', - value: GameMakerIssue.issuesDirectory.toString({ format: 'win32' }), +export const openCommand = cli('open', { + description: 'Open a GameMaker issue.', + handler: async () => { + const answers = await inquirer.prompt<{ + targetProject?: string; + }>([ + { + type: 'list', + name: 'targetProject', + message: 'Which Issue do you want to open?', + async choices() { + return [ + { + name: '📁 Issues Folder', + value: GameMakerIssue.issuesDirectory.toString({ + format: 'win32', + }), + }, + ...(await listIssueProjectChoices()), + ]; }, - ...(await listIssueProjectChoices()), - ]; - }, - }, -]); + }, + ]); -if (!answers.targetProject) { - process.exit(0); -} + if (!answers.targetProject) { + process.exit(0); + } -if (answers.targetProject.endsWith('.yyp')) { - const issueProject = await StitchProject.load({ - projectPath: answers.targetProject, - dangerouslyAllowDirtyWorkingDir: true, - readOnly: true, - }); + if (answers.targetProject.endsWith('.yyp')) { + const issueProject = await StitchProject.load({ + projectPath: answers.targetProject, + dangerouslyAllowDirtyWorkingDir: true, + readOnly: true, + }); - await openGameMakerIssue(issueProject); -} else { - await openPaths([ - { - path: answers.targetProject, - app: { name: 'explorer' }, - }, - ]); -} + await openGameMakerIssue(issueProject); + } else { + await openPaths([ + { + path: answers.targetProject, + app: { name: 'explorer' }, + }, + ]); + } + }, +}); diff --git a/packages/core/src/cli/stitch-issues-submit.ts b/packages/core/src/cli/stitch-issues-submit.ts index 911254c1..a43948c1 100644 --- a/packages/core/src/cli/stitch-issues-submit.ts +++ b/packages/core/src/cli/stitch-issues-submit.ts @@ -2,38 +2,46 @@ import { default as inquirer } from 'inquirer'; import { StitchProject } from '../index.js'; import { listIssueProjectChoices, openPaths } from './lib/issuesLib.js'; +import { cli } from 'cli-forge'; -const answers = await inquirer.prompt<{ - targetProject?: string; -}>([ - { - type: 'list', - name: 'targetProject', - message: 'Which Issue do you want to submit?', - async choices() { - return await listIssueProjectChoices(); - }, - }, -]); +export const submitCommand = cli('submit', { + description: 'Submit a GameMaker issue.', + handler: async () => { + const answers = await inquirer.prompt<{ + targetProject?: string; + }>([ + { + type: 'list', + name: 'targetProject', + message: 'Which Issue do you want to submit?', + async choices() { + return await listIssueProjectChoices(); + }, + }, + ]); -if (!answers.targetProject) { - process.exit(0); -} + if (!answers.targetProject) { + process.exit(0); + } -const issueProject = await StitchProject.load({ - projectPath: answers.targetProject, - dangerouslyAllowDirtyWorkingDir: true, - readOnly: true, -}); + const issueProject = await StitchProject.load({ + projectPath: answers.targetProject, + dangerouslyAllowDirtyWorkingDir: true, + readOnly: true, + }); -await issueProject.issue.collectLogs(); + await issueProject.issue.collectLogs(); -const report = await issueProject.issue.compileReport(); + const report = await issueProject.issue.compileReport(); -await openPaths([ - { - path: issueProject.issue.attachmentsDirectory.toString({ format: 'win32' }), - app: { name: 'explorer' }, + await openPaths([ + { + path: issueProject.issue.attachmentsDirectory.toString({ + format: 'win32', + }), + app: { name: 'explorer' }, + }, + report.mailTo, + ]); }, - report.mailTo, -]); +}); diff --git a/packages/core/src/cli/stitch-issues.ts b/packages/core/src/cli/stitch-issues.ts index a65c2627..b90dae80 100644 --- a/packages/core/src/cli/stitch-issues.ts +++ b/packages/core/src/cli/stitch-issues.ts @@ -1,19 +1,13 @@ #!/usr/bin/env node import { prettifyErrorTracing, replaceFilePaths } from '@bscotch/validation'; -import { program as cli } from 'commander'; +import { cli } from 'cli-forge'; +import { createCommand } from './stitch-issues-create.js'; +import { openCommand } from './stitch-issues-open.js'; +import { submitCommand } from './stitch-issues-submit.js'; prettifyErrorTracing({ replaceFilePaths }); -// Kick it off -cli - .description('Stitch Issues') - .command( - 'create', - 'Create an issue template for a bug report to submit to GameMaker.', - ) - .command('open', 'Open files and folders from an existing issue project.') - .command( - 'submit', - 'For a completed issue project, collect logs and compile a report for submission to GameMaker. (Only works with GameMaker Enterprise.)', - ) - .parse(); +export const issuesCommand = cli('issues', { + description: 'Create and manage issues to report to GameMaker.', + builder: (cli) => cli.commands(createCommand, openCommand, submitCommand), +}); diff --git a/packages/core/src/cli/stitch-lint.ts b/packages/core/src/cli/stitch-lint.ts index f40ffe77..b6c575fd 100644 --- a/packages/core/src/cli/stitch-lint.ts +++ b/packages/core/src/cli/stitch-lint.ts @@ -1,36 +1,38 @@ #!/usr/bin/env node -import { program as cli } from 'commander'; +import { cli, chain } from 'cli-forge'; import { StitchProject } from '../lib/StitchProject.js'; import cliOptions from './lib/cli-options.js'; -cli - .description('Generate a lint report.') - .option('--suffix', 'Filter out results that do not have the suffix') - .option(...cliOptions.targetProject) - .option(...cliOptions.force) - .parse(process.argv); +export const lintCommand = cli('lint', { + description: 'Generate a lint report for a GameMaker Studio 2 project.', + builder: (cli) => + chain(cli, cliOptions.targetProject, cliOptions.force).option('suffix', { + description: 'Filter out results that do not have the suffix', + type: 'string', + }), + handler: async (opts) => { + const project = await StitchProject.load({ + projectPath: opts.targetProject, + dangerouslyAllowDirtyWorkingDir: opts.force, + }); -const opts = cli.opts(); -const project = await StitchProject.load({ - projectPath: opts.targetProject, - dangerouslyAllowDirtyWorkingDir: opts.force, -}); - -const lintResults = project.lint({ versionSuffix: opts.suffix }); -const { outdatedFunctionReferences, nonreferencedFunctions } = - lintResults.getReport(); -outdatedFunctionReferences?.forEach((outdatedRef) => { - console.log(outdatedRef.name); - console.log(outdatedRef.location.line); - console.log( - `Outdated reference at: ${outdatedRef.name}, line ${outdatedRef.location.line}, column ${outdatedRef.location.column}`, - ); -}); + const lintResults = project.lint({ versionSuffix: opts.suffix }); + const { outdatedFunctionReferences, nonreferencedFunctions } = + lintResults.getReport(); + outdatedFunctionReferences?.forEach((outdatedRef) => { + console.log(outdatedRef.name); + console.log(outdatedRef.location.line); + console.log( + `Outdated reference at: ${outdatedRef.name}, line ${outdatedRef.location.line}, column ${outdatedRef.location.column}`, + ); + }); -nonreferencedFunctions?.forEach((ref) => { - console.log(ref.name); - console.log(ref.location.line); - console.log( - `Non-referenced function at: ${ref.name}, line ${ref.location.line}, column ${ref.location.column}`, - ); + nonreferencedFunctions?.forEach((ref) => { + console.log(ref.name); + console.log(ref.location.line); + console.log( + `Non-referenced function at: ${ref.name}, line ${ref.location.line}, column ${ref.location.column}`, + ); + }); + }, }); diff --git a/packages/core/src/cli/stitch-merge.ts b/packages/core/src/cli/stitch-merge.ts index f7ecabe0..efb8b710 100644 --- a/packages/core/src/cli/stitch-merge.ts +++ b/packages/core/src/cli/stitch-merge.ts @@ -1,24 +1,24 @@ #!/usr/bin/env node -import { Gms2MergeCliOptions, stitchCliMerge } from './lib/merge.js'; +import { cli, chain } from 'cli-forge'; +import { stitchCliMerge } from './lib/merge.js'; import { - globalParams, - mergeOptionsParams, - mergeSourceParams, - parseStitchArgs, - targetParams, + withGlobalParams, + withMergeOptionsParams, + withMergeSourceParams, + withTargetParams, } from './lib/params.js'; -export const args = parseStitchArgs( - { - ...globalParams, - ...targetParams, - ...mergeSourceParams, - ...mergeOptionsParams, +export const mergeCommand = cli('merge', { + description: 'Merge two GameMaker Studio 2 projects together.', + builder: (cli) => + chain( + cli, + withGlobalParams, + withTargetParams, + withMergeSourceParams, + withMergeOptionsParams, + ), + handler: async (args) => { + await stitchCliMerge(args); }, - { - title: 'Stitch Merge', - description: 'Merge assets from one GameMaker project into another.', - }, -); - -await stitchCliMerge(args); +}); diff --git a/packages/core/src/cli/stitch-open.ts b/packages/core/src/cli/stitch-open.ts index ab775a3f..e1d03313 100644 --- a/packages/core/src/cli/stitch-open.ts +++ b/packages/core/src/cli/stitch-open.ts @@ -3,98 +3,71 @@ import { GameMakerLauncher } from '@bscotch/stitch-launcher'; import { ok } from 'assert'; import debug from 'debug'; -import { ArgumentConfig, parse as parseArgs } from 'ts-command-line-args'; import { StitchProjectStatic } from '../lib/StitchProject.static.js'; import { loadProjectFromArgs } from './lib/params.js'; +import { cli } from 'cli-forge'; -const argsConfig: ArgumentConfig<{ - project: string; - ide?: string; - runtime?: string; - programFiles?: string; - help?: boolean; - debug?: boolean; -}> = { - project: { - description: 'The path to the GameMaker Studio 2 project to open.', - group: 'Open', - defaultValue: process.cwd(), - defaultOption: true, - type: String, - }, - ide: { - description: - 'The IDE version to use. Defaults to the last one used to open the project.', - group: 'Open', - optional: true, - type: String, - }, - runtime: { - description: 'The runtime version to use.', - group: 'Open', - type: String, - optional: true, - }, - programFiles: { - description: - 'If you have installed GameMaker to somewhere besides the default C:\\Program Files\\ directory, specify that directory here.', - group: 'Open', - type: String, - optional: true, - }, - debug: { - description: 'Enable debug logging.', - group: 'Open', - type: Boolean, - optional: true, - }, - help: { - description: 'Show help.', - alias: 'h', - optional: true, - type: Boolean, - }, -}; - -const args = parseArgs(argsConfig, { - helpArg: 'help', - headerContentSections: [ - { - header: 'Stitch Open', - content: - 'Open a GameMaker project using specified IDE and Runtime versions.\n\nThis command will install the IDE for you if the specified version is not already installed.\n\nIf no runtime version is specified, the IDE\'s specified "matching" runtime will be used.', - }, - ], -}); - -// Get the target project -const targetProjectPaths = await StitchProjectStatic.listYypFilesRecursively( - args.project, -); +export const openCommand = cli('open', { + description: + 'Open a GameMaker project with a specific IDE and Runtime version.', + builder: (cli) => + cli + .option('project', { + description: 'The path to the GameMaker Studio 2 project to open.', + default: { + value: process.cwd(), + description: 'Current directory', + }, + type: 'string', + }) + .option('ide', { + description: + 'The IDE version to use. Defaults to the last one used to open the project.', + type: 'string', + }) + .option('runtime', { + description: 'The runtime version to use.', + type: 'string', + }) + .option('programFiles', { + description: + 'If you have installed GameMaker to somewhere besides the default C:\\Program Files\\ directory, specify that directory here.', + type: 'string', + }) + .option('debug', { + description: 'Enable debug logging.', + type: 'boolean', + }), + handler: async (args) => { + // Get the target project + const targetProjectPaths = + await StitchProjectStatic.listYypFilesRecursively(args.project); -ok( - targetProjectPaths.length > 0, - `No GameMaker projects found in ${args.project}`, -); -ok( - targetProjectPaths.length === 1, - `Multiple GameMaker projects found in ${args.project}. Provide a specific project path with the --project option.`, -); + ok( + targetProjectPaths.length > 0, + `No GameMaker projects found in ${args.project}`, + ); + ok( + targetProjectPaths.length === 1, + `Multiple GameMaker projects found in ${args.project}. Provide a specific project path with the --project option.`, + ); -if (args.debug) { - debug.enable('@bscotch/stitch-launcher:*'); -} + if (args.debug) { + debug.enable('@bscotch/stitch-launcher:*'); + } -await GameMakerLauncher.openProject(targetProjectPaths[0], { - ideVersion: - args.ide || - ( - await loadProjectFromArgs({ - targetProject: targetProjectPaths[0], - readOnly: true, - force: true, - }) - ).ideVersion, - runtimeVersion: args.runtime, - programFiles: args.programFiles, + await GameMakerLauncher.openProject(targetProjectPaths[0], { + ideVersion: + args.ide || + ( + await loadProjectFromArgs({ + targetProject: targetProjectPaths[0], + readOnly: true, + force: true, + }) + ).ideVersion, + runtimeVersion: args.runtime, + programFiles: args.programFiles, + }); + }, }); diff --git a/packages/core/src/cli/stitch-set-audio-group.ts b/packages/core/src/cli/stitch-set-audio-group.ts index 74c62c05..f2047fa0 100644 --- a/packages/core/src/cli/stitch-set-audio-group.ts +++ b/packages/core/src/cli/stitch-set-audio-group.ts @@ -1,31 +1,31 @@ #!/usr/bin/env node import { oneline, undent } from '@bscotch/utility'; -import { program as cli } from 'commander'; +import { cli, chain } from 'cli-forge'; import { addDebugOptions } from './lib/addDebugOption.js'; -import { assignAudioGroups, AssignCliOptions } from './lib/assign.js'; +import { assignAudioGroups } from './lib/assign.js'; import options from './lib/cli-options.js'; -cli - .description( - undent` +export const audioGroupCommand = cli('audio-group', { + description: undent` Assign all audios in a GMS IDE folder to a group.`, - ) - .requiredOption( - '--folder ', - undent` - This is the folder name shown in the GMS IDE, not the folder name of the actual audio file. - For example, a audio called "snd_title" is shown in the "Sounds" folder in the IDE, whereas - the actual audio file might be at "project/sounds/snd_title/snd_title.yy". - `, - ) - .requiredOption( - '--group-name ', - oneline` - The name of the audio group. If it does not exist, it will be created. - `, - ) - .option(...options.targetProject) - .option(...options.force); -addDebugOptions(cli).parse(process.argv); - -await assignAudioGroups(cli.opts() as AssignCliOptions); + builder: (cli) => + chain(cli, options.targetProject, options.force, addDebugOptions) + .option('folder', { + type: 'string', + description: undent` + This is the folder name shown in the GMS IDE, not the folder name of the actual audio file. + For example, a audio called "snd_title" is shown in the "Sounds" folder in the IDE, whereas + the actual audio file might be at "project/sounds/snd_title/snd_title.yy". + `, + required: true, + }) + .option('groupName', { + type: 'string', + description: oneline` + The name of the audio group. If it does not exist, it will be created. + `, + }), + handler: async (opts) => { + await assignAudioGroups(opts); + }, +}); diff --git a/packages/core/src/cli/stitch-set-texture-group.ts b/packages/core/src/cli/stitch-set-texture-group.ts index 778efde9..775c845c 100644 --- a/packages/core/src/cli/stitch-set-texture-group.ts +++ b/packages/core/src/cli/stitch-set-texture-group.ts @@ -1,31 +1,31 @@ #!/usr/bin/env node import { oneline, undent } from '@bscotch/utility'; -import { program as cli } from 'commander'; +import { cli, chain } from 'cli-forge'; import { addDebugOptions } from './lib/addDebugOption.js'; -import { AssignCliOptions, assignTextureGroups } from './lib/assign.js'; +import { assignTextureGroups } from './lib/assign.js'; import options from './lib/cli-options.js'; -cli - .description( - undent` +export const textureGroupCommand = cli('texture-group', { + description: undent` Assign all sprites in a GMS IDE folder to a group.`, - ) - .requiredOption( - '--folder ', - undent` - This is the folder name shown in the GMS IDE, not the folder name of the actual sprite file. - For example, a sprite called "sp_title" is shown in the "Sprites" folder in the IDE, whereas - the actual sprite file might be at "project/sprites/sp_title/sp_title.yy". - `, - ) - .requiredOption( - '--group-name ', - oneline` - The name of the texture group. If it does not exist, it will be created. - `, - ) - .option(...options.targetProject) - .option(...options.force); -addDebugOptions(cli).parse(process.argv); - -await assignTextureGroups(cli.opts() as AssignCliOptions); + builder: (cli) => + chain(cli, options.targetProject, options.force, addDebugOptions) + .option('folder', { + type: 'string', + required: true, + description: undent` + This is the folder name shown in the GMS IDE, not the folder name of the actual sprite file. + For example, a sprite called "sp_title" is shown in the "Sprites" folder in the IDE, whereas + the actual sprite file might be at "project/sprites/sp_title/sp_title.yy". + `, + }) + .option('groupName', { + type: 'string', + description: oneline` + The name of the texture group. If it does not exist, it will be created. + `, + }), + handler: async (opts) => { + await assignTextureGroups(opts); + }, +}); diff --git a/packages/core/src/cli/stitch-set-version.ts b/packages/core/src/cli/stitch-set-version.ts index 8add84e7..348c3fa3 100644 --- a/packages/core/src/cli/stitch-set-version.ts +++ b/packages/core/src/cli/stitch-set-version.ts @@ -1,29 +1,30 @@ #!/usr/bin/env node import { undent } from '@bscotch/utility'; -import { program as cli } from 'commander'; +import { cli, chain } from 'cli-forge'; import { addDebugOptions } from './lib/addDebugOption.js'; import options from './lib/cli-options.js'; -import version, { VersionOptions } from './lib/version.js'; +import version from './lib/version.js'; -cli - .description( - undent` +export const versionCommand = cli('version', { + description: undent` Set the project version in all options files. (Note that the PS4 and Switch options files do not include the version and must be set outside of GameMaker).`, - ) - .requiredOption( - '--project-version ', - undent` + builder: (cli) => + chain(cli, options.targetProject, options.force, addDebugOptions).option( + 'projectVersion', + { + type: 'string', + description: undent` Can use one of: + "0.0.0.0" syntax (exactly as GameMaker stores versions) + "0.0.0" syntax (semver without prereleases -- the 4th value will always be 0) + "0.0.0-rc.0" syntax (the 4th number will be the RC number) The four numbers will appear in all cases as the string "major.minor.patch.candidate" `, - ) - .option(...options.targetProject) - .option(...options.force); -addDebugOptions(cli).parse(process.argv); - -await version(cli.opts() as VersionOptions); + }, + ), + handler: async (opts) => { + await version(opts); + }, +}); diff --git a/packages/core/src/cli/stitch-set.ts b/packages/core/src/cli/stitch-set.ts index a4c86697..0bc3b93b 100644 --- a/packages/core/src/cli/stitch-set.ts +++ b/packages/core/src/cli/stitch-set.ts @@ -1,9 +1,12 @@ #!/usr/bin/env node -import { program as cli } from 'commander'; +import { cli } from 'cli-forge'; -cli - .description('Modify metadata in GameMaker Studio 2 projects.') - .command('version', 'Modify the versions for all export platforms.') - .command('texture-group', 'Modify texture group assignments.') - .command('audio-group', 'Modify audio group assignments.') - .parse(process.argv); +import { audioGroupCommand } from './stitch-set-audio-group.js'; +import { textureGroupCommand } from './stitch-set-texture-group.js'; +import { versionCommand } from './stitch-set-version.js'; + +export const setCommand = cli('set', { + description: 'Modify metadata in GameMaker Studio 2 projects.', + builder: (cli) => + cli.commands(audioGroupCommand, textureGroupCommand, versionCommand), +}); diff --git a/packages/core/src/cli/stitch.ts b/packages/core/src/cli/stitch.ts index d94fe2fc..52cd95b0 100644 --- a/packages/core/src/cli/stitch.ts +++ b/packages/core/src/cli/stitch.ts @@ -1,37 +1,33 @@ #!/usr/bin/env node -import { oneline } from '@bscotch/utility'; import { prettifyErrorTracing, replaceFilePaths } from '@bscotch/validation'; -import { program as cli } from 'commander'; -import { dirname } from 'path'; -import { fileURLToPath } from 'url'; +import { cli } from 'cli-forge'; import version from './lib/package-version.js'; +import { addCommand } from './stitch-add.js'; +import { archiveCommand } from './stitch-archive.js'; +import { deborkCommand } from './stitch-debork.js'; +import { issuesCommand } from './stitch-issues.js'; +import { mergeCommand } from './stitch-merge.js'; +import { lintCommand } from './stitch-lint.js'; +import { openCommand } from './stitch-open.js'; +import { setCommand } from './stitch-set.js'; prettifyErrorTracing({ replaceFilePaths }); -const dir = dirname(fileURLToPath(import.meta.url)); + +const stitch = cli('stitch', { + builder: (cli) => + cli + .commands( + addCommand, + archiveCommand, + deborkCommand, + issuesCommand, + mergeCommand, + lintCommand, + openCommand, + setCommand, + ) + .version(version), +}); // Kick it off -cli - .executableDir(dir) - .version(version, '-v, --version') - .description('Stitch') - .command('archive', 'Create a .yyz archive of a GameMaker project.') - .command( - 'open', - 'Open a GameMaker project with a specific IDE and Runtime version.', - ) - .command('issues', 'Create and manage issues to report to GameMaker.') - .command('merge', 'Merge two GameMaker Studio 2 projects together.') - .command( - 'add', - 'Create assets (e.g. sprites) using external resources (e.g. images).', - ) - .command('set', 'Modify metadata in GameMaker Studio 2 projects.') - .command( - 'debork', - oneline` - Run Stitch on the project without making any changes, - which will clean up some common issues and normalize the file content. - `, - ) - .command('lint', 'Generate a lint report for a GameMaker Studio 2 project.') - .parse(); +await stitch.forge(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fa5b50f3..0961b7ea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -167,6 +167,9 @@ importers: change-case: specifier: 5.1.2 version: 5.1.2 + cli-forge: + specifier: 0.8.0 + version: 0.8.0 commander: specifier: 11.1.0 version: 11.1.0 @@ -1214,6 +1217,12 @@ packages: resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==} dev: false + /@cli-forge/parser@0.8.0: + resolution: {integrity: sha512-+MWoele6Yjm60smxkCvQXxMSWQpec2rLhZLwaURefX6oTKVyisP53SD14CdRMI3muy8v1OjwzMcXt7Rei35chw==} + dependencies: + tslib: 2.6.3 + dev: false + /@effect/data@0.17.1: resolution: {integrity: sha512-QCYkLE5Y5Dm5Yax5R3GmW4ZIgTx7W+kSZ7yq5eqQ/mFWa8i4yxbLuu8cudqzdeZtRtTGZKlhDxfFfgVtMywXJg==} dev: false @@ -4109,6 +4118,22 @@ packages: dependencies: restore-cursor: 3.1.0 + /cli-forge@0.8.0: + resolution: {integrity: sha512-lJa0JmtiWFFUxNFzHJvqozX1xzz4fIScmyLESKEBaK1+QsxVHRQUpQwCgzGS+gWvdup1WEfrSwuDhEIkWdb/ew==} + hasBin: true + peerDependencies: + markdown-factory: 0.2.0 + tsx: 4.19.0 + peerDependenciesMeta: + markdown-factory: + optional: true + tsx: + optional: true + dependencies: + '@cli-forge/parser': 0.8.0 + tslib: 2.6.3 + dev: false + /cli-spinners@2.9.2: resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} engines: {node: '>=6'}